All files / scripts test-budget-reporter.mjs

98.33% Statements 59/60
90.38% Branches 47/52
85.71% Functions 6/7
98.27% Lines 57/58

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132      1x     9x 9x 9x 9x 9x 9x 9x 1x   9x 9x 2x 9x 9x       9x 9x 9x 9x         9x 9x       9x 9x   9x                 9x 9x 9x 9x 9x   9x 9x 9x     9x 9x 9x 9x   9x       9x                                     9x 9x   9x 1x 1x     9x 6x       3x 3x   9x 9x 9x 9x         9x 3x         9x 2x 1x       1x       2x        
import { mkdirSync, writeFileSync } from "node:fs";
import { dirname, relative } from "node:path";
 
const TOP_TEST_COUNT = 10;
 
function readBudgetConfiguration(env) {
  const twoThresholdMode = env.TEST_BUDGET_WARN_MS !== undefined || env.TEST_BUDGET_FAIL_MS !== undefined;
  const legacyBudgetConfigured = env.TEST_BUDGET_MS !== undefined;
  const legacyBudgetMs = Number(env.TEST_BUDGET_MS ?? 0);
  const warningBudgetMs = env.TEST_BUDGET_WARN_MS === undefined ? null : Number(env.TEST_BUDGET_WARN_MS);
  const failureBudgetMs = env.TEST_BUDGET_FAIL_MS === undefined ? null : Number(env.TEST_BUDGET_FAIL_MS);
  let configurationError = null;
  if (twoThresholdMode && (warningBudgetMs <= 0 || failureBudgetMs <= 0 || failureBudgetMs < warningBudgetMs)) {
    configurationError = "TEST_BUDGET_WARN_MS und TEST_BUDGET_FAIL_MS müssen positiv sein; die Hard-Fail-Schwelle muss mindestens der Warnschwelle entsprechen.";
  }
  let effectiveFailureBudgetMs = null;
  if (twoThresholdMode) effectiveFailureBudgetMs = failureBudgetMs;
  else Eif (legacyBudgetConfigured && legacyBudgetMs > 0) effectiveFailureBudgetMs = legacyBudgetMs;
  const budgetMs = effectiveFailureBudgetMs;
  return { twoThresholdMode, warningBudgetMs: twoThresholdMode ? warningBudgetMs : null, effectiveFailureBudgetMs, budgetMs, configurationError };
}
 
function summarizeBudget(durationMs, configuration) {
  const { twoThresholdMode, warningBudgetMs, effectiveFailureBudgetMs, configurationError } = configuration;
  const warningExceeded = !configurationError && twoThresholdMode && durationMs > warningBudgetMs;
  const failureExceeded = !configurationError && effectiveFailureBudgetMs > 0 && durationMs > effectiveFailureBudgetMs;
  return { warningExceeded, failureExceeded };
}
 
export default class TestBudgetReporter {
  constructor() {
    this.startedAt = Date.now();
    this.tests = [];
  }
 
  onTestCaseResult(testCase) {
    const diagnostic = testCase.diagnostic();
    const result = testCase.result();
 
    this.tests.push({
      name: testCase.fullName,
      file: relative(process.cwd(), testCase.module.moduleId),
      durationMs: Math.round(diagnostic?.duration ?? 0),
      state: result.state,
    });
  }
 
  onTestRunEnd(_testModules, unhandledErrors, reason) {
    const finishedAt = Date.now();
    const durationMs = finishedAt - this.startedAt;
    const suite = process.env.TEST_BUDGET_SUITE ?? "vitest";
    const configuration = readBudgetConfiguration(process.env);
    const { twoThresholdMode, warningBudgetMs, effectiveFailureBudgetMs, budgetMs, configurationError } = configuration;
    const artifactPath =
      process.env.TEST_METRICS_FILE ?? `test-results/${suite}-metrics.json`;
    const effectiveWarningBudgetMs = warningBudgetMs;
    const { warningExceeded, failureExceeded } = summarizeBudget(durationMs, configuration);
    // In two-threshold mode the legacy field represents the hard-fail limit,
    // so existing consumers continue to interpret this as a blocking failure.
    const budgetExceeded = failureExceeded;
    const counts = this.tests.reduce((result, test) => {
      result[test.state] = (result[test.state] ?? 0) + 1;
      return result;
    }, {});
    const slowestTests = [...this.tests]
      .sort((left, right) => right.durationMs - left.durationMs)
      .slice(0, TOP_TEST_COUNT);
 
    const artifact = {
      schemaVersion: 1,
      suite,
      budgetMs,
      budgetExceeded,
      warningBudgetMs: effectiveWarningBudgetMs,
      failureBudgetMs: effectiveFailureBudgetMs,
      warningExceeded,
      failureExceeded,
      configurationError,
      startedAt: new Date(this.startedAt).toISOString(),
      finishedAt: new Date(finishedAt).toISOString(),
      durationMs,
      status: reason,
      counts,
      unhandledErrorCount: unhandledErrors.length,
      slowestTests,
    };
 
    mkdirSync(dirname(artifactPath), { recursive: true });
    writeFileSync(artifactPath, `${JSON.stringify(artifact, null, 2)}\n`);
 
    if (configurationError) {
      console.error(`Ungültige Testbudget-Konfiguration: ${configurationError}`);
      process.exitCode = 1;
    }
 
    if (twoThresholdMode && !configurationError) {
      console.log(
        `\nTestbudget [${suite}]: ${durationMs} ms | Warnschwelle: ${warningBudgetMs} ms | Hard Fail: ${effectiveFailureBudgetMs} ms`,
      );
    } else {
      const budgetLabel = budgetMs ? `${budgetMs} ms` : "deaktiviert";
      console.log(`\nTestbudget [${suite}]: ${durationMs} ms / ${budgetLabel}`);
    }
    console.log(`Messartefakt: ${artifactPath}`);
    console.log(`Langsamste ${slowestTests.length} Tests:`);
    for (const test of slowestTests) {
      console.log(
        `  ${test.durationMs.toString().padStart(6)} ms  ${test.name}`,
      );
    }
 
    if (warningExceeded && !failureExceeded) {
      console.warn(
        `Testbudget-Warnschwelle überschritten: ${suite} benötigte ${durationMs} ms, Warnschwelle ${warningBudgetMs} ms, Hard-Fail-Schwelle ${effectiveFailureBudgetMs} ms.`,
      );
    }
 
    if (failureExceeded) {
      if (twoThresholdMode) {
        console.error(
          `Testbudget-Hard-Limit überschritten: ${suite} benötigte ${durationMs} ms, erlaubt sind maximal ${effectiveFailureBudgetMs} ms.`,
        );
      } else {
        console.error(
          `Testbudget ueberschritten: ${suite} benoetigte ${durationMs} ms, erlaubt sind ${budgetMs} ms.`,
        );
      }
      process.exitCode = 1;
    }
  }
}