11import { logger } from "@coder/logger"
22import { readFile , writeFile , stat , utimes } from "fs/promises"
3+ import { setImmediate } from "timers"
34import { Heart } from "../../../src/node/heart"
45import { clean , mockLogger , tmpdir } from "../../utils/helpers"
56
6- const mockIsActive = ( resolveTo : boolean ) => jest . fn ( ) . mockResolvedValue ( resolveTo )
7-
87describe ( "Heart" , ( ) => {
98 const testName = "heartTests"
109 let testDir = ""
@@ -15,19 +14,23 @@ describe("Heart", () => {
1514 await clean ( testName )
1615 testDir = await tmpdir ( testName )
1716 } )
18- beforeEach ( ( ) => {
19- heart = new Heart ( `${ testDir } /shutdown.txt` , mockIsActive ( true ) )
20- } )
17+
2118 afterAll ( ( ) => {
2219 jest . restoreAllMocks ( )
2320 } )
21+
22+ beforeEach ( ( ) => {
23+ heart = new Heart ( `${ testDir } /shutdown.txt` , jest . fn ( ) . mockResolvedValue ( true ) )
24+ } )
25+
2426 afterEach ( ( ) => {
2527 jest . resetAllMocks ( )
2628 jest . useRealTimers ( )
2729 if ( heart ) {
2830 heart . dispose ( )
2931 }
3032 } )
33+
3134 it ( "should write to a file when given a valid file path" , async ( ) => {
3235 // Set up heartbeat file with contents
3336 const text = "test"
@@ -42,7 +45,7 @@ describe("Heart", () => {
4245
4346 expect ( fileContents ) . toBe ( text )
4447
45- heart = new Heart ( pathToFile , mockIsActive ( true ) )
48+ heart = new Heart ( pathToFile , jest . fn ( ) . mockResolvedValue ( true ) )
4649 await heart . beat ( )
4750 // Check that the heart wrote to the heartbeatFilePath and overwrote our text
4851 const fileContentsAfterBeat = await readFile ( pathToFile , { encoding : "utf8" } )
@@ -51,31 +54,30 @@ describe("Heart", () => {
5154 const fileStatusAfterEdit = await stat ( pathToFile )
5255 expect ( fileStatusAfterEdit . mtimeMs ) . toBeGreaterThan ( 0 )
5356 } )
57+
5458 it ( "should log a warning when given an invalid file path" , async ( ) => {
55- heart = new Heart ( `fakeDir/fake.txt` , mockIsActive ( false ) )
59+ heart = new Heart ( `fakeDir/fake.txt` , jest . fn ( ) . mockResolvedValue ( false ) )
5660 await heart . beat ( )
5761 expect ( logger . warn ) . toHaveBeenCalled ( )
5862 } )
63+
5964 it ( "should be active after calling beat" , async ( ) => {
6065 await heart . beat ( )
6166
6267 const isAlive = heart . alive ( )
6368 expect ( isAlive ) . toBe ( true )
6469 } )
70+
6571 it ( "should not be active after dispose is called" , ( ) => {
6672 heart . dispose ( )
6773
6874 const isAlive = heart . alive ( )
6975 expect ( isAlive ) . toBe ( false )
7076 } )
77+
7178 it ( "should beat twice without warnings" , async ( ) => {
72- // Use fake timers so we can speed up setTimeout
73- jest . useFakeTimers ( )
74- heart = new Heart ( `${ testDir } /hello.txt` , mockIsActive ( true ) )
79+ heart = new Heart ( `${ testDir } /hello.txt` , jest . fn ( ) . mockResolvedValue ( true ) )
7580 await heart . beat ( )
76- // we need to speed up clocks, timeouts
77- // call heartbeat again (and it won't be alive I think)
78- // then assert no warnings were called
7981 jest . runAllTimers ( )
8082 expect ( logger . warn ) . not . toHaveBeenCalled ( )
8183 } )
@@ -84,82 +86,103 @@ describe("Heart", () => {
8486describe ( "heartbeatTimer" , ( ) => {
8587 const testName = "heartbeatTimer"
8688 let testDir = ""
89+
8790 beforeAll ( async ( ) => {
8891 await clean ( testName )
8992 testDir = await tmpdir ( testName )
9093 mockLogger ( )
9194 } )
95+
9296 afterAll ( ( ) => {
9397 jest . restoreAllMocks ( )
9498 } )
99+
95100 beforeEach ( ( ) => {
96101 jest . useFakeTimers ( )
97102 } )
103+
98104 afterEach ( ( ) => {
99105 jest . resetAllMocks ( )
100106 jest . clearAllTimers ( )
101107 jest . useRealTimers ( )
102108 } )
109+
103110 it ( "should call isActive when timeout expires" , async ( ) => {
104- const isActive = true
105- const mockIsActive = jest . fn ( ) . mockResolvedValue ( isActive )
111+ const mockIsActive = jest . fn ( ) . mockResolvedValue ( true )
106112 const heart = new Heart ( `${ testDir } /shutdown.txt` , mockIsActive )
107113 await heart . beat ( )
108114 jest . advanceTimersByTime ( 60 * 1000 )
109115 expect ( mockIsActive ) . toHaveBeenCalled ( )
110116 } )
117+
111118 it ( "should log a warning when isActive rejects" , async ( ) => {
112- const errorMsg = "oh no"
113- const error = new Error ( errorMsg )
119+ const error = new Error ( "oh no" )
114120 const mockIsActive = jest . fn ( ) . mockRejectedValue ( error )
115121 const heart = new Heart ( `${ testDir } /shutdown.txt` , mockIsActive )
116122 await heart . beat ( )
117123 jest . advanceTimersByTime ( 60 * 1000 )
118- // Yield a real macrotask so the callback's rejection handler can run first
119- // (fake timers stub the global setImmediate).
120- await new Promise ( ( resolve ) => jest . requireActual ( "timers" ) . setImmediate ( resolve ) )
121-
122124 expect ( mockIsActive ) . toHaveBeenCalled ( )
123- expect ( logger . warn ) . toHaveBeenCalledWith ( errorMsg )
125+
126+ // The timer callback waits on mockIsActive, so we need to yield to let the
127+ // callback finish.
128+ await new Promise ( ( resolve ) => setImmediate ( resolve ) )
129+
130+ expect ( logger . warn ) . toHaveBeenCalledWith ( error . message )
124131 } )
125132} )
126133
127134describe ( "stateChange" , ( ) => {
128135 const testName = "stateChange"
129136 let testDir = ""
130137 let heart : Heart
138+
131139 beforeAll ( async ( ) => {
132140 await clean ( testName )
133141 testDir = await tmpdir ( testName )
134142 mockLogger ( )
135143 } )
144+
136145 afterAll ( ( ) => {
137146 jest . restoreAllMocks ( )
138147 } )
148+
149+ beforeEach ( ( ) => {
150+ jest . useFakeTimers ( )
151+ } )
152+
139153 afterEach ( ( ) => {
140154 jest . resetAllMocks ( )
155+ jest . useRealTimers ( )
141156 if ( heart ) {
142157 heart . dispose ( )
143158 }
144159 } )
160+
145161 it ( "should change to alive after a beat" , async ( ) => {
146- heart = new Heart ( `${ testDir } /shutdown.txt` , mockIsActive ( true ) )
162+ const mockIsActive = jest . fn ( ) . mockResolvedValue ( true )
163+ heart = new Heart ( `${ testDir } /shutdown.txt` , mockIsActive )
147164 const mockOnChange = jest . fn ( )
148165 heart . onChange ( mockOnChange )
166+
149167 await heart . beat ( )
150168
151169 expect ( mockOnChange . mock . calls [ 0 ] [ 0 ] ) . toBe ( "alive" )
152170 } )
171+
153172 it ( "should change to expired when not active" , async ( ) => {
154- jest . useFakeTimers ( )
155- heart = new Heart ( `${ testDir } /shutdown.txt` , ( ) => new Promise ( ( resolve ) => resolve ( false ) ) )
173+ const mockIsActive = jest . fn ( ) . mockResolvedValue ( false )
174+ heart = new Heart ( `${ testDir } /shutdown.txt` , mockIsActive )
156175 const mockOnChange = jest . fn ( )
157176 heart . onChange ( mockOnChange )
177+
158178 await heart . beat ( )
179+ jest . advanceTimersByTime ( 60 * 1000 )
180+ expect ( mockIsActive ) . toHaveBeenCalled ( )
181+
182+ // The timer callback waits on the isActive promise, so we need to yield to
183+ // let the callback finish.
184+ await new Promise ( ( resolve ) => setImmediate ( resolve ) )
159185
160- await jest . advanceTimersByTime ( 60 * 1000 )
161186 expect ( mockOnChange . mock . calls [ 1 ] [ 0 ] ) . toBe ( "expired" )
162- jest . clearAllTimers ( )
163- jest . useRealTimers ( )
164187 } )
165188} )
0 commit comments