classdef (Hidden = true) Bullseye < coder.coverage.CodeCoverageHook
%BULLSEYE provides code coverage utilities
% BULLSEYE
%
% 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-2021 The MathWorks, Inc.

properties(Constant=true, Hidden=true)
ToolNameForPreferences = 'BullseyeCoverage';
ValidateToolInstallationFile = i_getMarkerFile;
end

properties (GetAccess=private, Constant=true)
BuildZone = ['MATLAB_' num2str(system_dependent('getpid'))];
end

properties (Access=private)
CovFilePath;
TotalCovFilePath;
CovSrcDir;
CodeGenFolderIsUNC;
UNCMapping;
CoverageToolCleanup;
end

methods (Access=private, Static=true)

function srcsFiltered = filterOutHeaderFiles(srcs)
keepIdx = true(size(srcs));
for i=1:length(keepIdx)
[~,~,ext]=fileparts(srcs{i});
if any(strcmp(ext,{'.h', '.hpp'})) % It's a header file
keepIdx(i)=false;
end
end
srcsFiltered = srcs(keepIdx);
end

end

methods (Access=public, Static=true, Hidden=true)

function toolPath = getPath
try
toolPath = coder.coverage.getCoverageToolPath('coder.coverage.Bullseye');
catch e
if strcmp(e.identifier,'CoderCoverage:AllTools:toolPathNotSet')
toolPath = '';
else
rethrow(e);
end
end
end

function version = parseVersionFromCmdResult(cmdResult)

version = '';

if ~isempty(cmdResult)
expr = '([0-9]+\.[0-9]+\.[0-9]+)';
valueStr = regexp(cmdResult,expr, 'tokens', 'once','lineanchors');
if iscell(valueStr) && length(valueStr)==1
version = strtrim(valueStr{1});
end
end

end

function tmpxmlExplicit = xmlExplicitEncoding(tmpxml)

tmpxmlExplicit = [tempname '.xml'];

% Open file using default encoding for this system
fid1 = fopen(tmpxml,'r');
content = fread(fid1,'*char');
fclose(fid1);
content=content';

xmlStr = regexp(content, ...
'(<\?xml\s*)(.*)(\?>)','once','tokens');
xmlAttributes = xmlStr{2};
encoding = regexp(xmlAttributes, 'encoding\s*=\s*"(.*)"', 'once','tokens');
varargin = {};
contentOut = content;
if isempty(encoding)
encodingAttribute=' encoding="UTF-8"';
xmlAttributes = [xmlAttributes encodingAttribute];
xmlStrNew = [xmlStr{1}, xmlAttributes, xmlStr{3}];
contentOut = regexprep(contentOut,'<\?xml\s*.*\?>', xmlStrNew);
varargin = {'UTF-8'};
end

% Write back out using a specified encoding
fid = fopen(tmpxmlExplicit, 'w', 'native', varargin{:});
fprintf(fid,'%s',contentOut);
fclose(fid);
end

function filenamesCanonical = getCanonicalFullPath...
(filenames, relativePathToAnchor)

if nargin<2
inputPathsMayBeRelative = false;
else
inputPathsMayBeRelative = true;
end

isAbsPath = coder.coverage.CodeCoverageHook.isAbsPath(filenames);

filenamesCanonical = cell(size(filenames));
for i=1:length(filenames)
fileName = filenames{i};
if inputPathsMayBeRelative
if ~isAbsPath(i)
fileName=fullfile(pwd, relativePathToAnchor, fileName);
end
end
filenamesCanonical{i} = builtin('_canonicalizepath', fileName);
end
end

function [probes,fn_cov,cd_cov,fn_total,cd_total] = ...
mergeProbes(oldProbes, probes)
%MERGEPROBES returns merged coverage data for probes
%relating to the same section of source code

fn_cov=0;
cd_cov=0;
fn_total=0;
cd_total=0;
for i=1:length(probes)
probe=probes(i);
oldProbe=oldProbes(i);
assert(strcmp(oldProbe.line,probe.line), ...
'Line number for probe must remain consistent.');
assert(strcmp(oldProbe.kind,probe.kind), ...
'The kind of probe must remain consistent.');
assert(strcmp(oldProbe.column,probe.column), ...
'The probe column number must remain consistent.');
assert(strcmp(oldProbe.col_seq,probe.col_seq), ...
'The probe sequence number must remain consistent.');
events={oldProbe.event, probe.event};
eventsLogical=false(2, length(events));
for j=1:length(events)
event=events{j};
switch event
case 'none'
eventsLogical(j,:)=[0 0];
case 'true'
eventsLogical(j,:)=[1 0];
case 'false'
eventsLogical(j,:)=[0 1];
otherwise
assert(strcmp(event,'full')==1, ...
'Event must be one of the allowed values.');
eventsLogical(j,:)=[1 1];
end
end
eventMerged=eventsLogical(1,:) | eventsLogical(2,:);
kind = probe.kind;
if all(eventMerged==[0 0])
event = "none";
elseif all(eventMerged==[1 0])
event = "true";
cd_cov=cd_cov + 1;
elseif all(eventMerged==[0 1])
event = "false";
cd_cov = cd_cov + 1;
else
assert(all(eventMerged==[1 1]),...
'Merged event must have one of the allowed values.')
event = "full";
if strcmp(kind,'function')
fn_cov = fn_cov+1;
elseif any(strcmp(kind,{'catch', 'switch-label', 'try'}))
cd_cov = cd_cov+1;
elseif any(strcmp(kind,{'condition','decision'}))
cd_cov = cd_cov+2;
else
assert(strcmp(kind,'constant'), ...
'Kind of event must be one of the allowed values.')
end
end
if strcmp(kind,'function')
fn_total = fn_total+1;
elseif any(strcmp(kind,{'catch', 'switch-label', 'try'}))
cd_total = cd_total+1;
elseif any(strcmp(kind,{'condition','decision'}))
cd_total = cd_total+2;
else
assert(strcmp(kind,'constant'), ...
'The kind of event must be one of the allowed values.')
end
probes(i).event = event;
end
end

function componentCovData = mergeComponentCovData...
(componentCovData, componentDir, ...
covFilePath, codeGenFolder)
%MERGECOMPONENTCOVDATA merges new coverage data into an existing
%.mat file with previous coverage data for the same component
%build directory

rootFolder=codeGenFolder;
buildDir = fullfile(rootFolder,componentDir);
if exist(buildDir, 'dir')~=7
% It is possible that there are no shared utility object
% files and that the build folder e.g.
% slprj/ert/_sharedutils/sil/hostobj therefore does not
% already exist
mkdir(buildDir)
end
covResultsFile = coder.coverage.CoverageResultsFile(buildDir);
srcPaths=cell(size(componentCovData.srcs));
[srcPaths{:}]=deal(componentCovData.srcs.srcPath);
srcTimestamps=cell(size(componentCovData.srcs));
[srcTimestamps{:}]=deal(componentCovData.srcs.srcTimestamp);

if ~isempty(srcPaths)
assert(all(strcmp(srcPaths,sort(srcPaths))), ...
'All paths must in sorted order.');
end

isOldDataValid=false;

if covResultsFile.exists

% First we check timestamps saved in the covData.mat file against
% the source file timestamps from the time when they were most
% recently compiled with coverage instrumentation switched on
covResultsFile.load;
oldCovData = covResultsFile.getCovData;
oldSrcPaths=cell(size(oldCovData.srcs));
[oldSrcPaths{:}]=deal(oldCovData.srcs.srcPath);
oldTimestamps=cell(size(oldCovData.srcs));
[oldTimestamps{:}]=deal(oldCovData.srcs.srcTimestamp);
isOldDataValid=true;
for i=1:length(oldSrcPaths)
oldSrcPath=oldSrcPaths{i};
newSrcIdx = find(strcmp(oldSrcPath,srcPaths), 1 );
if isempty(newSrcIdx)
isOldDataValid=false;
break;
else
oldTimestamp=oldTimestamps{i};
srcTimestamp=srcTimestamps{newSrcIdx};
if ~isequal(oldTimestamp,srcTimestamp)
isOldDataValid=false;
break;
end
end
end
end

if isOldDataValid
for i=1:length(componentCovData.srcs)
probes=componentCovData.srcs(i).probes;
srcPath=srcPaths{i};
oldSrcIdx = find(strcmp(srcPath,oldSrcPaths), 1 );
if ~isempty(oldSrcIdx)
oldProbes=oldCovData.srcs(oldSrcIdx).probes;
[componentCovData.srcs(i).probes, ...
componentCovData.srcs(i).fn_cov, ...
componentCovData.srcs(i).cd_cov, ...
fn_total,cd_total] = ...
coder.coverage.Bullseye.mergeProbes...
(probes,oldProbes);
assert(fn_total==componentCovData.srcs(i).fn_total,...
'Total function coverage must remain consistent');
assert(cd_total==componentCovData.srcs(i).cd_total,...
'Total function coverage must remain consistent');
end
end
else
covResultsFile.clearCovFilePaths;
end

% Store the path to the .cov file so that it can be deleted
% when coverage data is cleared
covResultsFile.addCovFilePath(covFilePath);

covResultsFile.setCovData(componentCovData);
covResultsFile.save;

end

function timestamps = getCovSrcTimestamps...
(relativePathToAnchor, covSrcs)
isAbsPath = coder.coverage.CodeCoverageHook.isAbsPath(covSrcs);
timestamps = cell(size(covSrcs));
for i=1:length(covSrcs)
if ~isAbsPath(i)
srcFile=fullfile(relativePathToAnchor,covSrcs{i});
else
srcFile=covSrcs{i};
end

d = dir(srcFile);

assert(~isempty(d), ...
['The file %s must exist and its length must not exceed '...
'any system dependent limits.'], srcFile);

timestamps{i}= d.datenum;
end
end

function [allSrcs, readCovFileSuccess] = parseSrcFileList(w)

% We need to parse out list of files from text formatted as
%
% Header text
% Source Function Coverage
% ------------------- -----------------
% filepath1/file1.ext 0 /3 = 0%
% filepath2/file2.ext 0 /3 = 0%
% ------------------- -----------------
% Total

readCovFileSuccess = true;
allSrcs = {};

% Extract the lines containing file names
idx1 = strfind(w ,sprintf('\n---'));

if length(idx1) ~= 2
readCovFileSuccess = false;
return;
end

filesPart = w((idx1(1)+1):idx1(2));
idx2 = min(strfind(filesPart, newline));
filesPart = filesPart(idx2(1)+1:end);

% Get the width of the first column by processing the line like
%
% ------------------ --------------------
colSepLine = w((idx1(1)+1):(idx1(1)+idx2(1)));
colWidth = find(colSepLine==' ', 1 )-1;

% Convert extracted file names and place in a cell array
eolIdx = find(filesPart==newline);
numFiles = length(eolIdx);
allSrcs = cell(size(eolIdx));
nextLineIdx=1;
for i=1:numFiles
src = filesPart(nextLineIdx:eolIdx(i)-1);
src = strtrim(src(1:colWidth));
nextLineIdx = eolIdx(i)+1;
allSrcs{i}=src;
end
allSrcs = sort(allSrcs(:));
end
end

methods (Access=public)

function delete(this)
% Make sure clean up is done while other properties of this instance are still
% available
this.CoverageToolCleanup = [];
end

function version = getActualVersion(this)

cmd = fullfile(this.getToolAltPath,'bin','cov01 -h');

lVerbose = 'off';
lIgnoreError = true;
cmdResult = this.systemCommand(cmd, lVerbose, lIgnoreError);

version = coder.coverage.Bullseye.parseVersionFromCmdResult(cmdResult);

if isempty(version)
% Unknown version: call superclass method to get default string
version = getActualVersion@coder.coverage.CodeCoverageHook(this);
end

end

function allSrcs = listSrcFiles(this, covFile, lCovSrcDir)

if nargin < 3
lCovSrcDir = this.CovSrcDir;
end

folderToRunFrom = lCovSrcDir;

% Run the covsrc command from the folder where the .cov file resides otherwise
% we may get incorrect relative paths on the Windows platform
if ~coder.coverage.CodeCoverageHook.isAbsPath({covFile})
covFile = fullfile(pwd, covFile);
end

if this.CodeGenFolderIsUNC
% Ensure that folderToRunFrom uses a mapped drive letter
% and is not a UNC path
folderToRunFrom = this.transformUNCToMappedPath...
(folderToRunFrom);
end

cmd = fullfile(this.getToolAltPath,'bin','covsrc');
cmd = sprintf('%s -w0', cmd);
cmd = sprintf('%s --file "%s"', cmd, covFile);

% Override verbose setting because this command prints out potentially confusing
% information about coverage results
lVerbose = 'off';
lIgnoreError = false;

% Ensure that environment variable COVSRCDIR is set correctly,
% otherwise we may get incorrect paths
origCOVSRCDIR=getenv('COVSRCDIR');
setenv('COVSRCDIR',lCovSrcDir);
c = onCleanup(@() setenv('COVBUILDZONE',origCOVSRCDIR));

% Due to multi-threaded file system operations, the .cov file may initially be
% missing or incomplete. Therefore we need a retry/polling mechanism.
readCovFileSuccess = false;
attemptNumber = 1;
maxAttempts = 10;

while ~readCovFileSuccess
if exist(covFile, 'file')

% Run BullseyeCoverage command to analyze the coverage data
cmdResult = this.systemCommand...
(cmd, lVerbose, lIgnoreError, folderToRunFrom);

[allSrcs, readCovFileSuccess] = ...
coder.coverage.Bullseye.parseSrcFileList(cmdResult);

assert(attemptNumber <= maxAttempts, ...
'Failed to extract source file list from coverage file %s', covFile)
else
assert(attemptNumber <= maxAttempts, ...
'The coverage file %s does not exist', covFile)
end

if ~readCovFileSuccess
attemptNumber = attemptNumber + 1;
% Wait before next attempt
pause(1);
end
end
end

function this = Bullseye(varargin)

this.processConstructorArgs(varargin{:});

end

function covData = importCovData(this, covFilePath, srcTimestampsMap)

tmpxml = [tempname '.xml'];
covCmd=fullfile(this.getToolAltPath,'bin','covxml');
covCmdOpts=sprintf('--file "%s" > "%s"', covFilePath, tmpxml);

% Some BullseyeCoverage versions may return incorrect paths unless we run this
% command from the folder where the .cov file resides (in this case
% should be the same as -srcDir)
folderToRunFrom = fileparts(covFilePath);

if this.CodeGenFolderIsUNC
% Ensure that folderToRunFrom uses a mapped drive letter
% and is not a UNC path
folderToRunFrom = this.transformUNCToMappedPath...
(folderToRunFrom);
end

lIgnoreError = false;
this.systemCommand([covCmd ' ' covCmdOpts], '', lIgnoreError, ...
folderToRunFrom);

% Ensure that XML encoding is set explicitly to avoid issues
% with certain regional settings
tmpxmlExplicit = coder.coverage.Bullseye.xmlExplicitEncoding(tmpxml);

% Parse XML file
srcData = i_parse_xml(tmpxmlExplicit);

% Add the timestamp data and update srcPath
for i = 1:length(srcData)
srcPath = srcData(i).srcPath;

if this.CodeGenFolderIsUNC
% Paths in the .cov file may be using a temporarily mapped drive letter and we
% must transform this back to the original UNC path
srcPath = this.transformMappedPathToUNC({srcPath});
srcPath = srcPath{1};
end

srcPathKey = stripStartDir(this, srcPath);
srcData(i).srcPath=srcPath;

assert(srcTimestampsMap.isKey(srcPathKey), ...
'The key, %s,must be one of %s', ...
srcPathKey, strjoin(string(srcTimestampsMap.keys), ', '));

srcData(i).srcTimestamp=srcTimestampsMap(srcPathKey);
end

delete(tmpxml); % delete the XML file
delete(tmpxmlExplicit); % delete the XML file

covData.srcs = srcData;
end

function entry(this)

modelName = this.getModelName;

% If no coverage file location was specified in the
% constructor we use the model's build directory as the
% default location
lCodeGenFolder=this.getCodeGenFolder;

if strncmp(lCodeGenFolder, '\\', 2)
archStr = computer('arch');
if strcmp(archStr, 'win32')
DAStudio.error('CoderCoverage:Bullseye:BullseyeWin32UNC');
else
assert(strcmp(archStr, 'win64'), ...
'Can have UNC paths on non-Windows platforms');
end
this.CodeGenFolderIsUNC=true;
this.initializeUNCMapping;
else
this.CodeGenFolderIsUNC=false;
end

this.setInstrPrefix('i_');

if isempty(this.CovFilePath)

this.CovFilePath = ...
fullfile(this.getInstrumentedBuildFolder,...
[modelName '.cov']);
end

if any(strfind(lCodeGenFolder,' '))
DAStudio.error('CoderCoverage:AllTools:spacesInPwd', modelName,...
lCodeGenFolder)
end

% All .cov files should have source file paths relative to this
% dir
this.CovSrcDir = coder.make.internal.transformPaths(lCodeGenFolder,...
'pathType','alternate');

% The coverage file that is opened in the coverage browser must
% be in the same folder used to compute relative paths of the
% source files
this.TotalCovFilePath = fullfile(this.CovSrcDir, [modelName '.cov']);

% Set the environment variables for when the target application
% is launched
envVarNames={'COVBUILDZONE','COVFILE'};
envVarValues={coder.coverage.Bullseye.BuildZone, this.TotalCovFilePath};
this.setLaunchEnvironmentVariables(envVarNames, envVarValues);

% Set the PATH prepends for coverage build
this.setBuildPathPrepends({fullfile(this.getToolAltPath,'bin')});

% Ensure coverage is off initially
setCoverageBuildOff(this)
end

function after_code_generation(this)

% Check for specific unsupported compilers
lToolchainInfo = getToolchainInformation(this);
checkForUnsupportedCompiler(this, lToolchainInfo);

% Remove existing coverage information for any files that no longer exist
if exist(this.CovFilePath, 'file')==2
srcsFromCovFile = this.listSrcFiles(this.CovFilePath);

% Identify any sources that no longer exist (e.g. the names of generated files are
% controlled by model configuration settings)
nonExistentIdx = false(size(srcsFromCovFile));
for i=1:length(nonExistentIdx)
srcFile = srcsFromCovFile{i};
if ~coder.coverage.CodeCoverageHook.isAbsPath(srcsFromCovFile(i))
srcFile = fullfile(this.CovSrcDir,srcFile);
end
nonExistentIdx(i)=(exist(srcFile,'file')~=2);
end
nonExistentSrcs=srcsFromCovFile(nonExistentIdx);

this.removeSrcsFromCovFile(this.CovFilePath, nonExistentSrcs);
end

% Set environment variable values
envVarNames={'COVBUILDZONE','COVFILE','COVSRCDIR'};

if this.CodeGenFolderIsUNC
% Ensure that environment variables specify the mapped
% drive letter and not a UNC path
lCovFilePath = this.transformUNCToMappedPath...
(this.CovFilePath);
lCovSrcDir = this.transformUNCToMappedPath...
(this.CovSrcDir);
else
lCovFilePath = this.CovFilePath;
lCovSrcDir = this.CovSrcDir;
end
envVarValues={coder.coverage.Bullseye.BuildZone, lCovFilePath,...
lCovSrcDir};
this.setBuildEnvironmentVariables(envVarNames, envVarValues);

% If build we are building for the top model and coverage for
% the top model is checked off then don't switch on coverage
% at this stage
if this.getCoverageEnabledForThisComponent
this.setCoverageBuildOn;
this.CoverageToolCleanup = ...
onCleanup(@()setCoverageBuildOff(this));
end
end

function after_make(this)
if this.getCoverageEnabledForThisComponent
this.CoverageToolCleanup = [];

buildInfo = this.getOriginalComponentBuildInfo;

% Identify any files with code coverage info that are not
% already in buildInfo. Adding them to buildInfo ensures that
% they will be shown in the code coverage report.
addUnknownFilesToBuild(this, buildInfo);
end

end

function before_makefilebuilder_make(this)
% Switch on coverage build for the target (SIL or PIL) application; note that if
% the model build was up to date, the before_make and after_make will not have
% been called so it is necessary to independently switch on coverage build at
% this point.

% We must switch on code coverage for building the PIL test
% harness so that the Bullseye library is linked; since we are
% not interested in coverage results for the harness this file
% need not be used
if this.isSilBuild
testHarnessCovFile = 'sil_harness.cov';
else
testHarnessCovFile = 'pil_harness.cov';
end

envVarNames={'COVBUILDZONE','COVFILE','COVSRCDIR'};
envVarValues={coder.coverage.Bullseye.BuildZone, testHarnessCovFile,...
this.CovSrcDir};
this.setBuildEnvironmentVariables(envVarNames, envVarValues);
this.setCoverageBuildOn('pilApp',testHarnessCovFile);
this.CoverageToolCleanup = ...
onCleanup(@()setCoverageBuildOff(this));
end

function after_makefilebuilder_make(this)

this.CoverageToolCleanup = [];

% We need to merge coverage files for any referenced models
mergeCoverageFiles(this)
end

function after_on_target_execution(this) % e.g. PIL simulation

% Pause to ensure that coverage data has been serialized, g1167213.
pause(1);

% Protect against spaces in the paths
covBrowser = fullfile(this.getToolAltPath,'bin');
covBrowser = fullfile(covBrowser,'CoverageBrowser');
covFile=this.TotalCovFilePath;
[covFilePath,covFileName,covFileExt] = fileparts(covFile);
covFilePath=coder.make.internal.transformPaths(covFilePath,'pathType','alternate');
covFilePath=fullfile(covFilePath,[covFileName covFileExt]);

if exist(covFilePath,'file')
this.dispInfo(['### '...
DAStudio.message('CoderCoverage:AllTools:processingCodeCoverageData')]);

% Push coverage data back to build directories for use by html report
this.covForHtml(covFilePath);

% Link to open BullseyeCoverage browser
openBrowserCmd = ['!' covBrowser ' ' covFilePath ' &'];
hyperlinkBrowser = {openBrowserCmd, message('CoderCoverage:Bullseye:openBullseyeBrowser')};
hyperlinkBrowser = message('CoderCoverage:AllTools:CoverageHyperlink', hyperlinkBrowser);

this.dispInfo(...
sprintf('### %s',...
DAStudio.message('CoderCoverage:AllTools:codeCoverageResultLinks')));

% check to see if COVNOSAVE environment variable is set
% if it is set (value is 1) then code coverage measurements
% are not saved in the coverage file
covNoDataVal = getenv('COVNOSAVE');
if(~isempty(covNoDataVal) && covNoDataVal == '1')
% show warning
this.dispWarning(...
{'CoderCoverage:Bullseye:BullseyeCoverageDataNotSaved', ...
this.TotalCovFilePath, ...
covNoDataVal});
end

this.dispInfo(getString(hyperlinkBrowser));

% HTML code generation report with annotations
[rptFullPath, rptOpenCmd] = this.getHtmlReportFullPath;
if ~isempty(rptFullPath)
% Display hyperlinks based web browser type
hyperlinkHtmlReport = {rptOpenCmd, message('CoderCoverage:AllTools:openRtwReportForCoverage')};
hyperlinkHtmlReport = message('CoderCoverage:AllTools:CoverageHyperlink', hyperlinkHtmlReport);
dispInfo(this, getString(hyperlinkHtmlReport));
else
this.dispInfo(...
DAStudio.message('CoderCoverage:AllTools:noRtwReportForCoverage'));
end

else
this.dispWarning(...
{'CoderCoverage:Bullseye:bullseyeNoCoverageFile',...
this.TotalCovFilePath});
end
this.dispInfo(...
sprintf('### %s', DAStudio.message('CoderCoverage:AllTools:CompletedCoverage')));
end
end

methods (Access=private)

function componentCovData = getCovDataForComponent...
(this, covData, componentDirKey, srcFoldersMap)
%GETCOVDATAFORCOMPONENT extracts coverage data relating
%to the specified component
assert(srcFoldersMap.isKey(componentDirKey), ...
'The source folder %s must have been processed for code coverage.', ...
componentDirKey);
srcs = srcFoldersMap(componentDirKey);
covDataSrcs = cell(size(covData.srcs));
[covDataSrcs{:}]=deal(covData.srcs.srcPath);
covDataSrcKeys = stripStartDir(this, covDataSrcs);
[~, ~, srcIdx] = intersect(srcs, covDataSrcKeys, 'stable');
assert(length(srcIdx)==length(srcs), ...
'All source files must be present');
componentCovData.srcs = covData.srcs(srcIdx);
end

function filePathsStripped = stripStartDir(this, filePaths)
% If the filePath starts with an absolute path to the code generation folder
% then remove it, i.e. leaving a file path relative to the code
% generation folder.

lIsCell = true;
if ~iscell(filePaths)
filePaths={filePaths};
lIsCell = false;
end
filePathsStripped = cell(size(filePaths));

for i=1:length(filePaths)

% Ensure we have file separators normalized for the current platform
filePathPlatform = fullfile(filePaths{i});

startDir = this.getCodeGenFolder;
N = length(startDir);
if strncmp(filePathPlatform, startDir, N)
filePathsStripped{i} = filePaths{i}(N+2:end);
else
filePathsStripped{i} = filePaths{i};
end

end

if ~lIsCell
filePathsStripped = filePathsStripped{1};
end

end

function filenamesOut = transformMappedPathToUNC(this, filenamesIn)

filenamesOut = cell(size(filenamesIn));
drive = this.UNCMapping.drive;

for i=1:length(filenamesIn)
fileName = filenamesIn{i};
if strncmpi(fileName, ...
drive, ...
length(drive))
fileName = ...
[strrep(this.UNCMapping.lUNCPath,'\','/') ...
fileName(length(drive)+1:end)];
end
filenamesOut{i} = fileName;
end
end

function pathOut = transformUNCToMappedPath(this, pathIn)

lenUncPath = length(this.UNCMapping.lUNCPath);
if strncmpi(pathIn, this.UNCMapping.lUNCPath, lenUncPath)
drive = this.UNCMapping.drive;
pathOut = [drive pathIn(lenUncPath+1:end)];
else
pathOut = pathIn;
end

end

function initializeUNCMapping(this)
lUNCPath = this.getCodeGenFolder;
oldDir = cd(lUNCPath);
[~, pathMapped] = rtw.system('cd');
cd(oldDir);
pathMapped = strtrim(pathMapped);
drivePart = pathMapped(1:2);
assert(strcmp(drivePart(2), ':'),...
'Drive letter must have correct formatting.');
len = length(pathMapped);
assert(strcmpi(pathMapped(3:end),lUNCPath(end-len+3:end)),...
'Tail of mapped and UNC paths must match');
lUNCMapping=struct('drive', drivePart,...
'lUNCPath',lUNCPath(1:end-len+2));
this.UNCMapping = lUNCMapping;
end

function relativePath = getRelativePathFromPwdToStartDir(this)

lStartDir = fullfile(getCodeGenFolder(this));
lPwd = fullfile(pwd);
assert(strncmp(lStartDir, lPwd, length(lStartDir)), ...
'Current folder must be underneath %s.', lStartDir);

relativePath = '';
parentFolder = lPwd;

while ~strcmp(parentFolder, lStartDir)
relativePath = fullfile(relativePath, '..');
parentFolder = fileparts(parentFolder);
end

end

function addUnknownFilesToBuild(this, buildInfo)

relativePathToAnchor = getRelativePathFromPwdToStartDir(this);

[~, ~, srcsFromCovFile] = this.getSrcsFromCovFile(this.CovFilePath, relativePathToAnchor);

srcsFromCovFileCanonical = coder.coverage.Bullseye.getCanonicalFullPath(srcsFromCovFile, relativePathToAnchor);

srcsFromCovFileNoPath = cell(size(srcsFromCovFile));
for i = 1:length(srcsFromCovFile)
[~, fileNameTmp, extTmp] = fileparts(srcsFromCovFile{i});
srcsFromCovFileNoPath{i}=[fileNameTmp extTmp];
end

concatenatePaths=1; replaceMatlabroot=1;
updateFilePathsAndExtensions(buildInfo);
sourceFilesKnownToBuildInfo = buildInfo.getSourceFiles...
(concatenatePaths, replaceMatlabroot);

sourceFilesKnownToBuildInfoCanonical = ...
coder.coverage.Bullseye.getCanonicalFullPath ...
(sourceFilesKnownToBuildInfo);

[~, unknownSrcIdx] = setdiff(srcsFromCovFileCanonical, ...
sourceFilesKnownToBuildInfoCanonical);

for i=1:length(unknownSrcIdx)
unknownSrc = srcsFromCovFileCanonical{unknownSrcIdx(i)};
[pathToAdd, nameToAdd, extToAdd] = fileparts(unknownSrc);
% It is possible that the "unknown source" is already
% known to buildInfo but without the full path; in this
% case, buildInfo will simply update the path for this
% source file. This means it is not necessary to call
% buildInfo.updateFilePathsAndExtensions()
buildInfo.addSourceFiles([nameToAdd extToAdd], pathToAdd,'CustomCode');
end
end

function relPath = getPathFromBuildDirToCovFile(~,covFilePath,buildDir)
[~,covName,ext]=fileparts(covFilePath);

buildDir = strrep(buildDir,'\','/');
delimChar='/';
buildDirCa=textscan(buildDir,'%s','delimiter',delimChar);
buildDirCa=buildDirCa{1};
buildDirDepth=length(buildDirCa);
reversePath=repmat('../',1,buildDirDepth);
relPath=[reversePath covName ext];

fileName = fullfile(buildDir,relPath);
assert(exist(fileName,'file')==2,...
['The file %s must exist and in addition its length must not exceed '...
'any system dependent limits.'], fileName);

end

function covForHtml(this, covFilePath)

lCodeGenFolder = this.getCodeGenFolder;
% Locate and load the covInfo.mat file
covInfoFile = fullfile(this.ApplicationFolder,'covinfo.mat');
covInfo = load(covInfoFile);
srcFoldersMap = covInfo.srcFoldersMap;
srcTimestampsMap = covInfo.srcTimestampsMap;

% Import the coverage data for this simulation run
covData = this.importCovData(covFilePath, srcTimestampsMap);

componentObjDirsRelative = unique(srcFoldersMap.keys);
for i=1:length(componentObjDirsRelative)
componentObjDir=componentObjDirsRelative{i};
covFileRelPath=this.getPathFromBuildDirToCovFile...
(covFilePath,componentObjDir);
% Extract coverage data for this component
componentObjDirKey = stripStartDir(this, componentObjDir);
componentCovData = getCovDataForComponent(this,...
covData, componentObjDirKey, srcFoldersMap);
% Merge coverage data into the component covData.mat
coder.coverage.Bullseye.mergeComponentCovData...
(componentCovData, componentObjDir, covFileRelPath,...
lCodeGenFolder);
end

[componentHtmlFolders, allSrcFiles] = this.getHtmlFolders;

annotations = coder.coverage.BullseyeAnnotations...
(lCodeGenFolder, componentObjDirsRelative);

this.writeCoverageXml...
(componentHtmlFolders, allSrcFiles, annotations);

end

function checkForUnsupportedCompiler(~, lToolchainInfo)

if isempty(lToolchainInfo)
return;
end

toolchainAlias = lToolchainInfo.Alias;
toolchain = lToolchainInfo.Name;

mingwString = 'mingw';
% Check for unsupported compilers
if strncmpi(toolchain, mingwString, length(mingwString)) ||...
strncmpi(toolchain, 'intel', 5)

unsupportedCompiler = toolchain;
elseif strcmp(toolchainAlias, 'LCC-x')
unsupportedCompiler='LCC';
else
unsupportedCompiler='';
end

if ~isempty(unsupportedCompiler)
DAStudio.error('CoderCoverage:AllTools:unsupportedCompiler',...
unsupportedCompiler)
end
end

function setCoverageBuildOn(this, mode, covFile)
if nargin < 2
mode = 'on';
end

% Switch on coverage for this Build Zone
covCmd=fullfile(this.getToolAltPath,'bin','cov01');
switch mode
case 'on'
covCmd = sprintf('%s %s', covCmd, '-01');
case 'off'
covCmd = sprintf('%s %s', covCmd, '-00');
otherwise
assert(strcmp(mode,'pilApp'), ...
'The code coverage mode variable must be one of the allowed values.');
covCmd = sprintf('%s %s', covCmd, '-01');
% Command to switch off coverage for PIL harness files
covSelectCmd=sprintf('%s --file "%s" %s',...
fullfile(this.getToolAltPath,'bin','covselect'), ...
covFile,...
'--add _coverage_for_this_nonexistent_file_only.c');
end
% We use COVBUILDZONE to enable/disable coverage independently of
% any other processes (e.g. other sessions of MATLAB) that may be
% building an application
setenv('COVBUILDZONE',coder.coverage.Bullseye.BuildZone);
c = onCleanup(@() setenv('COVBUILDZONE',''));

this.systemCommand(covCmd);

if strcmp(mode,'pilApp')
this.systemCommand(covSelectCmd);
end
end

function setCoverageBuildOff(this)
setCoverageBuildOn(this, 'off');
end

function mergeFiles(this,resultCovFile,covFiles,create)
if create
createArg = '--create';
else
createArg = '';
end
if ~iscell(covFiles)
covFiles={covFiles};
end

baseCmd = fullfile(this.getToolAltPath,'bin','covmerge');
baseCmd = sprintf('%s --no-banner %s --file "%s"', baseCmd, ...
createArg, resultCovFile);

try
this.systemCommandWithSeparableArgs(baseCmd, covFiles);
catch e
if strcmp(e.identifier, 'CoderCoverage:AllTools:SystemCommand')
msgId = 'CoderCoverage:Bullseye:BullseyeCurrentFolderIsNetworked';
msg = DAStudio.message(msgId);
eCause = MException(msgId, msg);
eNew = addCause(e, eCause);
throw(eNew)
end
end
end

function removeSrcsFromCovFile(this, covFile, srcFiles)

% Make sure all paths are absolute
isAbsPath = coder.coverage.CodeCoverageHook.isAbsPath(srcFiles);
for i=1:length(srcFiles)
if ~isAbsPath(i)
srcFiles{i} = fullfile(this.CovSrcDir,...
srcFiles{i});
end
end

baseCmd = sprintf('%s --no-banner --file "%s" --srcdir "%s" ', ...
fullfile(this.getToolAltPath,'bin','covmgr'),...
covFile, ...
this.CovSrcDir);

perFileArgs = cell(size(srcFiles));
for i=1:length(srcFiles)
perFileArgs{i} = sprintf('--remove "%s"', srcFiles{i});
end

this.systemCommandWithSeparableArgs(baseCmd, perFileArgs);

end

function [sharedUtilCovFile, sharedUtilSrcs, nonSharedUtilSrcs, ...
sharedUtilCovFolder] = ...
getSrcsFromCovFile...
(this, covFile, relativePathToAnchor)

bDirInfo = RTW.getBuildDir(this.getModelName);
sharedUtilFolder = bDirInfo.SharedUtilsTgtDir;

% Identify any sharedutil source files in the .cov file
allSrcs = this.listSrcFiles(covFile);

if this.CodeGenFolderIsUNC
% Paths in the .cov file may be using a temporarily mapped drive letter and we
% must transform this back to the original UNC path
allSrcs = this.transformMappedPathToUNC(allSrcs);
end

sharedUtilFolderBullseyeFormat=strrep(sharedUtilFolder,filesep,'/');
sharedUtilIdx = strncmp(sharedUtilFolderBullseyeFormat,allSrcs,...
length(sharedUtilFolderBullseyeFormat));
sharedUtilSrcs = unique(allSrcs(sharedUtilIdx));
nonSharedUtilSrcs = unique(allSrcs(~sharedUtilIdx));

% sharedUtilCovFolder is the shared utils folder which
% contains the .zcov file for the shared utils src.
sharedUtilCovFolder = ...
fullfile(sharedUtilFolder, this.InstrumentedObjectCodeFolder);

sharedUtilCovFile = fullfile(sharedUtilCovFolder,...
'sharedutils.zcov');
sharedUtilCovFile = ...
fullfile(relativePathToAnchor,sharedUtilCovFile);

end

function updateSrcFileMaps...
(this, sharedUtilCovFile, sharedUtilSrcs, nonSharedUtilSrcs, ...
sharedUtilFolder, covFile, relativePathToAnchor, ...
srcFoldersMap, srcTimestampsMap)

if exist(sharedUtilCovFile, 'file')
% Identify all source files in the shared utils .cov file
existingSharedSrcs = this.listSrcFiles(sharedUtilCovFile);
else
existingSharedSrcs = {};
end

% Populate the hash table that maps source files to .cov file folder
timestamps=coder.coverage.Bullseye.getCovSrcTimestamps...
(relativePathToAnchor, sharedUtilSrcs);
allSharedUtilSrcs = unique([sharedUtilSrcs(:); existingSharedSrcs(:)]);

sharedUtilFolderKey = stripStartDir(this, sharedUtilFolder);
srcFoldersMap(sharedUtilFolderKey) = ...
stripStartDir(this, allSharedUtilSrcs);
for i=1:length(sharedUtilSrcs)
srcPathKey = stripStartDir(this, sharedUtilSrcs{i});
srcTimestampsMap(srcPathKey)=timestamps{i};
end
timestamps=coder.coverage.Bullseye.getCovSrcTimestamps...
(relativePathToAnchor, existingSharedSrcs);
for i=1:length(existingSharedSrcs)
srcPathKey = stripStartDir(this, existingSharedSrcs{i});
srcTimestampsMap(srcPathKey)=timestamps{i};
end

% Get .cov file folder relative to pwd
covFileFolderRelPwd=fileparts(covFile);

% Get canonical path to .cov file
covFilePath = fullfile(pwd, covFileFolderRelPwd);
covFilePathCanon = builtin('_canonicalizepath', covFilePath);

% Get canonical path to anchor folder
anchorFolder = fullfile(pwd, relativePathToAnchor);
anchorFolderCanon = builtin('_canonicalizepath',anchorFolder);

% Now get .cov file path relative to the anchor
lenAnch = length(anchorFolderCanon);
assert(strncmp(anchorFolderCanon, ....
covFilePathCanon, lenAnch), ...
'The current working folder must contain the anchor folder.');
covFileFolder = covFilePathCanon(lenAnch+2:end);

timestamps=coder.coverage.Bullseye.getCovSrcTimestamps...
(relativePathToAnchor, nonSharedUtilSrcs);

covFileFolderKey = stripStartDir(this, covFileFolder);
srcFoldersMap(covFileFolderKey) = stripStartDir(this, nonSharedUtilSrcs); %#ok
for i=1:length(nonSharedUtilSrcs)
srcPathKey = stripStartDir(this, nonSharedUtilSrcs{i});
srcTimestampsMap(srcPathKey)=timestamps{i};
end

if ~isempty(sharedUtilSrcs)

% make a copy of the model .cov file in the shared utils
% folder
[~,dstFile] = fileparts(covFile);
dstFile = [dstFile '.cov'];
dstFile = fullfile(relativePathToAnchor,sharedUtilFolder,...
dstFile);
createDstFile=true;
this.mergeFiles(dstFile,covFile,createDstFile);

% If shared utility sources include any header files outside the shared
% utilities folder, we must filter these out, g704748. Removing
% a source file from the .cov file causes any sources that include
% it to be removed as well.
nonSharedUtilSrcsToRemove = coder.coverage.Bullseye ...
.filterOutHeaderFiles(nonSharedUtilSrcs);

% We must remove non-shared util sources from .cov file
% in the shared utils folder
this.removeSrcsFromCovFile(dstFile, nonSharedUtilSrcsToRemove);

% Remove shared util sources from .cov file
% in the model build folder
this.removeSrcsFromCovFile(covFile, sharedUtilSrcs);

% If the sharedutils.cov already exists we must remove
% any existing coverage info for any new files we will add
if ~isempty(existingSharedSrcs)

srcsToRemove=intersect(existingSharedSrcs,sharedUtilSrcs);
if ~isempty(srcsToRemove)
this.removeSrcsFromCovFile(sharedUtilCovFile, ...
srcsToRemove);
end
end

% Now merge the coverage data into the shared utils .cov file
createCovFile = ~exist(sharedUtilCovFile, 'file');
this.mergeFiles(sharedUtilCovFile, dstFile, createCovFile);

% Clean up temporary file
delete(dstFile);
end
end

function mergeCoverageFiles(this)
% A separate .cov file is created for each model reference
% component; this is necessary to support parallel builds.
% The coverage files for each component must be merged into a
% single file to collect coverage date
[componentNames, componentPaths] = getComponentsWithCoverageEnabled(this);
covFiles = cell(size(componentNames));
for i=1:length(componentNames)
covFiles{i} = fullfile(componentPaths{i}, [componentNames{i} '.cov']);
end

covFileExists = exist(this.TotalCovFilePath,'file');

% We delete the total coverage result file if it exists
if covFileExists
delete(this.TotalCovFilePath);
end

% Load the covinfo.mat file if it exists
covInfoFile = fullfile(pwd,'covinfo.mat');

if isempty(covFiles)
if exist(covInfoFile,'file')
delete(covInfoFile);
end
else
% There is coverage data to collect

% Make sure covInfo.mat is deleted
if exist(covInfoFile,'file')
delete(covInfoFile);
end

% Hash table that maps source files to .cov file folder
srcFoldersMap = containers.Map;

% Hash table that maps source files to timestamps
srcTimestampsMap = containers.Map;

appBuildInfo = this.getApplicationBuildInfo;
[~,relativePathToAnchor]=appBuildInfo.findBuildArg...
('RELATIVE_PATH_TO_ANCHOR');

for i=1:length(covFiles)
% Data for any shared utility sources is placed
% in a separate .cov file in the sharedutils
% folder
[sharedUtilCovFile, sharedUtilSrcs, nonSharedUtilSrcs, ...
sharedUtilFolder] = ...
this.getSrcsFromCovFile...
(covFiles{i}, ...
relativePathToAnchor);

this.updateSrcFileMaps...
(sharedUtilCovFile, sharedUtilSrcs, nonSharedUtilSrcs, ...
sharedUtilFolder, covFiles{i}, ...
relativePathToAnchor, ...
srcFoldersMap, srcTimestampsMap);
end

% Copy the first .cov file
[filePath,fileName,fileExt]=fileparts(this.TotalCovFilePath);
filePath=coder.make.internal.transformPaths(filePath,'pathType','full');
totalCovFileFullPath=fullfile(filePath,[fileName fileExt]);
copyfile(covFiles{1}, totalCovFileFullPath);

covFiles=covFiles(2:end);

% Merge the remaining .cov files
for i=1:length(covFiles)
covFile = covFiles{i};
if ~isempty(covFile)
this.mergeFiles(this.TotalCovFilePath, ...
covFile, false);
end
end

% Merge the shared utils .cov file
if exist(sharedUtilCovFile,'file')
% If the shared utils .cov file exists we merge it
% irrespective of whether the SIL/PIL simulation
% needs to link against shared sources; a potential
% enhancement is to merge this .cov file only if
% shared utility functions are truly needed
this.mergeFiles(this.TotalCovFilePath, ...
sharedUtilCovFile, false);
end

% Save the mapping of source files to component/sharedutils dirs
srcFiles=this.listSrcFiles(this.TotalCovFilePath);
save(covInfoFile,'covFiles','srcFoldersMap',...
'srcTimestampsMap','srcFiles')
end
end
end

methods (Static)
function name = getName()
name = 'Bullseye';
end
function name = getDisplayName()
name = message('CoderCoverage:Bullseye:Bullseye').getString;
end
end
end

% Specify a file that can be used to validate the Bullseye installation
% including path relative to the installation root folder
function markerFile = i_getMarkerFile
if ispc
markerFile='cov01.exe';
else
markerFile='cov01';
end
markerFile = fullfile('bin',markerFile);
end

% Traverse the struct representing the XML file content and add data for each
% source file to the container
function c = i_extract_src_file_covdata(xmltree, c, cumulativeFolder)

if isfield(xmltree, 'src')
for ii2=1:length(xmltree.src)

src = xmltree.src(ii2);

if isa(src, 'missing')
% No source file to process
continue
end

key = char(join([cumulativeFolder, string(src.nameAttribute)], '/'));

% Add details for this source file to the container
c(key) = src;
end
end

if isfield(xmltree, 'folder')
for i=1:length(xmltree.folder)

folder = xmltree.folder(i);

if isa(folder, 'missing')
% No folder to process
continue
end

fullFolderName = join([cumulativeFolder, folder.nameAttribute], '/');

% Process sub-folder
i_extract_src_file_covdata(folder, c, fullFolderName);
end
end
end

% Update for named probe attribute to and ensure missing data is represented
% as an empty string.
function probe = i_replaceMissingDataWithEmptyString(probe, attrName)

if ~isfield(probe, attrName)
for ll3 = 1:length(probe)
probe(ll3).(attrName) = "";
end
end
for ll3 = 1:length(probe)
if isa(probe(ll3).(attrName), 'missing')
probe(ll3).(attrName) = "";
end
end
end

% Parse XML file to extract coverage data for source files
function srcs = i_parse_xml(tmpxmlExplicit)

% Don't allow readstruct to auto-detect types because folder names might be
% converted to double (e.g. if folder is named like =33 or 2e5). With this
% option, all values have string type.
detectTypes = false;

covxml = readstruct(tmpxmlExplicit, "DetectTypes", detectTypes);

% Use container.Map as intermediate store to collect per source file coverage data
% as we traverse the struct representing the XML data
c = containers.Map;

% The XML data is a tree structure corresponding to the folder hierarchy
% where source files are located. Call the recursive function
% i_extract_src_file_covdata to extract the data.
cumulativeFolder = string.empty;
c = i_extract_src_file_covdata(covxml, c, cumulativeFolder);

% Collect the coverage data into a struct array
srcPaths = c.keys;
srcValues = c.values;
[srcPaths,i]=sort(srcPaths);
srcValues = srcValues(i);
probes = cell(size(srcValues));
fn_cov = cell(size(srcValues));
fn_total = cell(size(srcValues));
cd_cov = cell(size(srcValues));
cd_total = cell(size(srcValues));

for i=1:length(srcValues)
fn = srcValues{i}.fn;
probeData = [];
for kk1 = 1:length(fn)
probe = fn(kk1).probe;

% Update column attribute to ensure missing data is represented by empty string
probe = i_replaceMissingDataWithEmptyString(probe, 'columnAttribute');

% Update col_seq attribute to ensure missing data is represented by empty string
probe = i_replaceMissingDataWithEmptyString(probe, 'col_seqAttribute');

probeData = [probeData probe]; %#ok<AGROW>
end

probes{i} = struct...
('line', {probeData(:).lineAttribute}, ...
'kind', {probeData(:).kindAttribute}, ...
'column', {probeData(:).columnAttribute}, ...
'event', {probeData(:).eventAttribute}, ...
'col_seq', {probeData(:).col_seqAttribute});

% probes must be column vector
probes{i} = probes{i}(:);

fn_cov{i} = str2double(srcValues{i}.fn_covAttribute);
fn_total{i} = str2double(srcValues{i}.fn_totalAttribute);
cd_cov{i} = str2double(srcValues{i}.cd_covAttribute);
cd_total{i} = str2double(srcValues{i}.cd_totalAttribute);
end

srcs = struct...
('srcPath', srcPaths, ...
'probes', probes, ...
'fn_cov', fn_cov, ...
'fn_total', fn_total, ...
'cd_cov', cd_cov, ...
'cd_total', cd_total);

% Ensure srcs are sorted by srcPath
srcPaths = cell(size(srcs));
[srcPaths{:}] = deal(srcs.srcPath);
[~,i] = sort(srcPaths);
srcs = srcs(i);
end