mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
chore: improve playwright logging (#19899)
This commit is contained in:
+22
-26
@@ -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}`,
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user