Skip to content

Commit 78f2096

Browse files
authored
Merge pull request #77 from matlab-actions/tarun/codeCovView
Code Coverage Summary View in GitHub Actions
2 parents 96a5f78 + 53841ca commit 78f2096

13 files changed

Lines changed: 276 additions & 55 deletions

File tree

.github/workflows/bat.yml

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
name: Build and Test
2-
on: [push]
2+
on: [push, pull_request]
33
permissions:
44
contents: read
55

@@ -46,12 +46,18 @@ jobs:
4646
with:
4747
source-folder: sample
4848
startup-options: -logfile output.log
49+
code-coverage-metric-level: mcdc # new option to configure metric level
4950
- name: Verify option application
5051
run: |
5152
set -e
5253
grep "Running TheTruth" output.log
5354
grep "Done TheTruth" output.log
54-
grep "1 Passed" output.log
55+
grep "Running QuadraticPolynomialTest" output.log
56+
grep "Done QuadraticPolynomialTest" output.log
57+
grep "Running tAssumptionFailure" output.log
58+
grep "Done tAssumptionFailure" output.log
59+
grep "3 Passed" output.log
60+
grep "1 Incomplete" output.log
5561
shell: bash
5662
- name: Generate Model
5763
uses: matlab-actions/run-command@v2
@@ -97,4 +103,4 @@ jobs:
97103
- name: Verify workflow command is added
98104
run: |
99105
grep "::group::TheTruth" output.log
100-
shell: bash
106+
shell: bash

.gitignore

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -124,4 +124,4 @@ out
124124
.yarn/unplugged
125125
.yarn/build-state.yml
126126
.yarn/install-state.gz
127-
.pnp.*
127+
.pnp.*

action.yml

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Copyright 2020-2024 The MathWorks, Inc.
1+
# Copyright 2020-2026 The MathWorks, Inc.
22

33
name: Run MATLAB Tests
44
description: >-
@@ -84,6 +84,11 @@ inputs:
8484
Startup options for MATLAB
8585
required: false
8686
default: ""
87+
code-coverage-metric-level:
88+
description: >-
89+
Level of coverage metrics to collect
90+
required: false
91+
default: mcdc
8792
runs:
8893
using: node24
8994
main: dist/index.js

package-lock.json

Lines changed: 39 additions & 39 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
"dependencies": {
2323
"@actions/core": "^3.0.0",
2424
"@actions/exec": "^3.0.0",
25-
"common-utils": "github:matlab-actions/common-utils#v2.0.1"
25+
"common-utils": "github:matlab-actions/common-utils#v2.1.0"
2626
},
2727
"devDependencies": {
2828
"@types/jest": "^30.0.0",
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
classdef CodeCoverageSummaryPluginService < matlab.buildtool.internal.services.ciplugins.CITestRunnerPluginService
2+
% Copyright 2026 The MathWorks, Inc.
3+
4+
methods
5+
function plugins = providePlugins(~, ~)
6+
7+
% Check if MATLAB Test license is available
8+
if license('test', 'matlab_test')
9+
10+
% Get metric level from environment variable
11+
metricLevel = getenv('INPUT_CODE_COVERAGE_METRIC_LEVEL');
12+
13+
% Create a shared CoverageResult format object
14+
format = matlab.unittest.plugins.codecoverage.CoverageResult;
15+
16+
% Create an array to hold multiple plugins
17+
plugins = matlab.unittest.plugins.TestRunnerPlugin.empty(0);
18+
19+
% Get source folder from environment variable
20+
sourceFolder = getenv('INPUT_SOURCE_FOLDER');
21+
if isempty(sourceFolder)
22+
sourceFolder = pwd;
23+
end
24+
25+
coveragePlugin = matlab.unittest.plugins.CodeCoveragePlugin.forFolder(...
26+
sourceFolder, 'Producing', format, 'MetricLevel', metricLevel);
27+
28+
plugins(end+1) = coveragePlugin;
29+
30+
% Add the summary plugin with the same format object
31+
summaryPlugin = testframework.CodeCoverageSummaryPlugin(format, metricLevel);
32+
plugins(end+1) = summaryPlugin;
33+
else
34+
plugins = matlab.unittest.plugins.TestRunnerPlugin.empty(1,0);
35+
end
36+
end
37+
end
38+
end
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
classdef CodeCoverageSummaryPlugin < matlab.unittest.plugins.TestRunnerPlugin
2+
% Copyright 2026 The MathWorks, Inc.
3+
4+
properties (Access=private)
5+
CoverageFormat
6+
MetricLevel
7+
end
8+
9+
methods
10+
function plugin = CodeCoverageSummaryPlugin(coverageFormat, metricLevel)
11+
plugin.CoverageFormat = coverageFormat;
12+
plugin.MetricLevel = metricLevel;
13+
end
14+
end
15+
16+
methods (Access=protected)
17+
function runSession(plugin, pluginData)
18+
% Checkout MATLAB Test license
19+
license('checkout', 'matlab_test');
20+
21+
% Run the session first (this ensures coverage data is collected)
22+
runSession@matlab.unittest.plugins.TestRunnerPlugin(plugin, pluginData);
23+
24+
% Now extract and save coverage data
25+
if isempty(plugin.CoverageFormat.Result)
26+
warning("testframework:CodeCoverageSummaryPlugin:NoCoverageData", "No coverage data collected.");
27+
return;
28+
end
29+
30+
result = plugin.CoverageFormat.Result;
31+
32+
% Create coverage summary structure
33+
coverageDetails = struct();
34+
coverageDetails.MetricLevel = plugin.MetricLevel;
35+
36+
% Always get function and statement coverage (available for all levels)
37+
functionCoverage = coverageSummary(result, "function");
38+
statementCoverage = coverageSummary(result, "statement");
39+
40+
coverageDetails.FunctionCoverage = sumCoverage(functionCoverage);
41+
coverageDetails.StatementCoverage = sumCoverage(statementCoverage);
42+
43+
% Get decision coverage if metric level is decision, condition, or mcdc
44+
if ismember(plugin.MetricLevel, {'decision', 'condition', 'mcdc'})
45+
decisionCoverage = coverageSummary(result, "decision");
46+
coverageDetails.DecisionCoverage = sumCoverage(decisionCoverage);
47+
end
48+
49+
% Get condition coverage if metric level is condition or mcdc
50+
if ismember(plugin.MetricLevel, {'condition', 'mcdc'})
51+
conditionCoverage = coverageSummary(result, "condition");
52+
coverageDetails.ConditionCoverage = sumCoverage(conditionCoverage);
53+
end
54+
55+
% Get MC/DC coverage if metric level is mcdc
56+
if strcmp(plugin.MetricLevel, 'mcdc')
57+
mcdcCoverage = coverageSummary(result, "mcdc");
58+
coverageDetails.MCDCCoverage = sumCoverage(mcdcCoverage);
59+
end
60+
61+
coverageResults = {coverageDetails};
62+
63+
% Determine file path for coverage results
64+
coverageArtifactFile = fullfile(getenv("RUNNER_TEMP"), "matlabCoverageResults" + getenv("GITHUB_RUN_ID") + ".json");
65+
66+
try
67+
JsonCoverageResults = jsonencode(coverageResults, "PrettyPrint", true);
68+
69+
[fID, msg] = fopen(coverageArtifactFile, "w");
70+
if fID == -1
71+
warning("testframework:CodeCoverageSummaryPlugin:UnableToOpenFile","Unable to open a file required to show code coverage data. (Cause: %s)", msg);
72+
else
73+
closeFile = onCleanup(@()fclose(fID));
74+
fprintf(fID, '%s', JsonCoverageResults);
75+
end
76+
catch e
77+
warning("testframework:CodeCoverageSummaryPlugin:UnableToJsonEncode","Unable to serialize code coverage data into JSON format. (Cause: %s)", e.message);
78+
end
79+
end
80+
end
81+
end
82+
83+
% Helper function to sum up the coverage data from multiple files
84+
function coverageStruct = sumCoverage(coverageMatrix)
85+
% Split the vector in half: first half is executed, second half is total
86+
executed = sum(coverageMatrix(:, 1));
87+
total = sum(coverageMatrix(:, 2));
88+
89+
coverageStruct = struct(...
90+
'Executed', executed, ...
91+
'Total', total, ...
92+
'Percentage', calculatePercentage([executed, total]) ...
93+
);
94+
end
95+
96+
% function to calculate percentage
97+
function percentage = calculatePercentage(coverageData)
98+
if coverageData(2) == 0
99+
percentage = NaN; % Avoid division by zero
100+
else
101+
percentage = (coverageData(1) / coverageData(2)) * 100;
102+
end
103+
end

0 commit comments

Comments
 (0)