chore: improve playwright logging (#19899)

This commit is contained in:
ケイラ
2025-09-22 10:17:03 -06:00
committed by GitHub
parent e189088c03
commit 98213d7fc4
3 changed files with 36 additions and 174 deletions
+22 -26
View File
@@ -3,44 +3,40 @@ import type { BrowserContext, Page } from "@playwright/test";
import { coderPort, gitAuth } from "./constants";
export const beforeCoderTest = (page: Page) => {
page.on("console", (msg) => console.info(`[onConsole] ${msg.text()}`));
page.on("request", (request) => {
if (!isApiCall(request.url())) {
page.on("console", (msg) => {
const location = msg.location();
// Filters out a bunch of junk warnings the browser produces.
if (!location.url) {
return;
}
console.info(
`[onRequest] method=${request.method()} url=${request.url()} postData=${
request.postData() ? request.postData() : ""
}`,
);
// Filters out the gigantic CODER logo we print on every page load, as well
// as some other noise.
if (msg.type() === "info") {
return;
}
console.info(`[console][${msg.type()}] ${msg.text()}`);
});
page.on("response", async (response) => {
// Don't log responses for static assets.
if (!isApiCall(response.url())) {
return;
}
// Don't log successful responses. Those are almost always less interesting.
if (response.ok()) {
return;
}
const shouldLogResponse =
!response.url().endsWith("/api/v2/deployment/config") &&
!response.url().endsWith("/api/v2/debug/health?force=false");
let responseText = "";
let responseText: string;
try {
if (shouldLogResponse) {
const buffer = await response.body();
responseText = buffer.toString("utf-8");
responseText = responseText.replace(/\n$/g, "");
} else {
responseText = "skipped...";
}
} catch (error) {
console.error(error);
responseText = "not_available";
responseText = await response.text();
responseText = responseText.replaceAll("\n", "");
} catch {
responseText = "<n/a>";
}
console.info(
`[onResponse] url=${response.url()} status=${response.status()} body=${responseText}`,
`[response] url=${response.url()} status=${response.status()} body=${responseText}`,
);
});
};
+1 -1
View File
@@ -47,7 +47,7 @@ export default defineConfig({
timeout: 30_000,
},
],
reporter: [["./reporter.ts"]],
reporter: [["list"], ["./reporter.ts"]],
use: {
actionTimeout: 5000,
baseURL: `http://localhost:${coderPort}`,
+13 -147
View File
@@ -1,146 +1,27 @@
import * as fs from "node:fs/promises";
import type { Writable } from "node:stream";
import type {
FullConfig,
FullResult,
Reporter,
Suite,
TestCase,
TestError,
TestResult,
} from "@playwright/test/reporter";
import type { Reporter, TestCase, TestResult } from "@playwright/test/reporter";
import { API } from "api/api";
import { coderdPProfPort, license } from "./constants";
import { coderdPProfPort } from "./constants";
class CoderReporter implements Reporter {
config: FullConfig | null = null;
testOutput = new Map<string, Array<[Writable, string]>>();
passedCount = 0;
skippedCount = 0;
failedTests: TestCase[] = [];
timedOutTests: TestCase[] = [];
onBegin(config: FullConfig, suite: Suite) {
this.config = config;
console.info(`==> Running ${suite.allTests().length} tests`);
}
onTestBegin(test: TestCase) {
this.testOutput.set(test.id, []);
console.info(`==> Starting test ${test.title}`);
}
onStdOut(chunk: string, test?: TestCase, _?: TestResult): void {
// If there's no associated test, just print it now
if (!test) {
for (const line of logLines(chunk)) {
console.info(`[stdout] ${line}`);
}
return;
}
// Will be printed if the test fails
this.testOutput.get(test.id)!.push([process.stdout, chunk]);
}
onStdErr(chunk: string, test?: TestCase, _?: TestResult): void {
// If there's no associated test, just print it now
if (!test) {
for (const line of logLines(chunk)) {
console.error(`[stderr] ${line}`);
}
return;
}
// Will be printed if the test fails
this.testOutput.get(test.id)!.push([process.stderr, chunk]);
}
async onTestEnd(test: TestCase, result: TestResult) {
try {
if (test.expectedStatus === "skipped") {
console.info(`==> Skipping test ${test.title}`);
this.skippedCount++;
return;
}
console.info(`==> Finished test ${test.title}: ${result.status}`);
if (result.status === "passed") {
this.passedCount++;
return;
}
if (result.status === "failed") {
this.failedTests.push(test);
}
if (result.status === "timedOut") {
this.timedOutTests.push(test);
}
const fsTestTitle = test.title.replaceAll(" ", "-");
const outputFile = `test-results/debug-pprof-goroutine-${fsTestTitle}.txt`;
await exportDebugPprof(outputFile);
console.info(`Data from pprof has been saved to ${outputFile}`);
console.info("==> Output");
const output = this.testOutput.get(test.id)!;
for (const [target, chunk] of output) {
target.write(`${chunk.replace(/\n$/g, "")}\n`);
}
if (result.errors.length > 0) {
console.info("==> Errors");
for (const error of result.errors) {
reportError(error);
}
}
if (result.attachments.length > 0) {
console.info("==> Attachments");
for (const attachment of result.attachments) {
console.info(attachment);
}
}
} finally {
this.testOutput.delete(test.id);
if (test.expectedStatus === "skipped") {
return;
}
}
onEnd(result: FullResult) {
console.info(`==> Tests ${result.status}`);
if (!license) {
console.info(
"==> Tests that require a license were skipped, because no license was provided",
);
}
console.info(`${this.passedCount} passed`);
if (this.skippedCount > 0) {
console.info(`${this.skippedCount} skipped`);
}
if (this.failedTests.length > 0) {
console.info(`${this.failedTests.length} failed`);
for (const test of this.failedTests) {
console.info(` ${test.location.file} › ${test.title}`);
}
}
if (this.timedOutTests.length > 0) {
console.info(`${this.timedOutTests.length} timed out`);
for (const test of this.timedOutTests) {
console.info(` ${test.location.file} › ${test.title}`);
}
if (result.status === "passed") {
return;
}
const fsTestTitle = test.title.replaceAll(" ", "-");
const outputFile = `test-results/debug-pprof-goroutine-${fsTestTitle}.txt`;
await exportDebugPprof(outputFile);
console.info(`Data from pprof has been saved to ${outputFile}`);
console.info("==> Output");
}
}
const logLines = (chunk: string | Buffer): string[] => {
if (chunk instanceof Buffer) {
// When running in a debugger, the input to this is a Buffer instead of a string.
// Unsure why, but this prevents the `trimEnd` from throwing an error.
return [chunk.toString()];
}
return chunk.trimEnd().split("\n");
};
const exportDebugPprof = async (outputFile: string) => {
const axiosInstance = API.getAxiosInstance();
const response = await axiosInstance.get(
@@ -154,19 +35,4 @@ const exportDebugPprof = async (outputFile: string) => {
await fs.writeFile(outputFile, response.data);
};
const reportError = (error: TestError) => {
if (error.location) {
console.info(`${error.location.file}:${error.location.line}:`);
}
if (error.snippet) {
console.info(error.snippet);
}
if (error.message) {
console.info(error.message);
} else {
console.info(error);
}
};
export default CoderReporter;