find-flakey-tests-inf.mjs 4.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164
  1. #!/usr/bin/env node
  2. import { spawn } from 'node:child_process'
  3. import {
  4. appendFileSync,
  5. writeFileSync,
  6. readFileSync,
  7. unlinkSync,
  8. } from 'node:fs'
  9. import { dirname, join } from 'node:path'
  10. import { fileURLToPath } from 'node:url'
  11. import { tmpdir } from 'node:os'
  12. import { randomUUID } from 'node:crypto'
  13. const rootDir = join(dirname(fileURLToPath(import.meta.url)), '..')
  14. const logFile = process.env.FLAKEY_LOG_FILE || join(rootDir, 'flakey-test-runs.log')
  15. const jsonlFile = process.env.FLAKEY_JSONL_FILE || join(rootDir, 'flakey-test-runs.jsonl')
  16. function formatFailure (failure) {
  17. const err = failure.err || {}
  18. const lines = [
  19. `FAILURE: ${failure.fullTitle || failure.title}`,
  20. ` file: ${failure.file || 'unknown'}`,
  21. ` message: ${(err.message || 'unknown').trim()}`,
  22. ]
  23. if (err.stack) {
  24. lines.push(' stack:')
  25. for (const line of err.stack.split('\n').slice(0, 8)) {
  26. lines.push(` ${line}`)
  27. }
  28. }
  29. return lines.join('\n')
  30. }
  31. function appendRunLog (run, exitCode, report, durationMs, spawnError) {
  32. const timestamp = new Date().toISOString()
  33. const stats = report?.stats || {}
  34. const passes = stats.passes ?? '?'
  35. const failures = stats.failures ?? '?'
  36. const pending = stats.pending ?? 0
  37. const passed = exitCode === 0 && !spawnError
  38. const result = passed ? 'PASS' : 'FAIL'
  39. const durationSec = (durationMs / 1000).toFixed(1)
  40. const block = [
  41. `=== RUN ${run} | ${timestamp} | ${result} | ${passes} pass ${failures} fail ${pending} pending | ${durationSec}s ===`,
  42. ]
  43. if (spawnError) {
  44. block.push(`SPAWN_ERROR: ${spawnError}`)
  45. }
  46. if (report?.failures?.length) {
  47. for (const failure of report.failures) {
  48. block.push(formatFailure(failure))
  49. }
  50. } else if (!passed && !report) {
  51. block.push('No JSON report captured (mocha may have crashed before writing results)')
  52. }
  53. block.push('')
  54. appendFileSync(logFile, `${block.join('\n')}\n`)
  55. const jsonl = {
  56. run,
  57. timestamp,
  58. exitCode,
  59. durationMs,
  60. passes,
  61. failures,
  62. pending,
  63. failureDetails: (report?.failures || []).map((failure) => ({
  64. fullTitle: failure.fullTitle || failure.title,
  65. file: failure.file,
  66. message: failure.err?.message,
  67. stack: failure.err?.stack,
  68. })),
  69. }
  70. appendFileSync(jsonlFile, `${JSON.stringify(jsonl)}\n`)
  71. }
  72. function runMochaOnce () {
  73. const reportPath = join(tmpdir(), `mocha-flakey-${randomUUID()}.json`)
  74. return new Promise((resolve) => {
  75. const proc = spawn('npx', [
  76. 'mocha',
  77. 'tests',
  78. '--recursive',
  79. '-t',
  80. '10000',
  81. '--reporter',
  82. 'json',
  83. '--reporter-option',
  84. `output=${reportPath}`,
  85. ], {
  86. cwd: rootDir,
  87. stdio: ['ignore', 'inherit', 'inherit'],
  88. })
  89. proc.on('close', (exitCode) => {
  90. let report = null
  91. try {
  92. report = JSON.parse(readFileSync(reportPath, 'utf8'))
  93. } catch {
  94. report = null
  95. }
  96. try {
  97. unlinkSync(reportPath)
  98. } catch {
  99. // ignore missing temp report
  100. }
  101. resolve({ exitCode: exitCode ?? 1, report })
  102. })
  103. proc.on('error', (spawnError) => {
  104. resolve({ exitCode: 1, report: null, spawnError: spawnError.message })
  105. })
  106. })
  107. }
  108. async function main () {
  109. const header = [
  110. `# Flaky test run log started ${new Date().toISOString()}`,
  111. `# Log file: ${logFile}`,
  112. `# JSONL file: ${jsonlFile}`,
  113. '',
  114. ].join('\n')
  115. writeFileSync(logFile, `${header}\n`)
  116. writeFileSync(jsonlFile, '')
  117. console.log(`Logging flaky test runs to ${logFile}`)
  118. console.log(`Structured run data: ${jsonlFile}`)
  119. let run = 0
  120. while (true) {
  121. run += 1
  122. console.log(`\n--- Starting run ${run} ---`)
  123. const start = Date.now()
  124. const { exitCode, report, spawnError } = await runMochaOnce()
  125. const durationMs = Date.now() - start
  126. appendRunLog(run, exitCode, report, durationMs, spawnError)
  127. const summary = exitCode === 0 ? 'PASS' : 'FAIL'
  128. console.log(`Run ${run}: ${summary} (${(durationMs / 1000).toFixed(1)}s) — logged`)
  129. if (exitCode !== 0) {
  130. console.log(`Failure on run ${run}, stopping. See ${logFile}`)
  131. process.exit(exitCode)
  132. }
  133. }
  134. }
  135. main().catch((err) => {
  136. console.error(err)
  137. process.exit(1)
  138. })