runUnitTest.ts 2.4 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091
  1. import * as path from "path"
  2. import { execa, parseCommandString } from "execa"
  3. import psTree from "ps-tree"
  4. import type { Task } from "../db/index"
  5. import { type ExerciseLanguage, EVALS_REPO_PATH } from "../exercises/index"
  6. import { Logger } from "./utils"
  7. const UNIT_TEST_TIMEOUT = 2 * 60 * 1_000
  8. const testCommands: Record<ExerciseLanguage, { commands: string[]; timeout?: number }> = {
  9. go: { commands: ["go test"] },
  10. java: { commands: ["./gradlew test"] },
  11. javascript: { commands: ["pnpm install", "pnpm test"] },
  12. python: { commands: ["uv run python3 -m pytest -o markers=task *_test.py"] },
  13. rust: { commands: ["cargo test"] },
  14. }
  15. type RunUnitTestOptions = {
  16. task: Task
  17. logger: Logger
  18. }
  19. export const runUnitTest = async ({ task, logger }: RunUnitTestOptions) => {
  20. const cmd = testCommands[task.language]
  21. const cwd = path.resolve(EVALS_REPO_PATH, task.language, task.exercise)
  22. const commands = cmd.commands.map((cs) => parseCommandString(cs))
  23. let passed = true
  24. for (const command of commands) {
  25. try {
  26. logger.info(`running "${command.join(" ")}"`)
  27. const subprocess = execa({ cwd, shell: "/bin/bash", reject: false })`${command}`
  28. subprocess.stdout.pipe(process.stdout)
  29. subprocess.stderr.pipe(process.stderr)
  30. const timeout = setTimeout(async () => {
  31. const descendants = await new Promise<number[]>((resolve, reject) => {
  32. psTree(subprocess.pid!, (err, children) => {
  33. if (err) {
  34. reject(err)
  35. }
  36. resolve(children.map((p) => parseInt(p.PID)))
  37. })
  38. })
  39. logger.info(
  40. `"${command.join(" ")}" timed out, killing ${subprocess.pid} + ${JSON.stringify(descendants)}`,
  41. )
  42. if (descendants.length > 0) {
  43. for (const descendant of descendants) {
  44. try {
  45. logger.info(`killing descendant process ${descendant}`)
  46. await execa`kill -9 ${descendant}`
  47. } catch (error) {
  48. logger.error(`failed to kill descendant process ${descendant}:`, error)
  49. }
  50. }
  51. }
  52. logger.info(`killing main process ${subprocess.pid}`)
  53. try {
  54. await execa`kill -9 ${subprocess.pid!}`
  55. } catch (error) {
  56. logger.error(`failed to kill main process ${subprocess.pid}:`, error)
  57. }
  58. }, UNIT_TEST_TIMEOUT)
  59. const result = await subprocess
  60. clearTimeout(timeout)
  61. if (result.failed) {
  62. passed = false
  63. break
  64. }
  65. } catch (error) {
  66. logger.error(`unexpected error:`, error)
  67. passed = false
  68. break
  69. }
  70. }
  71. return passed
  72. }