Skip to content

feat: add tests for node/heart.ts #5122

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 9 commits into from
Apr 26, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 18 additions & 11 deletions src/node/heart.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,17 +33,7 @@ export class Heart {
if (typeof this.heartbeatTimer !== "undefined") {
clearTimeout(this.heartbeatTimer)
}
this.heartbeatTimer = setTimeout(() => {
this.isActive()
.then((active) => {
if (active) {
this.beat()
}
})
.catch((error) => {
logger.warn(error.message)
})
}, this.heartbeatInterval)
this.heartbeatTimer = setTimeout(() => heartbeatTimer(this.isActive, this.beat), this.heartbeatInterval)
}

/**
Expand All @@ -55,3 +45,20 @@ export class Heart {
}
}
}

/**
* Helper function for the heartbeatTimer.
*
* If heartbeat is active, call beat. Otherwise do nothing.
*
* Extracted to make it easier to test.
*/
export async function heartbeatTimer(isActive: Heart["isActive"], beat: Heart["beat"]) {
try {
if (await isActive()) {
beat()
}
} catch (error: unknown) {
logger.warn((error as Error).message)
}
}
2 changes: 1 addition & 1 deletion test/e2e/downloads.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import * as path from "path"
import { promises as fs } from "fs"
import * as path from "path"
import { clean } from "../utils/helpers"
import { describe, test, expect } from "./baseFixture"

Expand Down
100 changes: 100 additions & 0 deletions test/unit/node/heart.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import { logger } from "@coder/logger"
import { readFile, writeFile, stat } from "fs/promises"
import { Heart, heartbeatTimer } from "../../../src/node/heart"
import { clean, mockLogger, tmpdir } from "../../utils/helpers"

const mockIsActive = (resolveTo: boolean) => jest.fn().mockResolvedValue(resolveTo)

describe("Heart", () => {
const testName = "heartTests"
let testDir = ""
let heart: Heart

beforeAll(async () => {
mockLogger()
await clean(testName)
testDir = await tmpdir(testName)
})
beforeEach(() => {
heart = new Heart(`${testDir}/shutdown.txt`, mockIsActive(true))
})
afterAll(() => {
jest.restoreAllMocks()
})
afterEach(() => {
jest.resetAllMocks()
if (heart) {
heart.dispose()
}
})
it("should write to a file when given a valid file path", async () => {
// Set up heartbeat file with contents
const text = "test"
const pathToFile = `${testDir}/file.txt`
await writeFile(pathToFile, text)
const fileContents = await readFile(pathToFile, { encoding: "utf8" })
const fileStatusBeforeEdit = await stat(pathToFile)
expect(fileContents).toBe(text)

heart = new Heart(pathToFile, mockIsActive(true))
heart.beat()
// Check that the heart wrote to the heartbeatFilePath and overwrote our text
const fileContentsAfterBeat = await readFile(pathToFile, { encoding: "utf8" })
expect(fileContentsAfterBeat).not.toBe(text)
// Make sure the modified timestamp was updated.
const fileStatusAfterEdit = await stat(pathToFile)
expect(fileStatusAfterEdit.mtimeMs).toBeGreaterThan(fileStatusBeforeEdit.mtimeMs)
})
it("should log a warning when given an invalid file path", async () => {
heart = new Heart(`fakeDir/fake.txt`, mockIsActive(false))
heart.beat()
// HACK@jsjoeio - beat has some async logic but is not an async method
// Therefore, we have to create an artificial wait in order to make sure
// all async code has completed before asserting
Comment on lines +51 to +53
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks good to me for now. Maybe long-term we can make beat async, at least up to the file write.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes! I briefly looked into it for this PR but since it happens in a request, I wasn't sure if changing it here to make the handler async would causes so I didn't.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah yeah I think the move would be to only await in tests. For other uses we can leave them as-is (since we do not want to delay the request to write to the heartbeat file).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah gotcha! I did use await when I was first writing the tests but TS said it would have no effect lol but I guess that's because it couldn't infer the callback logic? I'm not sure lol

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ahh yup we would need to make beat async first and then add the await in the tests.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But I think the current workaround is chill

await new Promise((r) => setTimeout(r, 100))
// expect(logger.trace).toHaveBeenCalled()
expect(logger.warn).toHaveBeenCalled()
})
it("should be active after calling beat", () => {
heart.beat()

const isAlive = heart.alive()
expect(isAlive).toBe(true)
})
it("should not be active after dispose is called", () => {
heart.dispose()

const isAlive = heart.alive()
expect(isAlive).toBe(false)
})
})

describe("heartbeatTimer", () => {
beforeAll(() => {
mockLogger()
})
afterAll(() => {
jest.restoreAllMocks()
})
afterEach(() => {
jest.resetAllMocks()
})
it("should call beat when isActive resolves to true", async () => {
const isActive = true
const mockIsActive = jest.fn().mockResolvedValue(isActive)
const mockBeatFn = jest.fn()
await heartbeatTimer(mockIsActive, mockBeatFn)
expect(mockIsActive).toHaveBeenCalled()
expect(mockBeatFn).toHaveBeenCalled()
})
it("should log a warning when isActive rejects", async () => {
const errorMsg = "oh no"
const error = new Error(errorMsg)
const mockIsActive = jest.fn().mockRejectedValue(error)
const mockBeatFn = jest.fn()
await heartbeatTimer(mockIsActive, mockBeatFn)
expect(mockIsActive).toHaveBeenCalled()
expect(mockBeatFn).not.toHaveBeenCalled()
expect(logger.warn).toHaveBeenCalledWith(errorMsg)
})
})