classdef TestbedAnalysis < handle
%Class to extract results of Testbed analysis
%
% See also
%
% This is an undocumented class. Its methods and properties are likely to
% change without warning from one release to the next.

% Copyright 2011-2015 The MathWorks, Inc.

properties (Access=private)
TestbedFolder;
SrcFiles;
DynFiles;
end

methods (Access=public, Static = true)

function srcFileName = parseDynForSrcFile(dynFile)

dynFileContent = fileread(dynFile);
expr = '.*?File\s*under\s*test\s*:\s*(.*?)[\r|\n]+\s*Date\s*of\s*Analysis\s*:.*';
srcFileName = regexprep(dynFileContent, expr, '$1', 'once');

% The file name may be split across multiple lines and the
% white space part of the file name may need to be
% reconstructed
srcFileName = regexprep(srcFileName, '[\r|\n]\s*', '*', 'lineanchors');
[srcFileName, rhPart] = strtok(srcFileName, '*');
while ~isempty(rhPart)
dirExpr = [srcFileName '*'];
dirResults = dir(dirExpr);
[dirNames{1:length(dirResults)}] = deal(dirResults.name);
[~, lhPartToMatch1, lhPartToMatchExt] = fileparts(srcFileName);
lhPartToMatch = [lhPartToMatch1 lhPartToMatchExt];
[rhPartToMatch, remPart] = strtok(rhPart, filesep);
rhPartToMatch = rhPartToMatch(2:end); % strip leading '*'
matches = regexprep(dirNames, [lhPartToMatch '\s*' rhPartToMatch], '');
matchIdx = strcmp(matches, '');
assert(sum(matchIdx)==1, ...
'The source file folder, %s, must exist', ...
[srcFileName rhPart]);
srcFileName = fullfile(fileparts(srcFileName), ...
dirNames{matchIdx}, remPart);
[srcFileName, rhPart] = strtok(srcFileName, '*');
end

end

function [setNames, setIdx, tbsetContent] = getSetNames(workareaBaseDir)

tbsetFile = fullfile(workareaBaseDir,'tbset.dat');

if isempty(dir(tbsetFile))
setNames = {};
return;
end

% Now identify the file by searching the tbset
tbsetContent = fileread(tbsetFile);

% Regular expression to parse out set names from tbset.dat
expr = '(?<=^1\s+[0-9]+\s+).*?(?=(\s|$))';
[setNames, setIdx] = regexp(tbsetContent, expr, 'match', 'lineanchors');

end

function fileNames = getFilesFromSet(workareaBaseDir, lSetName)

[setNames, setIdx, tbsetContent] = coder.coverage.TestbedAnalysis...
.getSetNames(workareaBaseDir);

matchIdx = find(strcmp(lSetName, setNames));
assert(length(matchIdx)==1, 'Set name must be found');

setIdx(end+1) = length(tbsetContent)+1;

% Extract content of tbset.dat for current set only
prunedContent = tbsetContent(setIdx(matchIdx):setIdx(matchIdx+1)-1);

% Now extract the actual file names
behindExpr = '(?<=^2\s+[0-9]+\s+[0-9]+\s+)';
expr = [behindExpr '[^\n]+'];

fileNames = ...
strtrim(regexp(prunedContent, expr, 'match', ...
'lineanchors'));
end

function lineNoMapOrig = readLineNoMap(ndxFile, srcContent)

ndxFileContent = fileread(ndxFile);
includesExpr = '^\s*([0-9]*)\s*NUMBER OF ENTRIES(.*)^\s*[0-9]*\s*NUMBER OF LINES';
includesTok = regexp(ndxFileContent, includesExpr, 'lineanchors', 'tokens',...
'once');
if ~isempty(includesTok)
numIncludes = str2double(includesTok{1});
includesSection = includesTok{2};
% Need to identify location of include files in reformatted source
includeLineExpr = '^\s*([0-9]*)\s+([0-9]*)\s+([0-9]*)\s*(((\S)|[ ])+)';
includeLineToks = regexp(includesSection, includeLineExpr, ...
'lineanchors', 'tokens');
assert(length(includeLineToks)==numIncludes,...
'Check expected number of include files');
incLineNos = zeros(numIncludes, 2);
incNamesFromNdx = cell(numIncludes, 1);
for ii2=1:numIncludes
incLineNos(ii2,1) = str2double(includeLineToks{ii2}{1});
incLineNos(ii2,2) = str2double(includeLineToks{ii2}{2});
tmpIncName = strtrim(includeLineToks{ii2}{4});
[~, incNamesFromNdx{ii2}] = fileparts(tmpIncName);
end
assert(all(diff(incLineNos(:,1))>=0),...
'Include line numbers must be sorted');
includeTree = zeros(numIncludes,1);
for ii2=2:numIncludes
parent = 0; % default is no nesting
ancestors = ...
incLineNos( (1:ii2-1) ,2) >= incLineNos(ii2,1);
if any(ancestors)
parent=find(ancestors, 1, 'last' );
end
includeTree(ii2)=parent;
end
lIncludeFiles = struct...
('Names', {incNamesFromNdx},...
'LineNos', incLineNos,...
'Parent', includeTree);

end

% We need to parse the original source file to identify include
% lines and in particular find out if they span multiple lines
solIdx = regexp(srcContent, '^.', 'lineanchors');
inclExpr = '^\s*#include(([^\n.]*?(\\(\r)?\n))*(.*?$))';
[toks, tokenExtents] = regexp(srcContent, inclExpr, 'tokens', ...
'tokenextents', 'lineanchors');
includeStartLines = zeros(size(tokenExtents));
includeEndLines = zeros(size(tokenExtents));

inclNames = cell(size(toks));
for i=1:length(includeEndLines)
tokenExtent=tokenExtents{i};
incStartIdx = tokenExtent(1);
incEndIdx = tokenExtent(2);
includeStartLines(i) = find(incStartIdx>solIdx, 1, 'last' );
includeEndLines(i) = find(incEndIdx>solIdx, 1, 'last' );

tok=toks{i};
namePart = tok{1};
if length(tok)==2
namePart = [namePart tok{2}]; %#ok<AGROW>
end
namePart = regexprep(namePart, '\\(\r)?\n','');
incName = regexp(namePart, '("|<)(.*?)("|>)', 'tokens','once');
incName = incName{2};
[~, inclNames{i}] = fileparts(incName);
end

% Now filter out any include files that don't also appear in
% the .ndx
topIncNamesFromNdx = incNamesFromNdx(includeTree==0);
keepIdx = false(size(inclNames));
matchedInclNamesFromNdx = false(size(topIncNamesFromNdx));
for i=1:length(keepIdx)
matchedNameIdx = strcmp(topIncNamesFromNdx, inclNames{i});
if any(matchedNameIdx)
% Skip any file that is already included
if ~matchedInclNamesFromNdx(matchedNameIdx)
matchedInclNamesFromNdx(matchedNameIdx) = true;
keepIdx(i) = true;
end
end
end

assert(all(matchedInclNamesFromNdx), ...
'Names in .ndx file must tally with names in original source');

includeStartLines = includeStartLines(keepIdx);
includeEndLines = includeEndLines(keepIdx);

noOfLinesExpr = '^\s*([0-9]*)\s*NUMBER OF LINES\s*([0-9\s]*)';
noOfLinesToks = regexp(ndxFileContent, noOfLinesExpr, ...
'lineanchors', 'tokens');
assert(length(noOfLinesToks{1})==2, ...
'.ndx file must have section like "N NUMBER OF LINES i_0 ... i_N"');
numLineValues = str2double(noOfLinesToks{1}{1});
lineNoMap = sscanf...
(noOfLinesToks{1}{2}, '%d', numLineValues);

% Calculate line mapping from reformatted source back to
% original source
includeOffset = zeros(max(lineNoMap), 1);
topIncludes = includeTree==0;
lastIncEnd = 1;
cumulativeOffset = 0;
topIncludeLineNos = lIncludeFiles.LineNos(topIncludes, :);
assert(length(topIncludeLineNos(:,1))==length(includeStartLines),...
'Number of include lines must be consistent');
for i=1:length(topIncludeLineNos(:,1))
incStart = topIncludeLineNos(i,1);
assert((incStart-cumulativeOffset)==includeStartLines(i),...
'Position of include lines must be consistent');
includeNumLinesOccupied = includeEndLines(i)-includeStartLines(i)+1;
incEnd = topIncludeLineNos(i,2);
includeOffset(lastIncEnd+1:incStart) ...
= cumulativeOffset;
extraOffset = 1:(incEnd-incStart);
includeOffset(incStart+1:incEnd) = extraOffset(:)+cumulativeOffset;
cumulativeOffset = cumulativeOffset+extraOffset(end) - ...
(includeNumLinesOccupied-1);
lastIncEnd = incEnd;
end
includeOffset(lastIncEnd:end)=cumulativeOffset;

lineNoMapOrig = zeros(size(lineNoMap));

for i=1:length(lineNoMap)
if lineNoMap(i)>0
lineNoMapOrig(i) = lineNoMap(i) - includeOffset(lineNoMap(i));
end
end
end

end

methods (Access=public)

function srcFileIdx = getSrcFileIdx(this, srcFile)

if ispc
cmpFcn = @strcmpi;
else
cmpFcn = @strcmp;
end

% Use fullfile to ensure canonical form of path separator (e.g. for arduino
% target the template makefile specifies use of \ instead of /).
srcFileIdx = cmpFcn(fullfile(srcFile), fullfile(this.SrcFiles));

end

function inSet = isSrcFileInSet(this, srcFile)

match = this.getSrcFileIdx(srcFile);

inSet = any(match);

end

function dynFile = getDynFile(this, lSourceFile)

match = this.getSrcFileIdx(lSourceFile);

if ~any(match)
dynFile='';
else
assert(sum(match)==1, 'Must be exactly one matching file name');
dynFile=this.DynFiles{match};
end
end

function this = TestbedAnalysis(lTestbedFolder, setName)
this.TestbedFolder = lTestbedFolder;

dynFiles = dir(fullfile(lTestbedFolder,'*.dyn'));

[dynFileNames{1:length(dynFiles)}] = deal(dynFiles.name);

fileNamesFromSet = coder.coverage.TestbedAnalysis.getFilesFromSet...
(fullfile(lTestbedFolder, '..'), setName);

% Associate each source with a .dyn file if there is one
this.SrcFiles = fileNamesFromSet;

this.DynFiles = cell(size(this.SrcFiles));
for i=1:length(dynFileNames)
dynFile=fullfile(lTestbedFolder, dynFileNames{i});
srcFileName = coder.coverage.TestbedAnalysis...
.parseDynForSrcFile(dynFile);

% Look for a match against one of the source files in
% the set
matchIdx = strcmp(fileNamesFromSet, srcFileName);
assert(sum(matchIdx)==1, ...
'There must be exactly one match for each source file');

this.DynFiles{matchIdx} = dynFile;
end
end
end
end