Implement TeamCity test reporting integration by adding new scripts and documentation. Update package.json to include a new test command for TeamCity. Introduce TeamcityReporter class for formatting service messages and enhance test setup for compatibility with TeamCity environment.

This commit is contained in:
2025-11-22 22:42:54 -05:00
parent a050c02dc1
commit 3a58ab03d7
6 changed files with 435 additions and 1 deletions

99
scripts/test-tap-teamcity.ts Executable file
View File

@@ -0,0 +1,99 @@
#!/usr/bin/env bun
import { TeamcityReporter } from "../tests/TeamcityReporter.ts";
const isTeamCity = process.env.TEAMCITY_VERSION !== undefined;
async function convertTAPToTeamCity() {
if (!isTeamCity) {
// Just pass through if not in TeamCity
const proc = Bun.spawn(["bun", "test", ...process.argv.slice(2)], {
stdout: "inherit",
stderr: "inherit",
});
const exitCode = await proc.exited;
process.exit(exitCode);
return;
}
const proc = Bun.spawn(["bun", "test", "--reporter=tap", ...process.argv.slice(2)], {
stdout: "pipe",
stderr: "pipe",
});
const decoder = new TextDecoder();
let currentSuite = "Tests";
let buffer = "";
let testCount = 0;
TeamcityReporter.reportSuiteStart(currentSuite);
// Read TAP output
for await (const chunk of proc.stdout) {
const text = decoder.decode(chunk);
buffer += text;
const lines = buffer.split("\n");
buffer = lines.pop() || "";
for (const line of lines) {
console.log(line); // Pass through original output
// Parse TAP format
// ok 1 - test description
// not ok 2 - failed test
// # describe block
if (line.startsWith("# ")) {
// Suite/describe block
const suiteName = line.substring(2).trim();
if (suiteName && !suiteName.startsWith("tests") && !suiteName.startsWith("pass")) {
if (testCount > 0) {
TeamcityReporter.reportSuiteEnd(currentSuite);
}
currentSuite = suiteName;
testCount = 0;
TeamcityReporter.reportSuiteStart(currentSuite);
}
} else if (line.match(/^(not )?ok \d+/)) {
// Test result
const match = line.match(/^(not )?ok (\d+)\s*-?\s*(.*)$/);
if (match) {
const [, not, id, description] = match;
const testName = description.trim() || `Test ${id}`;
const fullName = `${currentSuite} > ${testName}`;
testCount++;
TeamcityReporter.reportTestStart(fullName);
if (not) {
// Test failed
TeamcityReporter.reportTestFailed(
fullName,
"Test failed",
line,
{ comparisonFailure: false, expected: "", actual: "" }
);
}
TeamcityReporter.reportTestEnd(fullName);
}
}
}
}
// Process remaining buffer
if (buffer) {
console.log(buffer);
}
const exitCode = await proc.exited;
TeamcityReporter.reportSuiteEnd(currentSuite);
process.exit(exitCode);
}
convertTAPToTeamCity().catch((error) => {
console.error("Error:", error);
process.exit(1);
});

109
scripts/test-teamcity.ts Executable file
View File

@@ -0,0 +1,109 @@
#!/usr/bin/env bun
import { TeamcityReporter } from "../tests/TeamcityReporter.ts";
const isTeamCity = process.env.TEAMCITY_VERSION !== undefined;
// Strip ANSI color codes
function stripAnsi(str: string): string {
return str.replace(/\x1b\[[0-9;]*m/g, "");
}
async function runTests() {
if (!isTeamCity) {
// Just pass through if not in TeamCity
const proc = Bun.spawn(["bun", "test", ...process.argv.slice(2)], {
stdout: "inherit",
stderr: "inherit",
env: { ...process.env },
});
const exitCode = await proc.exited;
process.exit(exitCode);
return;
}
console.error("TeamCity reporter enabled");
// Run tests and capture output
const proc = Bun.spawn(["bun", "test", ...process.argv.slice(2)], {
stdout: "pipe",
stderr: "pipe",
env: { ...process.env },
});
const suiteStack: string[] = [];
// Read stdout as text
const output = await new Response(proc.stdout).text();
const lines = output.split("\n");
for (const rawLine of lines) {
// Pass through original output (with colors)
console.log(rawLine);
// Parse without colors
const line = stripAnsi(rawLine).trim();
// Check for test suite/describe blocks (files ending with .test.ts)
if (line.match(/tests\/.+\.test\.ts:$/)) {
const fileName = line.replace(":", "").replace("tests/", "").replace(".test.ts", "");
// End previous suite if exists
while (suiteStack.length > 0) {
const oldSuite = suiteStack.pop();
if (oldSuite) {
TeamcityReporter.reportSuiteEnd(oldSuite);
}
}
suiteStack.push(fileName);
TeamcityReporter.reportSuiteStart(fileName);
}
// Check for test pass: (pass) HashMap > constructor > should create an empty map...
const passMatch = line.match(/^\(pass\)\s+(.+?)(?:\s+\[[\d.]+m?s\])?$/);
if (passMatch) {
const testName = passMatch[1].trim();
TeamcityReporter.reportTestStart(testName);
TeamcityReporter.reportTestEnd(testName);
}
// Check for test fail: (fail) HashMap > constructor > should...
const failMatch = line.match(/^\(fail\)\s+(.+?)(?:\s+\[[\d.]+m?s\])?$/);
if (failMatch) {
const testName = failMatch[1].trim();
TeamcityReporter.reportTestStart(testName);
TeamcityReporter.reportTestFailed(
testName,
"Test failed",
line,
{ comparisonFailure: false, expected: "", actual: "" }
);
TeamcityReporter.reportTestEnd(testName);
}
}
// Capture stderr
const errorOutput = await new Response(proc.stderr).text();
if (errorOutput) {
console.error(errorOutput);
}
const exitCode = await proc.exited;
// Close all open suites
while (suiteStack.length > 0) {
const suite = suiteStack.pop();
if (suite) {
TeamcityReporter.reportSuiteEnd(suite);
}
}
console.error("TeamCity reporter finished");
process.exit(exitCode);
}
runTests().catch((error) => {
console.error("Error running tests:", error);
process.exit(1);
});