Skip to content

Commit 3cfb70c

Browse files
committed
test(cover): cover cover/code (was 0%)
1 parent 79998db commit 3cfb70c

1 file changed

Lines changed: 201 additions & 0 deletions

File tree

test/unit/cover/code.test.mts

Lines changed: 201 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,201 @@
1+
/**
2+
* @fileoverview Unit tests for code-coverage parsing.
3+
*
4+
* Mocks `readJson` from `@socketsecurity/lib/fs` and `spawn` from
5+
* `@socketsecurity/lib/spawn` so tests don't touch the real filesystem
6+
* or run external commands. Uses tmpdir + a real file when needed for
7+
* existsSync paths.
8+
*/
9+
10+
import { mkdtempSync, rmSync, writeFileSync } from 'node:fs'
11+
import { tmpdir } from 'node:os'
12+
import path from 'node:path'
13+
14+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
15+
16+
vi.mock('@socketsecurity/lib/spawn')
17+
18+
import { spawn } from '@socketsecurity/lib/spawn'
19+
import { getCodeCoverage } from '@socketsecurity/lib/cover/code'
20+
21+
let tmpDir: string
22+
23+
function writeCoverageFile(data: unknown): string {
24+
const file = path.join(tmpDir, 'coverage-final.json')
25+
writeFileSync(file, JSON.stringify(data))
26+
return file
27+
}
28+
29+
describe.sequential('cover/code', () => {
30+
beforeEach(() => {
31+
tmpDir = mkdtempSync(path.join(tmpdir(), 'socket-lib-cover-test-'))
32+
vi.mocked(spawn).mockReset()
33+
})
34+
35+
afterEach(() => {
36+
rmSync(tmpDir, { recursive: true, force: true })
37+
vi.restoreAllMocks()
38+
})
39+
40+
describe('getCodeCoverage', () => {
41+
it('aggregates statement / branch / function metrics', async () => {
42+
const coveragePath = writeCoverageFile({
43+
'/some/file.ts': {
44+
s: { '0': 5, '1': 0, '2': 3 },
45+
b: { '0': [1, 0], '1': [2, 2] },
46+
f: { '0': 1, '1': 0 },
47+
statementMap: {},
48+
},
49+
})
50+
51+
const result = await getCodeCoverage({ coveragePath })
52+
expect(result.statements).toEqual({
53+
covered: 2,
54+
total: 3,
55+
percent: '66.67',
56+
})
57+
expect(result.branches).toEqual({
58+
covered: 3,
59+
total: 4,
60+
percent: '75.00',
61+
})
62+
expect(result.functions).toEqual({
63+
covered: 1,
64+
total: 2,
65+
percent: '50.00',
66+
})
67+
// Lines are aggregated from statements.
68+
expect(result.lines).toEqual({ covered: 2, total: 3, percent: '66.67' })
69+
})
70+
71+
it('returns 0.00% for empty coverage data', async () => {
72+
const coveragePath = writeCoverageFile({})
73+
const result = await getCodeCoverage({ coveragePath })
74+
expect(result.statements.percent).toBe('0.00')
75+
expect(result.branches.percent).toBe('0.00')
76+
expect(result.functions.percent).toBe('0.00')
77+
expect(result.lines.percent).toBe('0.00')
78+
})
79+
80+
it('aggregates across multiple files', async () => {
81+
const coveragePath = writeCoverageFile({
82+
'/a.ts': {
83+
s: { '0': 1 },
84+
b: {},
85+
f: { '0': 1 },
86+
},
87+
'/b.ts': {
88+
s: { '0': 0, '1': 2 },
89+
b: {},
90+
f: { '0': 0 },
91+
},
92+
})
93+
const result = await getCodeCoverage({ coveragePath })
94+
// Total stmts: 3 (1 from a + 2 from b), covered: 2.
95+
expect(result.statements.covered).toBe(2)
96+
expect(result.statements.total).toBe(3)
97+
})
98+
99+
it('skips entries that are not objects', async () => {
100+
const coveragePath = writeCoverageFile({
101+
'/a.ts': null,
102+
'/b.ts': 'not an object',
103+
'/c.ts': { s: { '0': 1 }, b: {}, f: {} },
104+
})
105+
const result = await getCodeCoverage({ coveragePath })
106+
expect(result.statements.covered).toBe(1)
107+
expect(result.statements.total).toBe(1)
108+
})
109+
110+
it('skips non-object s/b/f buckets', async () => {
111+
const coveragePath = writeCoverageFile({
112+
'/a.ts': { s: 'not an object', b: null, f: undefined },
113+
})
114+
const result = await getCodeCoverage({ coveragePath })
115+
expect(result.statements.total).toBe(0)
116+
})
117+
118+
it('skips non-array branch buckets', async () => {
119+
const coveragePath = writeCoverageFile({
120+
'/a.ts': { b: { '0': 'not array', '1': [1, 2] } },
121+
})
122+
const result = await getCodeCoverage({ coveragePath })
123+
expect(result.branches.total).toBe(2)
124+
})
125+
126+
it('skips non-number counts', async () => {
127+
const coveragePath = writeCoverageFile({
128+
'/a.ts': {
129+
s: { '0': 'not a number', '1': 1 },
130+
f: { '0': null, '1': 2 },
131+
b: { '0': [1, 'bad', 3] },
132+
},
133+
})
134+
const result = await getCodeCoverage({ coveragePath })
135+
// Only valid number counts are counted.
136+
expect(result.statements.total).toBe(1)
137+
expect(result.functions.total).toBe(1)
138+
expect(result.branches.total).toBe(2)
139+
})
140+
141+
it('throws when coverage file does not exist and generateIfMissing=false', async () => {
142+
const coveragePath = path.join(tmpDir, 'missing.json')
143+
await expect(getCodeCoverage({ coveragePath })).rejects.toThrow(
144+
/Coverage file not found/,
145+
)
146+
})
147+
148+
it('runs vitest when coverage file is missing and generateIfMissing=true', async () => {
149+
const coveragePath = path.join(tmpDir, 'will-be-generated.json')
150+
vi.mocked(spawn).mockImplementationOnce(((..._args: any[]) => {
151+
// Simulate the vitest run creating the file.
152+
writeFileSync(coveragePath, JSON.stringify({}))
153+
const promise = Promise.resolve({
154+
code: 0,
155+
signal: null,
156+
stdout: Buffer.from(''),
157+
stderr: Buffer.from(''),
158+
}) as any
159+
promise.process = null
160+
promise.stdin = null
161+
return promise
162+
}) as any)
163+
const result = await getCodeCoverage({
164+
coveragePath,
165+
generateIfMissing: true,
166+
})
167+
expect(vi.mocked(spawn)).toHaveBeenCalledWith(
168+
'vitest',
169+
['run', '--coverage'],
170+
expect.objectContaining({ stdio: 'inherit' }),
171+
)
172+
expect(result.statements.percent).toBe('0.00')
173+
})
174+
175+
it('throws when coverage data is not an object', async () => {
176+
const coveragePath = writeCoverageFile('not an object')
177+
await expect(getCodeCoverage({ coveragePath })).rejects.toThrow(
178+
/Invalid coverage data format/,
179+
)
180+
})
181+
182+
it('throws when coveragePath is empty', async () => {
183+
await expect(getCodeCoverage({ coveragePath: '' })).rejects.toThrow(
184+
/Coverage path is required/,
185+
)
186+
})
187+
188+
it('uses default coveragePath when none provided', async () => {
189+
// Default is cwd/coverage/coverage-final.json. Either it exists (we
190+
// get a result) or it doesn't (we get the missing-file error). Both
191+
// are acceptable — the test exists to confirm the option default is
192+
// wired in.
193+
try {
194+
const result = await getCodeCoverage()
195+
expect(result).toHaveProperty('statements')
196+
} catch (e) {
197+
expect((e as Error).message).toMatch(/Coverage file not found/)
198+
}
199+
})
200+
})
201+
})

0 commit comments

Comments
 (0)