UNPKG

@jsenv/snapshot

Version:
384 lines (369 loc) 10.9 kB
import { parseFunction } from "@jsenv/assert/src/utils/function_parser.js"; import { ANSI, UNICODE } from "@jsenv/humanize"; import { createReplaceFilesystemWellKnownValues } from "../filesystem_well_known_values.js"; import { filesystemSideEffects } from "./filesystem/filesystem_side_effects.js"; import { logSideEffects } from "./log/log_side_effects.js"; const executionEffectsDefault = { catch: true, return: true, }; let currentCapture = false; export const createCaptureSideEffects = ({ sourceFileUrl, logEffects = true, filesystemEffects = true, filesystemActions, executionEffects = executionEffectsDefault, rootDirectoryUrl, replaceFilesystemWellKnownValues = createReplaceFilesystemWellKnownValues({ rootDirectoryUrl, }), }) => { if (executionEffects === false) { executionEffects = { catch: false, return: false, }; } executionEffects = { ...executionEffectsDefault, ...executionEffects }; const detectors = []; if (logEffects) { detectors.push(logSideEffects(logEffects === true ? {} : logEffects)); } let filesystemSideEffectsDetector; if (filesystemEffects) { filesystemSideEffectsDetector = filesystemSideEffects( filesystemEffects === true ? {} : filesystemEffects, { sourceFileUrl, replaceFilesystemWellKnownValues, filesystemActions, }, ); detectors.push(filesystemSideEffectsDetector); } const options = { rootDirectoryUrl, replaceFilesystemWellKnownValues, }; let functionExecutingCount = 0; let ignored = false; const capture = (fn, { callSite, baseDirectory } = {}) => { const unicodeSupported = UNICODE.supported; const ansiSupported = ANSI.supported; // this is fragile because if the copy of humanize we have // is not the same (different version) // we won't be able to force support // good enough for now UNICODE.supported = true; ANSI.supported = true; if (baseDirectory !== undefined && filesystemSideEffectsDetector) { filesystemSideEffectsDetector.setBaseDirectory(baseDirectory); } const startMs = Date.now(); const sideEffects = []; sideEffects.options = options; const sideEffectTypeCounterMap = new Map(); const onSideEffectAdded = (sideEffect) => { let counter = sideEffectTypeCounterMap.get(sideEffect.type) || 0; sideEffectTypeCounterMap.set(sideEffect.type, counter + 1); sideEffect.counter = counter; sideEffect.delay = Date.now() - startMs; }; const onSideEffectRemoved = () => {}; const addSideEffect = (sideEffect) => { if (ignored) return null; sideEffects.push(sideEffect); onSideEffectAdded(sideEffect); return sideEffect; }; const replaceSideEffect = (existingSideEffect, newSideEffect) => { const index = sideEffects.indexOf(existingSideEffect); sideEffects[index] = newSideEffect; onSideEffectRemoved(existingSideEffect); onSideEffectAdded(newSideEffect); }; const removeSideEffect = (sideEffect) => { const index = sideEffects.indexOf(sideEffect); if (index > -1) { sideEffects.splice(index, 1); onSideEffectRemoved(sideEffect); } }; sideEffects.replaceSideEffect = replaceSideEffect; sideEffects.removeSideEffect = removeSideEffect; const sourceCode = parseFunction(fn).body; addSideEffect({ code: "source_code", type: "source_code", value: { sourceCode, callSite }, render: { md: () => { return { type: "source_code", text: { type: "source_code", value: { sourceCode, callSite }, }, }; }, }, }); const finallyCallbackSet = new Set(); const addFinallyCallback = (finallyCallback) => { finallyCallbackSet.add(finallyCallback); }; const skippableHandlerSet = new Set(); const addSkippableHandler = (skippableHandler) => { skippableHandlerSet.add(skippableHandler); }; addFinallyCallback((sideEffects) => { let i = 0; while (i < sideEffects.length) { const sideEffect = sideEffects[i]; i++; let skippableHandlerResult; for (const skippableHandler of skippableHandlerSet) { skippableHandlerResult = skippableHandler(sideEffect); if (skippableHandlerResult) { // there is no skippable per sideEffect type today // so even if the skippable doe not skip in the end // we don't have to check if an other skippable handler could break; } } if (skippableHandlerResult) { let j = i; while (j < sideEffects.length) { const afterSideEffect = sideEffects[j]; j++; let stopCalled = false; let skipCalled = false; const stop = () => { stopCalled = true; }; const skip = () => { skipCalled = true; }; skippableHandlerResult(afterSideEffect, { skip, stop }); if (skipCalled) { sideEffect.skippable = true; break; } if (stopCalled) { break; } } } } }); addSkippableHandler((sideEffect) => { if (sideEffect.type === "return" && sideEffect.value === RETURN_PROMISE) { return (nextSideEffect, { skip }) => { if ( nextSideEffect.code === "resolve" || nextSideEffect.code === "reject" ) { skip(); } }; } return null; }); for (const detector of detectors) { const uninstall = detector.install(addSideEffect, { addSkippableHandler, addFinallyCallback, }); finallyCallbackSet.add(() => { uninstall(); }); } if (functionExecutingCount) { // The reason for this warning: // 1. fs side effect detectors is not yet fully compatible with that because // callback.oncomplete redefinition might be wrong for open, mkdir etc // (at least this is to be tested) // 2. It's usually a sign code forgets to put await in front of // collectFunctionSideEffects or snapshotFunctionSideEffects // 3. collectFunctionSideEffects is meant to collect a function side effect // during unit test. So in unit test the function being tested should be analyized // and should not in turn analyze an other one console.warn( `captureSideEffects called while other function(s) side effects are collected`, ); } const onCatch = (valueThrow) => { addSideEffect({ code: "throw", type: "throw", value: valueThrow, render: { md: () => { return { label: "throw", text: { type: "js_value", value: valueThrow, }, }; }, }, }); }; const onReturn = (valueReturned) => { if (valueReturned === RETURN_PROMISE) { addSideEffect({ code: "return", type: "return", value: valueReturned, render: { md: () => { return { label: "return promise", }; }, }, }); return; } addSideEffect({ code: "return", type: "return", value: valueReturned, render: { md: () => { return { label: "return", text: { type: "js_value", value: valueReturned, }, }; }, }, }); }; const onResolve = (value) => { addSideEffect({ code: "resolve", type: "resolve", value, render: { md: () => { return { label: "resolve", text: { type: "js_value", value, }, }; }, }, }); }; const onReject = (reason) => { addSideEffect({ code: "reject", type: "reject", value: reason, render: { md: () => { return { label: "reject", text: { type: "js_value", value: reason, }, }; }, }, }); }; const onFinally = () => { currentCapture = null; delete process.env.CAPTURING_SIDE_EFFECTS; UNICODE.supported = unicodeSupported; ANSI.supported = ansiSupported; functionExecutingCount--; for (const finallyCallback of finallyCallbackSet) { finallyCallback(sideEffects); } finallyCallbackSet.clear(); }; const onThrowOrReject = (value, isThrow) => { if (executionEffects.catch === false) { throw value; } if (typeof executionEffects.catch === "function") { executionEffects.catch(value); } if (isThrow) { onCatch(value); } else { onReject(value); } }; const onReturnOrResolve = (value, isReturn) => { if (executionEffects.return === false) { return; } if (typeof executionEffects.return === "function") { executionEffects.return(value); } if (isReturn) { onReturn(value); } else { onResolve(value); } }; process.env.CAPTURING_SIDE_EFFECTS = "1"; functionExecutingCount++; let returnedPromise = false; currentCapture = { ignoreWhile: (fn) => { ignored = true; fn(); ignored = false; }, }; try { const valueReturned = fn(); if (valueReturned && typeof valueReturned.then === "function") { onReturn(RETURN_PROMISE); returnedPromise = valueReturned.then( (value) => { onReturnOrResolve(value); onFinally(); return sideEffects; }, (e) => { onThrowOrReject(e); onFinally(); return sideEffects; }, ); return returnedPromise; } onReturnOrResolve(valueReturned, true); return sideEffects; } catch (e) { onThrowOrReject(e, true); return sideEffects; } finally { if (!returnedPromise) { onFinally(); } } }; capture.ignoreSideEffects = ignoreSideEffects; return capture; }; export const ignoreSideEffects = (fn) => { if (!currentCapture) { console.warn(`ignoreSideEffects called outside of captureSideEffects`); return; } currentCapture.ignoreWhile(fn); }; const RETURN_PROMISE = {};