11// Copyright 2024-26 The MathWorks, Inc.
22
33import { jest , describe , it , expect , beforeEach } from "@jest/globals" ;
4+ import * as path from "path" ;
5+ import * as nodeFs from "fs" ;
46
57jest . unstable_mockModule ( "@actions/core" , ( ) => ( {
68 summary : {
@@ -10,17 +12,56 @@ jest.unstable_mockModule("@actions/core", () => ({
1012 } ,
1113} ) ) ;
1214
15+ // Mock fs, passing through real implementations except unlinkSync
16+ const mockUnlinkSync = jest . fn ( ) ;
17+ jest . unstable_mockModule ( "fs" , ( ) => ( {
18+ readFileSync : nodeFs . readFileSync ,
19+ existsSync : nodeFs . existsSync ,
20+ writeFileSync : nodeFs . writeFileSync ,
21+ readdirSync : nodeFs . readdirSync ,
22+ unlinkSync : mockUnlinkSync ,
23+ } ) ) ;
24+
1325const core = await import ( "@actions/core" ) ;
26+ const fs = await import ( "fs" ) ;
1427const buildSummary = await import ( "./buildSummary.js" ) ;
1528
29+ const runnerTemp = path . join ( import . meta. dirname , ".." ) ;
30+
31+ function safeDelete ( filePath : string ) {
32+ try {
33+ nodeFs . unlinkSync ( filePath ) ;
34+ } catch ( e ) {
35+ /* ignore */
36+ }
37+ }
38+
39+ const validBuildData = JSON . stringify ( [
40+ {
41+ name : "compile" ,
42+ failed : false ,
43+ skipped : false ,
44+ description : "Compile source" ,
45+ duration : "00:00:10" ,
46+ } ,
47+ {
48+ name : "test" ,
49+ failed : true ,
50+ skipped : false ,
51+ description : "Run tests" ,
52+ duration : "00:00:25" ,
53+ } ,
54+ ] ) ;
55+
1656beforeEach ( ( ) => {
17- ( core . summary . addTable as jest . Mock ) . mockReturnThis ( ) ;
18- ( core . summary . addHeading as jest . Mock ) . mockReturnThis ( ) ;
19- ( core . summary . write as jest . Mock ) . mockReturnThis ( ) ;
57+ ( core . summary . addTable as jest . Mock ) . mockClear ( ) ;
58+ ( core . summary . addHeading as jest . Mock ) . mockClear ( ) ;
59+ ( core . summary . write as jest . Mock ) . mockClear ( ) ;
60+ mockUnlinkSync . mockReset ( ) ;
2061} ) ;
2162
22- describe ( "summaryGeneration " , ( ) => {
23- it ( "should process and return summary rows for valid JSON with different task statuses" , ( ) => {
63+ describe ( "getSummaryRows " , ( ) => {
64+ it ( "should return correct rows for different task statuses" , ( ) => {
2465 const mockBuildSummary = JSON . stringify ( [
2566 {
2667 name : "Task 1" ,
@@ -73,19 +114,151 @@ describe("summaryGeneration", () => {
73114 ] ) ;
74115 } ) ;
75116
76- it ( "writes the summary correctly" , ( ) => {
77- const mockTableRows = [
78- [ "MATLAB Task" , "Status" , "Description" , "Duration (HH:mm:ss)" ] ,
79- [ "Test Task" , "🔴 Failed" , "A test task" , "00:00:10" ] ,
80- ] ;
81- buildSummary . addSummary ( mockTableRows ) ;
82-
83- expect ( core . summary . addHeading ) . toHaveBeenCalledTimes ( 1 ) ;
84- expect ( core . summary . addHeading ) . toHaveBeenNthCalledWith (
85- 1 ,
86- expect . stringContaining ( "MATLAB Build Results" ) ,
117+ it ( "should return empty array for empty JSON array" , ( ) => {
118+ const result = buildSummary . getSummaryRows ( "[]" ) ;
119+ expect ( result ) . toEqual ( [ ] ) ;
120+ } ) ;
121+ } ) ;
122+
123+ describe ( "processAndAddBuildSummary" , ( ) => {
124+ it ( "should discover and process files matching the actionName" , ( ) => {
125+ const filePath = path . join ( runnerTemp , "buildSummarymy-action_20260509_100000_001.json" ) ;
126+ fs . writeFileSync ( filePath , validBuildData ) ;
127+
128+ try {
129+ buildSummary . processAndAddBuildSummary ( runnerTemp , "my-action" ) ;
130+
131+ expect ( core . summary . addHeading ) . toHaveBeenCalledWith ( "MATLAB Build Results" ) ;
132+ expect ( core . summary . addTable ) . toHaveBeenCalledTimes ( 1 ) ;
133+
134+ const tableArg = ( core . summary . addTable as jest . Mock ) . mock . calls [ 0 ] [ 0 ] as any [ ] [ ] ;
135+ expect ( tableArg [ 1 ] ) . toEqual ( [ "compile" , "🟢 Successful" , "Compile source" , "00:00:10" ] ) ;
136+ expect ( tableArg [ 2 ] ) . toEqual ( [ "test" , "🔴 Failed" , "Run tests" , "00:00:25" ] ) ;
137+ } finally {
138+ safeDelete ( filePath ) ;
139+ }
140+ } ) ;
141+
142+ it ( "should ignore files for a different actionName" , ( ) => {
143+ const matchingFile = path . join (
144+ runnerTemp ,
145+ "buildSummarymy-action_20260509_100000_001.json" ,
146+ ) ;
147+ const nonMatchingFile = path . join (
148+ runnerTemp ,
149+ "buildSummaryother-action_20260509_100000_001.json" ,
87150 ) ;
88- expect ( core . summary . addTable ) . toHaveBeenCalledTimes ( 1 ) ;
89- expect ( core . summary . addTable ) . toHaveBeenCalledWith ( mockTableRows ) ;
151+ fs . writeFileSync ( matchingFile , validBuildData ) ;
152+ fs . writeFileSync ( nonMatchingFile , validBuildData ) ;
153+
154+ try {
155+ buildSummary . processAndAddBuildSummary ( runnerTemp , "my-action" ) ;
156+
157+ expect ( core . summary . addTable ) . toHaveBeenCalledTimes ( 1 ) ;
158+ expect ( mockUnlinkSync ) . toHaveBeenCalledWith ( matchingFile ) ;
159+ expect ( mockUnlinkSync ) . not . toHaveBeenCalledWith ( nonMatchingFile ) ;
160+ } finally {
161+ safeDelete ( matchingFile ) ;
162+ safeDelete ( nonMatchingFile ) ;
163+ }
164+ } ) ;
165+
166+ it ( "should not add summary when no matching files exist" , ( ) => {
167+ buildSummary . processAndAddBuildSummary ( runnerTemp , "nonexistent-action" ) ;
168+
169+ expect ( core . summary . addHeading ) . not . toHaveBeenCalled ( ) ;
170+ expect ( core . summary . addTable ) . not . toHaveBeenCalled ( ) ;
171+ } ) ;
172+
173+ it ( "should handle non-existent directory gracefully" , ( ) => {
174+ const consoleSpy = jest . spyOn ( console , "error" ) . mockImplementation ( ( ) => { } ) ;
175+
176+ buildSummary . processAndAddBuildSummary ( "/nonexistent/directory/path" , "my-action" ) ;
177+
178+ expect ( core . summary . addTable ) . not . toHaveBeenCalled ( ) ;
179+ expect ( consoleSpy ) . toHaveBeenCalledWith (
180+ expect . stringContaining (
181+ "An error occurred while finding build summary file(s) in directory" ,
182+ ) ,
183+ expect . any ( Error ) ,
184+ ) ;
185+ consoleSpy . mockRestore ( ) ;
186+ } ) ;
187+
188+ it ( "should handle invalid JSON gracefully" , ( ) => {
189+ const consoleSpy = jest . spyOn ( console , "error" ) . mockImplementation ( ( ) => { } ) ;
190+ const filePath = path . join ( runnerTemp , "buildSummarymy-action_20260509_100000_002.json" ) ;
191+ fs . writeFileSync ( filePath , "{ invalid json" ) ;
192+
193+ try {
194+ buildSummary . processAndAddBuildSummary ( runnerTemp , "my-action" ) ;
195+
196+ expect ( core . summary . addTable ) . not . toHaveBeenCalled ( ) ;
197+ expect ( consoleSpy ) . toHaveBeenCalledWith (
198+ "An error occurred while reading the build summary file:" ,
199+ expect . any ( Error ) ,
200+ ) ;
201+ } finally {
202+ safeDelete ( filePath ) ;
203+ consoleSpy . mockRestore ( ) ;
204+ }
205+ } ) ;
206+
207+ it ( "should delete files after processing" , ( ) => {
208+ const filePath = path . join ( runnerTemp , "buildSummarymy-action_20260509_100000_003.json" ) ;
209+ fs . writeFileSync ( filePath , validBuildData ) ;
210+
211+ try {
212+ buildSummary . processAndAddBuildSummary ( runnerTemp , "my-action" ) ;
213+
214+ expect ( mockUnlinkSync ) . toHaveBeenCalledWith ( filePath ) ;
215+ } finally {
216+ safeDelete ( filePath ) ;
217+ }
218+ } ) ;
219+
220+ it ( "should handle file deletion errors gracefully" , ( ) => {
221+ const consoleSpy = jest . spyOn ( console , "error" ) . mockImplementation ( ( ) => { } ) ;
222+ mockUnlinkSync . mockImplementationOnce ( ( ) => {
223+ throw new Error ( "Permission denied" ) ;
224+ } ) ;
225+
226+ const filePath = path . join ( runnerTemp , "buildSummarymy-action_20260509_100000_004.json" ) ;
227+ fs . writeFileSync ( filePath , validBuildData ) ;
228+
229+ try {
230+ buildSummary . processAndAddBuildSummary ( runnerTemp , "my-action" ) ;
231+
232+ expect ( core . summary . addTable ) . toHaveBeenCalledTimes ( 1 ) ;
233+ expect ( consoleSpy ) . toHaveBeenCalledWith (
234+ expect . stringContaining (
235+ "An error occurred while trying to delete the build summary file" ,
236+ ) ,
237+ expect . any ( Error ) ,
238+ ) ;
239+ } finally {
240+ mockUnlinkSync . mockReset ( ) ;
241+ safeDelete ( filePath ) ;
242+ consoleSpy . mockRestore ( ) ;
243+ }
244+ } ) ;
245+
246+ it ( "should process multiple build summary files" , ( ) => {
247+ const file1 = path . join ( runnerTemp , "buildSummarymy-action_20260509_100000_005.json" ) ;
248+ const file2 = path . join ( runnerTemp , "buildSummarymy-action_20260509_100000_006.json" ) ;
249+ fs . writeFileSync ( file1 , validBuildData ) ;
250+ fs . writeFileSync ( file2 , validBuildData ) ;
251+
252+ try {
253+ buildSummary . processAndAddBuildSummary ( runnerTemp , "my-action" ) ;
254+
255+ expect ( core . summary . addHeading ) . toHaveBeenCalledTimes ( 2 ) ;
256+ expect ( core . summary . addTable ) . toHaveBeenCalledTimes ( 2 ) ;
257+ expect ( mockUnlinkSync ) . toHaveBeenCalledWith ( file1 ) ;
258+ expect ( mockUnlinkSync ) . toHaveBeenCalledWith ( file2 ) ;
259+ } finally {
260+ safeDelete ( file1 ) ;
261+ safeDelete ( file2 ) ;
262+ }
90263 } ) ;
91264} ) ;
0 commit comments