classdef (Hidden = true) CodeCoverageHook < coder.coverage.BuildHook
%CODECOVERAGEHOOK provides code coverage utilities
%
%   See also 
%
%   This is an undocumented class. Its methods and properties are likely to
%   change without warning from one release to the next.

%   Copyright 2009-2020 The MathWorks, Inc.

    properties (GetAccess=private, Constant=true)
        MaxCmdLength=8000; % Less than winxp limit of 8192
    end

    properties (Access=private)
        ToolPath;
        ToolNameForPreferences;
        ValidateToolInstallationFile;
        CoverageEnabledForThisComponent=true;
        RestoreSourceFilesCleanup;
        InstrPrefix;
    end
    
    methods (Access = public, Hidden = true)
            
        function xilCompatibilityCheck(this, isIDELinkTarget)
            
            if isIDELinkTarget
                DAStudio.error('CoderCoverage:AllTools:CodeCoverageIdeLink', ...
                               this.getModelName);
            end
            
        end
    
    end

    
    methods (Static=true, Access=private)
   
        
        function rtwAnnotateJS(htmlFolder, cmd)
            
            jsFile = 'rtwannotate.js';
            jsFileDst = fullfile(htmlFolder, jsFile);
            jsFileDstExists = ~isempty(dir(jsFileDst));
            
            switch cmd
                
              case 'add'
                if ~jsFileDstExists
                    jsFileSrc = fullfile(rtw.report.ReportInfo.getResourceDir, jsFile);
                    copyfile(jsFileSrc,jsFileDst);
                    fileattrib(jsFileDst,'+w');
                end
                
              case 'remove'
                if jsFileDstExists
                    delete(jsFileDst)
                end
                
              otherwise
                assert(true, 'Invalid argument for  rtwAnnotateJS');
                
            end
        end

    end
    
    methods (Static=true, Access=private)        
        
        function restoreSourceFiles(srcs, srcBackups, instrumentedSrcs, ...
                               instrumentedSrcBackups)
            
            assert(length(srcBackups)==length(instrumentedSrcs) && ...
                   length(srcBackups)==length(instrumentedSrcBackups),...
                   'Number original and backed up files must be consistent');
            
            for  i=1:length(srcBackups)
                src = srcs{i};
                srcBackup = srcBackups{i};
                instrumentedSrc = instrumentedSrcs{i};
                instrumentedSrcBackup = instrumentedSrcBackups{i};
                
                assert(isempty(dir(instrumentedSrc)), ...
                       'The instrumented source file %s must not exist.', ...
                       instrumentedSrc);
                
                % Restore the files
                readOnlyFolder = coder.coverage.CodeCoverageHook.isFolderReadOnly(fileparts(src));
                if ~readOnlyFolder
                    assert(~isempty(dir(instrumentedSrcBackup)), ...
                           'The backed up, instrumented source file %s must exist.', ...
                           instrumentedSrcBackup);
                    assert(~isempty(dir(srcBackup)), ...
                           'The backed up source file %s must exist.', ...
                           srcBackup);
                    movefile(instrumentedSrcBackup, instrumentedSrc);
                    delete(src);
                    movefile(srcBackup, src);
                end
            end
        end
        
        
        function [srcsBackup, instrumentedSrcBackup] =  ...
                substAndBackupInstrumentedSrcFiles(srcFiles, instrumentedSrc)
            
            backupSuffix = '.bak';
            backupSuffixInstr = '.ibak';
            
            srcsBackup = cell(size(srcFiles));
            instrumentedSrcBackup = cell(size(srcFiles));
           
            for i=1:length(srcFiles)
                % Check that the expected files exist
                srcFile = srcFiles{i};
                instrumentedSrcFile = instrumentedSrc{i};
                assert(~isempty(dir(srcFile)), ...
                       'The source file %s must exist.', srcFile);

                % We must choose a backup file name that does not match the regexp *.c*
                % otherwise it may match a makefile rule in the shared utilities folder
                [p,f,e]=fileparts(srcFile);
                backupName = [f e];
                backupName = strrep(backupName,'.','_');
                backupName = fullfile(p, backupName);
                srcBackupFile = [backupName backupSuffix];
                instrumentedSrcBackupFile = [backupName backupSuffixInstr];
                
                readOnlyFolder = coder.coverage.CodeCoverageHook.isFolderReadOnly(fileparts(srcFile));                

                if  ~readOnlyFolder
                    assert(~isempty(dir(instrumentedSrcFile)), ...
                        'The instrumented source file %s must exist.', ...
                        instrumentedSrcFile);
                    
                    movefile(srcFile, srcBackupFile);
                    copyfile(instrumentedSrcFile, instrumentedSrcBackupFile);
                    movefile(instrumentedSrcFile, srcFile);
                end

                srcsBackup{i} = srcBackupFile;
                instrumentedSrcBackup{i} = instrumentedSrcBackupFile;
            end
        end
    
    end
    
    methods (Static, Access=protected)
        
        function readOnly = isFolderReadOnly(folder)
            
            [~, w] = fileattrib(folder);
            readOnly = ~w.UserWrite;
            
        end
        
    end
    
    methods (Access=protected)
        
        function [componentNames, componentPaths] = ...
                getComponentsWithCoverageEnabled(this)  
            
            buildInfo = this.getInstrumentedComponentBuildInfo;
            
            if strcmp(this.getIncludeReferencedModels, 'on')
                mdlRefs = buildInfo.ModelRefs;
            else
                mdlRefs = {};
            end
            
            componentPaths = cell(length(mdlRefs),1);
            componentNames = cell(length(mdlRefs),1);
            isProtected = false(length(mdlRefs),1);
            
            % since we are in the sil or pil folder, we need to get 
            % relativePathToAnchor from the application buildInfo
            appBuildInfo = this.getApplicationBuildInfo;

            for i=1:length(mdlRefs)                
                refName=mdlRefs(i).Name;
                refName=regexprep(refName, '_rtwlib$', '');
                
                refPath = appBuildInfo.formatPaths(mdlRefs(i).Path, 'replaceStartDirWithRelativePath', true);
                
                % Identify protected models (library archive resides in the
                % same folder as code descriptor file and code descriptor
                % has an empty block hierarchy map)
                if isfile(fullfile(refPath, 'codedescriptor.dmr'))
                    codeDesc = coder.getCodeDescriptor(refPath);
                    isProtected(i) = isempty(codeDesc.getBlockHierarchyMap());
                else
                    isProtected(i) = false;
                end
                
                componentPaths{i} = refPath;
                componentNames{i} = refName;
            end
            
            % No coverage for protected models
            componentPaths = componentPaths(~isProtected);
            componentNames = componentNames(~isProtected);
            
            if this.CoverageEnabledForThisComponent                
                componentPaths{end+1} =  ...
                    fullfile(...
                        '..',...
                        this.InstrumentedObjectCodeFolder);
                componentNames{end+1} = buildInfo.ModelName;
            end

        end

        
        function writeCoverageXml...
                (this, componentHtmlFolders, allSrcFiles, annotations, forCodeViewOnly)

            % Flag for skipping the coverage results displayed
            % in the HTML code generation report
            if nargin < 5
                forCodeViewOnly = false;
            end

            for i=1:length(componentHtmlFolders)
                htmlFolder = componentHtmlFolders{i};
                componentDir = fileparts(htmlFolder);
                
                if ~isempty(dir(htmlFolder))
                    
                    srcs = allSrcFiles{i};
                    
                    folderHasXml = false;
                    
                    for k=1:length(srcs)
                        
                        src = srcs{k};
                        
                        annotations.setSrcFile...
                            (src, componentDir, this.InstrumentedObjectCodeFolderShared);
                        
                        if annotations.hasAnnotations
                        
                            [~, file, ext] = fileparts(src);

                            if ~forCodeViewOnly
                                [xml, encoding] = annotations.getXml;

                                % XML file name model.c -> model_c_cov.xml
                                fid = fopen(fullfile(htmlFolder,...
                                    [file '_' ext(2:end) '_cov.xml']),...
                                    'w', ...
                                    'n', ...
                                    encoding);
                                fprintf(fid,'%s',xml);
                                fclose(fid);

                                folderHasXml = true;
                            end
                            
                            % Data file name model.c -> model_c.cov_[name]
                            % used by new Code View
                            name = this.getName();
                            covData = annotations.getData;
                            covFile = fullfile(htmlFolder, [file, ext, '.cov_', name]);
                            save(covFile, 'covData', '-mat');
                        end
                    end
                    
                    if folderHasXml
                        % Need to copy the java script
                        coder.coverage.CodeCoverageHook.rtwAnnotateJS(htmlFolder, 'add');
                    else
                        coder.coverage.CodeCoverageHook.rtwAnnotateJS(htmlFolder, 'remove');
                    end
                    
                    % refresh code view if it is open
                    cr = simulinkcoder.internal.Report.getInstance;
                    cr.refresh(this.getModelName);
                end
            end
        end
    end            

    
    methods (Static, Access=public)
        
        function valueOut = coverageInfoFile...
                (lBuildFolder, cmd, valueName, valueIn)
            
            infoFile = fullfile(lBuildFolder, 'coverageInfo.mat');
            
            if ~isempty(dir(infoFile))
                info = load(infoFile);
                info = info.info;
            else
                info = struct('CodeChecksums','', 'InstrumentationPrefix','');
            end
            
            switch cmd
                case 'read'
                    
                    valueOut = info.(valueName);
                    
                case 'write'
                    
                    info.(valueName) = valueIn;
                    save(infoFile, 'info');
                    
                case 'delete'
                    
                    if ~isempty(dir(infoFile))
                        delete(infoFile);
                    end
            end
            
        end
        
        function forceCleanBuildSharedUtils(folder)

            % delete the coverage info file
            utilsFolder=fullfile(folder, '_sharedutils');
            coder.coverage.CodeCoverageHook.coverageInfoFile(utilsFolder, 'delete');
        
        end
        
        function instrumentedSrc = getInstrumentedSrcNames(srcFiles, lInstrPrefix)
            instrumentedSrc = cell(size(srcFiles));
            
            for i=1:length(srcFiles)
                srcFile = srcFiles{i};
                [srcFilePath, srcFileName, srcFileExt]=fileparts(srcFile);
                instrExt = [srcFileExt(1) lInstrPrefix srcFileExt(2:end)];
                instrumentedSrc{i} = fullfile(srcFilePath, ...
                                              [srcFileName instrExt]);
            end
        end
        
        
        function isAbs = isAbsPath(filePath)
        %GETABSPATH returns true if the path is absolute
            isAbs=false(size(filePath));
            expr='^(([A-Za-z]:(\\|\/))|(/)|(\\\\[A-Za-z])|(//[A-Za-z]))';
            for i=1:length(isAbs)
                match=regexp(filePath{i},expr,...
                    'once','lineanchors');
                if ~isempty(match)
                    isAbs(i)=true;
                else 
                    isAbs(i)=false;
                end
            end
        end
        
        

        
        function enabled = enabledForModel(model)
            
            hooks = coder.coverage.getBuildHooks(model);
            codeCoverageClass = coder.coverage...
                .getActiveCodeCoverageClass(hooks, true);
            enabled = ~isempty(codeCoverageClass);
        
        end
        
    end
    


    methods (Access=public)
        
        function enabled=getCoverageEnabledForThisComponent(this)
            enabled = this.CoverageEnabledForThisComponent;
        end        
        
        function result = systemCommand....
                (this, varargin)
            
            result = this.systemCommandPrivate...
                (varargin{:});
            
        end
        
        
        function result = systemCommandWithSeparableArgs...
                (this, baseCmd, separableArgs, lMaxCmdLength)
            
            if nargin < 4
                lMaxCmdLength = this.MaxCmdLength;
            end
            
            done = zeros(size(separableArgs));
            subCmdComplete = false;
            result = '';
            
            while ~all(done) 
                cmd = baseCmd;
                while(~all(done) && subCmdComplete==false)
                    i=find(~done,1,'first');
                    separableArg=separableArgs{i};
                    cmd = sprintf('%s %s', cmd, separableArg);
                    done(i)=1; 
                    
                    if ~all(done)
                        if (length(cmd)+length(separableArgs{i+1}) > ...
                                lMaxCmdLength)
                            subCmdComplete=true;
                        end
                    end

                end
                
                % Sub-class may override this method and we need to make
                % sure we don't use the sub-class implementation
                resultPart = this.systemCommandPrivate(cmd);
                
                if nargout > 0
                    result = [result resultPart]; %#ok
                end
               
                subCmdComplete=false;
            end
        end
        
        
        function setToolPath(this, toolPath, skipValidate)
            
            if nargin < 3
                skipValidate = false;
            end
            
            % Clear any cached tool path
            this.ToolPath = '';
            
            toolName = this.ToolNameForPreferences;
            assert(~isempty(toolName), 'The code coverage tool must be specified');
            
            % If a non-empty path is specified then validate it
            prefGroup = ['CodeCoverage_' toolName];
            prefName = 'Path';
            if ~isempty(toolPath)
                if ~skipValidate
                    isValid = this.validateToolPath(toolPath);
                    if ~isValid
                        DAStudio.error('CoderCoverage:AllTools:toolPathSetInvalid',...
                                       toolPath, toolName);
                    end
                end
                setpref(prefGroup, prefName, toolPath);
            else
                if ispref(prefGroup, prefName)
                    rmpref(prefGroup, prefName);
                end
            end
        end
        
        function version = getActualVersion(~)
            version = '"unknown"';
        end
        
        function toolPath = getToolAltPath(this)
        % Alternate form of path to handle spaces
            toolPath = coder.make.internal.transformPaths(...
                this.getToolPath,'pathType','alternate');
        end
        function toolPath = getToolPath(this, skipValidate)
            
            if nargin < 2
                skipValidate = false;
            end
            toolName = this.ToolNameForPreferences;
            assert(~isempty(toolName), 'The code coverage tool must be specified');
            
            
            if ~skipValidate && ~isempty(this.ToolPath)
                toolPath = this.ToolPath;
            else
                prefGroup = ['CodeCoverage_' toolName];
                if ispref(prefGroup,'Path')
                    toolPath = getpref(prefGroup,'Path');
                    if ~skipValidate
                        isValid = this.validateToolPath(toolPath);
                        if ~isValid
                            fixItMsg = DAStudio.message('CoderCoverage:AllTools:SetToolPathFixIt');
                            DAStudio.error('CoderCoverage:AllTools:toolPathGetInvalid',...
                                           toolPath, toolName, fixItMsg);
                        end
                        this.ToolPath = toolPath;
                    end
                elseif ~skipValidate           
                    fixItMsg = DAStudio.message('CoderCoverage:AllTools:SetToolPathFixIt');
                    DAStudio.error('CoderCoverage:AllTools:toolPathNotSet',...
                                   toolName, fixItMsg);
                else
                    toolPath = '';
                end
            end
        end
    end
    
        
    methods (Access=protected)
        
        function [componentHtmlFolders, allSrcFiles] = getHtmlFolders(this)

            originalBuildFolder = getOriginalBuildFolder(this);
            bi = load(fullfile(originalBuildFolder, 'buildInfo.mat'));
            buildInfo = bi.buildInfo;
            
            % As workaround for g1091018, temporarily cd to the build folder
            origDir = cd(originalBuildFolder);
            cdCleanup = onCleanup(@()cd(origDir));
            buildInfo.updateFilePathsAndExtensions;
            clear cdCleanup
            
            concatenatePaths = 1; replaceMatlabRoot = 1;
            
            if strcmp(this.getIncludeReferencedModels, 'on')
                mdlRefs = buildInfo.ModelRefs;
            else
                mdlRefs = {};
            end

            allSrcFiles = cell(size(mdlRefs));
            componentHtmlFolders = cell(size(mdlRefs));
            
            sharedSrcs = {};
            
            % Html folder and sources for any model references
            for i=1:length(mdlRefs)
                refBuildFolder = mdlRefs(i).getSourcePaths(true, 'BuildDir');
                refBuildFolder = refBuildFolder{1};
                
                refHtmlFolder = fullfile(refBuildFolder, 'html');
                refBuildInfo = load(fullfile(refBuildFolder, 'buildInfo.mat'));
                refSrcFiles = refBuildInfo.buildInfo.getSourceFiles...
                    (concatenatePaths, replaceMatlabRoot);
                
                if isempty(sharedSrcs)
                    sharedSrcs = coder.coverage.getSharedSrcFiles...
                        (refBuildInfo.buildInfo, getOriginalBuildFolder(this), ...
                                       this.InstrumentedObjectCodeFolder);
                end

                componentHtmlFolders{i} = refHtmlFolder;
                allSrcFiles{i} = refSrcFiles; 
            end
            
            % Html folder and sources for top model
            if this.getCoverageEnabledForThisComponent
                mainSrcPath = getSourcePaths(buildInfo, 1, 'BuildDir');
                mainSrcPath = mainSrcPath{1};
                componentHtmlFolders{end+1} = fullfile(mainSrcPath, 'html');
                topSrcFiles = buildInfo.getSourceFiles...
                    (concatenatePaths, replaceMatlabRoot);
                allSrcFiles{end+1} = topSrcFiles;
                if isempty(sharedSrcs)
                    sharedSrcs = coder.coverage.getSharedSrcFiles...
                        (buildInfo, getOriginalBuildFolder(this), ...
                                    this.InstrumentedObjectCodeFolder);
                end
            end
            
            % Html folder and sources for any shared utilities
            if ~isempty(sharedSrcs)
                if ~isempty(sharedSrcs)
                    htmlFolderShared = fullfile...
                        (fileparts(sharedSrcs{1}), 'html');
                    allSrcFiles{end+1} = sharedSrcs;
                    componentHtmlFolders{end+1} = htmlFolderShared;
                end
            end
            
        end
                
        function doRestoreSourceFilesCleanup(this)

            % clear the on cleanup object to swap back to the
            % non-instrumented source code.            
            this.RestoreSourceFilesCleanup = [];
            
        end
        
        function substInstrumentedSrcFiles(this, filesToInstrument)
            
            % Substitute instrumented source for the original source files
            lInstrumentedSrcFiles = coder.coverage.CodeCoverageHook.getInstrumentedSrcNames...
                (filesToInstrument, this.getInstrPrefix);
            
            [lSrcBackupFiles, ...
                lInstrumentedSrcBackupFiles] = ...
                coder.coverage.CodeCoverageHook.substAndBackupInstrumentedSrcFiles(filesToInstrument, ...
                lInstrumentedSrcFiles);
            
            this.RestoreSourceFilesCleanup = onCleanup...
                (@()coder.coverage.CodeCoverageHook.restoreSourceFiles ...
                (filesToInstrument, lSrcBackupFiles, ...
                 lInstrumentedSrcFiles, lInstrumentedSrcBackupFiles));
        end
                
        
        function setInstrPrefix(this, prefix)
            this.InstrPrefix = prefix;
        end
        
        function prefix = getInstrPrefix(this)
            prefix = this.InstrPrefix;
        end
        
        
        function [rptFullPath, rptOpenCmd] = getHtmlReportFullPath(this)
            
            buildInfo = this.getOriginalComponentBuildInfo;
            mainSrcPath = getSourcePaths(buildInfo, 1, 'BuildDir');
            mainSrcPath = mainSrcPath{1};
            htmlFolder = fullfile(mainSrcPath,'html');
            rptFullPath = '';
            rptOpenCmd = '';
            
            if exist(htmlFolder, 'dir')
                % does this folder contain an HTML report
                rpt = dir(fullfile(htmlFolder,'*_codegen_rpt.html'));
                if ~isempty(rpt)
                    rptFullPath = fullfile(htmlFolder,rpt.name);
                end
            end
            
            % Auto-launch code generation report using default web browser
            if ~isempty(rptFullPath)
                reportInfo = rtw.report.ReportInfo.instance(this.getModelName());
                if isa(reportInfo, 'rtw.report.ReportInfo')
                    auto_launch_cmd = sprintf('rtw.report.open(''%s'',''%s'')', ...
                                              reportInfo.getRootSystem, ...
                                              coder.report.internal.str2StrVar(reportInfo.BuildDirectory));
                    if ~strcmp(get_param(this.getModelName,'LaunchReport'),'off')
                        reportInfo.show();
                    end
                else
                    auto_launch_cmd = ['coder.internal.showHtml ''' ...
                                       rptFullPath ...
                                       ''' UseWebBrowserWidget'];
                    % Auto-launch the report if the option is on
                    if ~strcmp(get_param(this.getModelName,'LaunchReport'),'off')
                        evalin('base', auto_launch_cmd);
                    end                
                end
                
                % Display hyperlinks based web browser type
                rptOpenCmd = auto_launch_cmd;
            end
        end
        
        
        
        function isDormant = isHookDormant(~, hookClass)

             % It is possible that the model has a build hook for a code coverage tool that
             % is enabled, but the code coverage tool is no longer available via the
             % sl_customization mechanism. 
            
             [~, toolClasses] = coder.coverage.CodeCoverageHelper.getTools();
             
             isDormant = ~any(strcmp(hookClass, toolClasses));
             
         end
        
        
        function processConstructorArgs(this, varargin)

            argParser = inputParser();

            % This argument is only present when the current model is the top model (either
            % standalone or at the top of a model reference hierarchy). It
            % is set to 'on' if coverage is enabled for the top model, or
            % 'off' otherwise. If coverage is off for the top model, this
            % build hook is still responsible for ensuring that the SIL/PIL
            % application is linked and launched such that coverage
            % measurements can be collected for any referenced models.
            argParser.addParameter('CoverageForTopModel', '', @ischar);

            argParser.parse(varargin{:});
            args = argParser.Results;

            if ~isempty(args.CoverageForTopModel) && strcmp(args.CoverageForTopModel,'off')
                this.CoverageEnabledForThisComponent = false;
            else
                this.CoverageEnabledForThisComponent = true;
            end

        end

        function setToolNameForPreferences(this, toolName)
            this.ToolNameForPreferences = toolName;
        end
        
        function setValidateToolInstallationFile(this, validateFile)
            this.ValidateToolInstallationFile=validateFile;
        end
        
        function processSystemCommandResult(~, ~)
            % No license check is done for the base class
        end
        
    end

    methods (Access=private, Sealed=true)
        
        function result = systemCommandPrivate...
                (this, cmdArg, lVerbose, lIgnoreError, folderToRunFrom)
            if nargin < 3 || isempty(lVerbose)
                lVerbose = this.getVerbose;
            end
            
            if nargin < 4 || isempty(lIgnoreError)
                lIgnoreError=false;
            end
            
            if nargin < 5
                folderToRunFrom = '';
            end
            
            lDispHook = this.getDispHook;
            
            if strcmp(lVerbose, 'on')
                feval(lDispHook, cmdArg);
            end
            
            if ~isempty(folderToRunFrom)
                % Need to invoke the command from a temporary command file
                cmdFile = [tempname '.bat'];
                cmdFileCleanup = onCleanup(@()delete(cmdFile));
                
                fid = fopen(cmdFile,'w');
                if ~ispc
                    fprintf(fid, '#!/bin/sh\n'); % specify shell to use on Unix
                end
                fprintf(fid, 'cd %s\n%s\n', folderToRunFrom, cmdArg);
                fclose(fid);
                
                if ~ispc
                    fileattrib(cmdFile,'+x');
                end
                
                cmd = cmdFile;
            else
                cmd = cmdArg;
            end
            
            [status, result] = rtw.system(cmd);
            
            this.processSystemCommandResult(result);
            
            if strcmp(lVerbose, 'on')
                feval(lDispHook, result);
            end
            
            if status ~= 0 && ~lIgnoreError
                DAStudio.error('CoderCoverage:AllTools:SystemCommand',...
                    cmd, result);
            end
        end
    end
    
    methods (Access=private)
        
        function isValid = validateToolPath(this, toolPath)

           markerFile = this.ValidateToolInstallationFile;
           assert(~isempty(markerFile),...
                  ['A marker file must be specified to validate the '...
                   'coverage tool installation']);
           fullMarkerFilePath = fullfile(toolPath, markerFile);
           if ~exist(fullMarkerFilePath, 'file')
               isValid = false;
           else
               isValid = true;
           end
       end
    
    end
    
    methods (Static)
        function name = getName()        
            % get string id 
            name = 'CodeCoverage';
        end
        function name = getDisplayName() 
            % get display text
            name = 'Code Coverage';
        end
    end
    
end

% LocalWords:  replaceStartDirWithRelativePath nd
