Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 14 additions & 6 deletions .github/workflows/bat.yml
Original file line number Diff line number Diff line change
Expand Up @@ -116,34 +116,42 @@ jobs:
name: built-action
- name: Perform 'setup-matlab' without MATLAB Test
uses: matlab-actions/setup-matlab@a0180c939fb1a28de13f44f7b778b912384ced1f # v3.0.1
- name: Run MATLAB Tests without MATLAB Test # Test Case 1: Without MATLAB Test, no view is shown
- name: Run MATLAB Tests without MATLAB Test # Test Case 1: Without MATLAB Test, statement coverage view is shown
uses: ./
with:
source-folder: sample
code-coverage-metric-level: mcdc
- name: Perform 'setup-matlab' with MATLAB Test
uses: matlab-actions/setup-matlab@a0180c939fb1a28de13f44f7b778b912384ced1f # v3.0.1
with:
products: |
MATLAB_Test
- name: Run MATLAB Tests with other options # Test Case 2: With MATLAB Test, and other code-coverage options, no view is shown.
- name: Run MATLAB Tests with other options # Test Case 2: With MATLAB Test, and other code-coverage report options, no view is shown.
uses: ./
with:
model-coverage-cobertura: test-results/modelcoverage.xml
test-results-pdf: test-results/pdf-results.pdf
test-results-junit: test-results/results.xml
code-coverage-cobertura: test-results/coverage.xml
code-coverage-html: test-results/coverageHTML
code-coverage-metric-level: mcdc
code-coverage-metrics: mcdc
model-coverage-html: test-results/modelcoverageHTML
test-results-html: test-results/resultsHTML
select-by-folder: sample
strict: true
use-parallel: true
output-detail: Detailed
logging-level: Detailed
- name: Run MATLAB Tests # Test Case 3: With MATLAB Test, and only code-coverage-metric-level option, view is shown.
- name: Run MATLAB Tests # Test Case 3: With MATLAB Test and code-coverage-metrics option (w/o coverage report options), view is shown.
uses: ./
with:
source-folder: sample
code-coverage-metric-level: mcdc
code-coverage-metrics: decision
- name: Run MATLAB Tests with default metrics # Test Case 4: With MATLAB Test, and no code-coverage-metrics option ('auto' default), mcdc view is shown.
uses: ./
with:
source-folder: sample
- name: Run MATLAB Tests with empty metrics # Test Case 5: With MATLAB Test, and empty code-coverage-metrics option, no view is shown.
uses: ./
with:
source-folder: sample
code-coverage-metrics: ""
6 changes: 3 additions & 3 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -89,11 +89,11 @@ inputs:
Option to generate a summary for the GitHub job summary
required: false
default: true
code-coverage-metric-level:
code-coverage-metrics:
description: >-
Level of coverage metrics to collect
Code coverage metrics to collect
required: false
default: mcdc
default: auto
runs:
using: node24
main: dist/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,38 +3,53 @@

methods
function plugins = providePlugins(~, ~)
verInfo = ver;
productNames = string({verInfo.Name});
productName = 'MATLAB Test';
isProductInstalled = any(productNames.matches(productName));
hasCoverageHTML = ~isempty(getenv('MW_INPUT_CODE_COVERAGE_HTML'));
hasCoverageCobertura = ~isempty(getenv('MW_INPUT_CODE_COVERAGE_COBERTURA'));
hasCoverageRequest = hasCoverageHTML || hasCoverageCobertura;

% Check if MATLAB Test license is available and MATLAB Test is installed
if strcmpi(getenv("MW_INPUT_GENERATE_SUMMARY"), "true") && ~hasCoverageRequest && license('test', 'matlab_test') && isProductInstalled
% Get metric level from environment variable
metricLevel = getenv('MW_INPUT_CODE_COVERAGE_METRIC_LEVEL');
metricsStr = strtrim(getenv('MW_INPUT_CODE_COVERAGE_METRICS'));
if strcmpi(getenv("MW_INPUT_GENERATE_SUMMARY"), "true") && ~hasCoverageRequest && ~isempty(metricsStr)
% Parse metrics from environment variable (space-separated)
metrics = strsplit(strtrim(metricsStr));

% Resolve 'auto' to appropriate metrics
if isscalar(metrics) && strcmpi(metrics{1}, 'auto')
if any(strcmp({ver().Name}, 'MATLAB Test')) && license('test', 'MATLAB_Test')
if ~isMATLABReleaseOlderThan("R2023a")
metrics = {'mcdc'};
else
metrics = {'statement'};
end
else
metrics = {'statement'};
end
end

% Create a shared CoverageResult format object
format = matlab.unittest.plugins.codecoverage.CoverageResult;

% Create an array to hold multiple plugins
plugins = matlab.unittest.plugins.TestRunnerPlugin.empty(0);

% Get source folder from environment variable
sourceFolder = getenv('MW_INPUT_SOURCE_FOLDER');
if isempty(sourceFolder)
sourceFolder = pwd;
end

coveragePlugin = matlab.unittest.plugins.CodeCoveragePlugin.forFolder(...
sourceFolder, 'Producing', format, 'MetricLevel', metricLevel);

if isMATLABReleaseOlderThan("R2026b")
metric = metrics{1};
coveragePlugin = matlab.unittest.plugins.CodeCoveragePlugin.forFolder(...
sourceFolder, 'Producing', format, 'MetricLevel', metric);
else
coveragePlugin = matlab.unittest.plugins.CodeCoveragePlugin.forFolder(...
sourceFolder, 'Producing', format, 'Metrics', metrics);
end

plugins(end+1) = coveragePlugin;

% Add the summary plugin with the same format object
summaryPlugin = testframework.CodeCoverageSummaryPlugin(format, metricLevel);
summaryPlugin = testframework.CodeCoverageSummaryPlugin(format, metrics);
plugins(end+1) = summaryPlugin;
else
plugins = matlab.unittest.plugins.TestRunnerPlugin.empty(1,0);
Expand Down
34 changes: 15 additions & 19 deletions plugins/+testframework/CodeCoverageSummaryPlugin.m
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,18 @@

properties (Access=private)
CoverageFormat
MetricLevel
Metrics
end

methods
function plugin = CodeCoverageSummaryPlugin(coverageFormat, metricLevel)
function plugin = CodeCoverageSummaryPlugin(coverageFormat, metrics)
plugin.CoverageFormat = coverageFormat;
plugin.MetricLevel = metricLevel;
plugin.Metrics = metrics;
end
end

methods (Access=protected)
function runSession(plugin, pluginData)
% Checkout MATLAB Test license
license('checkout', 'matlab_test');

% Run the session first (this ensures coverage data is collected)
runSession@matlab.unittest.plugins.TestRunnerPlugin(plugin, pluginData);

Expand All @@ -31,29 +28,28 @@ function runSession(plugin, pluginData)

% Create coverage summary structure
coverageDetails = struct();
coverageDetails.MetricLevel = plugin.MetricLevel;

% Always get function and statement coverage (available for all levels)

% Always get function and statement coverage
functionCoverage = coverageSummary(result, "function");
statementCoverage = coverageSummary(result, "statement");

coverageDetails.FunctionCoverage = sumCoverage(functionCoverage);
coverageDetails.StatementCoverage = sumCoverage(statementCoverage);
% Get decision coverage if metric level is decision, condition, or mcdc
if ismember(plugin.MetricLevel, {'decision', 'condition', 'mcdc'})

% Get decision coverage if metrics contains decision, condition, or mcdc
if any(ismember({'decision', 'condition', 'mcdc'}, plugin.Metrics))
decisionCoverage = coverageSummary(result, "decision");
coverageDetails.DecisionCoverage = sumCoverage(decisionCoverage);
end
% Get condition coverage if metric level is condition or mcdc
if ismember(plugin.MetricLevel, {'condition', 'mcdc'})

% Get condition coverage if metrics contains condition or mcdc
if any(ismember({'condition', 'mcdc'}, plugin.Metrics))
conditionCoverage = coverageSummary(result, "condition");
coverageDetails.ConditionCoverage = sumCoverage(conditionCoverage);
end
% Get MC/DC coverage if metric level is mcdc
if strcmp(plugin.MetricLevel, 'mcdc')

% Get MC/DC coverage if metrics contains mcdc
if any(ismember({'mcdc'}, plugin.Metrics))
mcdcCoverage = coverageSummary(result, "mcdc");
coverageDetails.MCDCCoverage = sumCoverage(mcdcCoverage);
end
Expand Down
15 changes: 4 additions & 11 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ async function run() {
const architecture = process.arch;
const workspaceDir = process.cwd();

const codeCoverageMetrics = core.getInput("code-coverage-metrics").toLowerCase().trim();

const options: scriptgen.RunTestsOptions = {
JUnitTestResults: core.getInput("test-results-junit"),
CoberturaCodeCoverage: core.getInput("code-coverage-cobertura"),
Expand All @@ -29,19 +31,10 @@ async function run() {
UseParallel: core.getBooleanInput("use-parallel"),
OutputDetail: core.getInput("output-detail"),
LoggingLevel: core.getInput("logging-level"),
CodeCoverageMetrics: codeCoverageMetrics,
};

const generateSummary = core.getBooleanInput("generate-summary");
var codeCoverageMetricLevel = core.getInput("code-coverage-metric-level").toLowerCase();

// Validate metric level
const validMetricLevels = ["statement", "decision", "condition", "mcdc"];
if (!validMetricLevels.includes(codeCoverageMetricLevel)) {
core.warning(
`Invalid metric level '${codeCoverageMetricLevel}'. Using the default value ('mcdc') instead.`,
);
codeCoverageMetricLevel = "mcdc";
}

const command = scriptgen.generateCommand(options);
const startupOptions = core.getInput("startup-options").split(" ");
Expand All @@ -51,7 +44,7 @@ async function run() {
env: {
...process.env,
MW_BATCH_LICENSING_ONLINE: "true", // Remove when online batch licensing is the default
MW_INPUT_CODE_COVERAGE_METRIC_LEVEL: codeCoverageMetricLevel,
MW_INPUT_CODE_COVERAGE_METRICS: options.CodeCoverageMetrics!,
MW_INPUT_SOURCE_FOLDER: options.SourceFolder!, // Add source folder to environment
MW_INPUT_CODE_COVERAGE_HTML: options.HTMLCodeCoverage!,
MW_INPUT_CODE_COVERAGE_COBERTURA: options.CoberturaCodeCoverage!,
Expand Down
19 changes: 17 additions & 2 deletions src/scriptgen.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright 2020-2022 The MathWorks, Inc.
// Copyright 2020-2026 The MathWorks, Inc.

import * as path from "path";

Expand All @@ -22,6 +22,19 @@ export interface RunTestsOptions {
UseParallel?: boolean;
OutputDetail?: string;
LoggingLevel?: string;
CodeCoverageMetrics?: string;
}

function formatMetricsCellArray(metrics: string | undefined): string {
if (!metrics || metrics.trim() === "") {
return "{}";
}
const items = metrics
.trim()
.split(/\s+/)
.map((m) => `'${m}'`)
.join(",");
return `{${items}}`;
}

/**
Expand All @@ -30,6 +43,7 @@ export interface RunTestsOptions {
* @param options scriptgen options for running tests.
*/
export function generateCommand(options: RunTestsOptions): string {
const metricsCellArray = formatMetricsCellArray(options.CodeCoverageMetrics);
const command = `
addpath('${path.join(import.meta.dirname, "scriptgen")}');
testScript = genscript('Test',
Expand All @@ -47,7 +61,8 @@ export function generateCommand(options: RunTestsOptions): string {
'Strict',${options.Strict || false},
'UseParallel',${options.UseParallel || false},
'OutputDetail','${options.OutputDetail || ""}',
'LoggingLevel','${options.LoggingLevel || ""}'
'LoggingLevel','${options.LoggingLevel || ""}',
'Metrics',${metricsCellArray}
);
disp('Running MATLAB script with contents:');
disp(testScript.Contents);
Expand Down
55 changes: 34 additions & 21 deletions src/scriptgen.unit.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright 2020-2022 The MathWorks, Inc.
// Copyright 2020-2026 The MathWorks, Inc.

import * as scriptgen from "./scriptgen.js";

Expand All @@ -20,6 +20,7 @@ describe("command generation", () => {
UseParallel: false,
OutputDetail: "",
LoggingLevel: "",
CodeCoverageMetrics: "",
};

const actual = scriptgen.generateCommand(options);
Expand All @@ -40,18 +41,27 @@ describe("command generation", () => {
expect(actual.includes("'UseParallel',false")).toBeTruthy();
expect(actual.includes("'OutputDetail',''")).toBeTruthy();
expect(actual.includes("'LoggingLevel',''")).toBeTruthy();
expect(actual.includes("'Metrics',{}")).toBeTruthy();

const expected =
`genscript('Test', 'JUnitTestResults','', 'CoberturaCodeCoverage','', 'HTMLCodeCoverage','',
'SourceFolder','', 'PDFTestReport','', 'HTMLTestReport','', 'SimulinkTestResults','',
'CoberturaModelCoverage','', 'HTMLModelCoverage','', 'SelectByTag','', 'SelectByFolder','',
'Strict',false, 'UseParallel',false, 'OutputDetail','', 'LoggingLevel','')`.replace(
/\s+/g,
"",
);
`genscript('Test', 'JUnitTestResults','', 'CoberturaCodeCoverage','', 'HTMLCodeCoverage','',
'SourceFolder','', 'PDFTestReport','', 'HTMLTestReport','', 'SimulinkTestResults','',
'CoberturaModelCoverage','', 'HTMLModelCoverage','', 'SelectByTag','', 'SelectByFolder','',
'Strict',false, 'UseParallel',false, 'OutputDetail','', 'LoggingLevel','',
'Metrics',{})`.replace(/\s+/g, "");
expect(actual.replace(/\s+/g, "").includes(expected)).toBeTruthy();
});

it("contains genscript invocation with single metrics value", () => {
const options: scriptgen.RunTestsOptions = {
CodeCoverageMetrics: "statement",
};

const actual = scriptgen.generateCommand(options);

expect(actual.includes("'Metrics',{'statement'}")).toBeTruthy();
});

it("contains genscript invocation with all options specified", () => {
const options: scriptgen.RunTestsOptions = {
JUnitTestResults: "test-results/results.xml",
Expand All @@ -69,6 +79,7 @@ describe("command generation", () => {
UseParallel: true,
OutputDetail: "Detailed",
LoggingLevel: "Detailed",
CodeCoverageMetrics: "mcdc type-size",
};

const actual = scriptgen.generateCommand(options);
Expand Down Expand Up @@ -97,23 +108,25 @@ describe("command generation", () => {
expect(actual.includes("'UseParallel',true")).toBeTruthy();
expect(actual.includes("'OutputDetail','Detailed'")).toBeTruthy();
expect(actual.includes("'LoggingLevel','Detailed'")).toBeTruthy();
expect(actual.includes("'Metrics',{'mcdc','type-size'}")).toBeTruthy();

const expected = `genscript('Test',
'JUnitTestResults','test-results/results.xml',
const expected = `genscript('Test',
'JUnitTestResults','test-results/results.xml',
'CoberturaCodeCoverage','code-coverage/coverage.xml',
'HTMLCodeCoverage','code-coverage/coverage.html',
'HTMLCodeCoverage','code-coverage/coverage.html',
'SourceFolder','source',
'PDFTestReport','test-results/pdf-results.pdf',
'HTMLTestReport','test-results/html-results.html',
'PDFTestReport','test-results/pdf-results.pdf',
'HTMLTestReport','test-results/html-results.html',
'SimulinkTestResults','test-results/simulinkTest.mldatx',
'CoberturaModelCoverage','test-results/modelcoverage.xml',
'HTMLModelCoverage','test-results/modelcoverage.html',
'SelectByTag','FeatureA',
'SelectByFolder','test/tools;test/toolbox',
'Strict',true,
'UseParallel',true,
'OutputDetail','Detailed',
'LoggingLevel','Detailed' )`.replace(/\s+/g, "");
'CoberturaModelCoverage','test-results/modelcoverage.xml',
'HTMLModelCoverage','test-results/modelcoverage.html',
'SelectByTag','FeatureA',
'SelectByFolder','test/tools;test/toolbox',
'Strict',true,
'UseParallel',true,
'OutputDetail','Detailed',
'LoggingLevel','Detailed',
'Metrics',{'mcdc','type-size'})`.replace(/\s+/g, "");
expect(actual.replace(/\s+/g, "").includes(expected)).toBeTruthy();
});
});