Anforderungen  |   Konzepte  |   Entwurf  |   Entwicklung  |   Qualitätssicherung  |   Lebenszyklus  |   Steuerung
 
 
 
 


Quelle  test-group-report.mjs   Sprache: unbekannt

 
Spracherkennung für: .mjs vermutete Sprache: Unknown {[0] [0] [0]} [Methode: Schwerpunktbildung, einfache Gewichte, sechs Dimensionen]

import path from "node:path";
import { collectVitestFileDurations, normalizeTrackedRepoPath } from "../test-report-utils.mjs";
import { formatMs } from "./vitest-report-cli-utils.mjs";

export function formatBytesAsMb(valueBytes) {
  return valueBytes === null || valueBytes === undefined
    ? "n/a"
    : `${(valueBytes / 1024 / 1024).toFixed(1)}MB`;
}

export function formatSignedMs(value, digits = 1) {
  return `${value > 0 ? "+" : ""}${formatMs(value, digits)}`;
}

export function formatSignedBytesAsMb(valueBytes) {
  return valueBytes === null || valueBytes === undefined
    ? "n/a"
    : `${valueBytes > 0 ? "+" : ""}${formatBytesAsMb(valueBytes)}`;
}

export function normalizeConfigLabel(config) {
  return config.replace(/^test\/vitest\/vitest\./u, "").replace(/\.config\.ts$/u, "");
}

export function resolveTestArea(file) {
  const normalized = normalizeTrackedRepoPath(file);
  const parts = normalized.split("/");
  if (parts[0] === "extensions" && parts[1]) {
    return `extensions/${parts[1]}`;
  }
  if (parts[0] === "src" && parts[1]) {
    return `src/${parts[1]}`;
  }
  if (parts[0] === "packages" && parts[1]) {
    return `packages/${parts[1]}`;
  }
  if (parts[0] === "apps" && parts[1]) {
    return `apps/${parts[1]}`;
  }
  if (parts[0] === "ui") {
    return parts[3] ? `ui/${parts[3]}` : "ui";
  }
  if (parts[0] === "test" && parts[1]) {
    return `test/${parts[1]}`;
  }
  return parts[0] || normalized;
}

export function resolveTestFolder(file, depth = 2) {
  const normalized = normalizeTrackedRepoPath(file);
  const dir = path.posix.dirname(normalized);
  if (dir === ".") {
    return normalized;
  }
  return dir.split("/").slice(0, Math.max(1, depth)).join("/");
}

export function resolveGroupKey(file, mode = "area") {
  if (mode === "folder") {
    return resolveTestFolder(file, 3);
  }
  if (mode === "top") {
    return normalizeTrackedRepoPath(file).split("/")[0] || file;
  }
  return resolveTestArea(file);
}

function createCounter(key) {
  return {
    key,
    durationMs: 0,
    fileCount: 0,
    testCount: 0,
    configs: new Set(),
  };
}

function addFileEntry(target, entry, config) {
  target.durationMs += entry.durationMs;
  target.fileCount += 1;
  target.testCount += entry.testCount;
  target.configs.add(config);
}

function finalizeCounter(counter) {
  return {
    key: counter.key,
    durationMs: counter.durationMs,
    fileCount: counter.fileCount,
    testCount: counter.testCount,
    configs: [...counter.configs].toSorted((left, right) => left.localeCompare(right)),
  };
}

export function buildGroupedTestReport(params) {
  const byGroup = new Map();
  const byConfig = new Map();
  const files = [];

  for (const input of params.reports) {
    const config = normalizeConfigLabel(input.config);
    const fileEntries = collectVitestFileDurations(input.report, normalizeTrackedRepoPath);
    const configCounter = byConfig.get(config) ?? createCounter(config);
    byConfig.set(config, configCounter);

    for (const entry of fileEntries) {
      const groupKey = resolveGroupKey(entry.file, params.groupBy);
      const groupCounter = byGroup.get(groupKey) ?? createCounter(groupKey);
      byGroup.set(groupKey, groupCounter);
      addFileEntry(groupCounter, entry, config);
      addFileEntry(configCounter, entry, config);
      files.push({ ...entry, config, group: groupKey });
    }
  }

  const sortByDuration = (left, right) =>
    right.durationMs - left.durationMs || left.key.localeCompare(right.key);
  const sortFilesByDuration = (left, right) =>
    right.durationMs - left.durationMs || left.file.localeCompare(right.file);

  const groups = [...byGroup.values()].map(finalizeCounter).toSorted(sortByDuration);
  const configs = [...byConfig.values()].map(finalizeCounter).toSorted(sortByDuration);
  const topFiles = files.toSorted(sortFilesByDuration);
  const totals = groups.reduce(
    (acc, group) => ({
      durationMs: acc.durationMs + group.durationMs,
      fileCount: acc.fileCount + group.fileCount,
      testCount: acc.testCount + group.testCount,
    }),
    { durationMs: 0, fileCount: 0, testCount: 0 },
  );

  return {
    generatedAt: new Date().toISOString(),
    groupBy: params.groupBy,
    totals,
    groups,
    configs,
    topFiles,
  };
}

function percentDelta(beforeValue, afterValue) {
  if (beforeValue === 0) {
    return afterValue === 0 ? 0 : null;
  }
  return ((afterValue - beforeValue) / beforeValue) * 100;
}

function formatPercent(value) {
  if (value === null || value === undefined) {
    return "new";
  }
  return `${value > 0 ? "+" : ""}${value.toFixed(1)}%`;
}

function normalizeCounter(item) {
  return {
    durationMs: item?.durationMs ?? 0,
    fileCount: item?.fileCount ?? 0,
    testCount: item?.testCount ?? 0,
  };
}

function compareStatus(beforeItem, afterItem) {
  if (beforeItem && afterItem) {
    return "changed";
  }
  return beforeItem ? "removed" : "added";
}

function compareCounters(beforeItems = [], afterItems = []) {
  const beforeByKey = new Map(beforeItems.map((item) => [item.key, item]));
  const afterByKey = new Map(afterItems.map((item) => [item.key, item]));
  const keys = new Set([...beforeByKey.keys(), ...afterByKey.keys()]);

  return [...keys]
    .map((key) => {
      const beforeItem = beforeByKey.get(key);
      const afterItem = afterByKey.get(key);
      const before = normalizeCounter(beforeItem);
      const after = normalizeCounter(afterItem);
      return {
        key,
        status: compareStatus(beforeItem, afterItem),
        before,
        after,
        delta: {
          durationMs: after.durationMs - before.durationMs,
          fileCount: after.fileCount - before.fileCount,
          testCount: after.testCount - before.testCount,
        },
        percent: {
          durationMs: percentDelta(before.durationMs, after.durationMs),
        },
      };
    })
    .toSorted(
      (left, right) =>
        Math.abs(right.delta.durationMs) - Math.abs(left.delta.durationMs) ||
        left.key.localeCompare(right.key),
    );
}

function normalizeFileCounter(item) {
  return {
    durationMs: item?.durationMs ?? 0,
    testCount: item?.testCount ?? 0,
  };
}

function fileKey(item) {
  return `${item.config}\0${item.file}`;
}

function compareFiles(beforeFiles = [], afterFiles = []) {
  const beforeByKey = new Map(beforeFiles.map((item) => [fileKey(item), item]));
  const afterByKey = new Map(afterFiles.map((item) => [fileKey(item), item]));
  const keys = new Set([...beforeByKey.keys(), ...afterByKey.keys()]);

  return [...keys]
    .map((key) => {
      const beforeItem = beforeByKey.get(key);
      const afterItem = afterByKey.get(key);
      const before = normalizeFileCounter(beforeItem);
      const after = normalizeFileCounter(afterItem);
      const source = afterItem ?? beforeItem;
      return {
        key,
        config: source.config,
        file: source.file,
        group: source.group,
        status: compareStatus(beforeItem, afterItem),
        before,
        after,
        delta: {
          durationMs: after.durationMs - before.durationMs,
          testCount: after.testCount - before.testCount,
        },
        percent: {
          durationMs: percentDelta(before.durationMs, after.durationMs),
        },
      };
    })
    .toSorted(
      (left, right) =>
        Math.abs(right.delta.durationMs) - Math.abs(left.delta.durationMs) ||
        left.file.localeCompare(right.file) ||
        left.config.localeCompare(right.config),
    );
}

function runKey(run) {
  return normalizeConfigLabel(run.config);
}

function compareOptionalNumber(beforeValue, afterValue) {
  if (typeof beforeValue !== "number" || typeof afterValue !== "number") {
    return null;
  }
  return afterValue - beforeValue;
}

function normalizeRun(run) {
  return run
    ? {
        elapsedMs: typeof run.elapsedMs === "number" ? run.elapsedMs : null,
        maxRssBytes: typeof run.maxRssBytes === "number" ? run.maxRssBytes : null,
        status: typeof run.status === "number" ? run.status : null,
      }
    : {
        elapsedMs: null,
        maxRssBytes: null,
        status: null,
      };
}

function compareRuns(beforeRuns = [], afterRuns = []) {
  const beforeByKey = new Map(beforeRuns.map((run) => [runKey(run), run]));
  const afterByKey = new Map(afterRuns.map((run) => [runKey(run), run]));
  const keys = new Set([...beforeByKey.keys(), ...afterByKey.keys()]);

  return [...keys]
    .map((key) => {
      const beforeRun = beforeByKey.get(key);
      const afterRun = afterByKey.get(key);
      const before = normalizeRun(beforeRun);
      const after = normalizeRun(afterRun);
      return {
        key,
        status: compareStatus(beforeRun, afterRun),
        before,
        after,
        delta: {
          elapsedMs: compareOptionalNumber(before.elapsedMs, after.elapsedMs),
          maxRssBytes: compareOptionalNumber(before.maxRssBytes, after.maxRssBytes),
        },
      };
    })
    .toSorted((left, right) => {
      const leftMagnitude = Math.abs(left.delta.elapsedMs ?? left.delta.maxRssBytes ?? 0);
      const rightMagnitude = Math.abs(right.delta.elapsedMs ?? right.delta.maxRssBytes ?? 0);
      return rightMagnitude - leftMagnitude || left.key.localeCompare(right.key);
    });
}

export function buildGroupedTestComparison(params) {
  const before = params.before;
  const after = params.after;
  const beforeTotals = normalizeCounter(before.totals);
  const afterTotals = normalizeCounter(after.totals);
  const warnings = [];

  if (before.groupBy !== after.groupBy) {
    warnings.push(`groupBy differs: before=${before.groupBy} after=${after.groupBy}`);
  }

  return {
    generatedAt: new Date().toISOString(),
    command: "test-group-report:compare",
    groupBy: after.groupBy ?? before.groupBy,
    warnings,
    totals: {
      before: beforeTotals,
      after: afterTotals,
      delta: {
        durationMs: afterTotals.durationMs - beforeTotals.durationMs,
        fileCount: afterTotals.fileCount - beforeTotals.fileCount,
        testCount: afterTotals.testCount - beforeTotals.testCount,
      },
      percent: {
        durationMs: percentDelta(beforeTotals.durationMs, afterTotals.durationMs),
      },
    },
    groups: compareCounters(before.groups, after.groups),
    configs: compareCounters(before.configs, after.configs),
    files: compareFiles(before.topFiles, after.topFiles),
    runs: compareRuns(before.runs, after.runs),
    inputs: {
      before: params.beforePath ?? null,
      after: params.afterPath ?? null,
    },
  };
}

function formatCountDelta(value) {
  return `${value > 0 ? "+" : ""}${value}`;
}

function formatOptionalMs(value) {
  return typeof value === "number" ? formatMs(value) : "n/a";
}

function formatOptionalSignedMs(value) {
  return typeof value === "number" ? formatSignedMs(value) : "n/a";
}

function formatOptionalBytes(value) {
  return typeof value === "number" ? formatBytesAsMb(value) : "n/a";
}

function formatOptionalSignedBytes(value) {
  return typeof value === "number" ? formatSignedBytesAsMb(value) : "n/a";
}

function pushChangeRows(lines, entries, options) {
  const selected = entries.slice(0, options.limit);
  if (selected.length === 0) {
    lines.push("  (none)");
    return;
  }

  for (const [index, entry] of selected.entries()) {
    lines.push(
      `${String(index + 1).padStart(2, " ")}. ${formatSignedMs(entry.delta.durationMs).padStart(11, " ")} (${formatPercent(entry.percent.durationMs).padStart(7, " ")}) | before=${formatMs(entry.before.durationMs).padStart(10, " ")} after=${formatMs(entry.after.durationMs).padStart(10, " ")} | files=${formatCountDelta(entry.delta.fileCount ?? 0).padStart(4, " ")} tests=${formatCountDelta(entry.delta.testCount ?? 0).padStart(5, " ")} | ${entry.key}`,
    );
  }
}

function pushFileChangeRows(lines, entries, options) {
  const selected = entries.slice(0, options.limit);
  if (selected.length === 0) {
    lines.push("  (none)");
    return;
  }

  for (const [index, entry] of selected.entries()) {
    lines.push(
      `${String(index + 1).padStart(2, " ")}. ${formatSignedMs(entry.delta.durationMs).padStart(11, " ")} (${formatPercent(entry.percent.durationMs).padStart(7, " ")}) | before=${formatMs(entry.before.durationMs).padStart(10, " ")} after=${formatMs(entry.after.durationMs).padStart(10, " ")} | tests=${formatCountDelta(entry.delta.testCount).padStart(4, " ")} | ${entry.config} | ${entry.file}`,
    );
  }
}

export function renderGroupedTestComparison(comparison, options = {}) {
  const limit = options.limit ?? 25;
  const topFiles = options.topFiles ?? 25;
  const groupRegressions = comparison.groups.filter((entry) => entry.delta.durationMs > 0);
  const groupGains = comparison.groups.filter((entry) => entry.delta.durationMs < 0);
  const fileRegressions = comparison.files.filter((entry) => entry.delta.durationMs > 0);
  const fileGains = comparison.files.filter((entry) => entry.delta.durationMs < 0);
  const addedFiles = comparison.files.filter((entry) => entry.status === "added").length;
  const removedFiles = comparison.files.filter((entry) => entry.status === "removed").length;
  const lines = [
    `[test-group-report:compare] groupBy=${comparison.groupBy} file-sum=${formatMs(comparison.totals.before.durationMs)} -> ${formatMs(comparison.totals.after.durationMs)} (${formatSignedMs(comparison.totals.delta.durationMs)}, ${formatPercent(comparison.totals.percent.durationMs)}) files=${comparison.totals.before.fileCount}->${comparison.totals.after.fileCount} (${formatCountDelta(comparison.totals.delta.fileCount)}) tests=${comparison.totals.before.testCount}->${comparison.totals.after.testCount} (${formatCountDelta(comparison.totals.delta.testCount)}) addedFiles=${addedFiles} removedFiles=${removedFiles}`,
  ];

  for (const warning of comparison.warnings) {
    lines.push(`[test-group-report:compare] warning: ${warning}`);
  }

  lines.push(
    "",
    `Top group regressions (${Math.min(limit, groupRegressions.length)} of ${groupRegressions.length})`,
  );
  pushChangeRows(lines, groupRegressions, { limit });

  lines.push("", `Top group gains (${Math.min(limit, groupGains.length)} of ${groupGains.length})`);
  pushChangeRows(lines, groupGains, { limit });

  lines.push(
    "",
    `Config duration deltas (${Math.min(limit, comparison.configs.length)} of ${comparison.configs.length})`,
  );
  pushChangeRows(lines, comparison.configs, { limit });

  if (comparison.runs.length > 0) {
    lines.push(
      "",
      `Config wall/RSS deltas (${Math.min(limit, comparison.runs.length)} of ${comparison.runs.length})`,
    );
    for (const [index, run] of comparison.runs.slice(0, limit).entries()) {
      lines.push(
        `${String(index + 1).padStart(2, " ")}. wall=${formatOptionalSignedMs(run.delta.elapsedMs).padStart(11, " ")} before=${formatOptionalMs(run.before.elapsedMs).padStart(10, " ")} after=${formatOptionalMs(run.after.elapsedMs).padStart(10, " ")} | rss=${formatOptionalSignedBytes(run.delta.maxRssBytes).padStart(10, " ")} before=${formatOptionalBytes(run.before.maxRssBytes).padStart(9, " ")} after=${formatOptionalBytes(run.after.maxRssBytes).padStart(9, " ")} | status=${run.before.status ?? "n/a"}->${run.after.status ?? "n/a"} | ${run.key}`,
      );
    }
  }

  lines.push(
    "",
    `Top file regressions (${Math.min(topFiles, fileRegressions.length)} of ${fileRegressions.length})`,
  );
  pushFileChangeRows(lines, fileRegressions, { limit: topFiles });

  lines.push("", `Top file gains (${Math.min(topFiles, fileGains.length)} of ${fileGains.length})`);
  pushFileChangeRows(lines, fileGains, { limit: topFiles });

  return lines.join("\n");
}

export function renderGroupedTestReport(report, options = {}) {
  const limit = options.limit ?? 25;
  const topFiles = options.topFiles ?? 25;
  const lines = [
    `[test-group-report] groupBy=${report.groupBy} files=${report.totals.fileCount} tests=${report.totals.testCount} file-sum=${formatMs(report.totals.durationMs)}`,
    "",
    `Top groups (${Math.min(limit, report.groups.length)} of ${report.groups.length})`,
  ];

  for (const [index, group] of report.groups.slice(0, limit).entries()) {
    lines.push(
      `${String(index + 1).padStart(2, " ")}. ${formatMs(group.durationMs).padStart(10, " ")} | files=${String(group.fileCount).padStart(4, " ")} | tests=${String(group.testCount).padStart(5, " ")} | ${group.key}`,
    );
  }

  lines.push(
    "",
    `Top configs (${Math.min(limit, report.configs.length)} of ${report.configs.length})`,
  );
  for (const [index, config] of report.configs.slice(0, limit).entries()) {
    lines.push(
      `${String(index + 1).padStart(2, " ")}. ${formatMs(config.durationMs).padStart(10, " ")} | files=${String(config.fileCount).padStart(4, " ")} | tests=${String(config.testCount).padStart(5, " ")} | ${config.key}`,
    );
  }

  lines.push(
    "",
    `Top files (${Math.min(topFiles, report.topFiles.length)} of ${report.topFiles.length})`,
  );
  for (const [index, file] of report.topFiles.slice(0, topFiles).entries()) {
    lines.push(
      `${String(index + 1).padStart(2, " ")}. ${formatMs(file.durationMs).padStart(10, " ")} | tests=${String(file.testCount).padStart(4, " ")} | ${file.config} | ${file.file}`,
    );
  }

  return lines.join("\n");
}

[Dauer der Verarbeitung: 0.23 Sekunden, vorverarbeitet 2026-04-27]

                                                                                                                                                                                                                                                                                                                                                                                                     


Neuigkeiten

     Aktuelles
     Motto des Tages

Software

     Produkte
     Quellcodebibliothek

Aktivitäten

     Artikel über Sicherheit
     Anleitung zur Aktivierung von SSL

Muße

     Gedichte
     Musik
     Bilder

Jenseits des Üblichen ....

Besucherstatistik

Besucherstatistik

Monitoring

Montastic status badge