Skip to content

Commit

Permalink
capture output from webpack in tests
Browse files Browse the repository at this point in the history
and also change expected script name to be a parameter instead of "index.js"
  • Loading branch information
Cass Fridkin committed Apr 8, 2022
1 parent 6b6f81e commit 614bc4b
Show file tree
Hide file tree
Showing 5 changed files with 168 additions and 86 deletions.
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import childProcess from "node:child_process";
import fs from "node:fs";
import path from "node:path";
import process from "node:process";
import { Writable as WritableStream } from "node:stream";
import { execa } from "execa";
import { mockConsoleMethods } from "wrangler/src/__tests__/helpers/mock-console";
import { runWrangler as runWrangler2 } from "wrangler/src/__tests__/helpers/run-wrangler";
Expand All @@ -9,13 +10,15 @@ import writeWranglerToml from "wrangler/src/__tests__/helpers/write-wrangler-tom
import { PATH_TO_PLUGIN } from "./constants";
import { mockSubDomainRequest } from "./mock-subdomain-request";
import { mockUploadWorkerRequest } from "./mock-upload-worker-request";
import { pipe } from "./pipe";
import { runWrangler1 } from "./run-wrangler-1";
import { writePackageJson } from "./write-package-json";
import { writeWebpackConfig } from "./write-webpack-config";
import type { CoreProperties } from "@schemastore/package";
import type { ExecaError, ExecaReturnValue } from "execa";
import type webpack from "webpack";
import type { RawConfig } from "wrangler/src/config";
// import process from "node:process";

type PartialWranglerConfig = Omit<RawConfig, "type" | "webpack_config">;
type PartialWorker = Omit<
Expand Down Expand Up @@ -81,6 +84,7 @@ export async function compareOutputs({

mockUploadWorkerRequest({
expectedType: worker?.type,
expectedName: "script.js",
});
mockSubDomainRequest();

Expand Down Expand Up @@ -111,18 +115,43 @@ export async function compareOutputs({
},
});

await execa("npm", ["install"]);
await execa("npm", ["install"], {
cwd: wrangler2Dir,
});

let wrangler2result: Error | undefined = undefined;

// we need to capture webpack output
const stdout = new WritableStream({
write: pipe((message) => {
if (!message.includes("WARNING")) {
console.log(message);
} else {
const [output, warning] = message.split("WARNING");
console.log(output);
console.warn(`WARNING ${warning}`);
}
}),
});
const stderr = new WritableStream({
write: pipe(console.error),
});

let wrangler2result: Error | undefined;
try {
await runWrangler2("publish");
await withCapturedChildProcessOutput(() => runWrangler2("publish"), {
stdout,
stderr,
});
} catch (e) {
const error = e as Error;
if (isAssertionError(error)) {
throw error;
} else {
wrangler2result = error;
}
wrangler2result = e as Error;
} finally {
process.stdout.unpipe(stdout);
process.stderr.unpipe(stderr);
}

// an assertion failed, so we should throw
if (wrangler2result !== undefined && isAssertionError(wrangler2result)) {
throw wrangler2result;
}

const wrangler2 = {
Expand Down Expand Up @@ -157,3 +186,31 @@ const clearConsole = () => {
*/
const isAssertionError = (e: Error) =>
Object.prototype.hasOwnProperty.bind(e)("matcherResult");

async function withCapturedChildProcessOutput<T>(
fn: () => T | Promise<T>,
{ stdout, stderr }: { stdout: WritableStream; stderr: WritableStream }
): Promise<T> {
const { spawn } = childProcess;
let process: childProcess.ChildProcess | undefined = undefined;
const childProcessMock = jest
.spyOn(childProcess, "spawn")
.mockImplementation((command, args, options) => {
process = spawn(command, args, options);
if (process.stdout !== null && process.stderr !== null) {
process.stdout.pipe(stdout);
process.stderr.pipe(stderr);
}
return process;
});

try {
return await fn();
} finally {
if (process.stdout !== null && process.stderr !== null) {
process.stdout.unpipe(stdout);
process.stderr.unpipe(stderr);
}
childProcessMock.mockRestore();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export function mockUploadWorkerRequest(
expectedMigrations?: CfWorkerInit["migrations"];
env?: string;
legacyEnv?: boolean;
expectedName?: string;
} = {}
) {
const {
Expand All @@ -29,6 +30,7 @@ export function mockUploadWorkerRequest(
env = undefined,
legacyEnv = false,
expectedMigrations,
expectedName = "index.js",
} = options;
setMockResponse(
env && !legacyEnv
Expand All @@ -46,17 +48,17 @@ export function mockUploadWorkerRequest(
expect(queryParams.get("available_on_subdomain")).toEqual("true");
const formBody = body as FormData;
if (expectedEntry !== undefined) {
expect(await (formBody.get("index.js") as File).text()).toMatch(
expect(await (formBody.get(expectedName) as File).text()).toMatch(
expectedEntry
);
}
const metadata = JSON.parse(
formBody.get("metadata") as string
) as WorkerMetadata;
if (expectedType === "esm") {
expect(metadata.main_module).toEqual("index.js");
expect(metadata.main_module).toEqual(expectedName);
} else {
expect(metadata.body_part).toEqual("script.js"); // ? "index.js"
expect(metadata.body_part).toEqual(expectedName);
}
if ("expectedBindings" in options) {
expect(metadata.bindings).toEqual(expectedBindings);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { PATH_TO_WRANGLER } from "./constants";
import type { WritableOptions } from "node:stream";

/**
* Helper utility for use in piping child process outputs
* into `console` functions
*
* @param logger Any function that takes a string as input
* @returns an implementaion of WritableOptions.write
*/
export function pipe(
logger: (message: string) => void
): WritableOptions["write"] {
return (chunk, encoding, callback) => {
let error: Error | null | undefined = undefined;

try {
const message = cleanMessage(stringifyChunk(chunk, encoding));
if (message !== "") {
logger(message);
}
} catch (e) {
if (e === null || e === undefined || e instanceof Error) {
error = e;
} else {
throw new Error(`Encountered unexpected error ${e}`);
}
}

callback(error);
};
}
/**
* Even though they're not supposed to, sometimes `encoding` will be "buffer"
* Which just means, like...it's a buffer. It really should be "utf-8" instead
* but whatever.
*/
const stringifyChunk = (
chunk: unknown,
encoding: BufferEncoding | "buffer"
): string => {
if (chunk instanceof Buffer) {
if (encoding !== "buffer") {
return chunk.toString(encoding);
}

return chunk.toString();
}

if (typeof chunk === "string") {
return chunk;
}

throw new Error("Unsure what type of chunk this is.");
};
/**
* Find-and-replace various things in console output that vary between
* runs with standardized text
*/
export const cleanMessage = (message: string): string =>
message
.replaceAll(/^.*debugger.*$/gim, "") // remove debugger statements
.replaceAll(/\d+ms/gm, "[timing]") // standardize timings
.replaceAll(process.cwd(), "[temp dir]") // standardize directories
.replaceAll(PATH_TO_WRANGLER, "[wrangler 1]") // standardize calls to wrangler 1
.replaceAll(/found .+ vulnerabilities/gm, "found [some] vulnerabilities") // vuln counts
.replaceAll(
// remove specific paths to node, wranglerjs, and output file
/(Error: failed to execute `)(\S*node\S*) (\S*wranglerjs\S*) \S*(--output-file=)(\S+)(.+)/gm,
'$1"node" "wranglerjs" "$4[file]"$6'
)
.replaceAll(/^Built at: .+$/gim, "Built at: [time]")
.trim();
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { Writable as WritableStream } from "node:stream";
import { execaCommand } from "execa";
import { PATH_TO_WRANGLER } from "./constants";
import { pipe, cleanMessage } from "./pipe";
import type { ExecaError } from "execa";
import type { WritableOptions } from "node:stream";

export async function runWrangler1(command?: string) {
const stdout = new WritableStream({
Expand All @@ -29,73 +29,3 @@ export async function runWrangler1(command?: string) {
throw error;
}
}

/**
* Helper utility for use in piping child process outputs
* into `console` functions
*
* @param logger Any function that takes a string as input
* @returns an implementaion of WritableOptions.write
*/
function pipe(logger: (message: string) => void): WritableOptions["write"] {
return (chunk, encoding, callback) => {
let error: Error | null | undefined = undefined;

try {
const message = cleanMessage(stringifyChunk(chunk, encoding));
if (message !== "") {
logger(message);
}
} catch (e) {
if (e === null || e === undefined || e instanceof Error) {
error = e;
} else {
throw new Error(`Encountered unexpected error ${e}`);
}
}

callback(error);
};
}

/**
* Even though they're not supposed to, sometimes `encoding` will be "buffer"
* Which just means, like...it's a buffer. It really should be "utf-8" instead
* but whatever.
*/
const stringifyChunk = (
chunk: unknown,
encoding: BufferEncoding | "buffer"
): string => {
if (chunk instanceof Buffer) {
if (encoding !== "buffer") {
return chunk.toString(encoding);
}

return chunk.toString();
}

if (typeof chunk === "string") {
return chunk;
}

throw new Error("Unsure what type of chunk this is.");
};

/**
* Find-and-replace various things in console output that vary between
* runs with standardized text
*/
const cleanMessage = (message: string): string =>
message
.replaceAll(/^.*debugger.*$/gim, "") // remove debugger statements
.replaceAll(/\d+ms/gm, "[timing]") // standardize timings
.replaceAll(process.cwd(), "[temp dir]") // standardize directories
.replaceAll(PATH_TO_WRANGLER, "[wrangler 1]") // standardize calls to wrangler 1
.replaceAll(/found .+ vulnerabilities/gm, "found [some] vulnerabilities") // vuln counts
.replaceAll(
// remove specific paths to node, wranglerjs, and output file
/(Error: failed to execute `)(\S*node\S*) (\S*wranglerjs\S*) \S*(--output-file=)(\S+)(.+)/gm,
'$1"node" "wranglerjs" "$4[file]"$6'
)
.trim();
Original file line number Diff line number Diff line change
Expand Up @@ -48,10 +48,30 @@ it("works with a basic webpack config", async () => {

expect(wrangler2.std.out).toMatchInlineSnapshot(`
"running: npm run build
> build
> webpack
Hash: e96932fc5c1ce19ddd05
Version: webpack 4.46.0
Time: [timing]
Built at: [time]
Asset Size Chunks Chunk Names
worker.js 1020 bytes 0 main
Entrypoint main = worker.js
[0] ./index.js + 1 modules 163 bytes {0} [built]
| ./index.js 140 bytes [built]
| ./another.js 23 bytes [built]
Uploaded test-name (TIMINGS)
Published test-name (TIMINGS)
test-name.test-sub-domain.workers.dev"
`);
expect(wrangler2.std.err).toMatchInlineSnapshot(`""`);
expect(wrangler2.std.warn).toMatchInlineSnapshot(`""`);
expect(wrangler2.std.err).toMatchInlineSnapshot(
`"You should set \`output.filename\` to \\"worker.js\\" in your webpack config."`
);
expect(wrangler2.std.warn).toMatchInlineSnapshot(`
"WARNING in configuration
The 'mode' option has not been set, webpack will fallback to 'production' for this value. Set 'mode' option to 'development' or 'production' to enable defaults for each environment.
You can also set it to 'none' to disable any default behavior. Learn more: https://webpack.js.org/configuration/mode/"
`);
});

0 comments on commit 614bc4b

Please sign in to comment.