coverage-partitions.spec.ts 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229
  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 }
  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(runCommand).toHaveBeenCalledTimes(3)
  129. })
  130. it('rejects a missing partition blob before merge', async () => {
  131. const root = await temporaryRoot()
  132. const runCommand = vi.fn(async (command: CoverageCommand) => {
  133. if (command.label !== 'partition 2/2') await writeBlob(command)
  134. return passed
  135. })
  136. const coordinator = new CoveragePartitionCoordinator({
  137. root,
  138. partitions: 2,
  139. pnpmEntrypoint: '/pnpm.cjs',
  140. runCommand,
  141. })
  142. await expect(coordinator.run()).rejects.toThrow('coverage partitions produced')
  143. expect(runCommand).toHaveBeenCalledTimes(2)
  144. })
  145. it('reports signal termination before missing-blob validation', async () => {
  146. const root = await temporaryRoot()
  147. const reported = vi.spyOn(console, 'error').mockImplementation(() => undefined)
  148. const runCommand = vi.fn(async (command: CoverageCommand) => {
  149. if (command.label === 'partition 1/2') await writeBlob(command)
  150. return command.label === 'partition 2/2'
  151. ? { exitCode: null, signalCode: 'SIGTERM' as const }
  152. : passed
  153. })
  154. const coordinator = new CoveragePartitionCoordinator({
  155. root,
  156. partitions: 2,
  157. pnpmEntrypoint: '/pnpm.cjs',
  158. runCommand,
  159. })
  160. await expect(coordinator.run()).rejects.toThrow('coverage partitions produced')
  161. expect(reported).toHaveBeenCalledWith('coverage-partitions: FAIL partition 2/2 (signal SIGTERM)')
  162. })
  163. it('waits for every partition after one spawn failure', async () => {
  164. const root = await temporaryRoot()
  165. const reported = vi.spyOn(console, 'error').mockImplementation(() => undefined)
  166. let secondFinished = false
  167. const runCommand = vi.fn(async (command: CoverageCommand) => {
  168. await writeBlob(command)
  169. if (command.label === 'partition 1/2') {
  170. return { exitCode: null, signalCode: null, error: 'spawn unavailable' }
  171. }
  172. if (command.label === 'partition 2/2') secondFinished = true
  173. return passed
  174. })
  175. const coordinator = new CoveragePartitionCoordinator({
  176. root,
  177. partitions: 2,
  178. pnpmEntrypoint: '/pnpm.cjs',
  179. runCommand,
  180. })
  181. await expect(coordinator.run()).resolves.toBe(1)
  182. expect(reported).toHaveBeenCalledWith('coverage-partitions: FAIL partition 1/2 (spawn unavailable)')
  183. expect(secondFinished).toBe(true)
  184. expect(runCommand).toHaveBeenCalledTimes(3)
  185. })
  186. it('unlinks a link-shaped coverage path without touching its target', async () => {
  187. const root = await temporaryRoot()
  188. const target = await temporaryRoot()
  189. const marker = join(target, 'marker.txt')
  190. await writeFile(marker, 'owned elsewhere')
  191. await symlink(target, join(root, 'coverage'), process.platform === 'win32' ? 'junction' : 'dir')
  192. const runCommand = vi.fn(async (command: CoverageCommand) => {
  193. await writeBlob(command)
  194. return passed
  195. })
  196. const coordinator = new CoveragePartitionCoordinator({
  197. root,
  198. partitions: 2,
  199. pnpmEntrypoint: '/pnpm.cjs',
  200. runCommand,
  201. })
  202. await expect(coordinator.run()).resolves.toBe(0)
  203. await expect(access(marker)).resolves.toBeUndefined()
  204. })
  205. })