coverage-partitions.spec.ts 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232
  1. import { access, mkdir, mkdtemp, symlink, writeFile } from 'node:fs/promises'
  2. import { tmpdir } from 'node:os'
  3. import { dirname, join } from 'node:path'
  4. import { afterEach, describe, expect, it, vi } from 'vitest'
  5. import {
  6. COVERAGE_PARTITION_MODE_ENV,
  7. COVERAGE_PARTITIONS_ENV,
  8. COVERAGE_TEST_TIMEOUT_ENV,
  9. CoveragePartitionCoordinator,
  10. coverageTestTimeoutArgs,
  11. forwardedCoverageArgs,
  12. parseCoveragePartitionCount,
  13. type CoverageCommand,
  14. type CoverageCommandResult,
  15. } from './coverage-partitions.ts'
  16. const passed: CoverageCommandResult = { exitCode: 0, signalCode: null }
  17. afterEach(() => vi.restoreAllMocks())
  18. async function writeBlob(command: CoverageCommand): Promise<void> {
  19. if (command.blobPath === undefined) return
  20. await mkdir(dirname(command.blobPath), { recursive: true })
  21. await writeFile(command.blobPath, '{}')
  22. }
  23. async function temporaryRoot(): Promise<string> {
  24. return await mkdtemp(join(tmpdir(), 'dsh-coverage-partitions-'))
  25. }
  26. describe('coverage partition count', () => {
  27. it.each([
  28. [undefined, undefined],
  29. ['', undefined],
  30. ['2', 2],
  31. ['3', 3],
  32. ])('parses %j as %j', (raw, expected) => {
  33. expect(parseCoveragePartitionCount(raw)).toBe(expected)
  34. })
  35. it.each(['0', '1', '2.5', '02', 'many'])('rejects %j', (raw) => {
  36. expect(() => parseCoveragePartitionCount(raw))
  37. .toThrow(`${COVERAGE_PARTITIONS_ENV} must be an integer greater than 1`)
  38. })
  39. })
  40. describe('coverage partition timeout', () => {
  41. it('applies one configured timeout to tests and polling', () => {
  42. expect(coverageTestTimeoutArgs('30000')).toEqual([
  43. '--testTimeout=30000',
  44. '--expect.poll.timeout=30000',
  45. ])
  46. })
  47. it('keeps Vitest defaults when the timeout is absent', () => {
  48. expect(coverageTestTimeoutArgs(undefined)).toEqual([])
  49. })
  50. it('rejects invalid timeout input', () => {
  51. expect(() => coverageTestTimeoutArgs('0'))
  52. .toThrow(`${COVERAGE_TEST_TIMEOUT_ENV} must be a positive integer`)
  53. })
  54. })
  55. describe('coverage forwarded arguments', () => {
  56. it('removes one package-script separator', () => {
  57. expect(forwardedCoverageArgs(['--', 'scripts/example.spec.ts'])).toEqual(['scripts/example.spec.ts'])
  58. })
  59. it('preserves direct arguments and a subsequent Vitest separator', () => {
  60. expect(forwardedCoverageArgs(['--testNamePattern=example'])).toEqual(['--testNamePattern=example'])
  61. expect(forwardedCoverageArgs(['--', '--', 'example'])).toEqual(['--', 'example'])
  62. })
  63. })
  64. describe('coverage partition coordinator', () => {
  65. it('runs every single-worker partition before one merged threshold check', async () => {
  66. const root = await temporaryRoot()
  67. const commands: CoverageCommand[] = []
  68. const runCommand = vi.fn(async (command: CoverageCommand) => {
  69. commands.push(command)
  70. await writeBlob(command)
  71. return passed
  72. })
  73. const coordinator = new CoveragePartitionCoordinator({
  74. root,
  75. partitions: 3,
  76. pnpmEntrypoint: '/pnpm.cjs',
  77. vitestArgs: ['--testTimeout=30000'],
  78. runCommand,
  79. })
  80. await expect(coordinator.run()).resolves.toBe(0)
  81. expect(commands.map(command => command.label)).toEqual([
  82. 'partition 1/3',
  83. 'partition 2/3',
  84. 'partition 3/3',
  85. 'merged coverage report',
  86. ])
  87. for (const [index, command] of commands.slice(0, 3).entries()) {
  88. expect(command.args).toEqual(expect.arrayContaining([
  89. '--coverage',
  90. '--coverage.reportOnFailure',
  91. '--maxWorkers=1',
  92. `--shard=${index + 1}/3`,
  93. '--reporter=default',
  94. '--reporter=blob',
  95. '--testTimeout=30000',
  96. ]))
  97. expect(command.env).toEqual({
  98. [COVERAGE_PARTITIONS_ENV]: undefined,
  99. [COVERAGE_PARTITION_MODE_ENV]: '1',
  100. })
  101. }
  102. const mergeCommand = commands[3]
  103. if (mergeCommand === undefined) throw new Error('coverage merge command was not observed')
  104. expect(mergeCommand.args).toContain('--coverage')
  105. expect(mergeCommand.args.some(argument => argument.startsWith('--merge-reports='))).toBe(true)
  106. expect(mergeCommand.env).toEqual({
  107. [COVERAGE_PARTITIONS_ENV]: undefined,
  108. [COVERAGE_PARTITION_MODE_ENV]: undefined,
  109. })
  110. })
  111. it('merges normal test failures and returns their failed status', async () => {
  112. const root = await temporaryRoot()
  113. const reported = vi.spyOn(console, 'error').mockImplementation(() => undefined)
  114. const runCommand = vi.fn(async (command: CoverageCommand) => {
  115. await writeBlob(command)
  116. return command.label === 'partition 2/2'
  117. ? { exitCode: 1, signalCode: null, outputTail: 'specific Vitest failure' }
  118. : passed
  119. })
  120. const coordinator = new CoveragePartitionCoordinator({
  121. root,
  122. partitions: 2,
  123. pnpmEntrypoint: '/pnpm.cjs',
  124. runCommand,
  125. })
  126. await expect(coordinator.run()).resolves.toBe(1)
  127. expect(reported).toHaveBeenCalledWith('coverage-partitions: FAIL partition 2/2 (exit 1)')
  128. expect(reported).toHaveBeenCalledWith(
  129. 'coverage-partitions: output tail for partition 2/2:\nspecific Vitest failure',
  130. )
  131. expect(runCommand).toHaveBeenCalledTimes(3)
  132. })
  133. it('rejects a missing partition blob before merge', async () => {
  134. const root = await temporaryRoot()
  135. const runCommand = vi.fn(async (command: CoverageCommand) => {
  136. if (command.label !== 'partition 2/2') await writeBlob(command)
  137. return passed
  138. })
  139. const coordinator = new CoveragePartitionCoordinator({
  140. root,
  141. partitions: 2,
  142. pnpmEntrypoint: '/pnpm.cjs',
  143. runCommand,
  144. })
  145. await expect(coordinator.run()).rejects.toThrow('coverage partitions produced')
  146. expect(runCommand).toHaveBeenCalledTimes(2)
  147. })
  148. it('reports signal termination before missing-blob validation', async () => {
  149. const root = await temporaryRoot()
  150. const reported = vi.spyOn(console, 'error').mockImplementation(() => undefined)
  151. const runCommand = vi.fn(async (command: CoverageCommand) => {
  152. if (command.label === 'partition 1/2') await writeBlob(command)
  153. return command.label === 'partition 2/2'
  154. ? { exitCode: null, signalCode: 'SIGTERM' as const }
  155. : passed
  156. })
  157. const coordinator = new CoveragePartitionCoordinator({
  158. root,
  159. partitions: 2,
  160. pnpmEntrypoint: '/pnpm.cjs',
  161. runCommand,
  162. })
  163. await expect(coordinator.run()).rejects.toThrow('coverage partitions produced')
  164. expect(reported).toHaveBeenCalledWith('coverage-partitions: FAIL partition 2/2 (signal SIGTERM)')
  165. })
  166. it('waits for every partition after one spawn failure', async () => {
  167. const root = await temporaryRoot()
  168. const reported = vi.spyOn(console, 'error').mockImplementation(() => undefined)
  169. let secondFinished = false
  170. const runCommand = vi.fn(async (command: CoverageCommand) => {
  171. await writeBlob(command)
  172. if (command.label === 'partition 1/2') {
  173. return { exitCode: null, signalCode: null, error: 'spawn unavailable' }
  174. }
  175. if (command.label === 'partition 2/2') secondFinished = true
  176. return passed
  177. })
  178. const coordinator = new CoveragePartitionCoordinator({
  179. root,
  180. partitions: 2,
  181. pnpmEntrypoint: '/pnpm.cjs',
  182. runCommand,
  183. })
  184. await expect(coordinator.run()).resolves.toBe(1)
  185. expect(reported).toHaveBeenCalledWith('coverage-partitions: FAIL partition 1/2 (spawn unavailable)')
  186. expect(secondFinished).toBe(true)
  187. expect(runCommand).toHaveBeenCalledTimes(3)
  188. })
  189. it('unlinks a link-shaped coverage path without touching its target', async () => {
  190. const root = await temporaryRoot()
  191. const target = await temporaryRoot()
  192. const marker = join(target, 'marker.txt')
  193. await writeFile(marker, 'owned elsewhere')
  194. await symlink(target, join(root, 'coverage'), process.platform === 'win32' ? 'junction' : 'dir')
  195. const runCommand = vi.fn(async (command: CoverageCommand) => {
  196. await writeBlob(command)
  197. return passed
  198. })
  199. const coordinator = new CoveragePartitionCoordinator({
  200. root,
  201. partitions: 2,
  202. pnpmEntrypoint: '/pnpm.cjs',
  203. runCommand,
  204. })
  205. await expect(coordinator.run()).resolves.toBe(0)
  206. await expect(access(marker)).resolves.toBeUndefined()
  207. })
  208. })