| import { exec } from "child_process"; |
| import { promisify } from "util"; |
| import Handlebars from "handlebars"; |
| import type { Task, Validation } from "./model.js"; |
| import { escapeRegex } from "./utils.js"; |
|
|
| const execAsync = promisify(exec); |
|
|
| |
| Handlebars.registerHelper("escapeRegex", escapeRegex); |
|
|
| export type ValidationResult = { |
| name: string; |
| passed: boolean; |
| message: string; |
| }; |
|
|
| |
| |
| |
| function validateRegex( |
| output: string, |
| regex: string, |
| name: string, |
| ): ValidationResult { |
| const pattern = new RegExp(regex); |
| const passed = pattern.test(output); |
|
|
| return { |
| name, |
| passed, |
| message: passed ? `Matched: ${regex}` : `Did not match: ${regex}`, |
| }; |
| } |
|
|
| |
| |
| |
| async function validateShellCommand( |
| output: string, |
| command: string, |
| expectedExitCode: number, |
| name: string, |
| ): Promise<ValidationResult> { |
| try { |
| const { spawn } = await import("child_process"); |
|
|
| |
| const result = await new Promise<{ code: number }>((resolve, reject) => { |
| const child = spawn(command, { |
| shell: true, |
| stdio: ["pipe", "pipe", "pipe"], |
| }); |
|
|
| let processExited = false; |
|
|
| |
| child.stdin.on("error", (err: NodeJS.ErrnoException) => { |
| |
| |
| if (err.code !== "EPIPE") { |
| reject(err); |
| } |
| }); |
|
|
| child.on("close", (code) => { |
| processExited = true; |
| resolve({ code: code ?? 0 }); |
| }); |
|
|
| child.on("error", (err) => { |
| reject(err); |
| }); |
|
|
| |
| |
| setImmediate(() => { |
| if (!processExited && child.stdin.writable) { |
| child.stdin.write(output, (err?: Error | null) => { |
| if (err && (err as NodeJS.ErrnoException).code !== "EPIPE") { |
| |
| reject(err); |
| } else { |
| child.stdin.end(); |
| } |
| }); |
| } else { |
| |
| child.stdin.end(); |
| } |
| }); |
| }); |
|
|
| |
| const passed = result.code === expectedExitCode; |
| return { |
| name, |
| passed, |
| message: passed |
| ? `Command succeeded with exit code ${result.code}` |
| : `Expected exit code ${expectedExitCode}, got ${result.code}`, |
| }; |
| } catch (error: any) { |
| |
| return { |
| name, |
| passed: false, |
| message: `Command failed: ${error.message}`, |
| }; |
| } |
| } |
|
|
| |
| |
| |
| export async function runValidations( |
| output: string, |
| validations: Array<Validation>, |
| context?: Record<string, string>, |
| ): Promise<ValidationResult[]> { |
| const results: ValidationResult[] = []; |
|
|
| for (const validation of validations) { |
| if (validation.type === "regex") { |
| |
| let regex = validation.regex; |
| if (context) { |
| const template = Handlebars.compile(regex, { strict: true }); |
| regex = template(context); |
| } |
| results.push(validateRegex(output, regex, validation.name)); |
| } else if (validation.type === "shell") { |
| |
| let command = validation.command; |
| if (context) { |
| const template = Handlebars.compile(command, { strict: true }); |
| command = template(context); |
| } |
| const expectedExitCode = validation.exit_code ?? 0; |
| results.push( |
| await validateShellCommand( |
| output, |
| command, |
| expectedExitCode, |
| validation.name, |
| ), |
| ); |
| } |
| } |
|
|
| return results; |
| } |
|
|
| |
| |
| |
| export function allValidationsPassed(results: ValidationResult[]): boolean { |
| return results.every((result) => result.passed); |
| } |
|
|
| |
| |
| |
| export function countPassed(results: ValidationResult[]): number { |
| return results.filter((result) => result.passed).length; |
| } |
|
|
| export type ProcessValidationsResult = { |
| validationResults: ValidationResult[]; |
| status: "passed" | "validation_failed"; |
| }; |
|
|
| |
| |
| |
| export async function processValidations( |
| output: string | undefined, |
| task: Task, |
| logger: { |
| info: (data: any, message: string) => void; |
| warn: (data: any, message: string) => void; |
| error: (data: any, message: string) => void; |
| }, |
| task_id: number, |
| duration: number, |
| logFile: string, |
| context?: Record<string, string>, |
| ): Promise<ProcessValidationsResult> { |
| |
| const validationResults = |
| task.validations && task.validations.length > 0 && output |
| ? await runValidations(output, task.validations, context) |
| : []; |
|
|
| const allPassed = allValidationsPassed(validationResults); |
| const status = allPassed ? "passed" : "validation_failed"; |
|
|
| |
| if (validationResults.length > 0) { |
| const passedCount = countPassed(validationResults); |
| const totalCount = validationResults.length; |
|
|
| if (allPassed) { |
| logger.info( |
| { |
| task_id, |
| duration, |
| log: logFile, |
| parameters: context, |
| passed: validationResults.map((r) => r.name), |
| }, |
| "Validation passed", |
| ); |
| } else { |
| logger.error( |
| { |
| task_id, |
| duration, |
| log_file: logFile, |
| parameters: context, |
| failed: validationResults |
| .filter((r) => !r.passed) |
| .map((r) => ({ |
| name: r.name, |
| message: r.message, |
| })), |
| summary: `${passedCount}/${totalCount} passed`, |
| }, |
| "Validation Failed", |
| ); |
| } |
| } |
|
|
| return { validationResults, status }; |
| } |
|
|