1+ // Copyright 2025 The MathWorks, Inc.
2+ import { readFileSync , unlinkSync , existsSync } from 'fs' ;
3+ import * as path from 'path' ;
4+ import * as core from "@actions/core" ;
5+
6+ // doesn't have id
7+ // number instead of float
8+
9+ export enum MatlabTestStatus {
10+ PASSED = 'PASSED' ,
11+ FAILED = 'FAILED' ,
12+ INCOMPLETE = 'INCOMPLETE' ,
13+ NOT_RUN = 'NOT_RUN'
14+ }
15+
16+ interface MatlabTestDiagnostics {
17+ event : string ;
18+ report : string ;
19+ }
20+
21+ interface MatlabTestCase {
22+ name : string ;
23+ duration : number ;
24+ status : MatlabTestStatus ;
25+ diagnostics : MatlabTestDiagnostics [ ] ;
26+ }
27+
28+ interface MatlabTestFile {
29+ name : string ;
30+ path : string ;
31+ testCases : MatlabTestCase [ ] ;
32+ duration : number ;
33+ status : MatlabTestCase [ 'status' ] ;
34+ }
35+
36+ interface TestCounts {
37+ total : number ;
38+ passed : number ;
39+ failed : number ;
40+ incomplete : number ;
41+ notRun : number ;
42+ }
43+
44+ interface TestResultsData {
45+ testResults : MatlabTestFile [ ] [ ] ;
46+ counts : TestCounts ;
47+ }
48+
49+ export function writeSummary ( testResults : MatlabTestFile [ ] [ ] , counts : TestCounts ) {
50+ try {
51+ const header = getTestHeader ( testResults , counts ) ;
52+ const detailedResults = getDetailedResults ( testResults ) ;
53+ const failedTests = getFailedTests ( testResults ) ;
54+
55+ core . summary
56+ . addHeading ( 'MATLAB Test Results' )
57+ . addRaw ( header , true )
58+ . addRaw ( detailedResults , true )
59+ . addRaw ( failedTests , true )
60+ . write ( ) ;
61+ } catch ( e ) {
62+ console . error ( 'An error occurred while adding the test results to the summary:' , e ) ;
63+ }
64+ }
65+
66+ function getTestHeader ( testResults : MatlabTestFile [ ] [ ] , counts : TestCounts ) : string {
67+ return `<table>
68+ <tr align="center">
69+ <th>Total tests</th>
70+ <th>Passed ✅</th>
71+ <th>Failed ❌</th>
72+ <th>Incomplete ⚠️</th>
73+ <th>Not Run 🚫</th>
74+ <th>Duration(s) ⌛</th>
75+ </tr>
76+ <tr align="center">
77+ <td>${ counts . total } </td>
78+ <td>${ counts . passed } </td>
79+ <td>${ counts . failed } </td>
80+ <td>${ counts . incomplete } </td>
81+ <td>${ counts . notRun } </td>
82+ // <td>${ calculateTotalDuration ( testResults ) } </td>
83+ </tr>
84+ </table>` ;
85+ }
86+
87+ function getDetailedResults ( testResults : MatlabTestFile [ ] [ ] ) : string {
88+ return `<details><summary><h3>All tests</h3></summary>
89+ <table>
90+ <tr>
91+ <th>Test</th>
92+ <th>Duration(s)</th>
93+ </tr>
94+ ${ testResults . flat ( ) . map ( file => generateTestFileRow ( file ) ) . join ( '\n' ) }
95+ </table>
96+ </details>` ;
97+ }
98+
99+ function generateTestFileRow ( file : MatlabTestFile ) : string {
100+ const statusEmoji = getStatusEmoji ( file . status ) ;
101+ return `
102+ <tr>
103+ <td>
104+ <details>
105+ <summary><b>${ statusEmoji } ${ file . name } </b></summary>
106+ <ul style="list-style-type: none;">
107+ ${ file . testCases . map ( tc => `<li>${ getStatusEmoji ( tc . status ) } ${ tc . name } </li>` ) . join ( '\n' ) }
108+ </ul>
109+ </details>
110+ <td align="center" valign="top"><b>${ file . duration . toFixed ( 2 ) } </b>
111+ </td>
112+ </tr>` ;
113+ }
114+
115+ function getFailedTests ( testResults : MatlabTestFile [ ] [ ] ) : string {
116+ const failedTests = testResults . flat ( )
117+ . flatMap ( file => file . testCases )
118+ . filter ( test => test . status === MatlabTestStatus . FAILED ) ;
119+
120+ if ( failedTests . length === 0 ) return '' ;
121+
122+ return `<details><summary><h3>Failed tests</h3></summary>
123+ ${ failedTests . map ( test => generateFailedTestDetails ( test ) ) . join ( '\n' ) }
124+ </details>` ;
125+ }
126+
127+ function generateFailedTestDetails ( test : MatlabTestCase ) : string {
128+ return `<h4><b>❌ <u>${ test . name } failed</u></b></h4>
129+ <details><summary>View stack trace</summary></br>
130+ <pre>${ test . diagnostics . map ( d => d . report ) . join ( '\n' ) } </pre>
131+ </details>` ;
132+ }
133+
134+ function getStatusEmoji ( status : MatlabTestStatus ) : string {
135+ switch ( status ) {
136+ case MatlabTestStatus . PASSED : return '✅' ;
137+ case MatlabTestStatus . FAILED : return '❌' ;
138+ case MatlabTestStatus . INCOMPLETE : return '⚠️' ;
139+ case MatlabTestStatus . NOT_RUN : return '🚫' ;
140+ }
141+ }
142+
143+ export function getTestResults ( workspace : string ) : TestResultsData {
144+ const testResults : MatlabTestFile [ ] [ ] = [ ] ;
145+ const counts : TestCounts = { total : 0 , passed : 0 , failed : 0 , incomplete : 0 , notRun : 0 } ;
146+ const runId = process . env . GITHUB_RUN_ID || '' ;
147+ const runnerTemp = process . env . RUNNER_TEMP || '' ;
148+ const resultsPath = path . join ( runnerTemp , `matlabTestResults${ runId } .json` ) ;
149+
150+ if ( existsSync ( resultsPath ) ) {
151+ try {
152+ const testArtifact = JSON . parse ( fs . readFileSync ( resultsPath , 'utf8' ) ) ;
153+
154+ for ( const jsonTestSessionResults of testArtifact ) {
155+ const testSessionResults : MatlabTestFile [ ] = [ ] ;
156+ const map = new Map < string , MatlabTestFile > ( ) ;
157+
158+ const testCases = Array . isArray ( jsonTestSessionResults ) ?
159+ jsonTestSessionResults : [ jsonTestSessionResults ] ;
160+
161+ for ( const jsonTestCase of testCases ) {
162+ processTestCase ( testSessionResults , jsonTestCase , map , workspace , counts ) ;
163+ }
164+
165+ testResults . push ( testSessionResults ) ;
166+ }
167+ } catch ( e ) {
168+ console . error ( 'An error occurred while reading the test results summary file ${resultsPath}:' , e ) ;
169+ // return;
170+ } finally {
171+ try {
172+ unlinkSync ( resultsPath ) ;
173+ } catch ( e ) {
174+ console . error ( `An error occurred while trying to delete the test results summary file ${ resultsPath } :` , e ) ;
175+ }
176+ }
177+ }
178+
179+ return { testResults, counts } ;
180+ }
181+
182+ function processTestCase (
183+ testSessionResults : MatlabTestFile [ ] ,
184+ jsonTestCase : any ,
185+ map : Map < string , MatlabTestFile > ,
186+ workspace : string ,
187+ counts : TestCounts
188+ ) : void {
189+ const baseFolder = jsonTestCase . BaseFolder ;
190+ const testResult = jsonTestCase . TestResult ;
191+
192+ const [ testFileName , testCaseName ] = testResult . Name . split ( '/' ) ;
193+ const filePath = path . join ( baseFolder , testFileName ) ;
194+
195+ let testFile = map . get ( filePath ) ;
196+ if ( ! testFile ) {
197+ testFile = {
198+ name : testFileName ,
199+ // path: getRelativePath(workspace, baseFolder, testFileName),
200+ path : "" ,
201+ testCases : [ ] ,
202+ duration : 0 ,
203+ status : MatlabTestStatus . NOT_RUN
204+ } ;
205+ map . set ( filePath , testFile ) ;
206+ testSessionResults . push ( testFile ) ;
207+ }
208+
209+ const testCase : MatlabTestCase = {
210+ name : testCaseName ,
211+ duration : Number ( testResult . Duration . toFixed ( 2 ) ) ,
212+ status : determineTestStatus ( testResult ) ,
213+ diagnostics : processDiagnostics ( testResult . Details . DiagnosticRecord )
214+ } ;
215+
216+ testFile . testCases . push ( testCase ) ;
217+ incrementDuration ( testFile , testCase . duration ) ;
218+ updateFileStatus ( testFile , testCase ) ;
219+ updateCount ( testCase , counts ) ;
220+ }
221+
222+ function incrementDuration ( testFile : MatlabTestFile , testCaseDuration : number ) : void {
223+ testFile . duration = ( testFile . duration || 0 ) + testCaseDuration ;
224+ }
225+
226+ function updateFileStatus ( testFile : MatlabTestFile , testCase : MatlabTestCase ) : void {
227+ if ( testFile . status !== MatlabTestStatus . FAILED ) {
228+ if ( testCase . status === MatlabTestStatus . FAILED ) {
229+ testFile . status = MatlabTestStatus . FAILED ;
230+ } else if ( testFile . status !== MatlabTestStatus . INCOMPLETE ) {
231+ if ( testCase . status === MatlabTestStatus . INCOMPLETE ) {
232+ testFile . status = MatlabTestStatus . INCOMPLETE ;
233+ } else if ( testCase . status === MatlabTestStatus . PASSED ) {
234+ testFile . status = MatlabTestStatus . PASSED ;
235+ }
236+ }
237+ }
238+ }
239+
240+ function determineTestStatus ( testResult : any ) : MatlabTestStatus {
241+ if ( testResult . Failed ) return MatlabTestStatus . FAILED ;
242+ if ( testResult . Incomplete ) return MatlabTestStatus . INCOMPLETE ;
243+ if ( testResult . Passed ) return MatlabTestStatus . PASSED ;
244+ return MatlabTestStatus . NOT_RUN ;
245+ }
246+
247+ function processDiagnostics ( diagnostics : any ) : MatlabTestDiagnostics [ ] {
248+ const results : MatlabTestDiagnostics [ ] = [ ] ;
249+
250+ if ( ! diagnostics ) return results ;
251+
252+ const diagnosticItems = Array . isArray ( diagnostics ) ? diagnostics : [ diagnostics ] ;
253+
254+ for ( const item of diagnosticItems ) {
255+ if ( item . Event && item . Report ) {
256+ results . push ( {
257+ event : item . Event ,
258+ report : item . Report
259+ } ) ;
260+ }
261+ }
262+
263+ return results ;
264+ }
265+
266+ function getRelativePath ( workspace : string , baseFolder : string , fileName : string ) : string {
267+ const relativePath = path . relative ( workspace , baseFolder ) ;
268+ return path . join ( workspace , relativePath , fileName ) ;
269+ }
270+
271+ function updateCount ( testCase : MatlabTestCase , counts : TestCounts ) : void {
272+ counts . total ++ ;
273+ switch ( testCase . status ) {
274+ case 'PASSED' : counts . passed ++ ; break ;
275+ case 'FAILED' : counts . failed ++ ; break ;
276+ case 'INCOMPLETE' : counts . incomplete ++ ; break ;
277+ case 'NOT_RUN' : counts . notRun ++ ; break ;
278+ }
279+ }
0 commit comments