Files
imdd_silas/Tests/test_dashboard.m
2026-03-25 09:38:48 +01:00

940 lines
34 KiB
Matlab

function app = test_dashboard(options)
%TEST_DASHBOARD Minimal GUI for test discovery and execution.
% test_dashboard() opens a small dashboard that:
% - discovers implemented, placeholder, and missing class-backed tests
% - lists standalone tests such as integration or helper tests
% - runs the selected test file
% - runs the existing fast/full test profiles
%
% app = test_dashboard("Visible", false) builds the dashboard without
% showing it. This is useful for non-interactive validation.
arguments
options.Visible (1, 1) logical = true
end
app = struct();
app.Inventory = table();
app.SelectedRow = [];
app.ActiveProfileRowIndices = [];
app.LastRunStatusMap = containers.Map('KeyType', 'char', 'ValueType', 'char');
app.LastRunSummaryMap = containers.Map('KeyType', 'char', 'ValueType', 'char');
app.LastRunDateMap = containers.Map('KeyType', 'char', 'ValueType', 'char');
app.LastRunCommitMap = containers.Map('KeyType', 'char', 'ValueType', 'char');
testsRoot = fileparts(mfilename("fullpath"));
repoRoot = fileparts(testsRoot);
reportsRoot = fullfile(testsRoot, 'reports');
latestRunFile = fullfile(testsRoot, '.last_test_run.json');
createUi();
refreshInventory();
if options.Visible
app.Figure.Visible = "on";
end
function createUi()
app.Figure = uifigure( ...
"Name", "IMDD Test Dashboard", ...
"Position", [80, 80, 1360, 820], ...
"Color", [0.965, 0.973, 0.988], ...
"Visible", "off");
app.Layout = uigridlayout(app.Figure, [3, 1]);
app.Layout.RowHeight = {92, "1x", 150};
app.Layout.Padding = [14, 14, 14, 14];
app.Layout.RowSpacing = 12;
app.HeaderPanel = uipanel(app.Layout, ...
"Title", "Suite Overview", ...
"BackgroundColor", [0.985, 0.989, 0.997]);
app.HeaderPanel.Layout.Row = 1;
headerGrid = uigridlayout(app.HeaderPanel, [2, 10]);
headerGrid.RowHeight = {28, 32};
headerGrid.ColumnWidth = {110, 110, 110, 90, 90, 90, "1x", 120, 120, 120};
headerGrid.Padding = [10, 8, 10, 8];
app.ImplementedLabel = makeMetricLabel(headerGrid, "Implemented", 1);
app.PlaceholderLabel = makeMetricLabel(headerGrid, "Placeholder", 2);
app.MissingLabel = makeMetricLabel(headerGrid, "Missing", 3);
app.PassedLabel = makeMetricLabel(headerGrid, "Passed", 4);
app.FailedLabel = makeMetricLabel(headerGrid, "Failed", 5);
app.IncompleteLabel = makeMetricLabel(headerGrid, "Incomplete", 6);
app.TotalLabel = makeMetricLabel(headerGrid, "Total", 7);
app.FilterDropDown = uidropdown(headerGrid, ...
"Items", {'All', 'Implemented', 'Placeholder', 'Missing', 'Standalone'}, ...
"Value", 'All', ...
"ValueChangedFcn", @filterChanged);
app.FilterDropDown.Layout.Row = 2;
app.FilterDropDown.Layout.Column = 8;
app.RefreshButton = uibutton(headerGrid, ...
"Text", "Refresh", ...
"ButtonPushedFcn", @refreshInventoryButtonPushed);
app.RefreshButton.Layout.Row = 2;
app.RefreshButton.Layout.Column = 9;
app.OpenButton = uibutton(headerGrid, ...
"Text", "Open Selected", ...
"ButtonPushedFcn", @openSelectedTest);
app.OpenButton.Layout.Row = 2;
app.OpenButton.Layout.Column = 10;
app.TablePanel = uipanel(app.Layout, ...
"Title", "Discovered Tests", ...
"BackgroundColor", [0.985, 0.989, 0.997]);
app.TablePanel.Layout.Row = 2;
tableGrid = uigridlayout(app.TablePanel, [2, 1]);
tableGrid.RowHeight = {36, "1x"};
tableGrid.Padding = [10, 8, 10, 10];
buttonGrid = uigridlayout(tableGrid, [1, 6]);
buttonGrid.ColumnWidth = {150, 120, 120, 140, 170, "1x"};
buttonGrid.Padding = [0, 0, 0, 0];
buttonGrid.ColumnSpacing = 10;
app.RunSelectedButton = uibutton(buttonGrid, ...
"Text", "Run Selected", ...
"ButtonPushedFcn", @runSelectedTest);
app.RunFastButton = uibutton(buttonGrid, ...
"Text", "Run Fast", ...
"ButtonPushedFcn", @(~, ~) runProfile("fast"));
app.RunFullButton = uibutton(buttonGrid, ...
"Text", "Run Full", ...
"ButtonPushedFcn", @(~, ~) runProfile("full"));
app.RunIntegrationButton = uibutton(buttonGrid, ...
"Text", "Run Integration", ...
"ButtonPushedFcn", @(~, ~) runProfile("integration"));
app.GenerateButton = uibutton(buttonGrid, ...
"Text", "Generate Missing Tests", ...
"ButtonPushedFcn", @generateMissingTests);
app.TableHintLabel = uilabel(buttonGrid, ...
"Text", "Select a row to run or open the corresponding test file.", ...
"HorizontalAlignment", "right", ...
"FontColor", [0.28, 0.32, 0.38]);
app.TableHintLabel.Layout.Column = 6;
app.Table = uitable(tableGrid, ...
"ColumnName", {"Status", "Last Run", "Result", "Date", "Commit", "Scope", "Target", "Test File", "Tags"}, ...
"ColumnEditable", false(1, 9), ...
"ColumnSortable", true(1, 9), ...
"ColumnWidth", {95, 95, 90, 150, 90, 100, 240, 300, "auto"}, ...
"SelectionType", "row", ...
"Multiselect", "off", ...
"CellSelectionCallback", @tableSelectionChanged, ...
"FontName", "Consolas");
app.Table.Layout.Row = 2;
app.LogPanel = uipanel(app.Layout, ...
"Title", "Run Log", ...
"BackgroundColor", [0.985, 0.989, 0.997]);
app.LogPanel.Layout.Row = 3;
logGrid = uigridlayout(app.LogPanel, [1, 1]);
logGrid.Padding = [10, 8, 10, 10];
app.LogArea = uitextarea(logGrid, ...
"Editable", "off", ...
"FontName", "Consolas", ...
"Value", {'IMDD Test Dashboard ready.'});
end
function label = makeMetricLabel(parent, titleText, columnIdx)
label = uilabel(parent, ...
"Text", sprintf("%s: --", titleText), ...
"FontWeight", "bold", ...
"FontSize", 13, ...
"FontColor", [0.12, 0.18, 0.26]);
label.Layout.Row = 1;
label.Layout.Column = columnIdx;
end
function refreshInventoryButtonPushed(~, ~)
refreshInventory();
end
function refreshInventory()
app.Inventory = buildInventory(repoRoot, testsRoot);
app.SelectedRow = [];
loadPersistedRunState();
updateSummary(app.Inventory);
applyFilter();
appendLog(sprintf("[%s] Refreshed test inventory.", stamp()));
end
function filterChanged(~, ~)
applyFilter();
end
function applyFilter()
filtered = app.Inventory;
switch string(app.FilterDropDown.Value)
case "Implemented"
filtered = filtered(filtered.Status == "Implemented", :);
case "Placeholder"
filtered = filtered(filtered.Status == "Placeholder", :);
case "Missing"
filtered = filtered(filtered.Status == "Missing", :);
case "Standalone"
filtered = filtered(filtered.Scope == "Standalone", :);
otherwise
% keep all rows
end
app.Table.Data = filtered(:, ["Status", "LastRun", "ResultSummary", "LastRunDate", "LastRunCommit", "Scope", "Target", "TestFile", "Tags"]);
applyRowStyles(filtered);
end
function updateSummary(inventory)
implementedCount = nnz(inventory.Status == "Implemented");
placeholderCount = nnz(inventory.Status == "Placeholder");
missingCount = nnz(inventory.Status == "Missing");
passedCount = nnz(inventory.LastRun == "Passed");
failedCount = nnz(inventory.LastRun == "Failed");
incompleteCount = nnz(inventory.LastRun == "Incomplete");
totalCount = height(inventory);
app.ImplementedLabel.Text = sprintf("Implemented: %d", implementedCount);
app.PlaceholderLabel.Text = sprintf("Placeholder: %d", placeholderCount);
app.MissingLabel.Text = sprintf("Missing: %d", missingCount);
app.PassedLabel.Text = sprintf("Passed: %d", passedCount);
app.FailedLabel.Text = sprintf("Failed: %d", failedCount);
app.IncompleteLabel.Text = sprintf("Incomplete: %d", incompleteCount);
app.TotalLabel.Text = sprintf("Total: %d", totalCount);
app.PassedLabel.FontColor = metricColor(passedCount > 0, [0.18, 0.56, 0.24]);
app.FailedLabel.FontColor = metricColor(failedCount > 0, [0.78, 0.22, 0.22]);
app.IncompleteLabel.FontColor = metricColor(incompleteCount > 0, [0.72, 0.48, 0.08]);
end
function applyRowStyles(filtered)
removeStyle(app.Table);
greenStyle = uistyle("BackgroundColor", [0.90, 0.97, 0.92]);
amberStyle = uistyle("BackgroundColor", [0.995, 0.95, 0.85]);
redStyle = uistyle("BackgroundColor", [0.985, 0.90, 0.90]);
blueStyle = uistyle("BackgroundColor", [0.90, 0.94, 0.995]);
runningStyle = uistyle("BackgroundColor", [0.89, 0.95, 1.00]);
for rowIdx = 1:height(filtered)
if filtered.LastRun(rowIdx) == "Passed"
addStyle(app.Table, greenStyle, "row", rowIdx);
elseif filtered.LastRun(rowIdx) == "Failed"
addStyle(app.Table, redStyle, "row", rowIdx);
elseif filtered.LastRun(rowIdx) == "Incomplete"
addStyle(app.Table, amberStyle, "row", rowIdx);
elseif filtered.LastRun(rowIdx) == "Running"
addStyle(app.Table, runningStyle, "row", rowIdx);
elseif filtered.Scope(rowIdx) == "Standalone"
addStyle(app.Table, blueStyle, "row", rowIdx);
elseif filtered.Status(rowIdx) == "Implemented"
addStyle(app.Table, greenStyle, "row", rowIdx);
elseif filtered.Status(rowIdx) == "Placeholder"
addStyle(app.Table, amberStyle, "row", rowIdx);
else
addStyle(app.Table, redStyle, "row", rowIdx);
end
end
end
function tableSelectionChanged(src, event)
if isempty(event.Indices)
app.SelectedRow = [];
return
end
selectedRow = event.Indices(1);
visibleRows = src.Data;
selectedTestFile = string(visibleRows.TestFile(selectedRow));
matchIdx = find(app.Inventory.TestFile == selectedTestFile, 1);
if isempty(matchIdx)
selectedTarget = string(visibleRows.Target(selectedRow));
matchIdx = find(app.Inventory.Target == selectedTarget, 1);
end
app.SelectedRow = matchIdx;
end
function openSelectedTest(~, ~)
record = selectedRecord();
if isempty(record)
uialert(app.Figure, "Select a test row first.", "No Selection");
return
end
if strlength(record.TestFile) == 0 || ~isfile(record.TestFile)
uialert(app.Figure, "The selected class does not have a test file yet.", "No Test File");
return
end
edit(record.TestFile);
appendLog(sprintf("[%s] Opened %s", stamp(), record.TestFile));
end
function runSelectedTest(~, ~)
record = selectedRecord();
if isempty(record)
uialert(app.Figure, "Select a test row first.", "No Selection");
return
end
if strlength(record.TestFile) == 0 || ~isfile(record.TestFile)
uialert(app.Figure, "The selected class does not have a runnable test file yet.", "Missing Test");
return
end
titleText = sprintf("Running %s", record.TestFileName);
markRowAsRunning(record);
runWithProgress(titleText, @() runSingleTestFile(record.TestFile), record);
end
function runProfile(profile)
titleText = sprintf("Running profile '%s'", profile);
rowIndices = profileRowIndices(profile);
app.ActiveProfileRowIndices = rowIndices;
markRowsAsRunning(rowIndices);
runWithProgress(titleText, @() runProfileInternal(profile, rowIndices), table());
app.ActiveProfileRowIndices = [];
end
function generateMissingTests(~, ~)
setupRepositoryPath();
try
summary = generateTests("verbose", false);
refreshInventory();
appendLog(sprintf("[%s] Generated missing tests: %d new, %d existing skipped, %d classes tracked.", ...
stamp(), summary.generated, summary.skippedExisting, summary.classCount));
catch err
appendLog(sprintf("[%s] generateTests failed.", stamp()));
appendLog(getReport(err, "extended", "hyperlinks", "off"));
uialert(app.Figure, err.message, "Generate Tests Failed");
end
end
function runWithProgress(titleText, runner, record)
try
[output, results] = runner();
appendLog(sprintf("[%s] %s", stamp(), titleText));
appendLog(output);
updateRunStateFromResults(results, record);
writeRunArtifacts(titleText, results, record);
catch err
appendLog(sprintf("[%s] %s failed.", stamp(), titleText));
appendLog(getReport(err, "extended", "hyperlinks", "off"));
updateFailureState(record);
uialert(app.Figure, err.message, "Run Failed");
end
end
function [output, results] = runProfileInternal(profile, rowIndices)
setupRepositoryPath();
profile = string(profile);
results = matlab.unittest.TestResult.empty(0, 1);
logLines = strings(0, 1);
logLines(end + 1, 1) = sprintf("Running profile '%s' with %d test files.", profile, numel(rowIndices));
for idx = 1:numel(rowIndices)
record = app.Inventory(rowIndices(idx), :);
if strlength(record.TestFile) == 0 || ~isfile(record.TestFile)
continue
end
logLines(end + 1, 1) = sprintf("Running %s", record.TestFileName);
drawnow;
currentResults = runtests(char(record.TestFile));
currentResults = currentResults(:);
if isempty(results)
results = currentResults;
else
results = [results; currentResults]; %#ok<AGROW>
end
updateRunStateFromResults(currentResults, record);
[statusText, summaryText] = summarizeResults(currentResults);
logLines(end + 1, 1) = sprintf("Finished %s: %s (%s)", ...
record.TestFileName, statusText, summaryText);
drawnow;
end
logLines(end + 1, 1) = sprintf("Passed: %d | Failed: %d | Incomplete: %d", ...
nnz([results.Passed]), nnz([results.Failed]), nnz([results.Incomplete]));
output = strjoin(logLines, newline);
end
function [output, results] = runSingleTestFile(testFile)
setupRepositoryPath();
results = runtests(char(testFile));
output = evalc('disp(results);');
end
function record = selectedRecord()
record = table();
if isempty(app.SelectedRow)
return
end
if app.SelectedRow < 1 || app.SelectedRow > height(app.Inventory)
return
end
record = app.Inventory(app.SelectedRow, :);
end
function setupRepositoryPath()
addpath(repoRoot);
addpath(testsRoot);
folders = ["Classes", "Functions", "Datatypes", "Libs"];
for folder = folders
fullFolder = fullfile(repoRoot, folder);
if isfolder(fullFolder)
addpath(genpath(fullFolder));
end
end
end
function appendLog(textIn)
newLines = string(splitlines(string(textIn)));
current = string(app.LogArea.Value);
current = current(current ~= "");
newLines = newLines(newLines ~= "");
combined = [current; newLines];
if isempty(combined)
combined = " ";
end
app.LogArea.Value = cellstr(combined);
drawnow;
end
function markRowAsRunning(record)
if isempty(record) || strlength(record.TestFile) == 0
return
end
updateMappedState(char(record.TestFile), 'Running', '...');
applyStoredRunState();
pause(0.05);
end
function markRowsAsRunning(rowIndices)
for idx = 1:numel(rowIndices)
key = char(app.Inventory.TestFile(rowIndices(idx)));
if ~isempty(key)
updateMappedState(key, 'Running', '...');
end
end
applyStoredRunState();
pause(0.05);
end
function updateRunStateFromResults(results, record)
if ~isempty(record)
[statusText, summaryText] = summarizeResults(results);
updateMappedState(char(record.TestFile), statusText, summaryText);
else
resultNames = string({results.Name})';
uniqueNames = unique(resultNames);
for idx = 1:numel(uniqueNames)
currentResults = results(resultNames == uniqueNames(idx));
[statusText, summaryText] = summarizeResults(currentResults);
targetRows = find(app.Inventory.TestFileName == uniqueNames(idx));
for rowIdx = targetRows'
updateMappedState(char(app.Inventory.TestFile(rowIdx)), statusText, summaryText);
end
end
end
applyStoredRunState();
end
function updateFailureState(record)
if ~isempty(record) && strlength(record.TestFile) > 0
updateMappedState(char(record.TestFile), 'Failed', 'error');
applyStoredRunState();
end
end
function updateMappedState(key, statusText, summaryText)
if isempty(key)
return
end
app.LastRunStatusMap(key) = char(statusText);
app.LastRunSummaryMap(key) = char(summaryText);
end
function applyStoredRunState()
for idx = 1:height(app.Inventory)
key = char(app.Inventory.TestFile(idx));
if isempty(key)
continue
end
if isKey(app.LastRunStatusMap, key)
app.Inventory.LastRun(idx) = string(app.LastRunStatusMap(key));
app.Inventory.ResultSummary(idx) = string(app.LastRunSummaryMap(key));
if isKey(app.LastRunDateMap, key)
app.Inventory.LastRunDate(idx) = string(app.LastRunDateMap(key));
end
if isKey(app.LastRunCommitMap, key)
app.Inventory.LastRunCommit(idx) = string(app.LastRunCommitMap(key));
end
end
end
updateSummary(app.Inventory);
applyFilter();
end
function loadPersistedRunState()
app.LastRunStatusMap = containers.Map('KeyType', 'char', 'ValueType', 'char');
app.LastRunSummaryMap = containers.Map('KeyType', 'char', 'ValueType', 'char');
app.LastRunDateMap = containers.Map('KeyType', 'char', 'ValueType', 'char');
app.LastRunCommitMap = containers.Map('KeyType', 'char', 'ValueType', 'char');
if ~isfile(latestRunFile)
return
end
try
payload = jsondecode(fileread(latestRunFile));
fallbackDate = stringOrEmpty(payload, 'timestamp');
fallbackCommit = "";
if isfield(payload, 'git')
fallbackCommit = stringOrEmpty(payload.git, 'short_commit');
end
if isfield(payload, 'row_states')
rowStates = payload.row_states;
for idx = 1:numel(rowStates)
key = stringOrEmpty(rowStates(idx), 'test_file');
if strlength(key) == 0
continue
end
app.LastRunStatusMap(char(key)) = char(stringOrEmpty(rowStates(idx), 'last_run'));
app.LastRunSummaryMap(char(key)) = char(stringOrEmpty(rowStates(idx), 'result_summary'));
rowDate = stringOrEmpty(rowStates(idx), 'last_run_date');
rowCommit = stringOrEmpty(rowStates(idx), 'last_run_commit');
if strlength(rowDate) == 0
rowDate = fallbackDate;
end
if strlength(rowCommit) == 0
rowCommit = fallbackCommit;
end
app.LastRunDateMap(char(key)) = char(rowDate);
app.LastRunCommitMap(char(key)) = char(rowCommit);
end
end
applyStoredRunState();
catch
% Ignore malformed local cache files and rebuild from scratch.
end
end
function writeRunArtifacts(runLabel, results, record)
if ~isfolder(reportsRoot)
mkdir(reportsRoot);
end
gitInfo = collectGitMetadata(repoRoot);
runTimestamp = char(datetime("now", "TimeZone", "local", "Format", "yyyy-MM-dd'T'HH:mm:ssXXX"));
updateMetadataMaps(results, record, runTimestamp, gitInfo.short_commit);
report = struct();
report.timestamp = runTimestamp;
rowStates = inventoryRowStates(app.Inventory);
report.run_label = char(runLabel);
report.repo_root = repoRoot;
report.git = gitInfo;
report.totals = struct( ...
'passed', nnz([results.Passed]), ...
'failed', nnz([results.Failed]), ...
'incomplete', nnz([results.Incomplete]));
report.method_results = resultEntries(results);
report.row_states = rowStates;
jsonText = jsonencode(report, PrettyPrint=true);
writeTextFile(latestRunFile, jsonText);
reportFile = fullfile(reportsRoot, sprintf('%s_%s.json', ...
datestr(now, 'yyyy-mm-dd_HHMMSS'), sanitizeFileLabel(runLabel)));
writeTextFile(reportFile, jsonText);
appendLog(sprintf("[%s] Saved run report: %s", stamp(), reportFile));
end
function updateMetadataMaps(results, record, runDate, shortCommit)
if ~isempty(record) && strlength(record.TestFile) > 0
key = char(record.TestFile);
app.LastRunDateMap(key) = char(string(runDate));
app.LastRunCommitMap(key) = char(string(shortCommit));
applyStoredRunState();
return
end
if ~isempty(app.ActiveProfileRowIndices)
for rowIdx = app.ActiveProfileRowIndices(:)'
key = char(app.Inventory.TestFile(rowIdx));
if isempty(key)
continue
end
app.LastRunDateMap(key) = char(string(runDate));
app.LastRunCommitMap(key) = char(string(shortCommit));
end
end
applyStoredRunState();
end
function rowIndices = profileRowIndices(profile)
rowIndices = [];
normalizedTags = "," + erase(app.Inventory.Tags, " ") + ",";
switch string(profile)
case "fast"
rowIndices = find(app.Inventory.Status ~= "Missing" & ...
contains(normalizedTags, ",fast,") & ...
~contains(normalizedTags, ",placeholder,") & ...
~contains(normalizedTags, ",hardware,") & ...
~contains(normalizedTags, ",slow,"));
case "full"
rowIndices = find(app.Inventory.Status ~= "Missing" & ...
~contains(normalizedTags, ",placeholder,") & ...
~contains(normalizedTags, ",hardware,"));
case "integration"
rowIndices = find(app.Inventory.Status ~= "Missing" & ...
contains(normalizedTags, ",integration,") & ...
~contains(normalizedTags, ",placeholder,") & ...
~contains(normalizedTags, ",hardware,"));
case "placeholder"
rowIndices = find(contains(normalizedTags, ",placeholder,"));
end
end
function textOut = stamp()
textOut = char(datetime("now", "Format", "yyyy-MM-dd HH:mm:ss"));
end
end
function color = metricColor(hasRuns, activeColor)
if hasRuns
color = activeColor;
else
color = [0.48, 0.52, 0.58];
end
end
function inventory = buildInventory(repoRoot, testsRoot)
repoRoot = char(string(repoRoot));
testsRoot = char(string(testsRoot));
classesRoot = fullfile(repoRoot, 'Classes');
classFiles = collectMFiles(classesRoot);
records = struct( ...
"Status", {}, ...
"LastRun", {}, ...
"ResultSummary", {}, ...
"LastRunDate", {}, ...
"LastRunCommit", {}, ...
"Scope", {}, ...
"Target", {}, ...
"TargetFile", {}, ...
"TestFile", {}, ...
"TestFileName", {}, ...
"Tags", {});
matchedTestFiles = strings(0, 1);
for idx = 1:numel(classFiles)
classFile = fullfile(classFiles(idx).folder, classFiles(idx).name);
if ~isClassFile(classFile)
continue
end
relativePath = strrep(classFile, [classesRoot, filesep], "");
[relativeDir, className] = fileparts(relativePath);
relativeDirText = char(join(string(relativeDir), ""));
classNameText = char(string(className));
testFile = findMatchingTestFile(testsRoot, relativeDirText, classNameText);
if strlength(testFile) == 0
status = "Missing";
tags = "";
testFileName = "";
else
matchedTestFiles(end + 1, 1) = testFile; %#ok<AGROW>
source = fileread(testFile);
if contains(source, "assumeFail(")
status = "Placeholder";
else
status = "Implemented";
end
tags = strjoin(extractTestTags(source), ", ");
[~, testFileName] = fileparts(testFile);
end
targetLabel = fullfile('Classes', relativeDirText, [classNameText, '.m']);
targetLabel = strrep(targetLabel, "\", "/");
records(end + 1) = struct( ... %#ok<AGROW>
"Status", status, ...
"LastRun", "", ...
"ResultSummary", "", ...
"LastRunDate", "", ...
"LastRunCommit", "", ...
"Scope", "Class", ...
"Target", string(targetLabel), ...
"TargetFile", string(classFile), ...
"TestFile", string(testFile), ...
"TestFileName", string(testFileName), ...
"Tags", string(tags));
end
allTestFiles = dir(fullfile(testsRoot, "**", "*_test.m"));
for idx = 1:numel(allTestFiles)
testFile = fullfile(allTestFiles(idx).folder, allTestFiles(idx).name);
if any(strcmpi(testFile, matchedTestFiles))
continue
end
source = fileread(testFile);
if contains(source, "assumeFail(")
status = "Placeholder";
else
status = "Implemented";
end
[~, testFileName] = fileparts(testFile);
relativeTest = strrep(testFile, [testsRoot, filesep], "");
relativeTest = strrep(relativeTest, "\", "/");
tags = strjoin(extractTestTags(source), ", ");
records(end + 1) = struct( ... %#ok<AGROW>
"Status", status, ...
"LastRun", "", ...
"ResultSummary", "", ...
"LastRunDate", "", ...
"LastRunCommit", "", ...
"Scope", "Standalone", ...
"Target", string(relativeTest), ...
"TargetFile", "", ...
"TestFile", string(testFile), ...
"TestFileName", string(testFileName), ...
"Tags", string(tags));
end
inventory = struct2table(records);
if isempty(inventory)
inventory = table(strings(0, 1), strings(0, 1), strings(0, 1), ...
strings(0, 1), strings(0, 1), strings(0, 1), strings(0, 1), ...
strings(0, 1), strings(0, 1), strings(0, 1), strings(0, 1), ...
"VariableNames", ["Status", "LastRun", "ResultSummary", "LastRunDate", ...
"LastRunCommit", "Scope", "Target", "TargetFile", "TestFile", ...
"TestFileName", "Tags"]);
return
end
statusOrder = containers.Map( ...
{'Implemented', 'Missing', 'Placeholder'}, ...
{1, 2, 3});
scopeOrder = containers.Map({'Class', 'Standalone'}, {1, 2});
statusRank = zeros(height(inventory), 1);
scopeRank = zeros(height(inventory), 1);
for idx = 1:height(inventory)
statusRank(idx) = statusOrder(char(inventory.Status(idx)));
scopeRank(idx) = scopeOrder(char(inventory.Scope(idx)));
end
inventory.statusRank = statusRank;
inventory.scopeRank = scopeRank;
inventory = sortrows(inventory, {'scopeRank', 'statusRank', 'Target'});
inventory = removevars(inventory, {'statusRank', 'scopeRank'});
end
function testFile = findMatchingTestFile(testsRoot, relativeDir, className)
testsRoot = char(string(testsRoot));
relativeDir = char(join(string(relativeDir), ""));
className = char(string(className));
testDir = fullfile(testsRoot, relativeDir);
testFile = "";
if ~isfolder(testDir)
return
end
exactFile = fullfile(testDir, [className, '_test.m']);
if isfile(exactFile)
testFile = string(exactFile);
return
end
candidates = dir(fullfile(testDir, '*_test.m'));
if isempty(candidates)
return
end
candidateNames = string({candidates.name});
suffix = ['_', className, '_test.m'];
matchMask = endsWith(candidateNames, suffix, "IgnoreCase", true);
if any(matchMask)
firstMatch = candidates(find(matchMask, 1));
testFile = string(fullfile(firstMatch.folder, firstMatch.name));
end
end
function tags = extractTestTags(source)
tokens = regexp(source, "TestTags\s*=\s*\{([^}]*)\}", "tokens", "once");
if isempty(tokens)
tags = strings(0, 1);
return
end
tags = regexp(tokens{1}, "'([^']*)'", "tokens");
tags = string(cellfun(@(entry) entry{1}, tags, "UniformOutput", false));
end
function tf = isClassFile(filePath)
source = fileread(filePath);
tf = contains(source, "classdef");
end
function files = collectMFiles(rootFolder)
pathString = genpath(rootFolder);
folders = regexp(pathString, pathsep, "split");
folders = folders(~cellfun("isempty", folders));
files = dir(fullfile(rootFolder, "*.m"));
files = files([]); %#ok<NASGU>
files = dir(fullfile(rootFolder, "*.m"));
files = files([]);
for idx = 1:numel(folders)
currentFiles = dir(fullfile(folders{idx}, "*.m"));
if isempty(currentFiles)
continue
end
currentFiles = currentFiles(~[currentFiles.isdir]);
files = [files; currentFiles]; %#ok<AGROW>
end
end
function [statusText, summaryText] = summarizeResults(results)
passedCount = nnz([results.Passed]);
failedCount = nnz([results.Failed]);
incompleteCount = nnz([results.Incomplete]);
if failedCount > 0
statusText = "Failed";
elseif incompleteCount > 0
statusText = "Incomplete";
else
statusText = "Passed";
end
summaryText = sprintf('%d/%d/%d', passedCount, failedCount, incompleteCount);
end
function out = stringOrEmpty(structValue, fieldName)
if isfield(structValue, fieldName)
out = string(structValue.(fieldName));
else
out = "";
end
end
function rows = inventoryRowStates(inventory)
rows = repmat(struct( ...
'test_file', '', ...
'target', '', ...
'scope', '', ...
'last_run', '', ...
'result_summary', '', ...
'last_run_date', '', ...
'last_run_commit', ''), height(inventory), 1);
for idx = 1:height(inventory)
rows(idx).test_file = char(inventory.TestFile(idx));
rows(idx).target = char(inventory.Target(idx));
rows(idx).scope = char(inventory.Scope(idx));
rows(idx).last_run = char(inventory.LastRun(idx));
rows(idx).result_summary = char(inventory.ResultSummary(idx));
rows(idx).last_run_date = char(inventory.LastRunDate(idx));
rows(idx).last_run_commit = char(inventory.LastRunCommit(idx));
end
end
function entries = resultEntries(results)
entries = repmat(struct( ...
'name', '', ...
'passed', false, ...
'failed', false, ...
'incomplete', false, ...
'duration_seconds', 0), numel(results), 1);
for idx = 1:numel(results)
entries(idx).name = char(string(results(idx).Name));
entries(idx).passed = results(idx).Passed;
entries(idx).failed = results(idx).Failed;
entries(idx).incomplete = results(idx).Incomplete;
entries(idx).duration_seconds = durationToSeconds(results(idx).Duration);
end
end
function gitInfo = collectGitMetadata(repoRoot)
gitInfo = struct();
gitInfo.commit = strtrim(runCommand(sprintf('git -C "%s" rev-parse HEAD', repoRoot)));
gitInfo.short_commit = strtrim(runCommand(sprintf('git -C "%s" rev-parse --short HEAD', repoRoot)));
gitInfo.branch = strtrim(runCommand(sprintf('git -C "%s" rev-parse --abbrev-ref HEAD', repoRoot)));
statusOutput = runCommand(sprintf('git -C "%s" status --porcelain', repoRoot));
gitInfo.dirty = ~isempty(strtrim(statusOutput));
end
function output = runCommand(commandText)
[status, output] = system(commandText);
if status ~= 0
output = '';
end
end
function writeTextFile(filePath, content)
fid = fopen(filePath, 'w');
assert(fid ~= -1, 'Could not write file: %s', filePath);
cleaner = onCleanup(@() fclose(fid)); %#ok<NASGU>
fprintf(fid, '%s', content);
end
function label = sanitizeFileLabel(labelText)
label = regexprep(char(string(labelText)), '[^A-Za-z0-9]+', '_');
label = regexprep(label, '^_+|_+$', '');
if isempty(label)
label = 'run';
end
end
function value = durationToSeconds(durationValue)
try
value = seconds(durationValue);
if isa(value, 'duration')
value = str2double(char(value));
end
catch
try
value = posixtime(datetime(durationValue) - datetime(0, 0, 0));
catch
durationText = char(string(durationValue));
tokens = regexp(durationText, '(\d+):(\d+):([\d\.]+)', 'tokens', 'once');
if isempty(tokens)
value = NaN;
else
value = str2double(tokens{1}) * 3600 + ...
str2double(tokens{2}) * 60 + ...
str2double(tokens{3});
end
end
end
if ~isnumeric(value) || ~isscalar(value)
value = NaN;
end
end