Skip to content

Commit 18ff996

Browse files
jsjoeiocode-asher
andauthored
feat: add tests for node/heart.ts (#5122)
* refactor(heart): extract logic into heartbeatTimer fn To make it easier to test, I extract heartbeatTimer into it's own function. * feat(testing): add tests for heart.ts * fixup * fixup!: remove unneeded heart call * Update src/node/heart.ts Co-authored-by: Asher <[email protected]> * fixup!: use mockResolvedValue everywhere * fixup!: add stat test for timestamp check Co-authored-by: Asher <[email protected]>
1 parent ed7bd2e commit 18ff996

File tree

3 files changed

+119
-12
lines changed

3 files changed

+119
-12
lines changed

src/node/heart.ts

+18-11
Original file line numberDiff line numberDiff line change
@@ -33,17 +33,7 @@ export class Heart {
3333
if (typeof this.heartbeatTimer !== "undefined") {
3434
clearTimeout(this.heartbeatTimer)
3535
}
36-
this.heartbeatTimer = setTimeout(() => {
37-
this.isActive()
38-
.then((active) => {
39-
if (active) {
40-
this.beat()
41-
}
42-
})
43-
.catch((error) => {
44-
logger.warn(error.message)
45-
})
46-
}, this.heartbeatInterval)
36+
this.heartbeatTimer = setTimeout(() => heartbeatTimer(this.isActive, this.beat), this.heartbeatInterval)
4737
}
4838

4939
/**
@@ -55,3 +45,20 @@ export class Heart {
5545
}
5646
}
5747
}
48+
49+
/**
50+
* Helper function for the heartbeatTimer.
51+
*
52+
* If heartbeat is active, call beat. Otherwise do nothing.
53+
*
54+
* Extracted to make it easier to test.
55+
*/
56+
export async function heartbeatTimer(isActive: Heart["isActive"], beat: Heart["beat"]) {
57+
try {
58+
if (await isActive()) {
59+
beat()
60+
}
61+
} catch (error: unknown) {
62+
logger.warn((error as Error).message)
63+
}
64+
}

test/e2e/downloads.test.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import * as path from "path"
21
import { promises as fs } from "fs"
2+
import * as path from "path"
33
import { clean } from "../utils/helpers"
44
import { describe, test, expect } from "./baseFixture"
55

test/unit/node/heart.test.ts

+100
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
import { logger } from "@coder/logger"
2+
import { readFile, writeFile, stat } from "fs/promises"
3+
import { Heart, heartbeatTimer } from "../../../src/node/heart"
4+
import { clean, mockLogger, tmpdir } from "../../utils/helpers"
5+
6+
const mockIsActive = (resolveTo: boolean) => jest.fn().mockResolvedValue(resolveTo)
7+
8+
describe("Heart", () => {
9+
const testName = "heartTests"
10+
let testDir = ""
11+
let heart: Heart
12+
13+
beforeAll(async () => {
14+
mockLogger()
15+
await clean(testName)
16+
testDir = await tmpdir(testName)
17+
})
18+
beforeEach(() => {
19+
heart = new Heart(`${testDir}/shutdown.txt`, mockIsActive(true))
20+
})
21+
afterAll(() => {
22+
jest.restoreAllMocks()
23+
})
24+
afterEach(() => {
25+
jest.resetAllMocks()
26+
if (heart) {
27+
heart.dispose()
28+
}
29+
})
30+
it("should write to a file when given a valid file path", async () => {
31+
// Set up heartbeat file with contents
32+
const text = "test"
33+
const pathToFile = `${testDir}/file.txt`
34+
await writeFile(pathToFile, text)
35+
const fileContents = await readFile(pathToFile, { encoding: "utf8" })
36+
const fileStatusBeforeEdit = await stat(pathToFile)
37+
expect(fileContents).toBe(text)
38+
39+
heart = new Heart(pathToFile, mockIsActive(true))
40+
heart.beat()
41+
// Check that the heart wrote to the heartbeatFilePath and overwrote our text
42+
const fileContentsAfterBeat = await readFile(pathToFile, { encoding: "utf8" })
43+
expect(fileContentsAfterBeat).not.toBe(text)
44+
// Make sure the modified timestamp was updated.
45+
const fileStatusAfterEdit = await stat(pathToFile)
46+
expect(fileStatusAfterEdit.mtimeMs).toBeGreaterThan(fileStatusBeforeEdit.mtimeMs)
47+
})
48+
it("should log a warning when given an invalid file path", async () => {
49+
heart = new Heart(`fakeDir/fake.txt`, mockIsActive(false))
50+
heart.beat()
51+
// HACK@jsjoeio - beat has some async logic but is not an async method
52+
// Therefore, we have to create an artificial wait in order to make sure
53+
// all async code has completed before asserting
54+
await new Promise((r) => setTimeout(r, 100))
55+
// expect(logger.trace).toHaveBeenCalled()
56+
expect(logger.warn).toHaveBeenCalled()
57+
})
58+
it("should be active after calling beat", () => {
59+
heart.beat()
60+
61+
const isAlive = heart.alive()
62+
expect(isAlive).toBe(true)
63+
})
64+
it("should not be active after dispose is called", () => {
65+
heart.dispose()
66+
67+
const isAlive = heart.alive()
68+
expect(isAlive).toBe(false)
69+
})
70+
})
71+
72+
describe("heartbeatTimer", () => {
73+
beforeAll(() => {
74+
mockLogger()
75+
})
76+
afterAll(() => {
77+
jest.restoreAllMocks()
78+
})
79+
afterEach(() => {
80+
jest.resetAllMocks()
81+
})
82+
it("should call beat when isActive resolves to true", async () => {
83+
const isActive = true
84+
const mockIsActive = jest.fn().mockResolvedValue(isActive)
85+
const mockBeatFn = jest.fn()
86+
await heartbeatTimer(mockIsActive, mockBeatFn)
87+
expect(mockIsActive).toHaveBeenCalled()
88+
expect(mockBeatFn).toHaveBeenCalled()
89+
})
90+
it("should log a warning when isActive rejects", async () => {
91+
const errorMsg = "oh no"
92+
const error = new Error(errorMsg)
93+
const mockIsActive = jest.fn().mockRejectedValue(error)
94+
const mockBeatFn = jest.fn()
95+
await heartbeatTimer(mockIsActive, mockBeatFn)
96+
expect(mockIsActive).toHaveBeenCalled()
97+
expect(mockBeatFn).not.toHaveBeenCalled()
98+
expect(logger.warn).toHaveBeenCalledWith(errorMsg)
99+
})
100+
})

0 commit comments

Comments
 (0)