[be] make evaluator and worker easier to debug in sprout

--- 

jsdom and other libraries seem to cause jest workers to exit with 
`forceExit:true`. Not sure what option I set (or global I've overwritten) but 
console logs aren't flushed as a result, making debugging a bit confusing. 

This PR: 

- Moves some code from `eval(...)` to a typechecked real js function. I always 
had trouble debugging the `eval`ed code, so smaller code snippet is better here. 

- waits for jest workers to end before exiting
This commit is contained in:
Mofei Zhang
2023-11-16 18:12:00 -05:00
parent 94348be2d9
commit fa07eb0b3b
7 changed files with 78 additions and 77 deletions
@@ -11,7 +11,6 @@ function Component(props) {
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{ items: [0, 42, null, undefined, { object: true }] }],
isComponent: "Component",
};
```
@@ -52,7 +51,6 @@ function Component(props) {
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{ items: [0, 42, null, undefined, { object: true }] }],
isComponent: "Component",
};
```
@@ -7,5 +7,4 @@ function Component(props) {
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{ items: [0, 42, null, undefined, { object: true }] }],
isComponent: "Component",
};
@@ -17,7 +17,6 @@ function Component(props) {
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{ wat: "/dev/null", itemID: 42 }],
isComponent: "Component",
};
```
@@ -60,7 +59,6 @@ function Component(props) {
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{ wat: "/dev/null", itemID: 42 }],
isComponent: "Component",
};
```
@@ -13,5 +13,4 @@ function Component(props) {
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{ wat: "/dev/null", itemID: 42 }],
isComponent: "Component",
};
@@ -509,7 +509,13 @@ const skipFilter = new Set([
"rules-of-hooks/todo.invalid.invalid-rules-of-hooks-e69ffce323c3",
"todo.unnecessary-lambda-memoization",
"rules-of-hooks/rules-of-hooks-93dc5d5e538a",
"rules-of-hooks/rules-of-hooks-69521d94fa03"
"rules-of-hooks/rules-of-hooks-69521d94fa03",
// TODO: remove
"capture_mutate-across-fns-iife",
"capture-indirect-mutate-alias-iife",
"capturing-function-alias-computed-load-3-iife",
"capturing-function-alias-computed-load-iife",
]);
export default skipFilter;
@@ -5,14 +5,22 @@
* LICENSE file in the root directory of this source tree.
*/
import { render } from "@testing-library/react";
import { JSDOM } from "jsdom";
import util from "util";
import { z } from "zod";
import { fromZodError } from "zod-validation-error";
import { initFbt, toJSON } from "./shared-runtime";
const React = require("react");
const render = require("@testing-library/react").render;
/**
* Set up the global environment for JSDOM tests.
* This is a hack to let us share code and setup between the test
* and runner environments. As an alternative, we could evaluate all setup
* in the jsdom test environment (which provides more isolation), but that
* may be slower.
*/
const { window: testWindow } = new JSDOM(undefined);
(globalThis as any).document = testWindow.document;
(globalThis as any).window = testWindow.window;
(globalThis as any).navigator = testWindow.navigator;
@@ -20,20 +28,29 @@ const { window: testWindow } = new JSDOM(undefined);
(globalThis as any).render = render;
initFbt();
(globalThis as any).placeholderFn = function (..._args: Array<any>) {
throw new Error("Fixture not implemented!");
};
export type EvaluatorResult = {
kind: "ok" | "exception" | "UnexpectedError";
value: string;
logs: Array<string>;
};
const PLACEHOLDER_VALUE = Symbol();
(globalThis as any).placeholderFn = function (..._args: Array<any>) {
throw PLACEHOLDER_VALUE;
};
(globalThis as any).WrapperTestComponent = function (props: {
fn: any;
params: Array<any>;
}) {
/**
* Define types and schemas for fixture entrypoint
*/
const EntrypointSchema = z.strictObject({
fn: z.union([z.function(), z.object({})]),
params: z.array(z.any()),
isComponent: z.optional(z.boolean()),
});
const ExportSchema = z.object({
FIXTURE_ENTRYPOINT: EntrypointSchema,
});
function WrapperTestComponent(props: { fn: any; params: Array<any> }) {
const result = props.fn(...props.params);
// Hacky solution to determine whether the fixture returned jsx (which
// needs to passed through to React's runtime as-is) or a non-jsx value
@@ -43,24 +60,44 @@ const PLACEHOLDER_VALUE = Symbol();
} else {
return toJSON(result);
}
};
function validateEntrypoint(entrypoint: object) {
if (!("params" in entrypoint)) {
return "missing `params` property";
} else if (!Array.isArray(entrypoint.params)) {
return "unexpected type for `params` property";
} else if (!(`fn` in entrypoint) || entrypoint == null) {
return "missing `fn` property";
} else if (
typeof entrypoint.fn !== "function" &&
typeof entrypoint.fn !== "object"
) {
return "expected `fn` property to be a function or React object";
} else {
return null;
}
}
type FixtureEvaluatorResult = Omit<EvaluatorResult, "logs">;
(globalThis as any).evaluateFixtureExport = function (
exports: unknown
): FixtureEvaluatorResult {
const parsedExportResult = ExportSchema.safeParse(exports);
if (!parsedExportResult.success) {
const exportDetail =
typeof exports === "object" && exports != null
? `object ${util.inspect(exports)}`
: `${exports}`;
return {
kind: "UnexpectedError",
value: `${fromZodError(parsedExportResult.error)}\nFound ` + exportDetail,
};
}
const entrypoint = parsedExportResult.data.FIXTURE_ENTRYPOINT;
if (typeof entrypoint.fn === "object") {
// Try to run fixture as a react component. This is necessary because not
// all components are functions (some are ForwardRef or Memo objects).
const result = render(
React.createElement(entrypoint.fn, entrypoint.params[0])
).container.innerHTML;
return {
kind: "ok",
value: result ?? "null",
};
} else {
const result = render(React.createElement(WrapperTestComponent, entrypoint))
.container.innerHTML;
return {
kind: "ok",
value: result ?? "null",
};
}
};
export function doEval(source: string): EvaluatorResult {
"use strict";
@@ -95,49 +132,7 @@ export function doEval(source: string): EvaluatorResult {
// run in an iife to avoid naming collisions
(() => {${source}})();
reachedInvoke = true;
if (exports.FIXTURE_ENTRYPOINT == null ||
exports.FIXTURE_ENTRYPOINT.fn === globalThis.placeholderFn
) {
return {
kind: "UnexpectedError",
value: 'FIXTURE_ENTRYPOINT not exported! Found {'
+ Object.keys(exports).filter(e => e !== 'FIXTURE_ENTRYPOINT').toString()
+ '}',
};
}
const validationError = validateEntrypoint(exports.FIXTURE_ENTRYPOINT);
if (validationError) {
return {
kind: "UnexpectedError",
value: 'Bad shape for FIXTURE_ENTRYPOINT (' + validationError + ').',
};
}
if (typeof exports.FIXTURE_ENTRYPOINT.fn === 'object') {
// try to run fixture as a react component
const result = render(
React.createElement(
exports.FIXTURE_ENTRYPOINT.fn,
exports.FIXTURE_ENTRYPOINT.params[0])
).container.innerHTML;
return {
kind: "ok",
value: result ?? 'null',
};
} else {
const result = render(
React.createElement(
WrapperTestComponent,
exports.FIXTURE_ENTRYPOINT
)
).container.innerHTML;
return {
kind: "ok",
value: result ?? 'null',
};
}
return evaluateFixtureExport(exports);
} catch (e) {
if (!reachedInvoke) {
return {
+6
View File
@@ -216,6 +216,12 @@ export async function main(opts: RunnerOptions): Promise<void> {
}
const isSuccess = reportResults(results, opts.verbose);
/**
* This is important, as we're using jsdom (which seems to be attaching some
* tasks that require force exiting. If we do not await workers terminating,
* we may miss some console logs from jest workers.
*/
await worker.end();
process.exit(isSuccess ? 0 : 1);
}