Skip to content
Open
Show file tree
Hide file tree
Changes from 2 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
8 changes: 4 additions & 4 deletions .github/workflows/bat.yml
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ jobs:
uses: ./
with:
source-folder: sample
code-coverage-metric-level: mcdc
code-coverage-metrics: mcdc
- name: Perform 'setup-matlab' with MATLAB Test
uses: matlab-actions/setup-matlab@v3
with:
Expand All @@ -134,16 +134,16 @@ jobs:
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 only code-coverage-metrics option, view is shown.
uses: ./
with:
source-folder: sample
code-coverage-metric-level: mcdc
code-coverage-metrics: mcdc
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,52 @@

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")
coveragePlugin = matlab.unittest.plugins.CodeCoveragePlugin.forFolder(...
sourceFolder, 'Producing', format, 'MetricLevel', metrics);
Comment thread
mw-kapilg marked this conversation as resolved.
Outdated
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
15 changes: 13 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,15 @@ 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 +39,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 +57,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
52 changes: 34 additions & 18 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,30 @@ 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(
`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 +82,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 +111,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();
});
});
Loading