Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
// Unless explicitly stated otherwise all files in this repository are licensed under the MIT License.
// This product includes software developed at Datadog (https://www.datadoghq.com/).
// Copyright 2019-Present Datadog, Inc.

// Deliberately not named `*.test.*` so `yarn test:unit`'s normal testMatch never picks it up —
// local-execution.resilience.test.ts spawns it as its own Jest process (via --testMatch override)
// to observe a real process.exit() call inside the actual executeScriptLocally() code path; running
// it in-process would kill the parent test's own Jest worker. Not matched by the repo's `**/*.test.ts`
// eslint override for the same reason, hence the disables below (Jest still injects the `test`
// global at runtime for any file it executes, matched or not).

// eslint-disable-next-line import/no-extraneous-dependencies -- test-only helper, same as any *.test.ts file; this file just isn't named like one (see note above)
import { mockLogger, moduleResolverFor } from '@dd/tests/_jest/helpers/mocks';

import type { BackendFunction } from '../backend/types';

import type { ExecuteAction } from './local-execution';
import { executeScriptLocally } from './local-execution';

const func: BackendFunction = {
relativePath: 'src/example',
name: 'example',
absolutePath: '/src/example.backend.ts',
allowedConnectionIds: [],
};

const stubExecuteAction: ExecuteAction = async (fqn) => ({ data: null, stub: true, fqn });

// eslint-disable-next-line no-undef -- Jest injects this global at runtime; not declared here because this file isn't matched by the repo's jest eslint override (see note above)
test('process.exit fixture', async () => {
// eslint-disable-next-line no-console -- this file's stdout is the only channel the spawning parent test can observe
console.log('FIXTURE_STARTED');
await executeScriptLocally(
func,
'/project',
[],
stubExecuteAction,
moduleResolverFor(func, {
example: () => {
process.exit(7);
},
}),
mockLogger,
5000,
);
// eslint-disable-next-line no-console -- see note above; unreachable if process.exit() truly bypasses cleanup
console.log('FIXTURE_CLEANUP_RAN');
});
99 changes: 99 additions & 0 deletions packages/plugins/apps/src/vite/local-execution.resilience.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
// Unless explicitly stated otherwise all files in this repository are licensed under the MIT License.
// This product includes software developed at Datadog (https://www.datadoghq.com/).
// Copyright 2019-Present Datadog, Inc.

/** Two targeted checks that empirically confirm real failure modes of running backend functions in-process rather than in an isolated child process/thread — accepted v1 limitations, not bugs this file fixes. */

import { mockLogger, moduleResolverFor } from '@dd/tests/_jest/helpers/mocks';
import { spawnSync } from 'child_process';
import path from 'path';

import type { BackendFunction } from '../backend/types';

import type { ExecuteAction } from './local-execution';
import { executeScriptLocally } from './local-execution';

const func: BackendFunction = {
relativePath: 'src/example',
name: 'example',
absolutePath: '/src/example.backend.ts',
allowedConnectionIds: [],
};

const stubExecuteAction: ExecuteAction = async (fqn) => ({ data: null, stub: true, fqn });

describe('local-execution resilience (in-process execution known limitations)', () => {
// A real `while (true) {}` would hang this test (and the whole Jest
// worker) forever, since nothing — including the timeout's own
// setTimeout callback — can run while the event loop is synchronously
// blocked. A bounded, time-boxed busy-wait demonstrates the exact same
// mechanism without actually hanging: if the 20ms timeout could
// interrupt a synchronous loop, this would settle around 20ms with a
// timeout rejection; instead it can only settle once the loop itself
// finishes on its own, ~200ms later, with the loop's real result.
test('Should NOT interrupt a synchronous CPU-bound loop with the current timeout — known, accepted v1 limitation', async () => {
const start = Date.now();

const result = await executeScriptLocally(
func,
'/project',
[],
stubExecuteAction,
moduleResolverFor(func, {
example: () => {
const deadline = Date.now() + 200;
// eslint-disable-next-line no-empty
while (Date.now() < deadline) {}
return 'loop finished on its own';
},
}),
mockLogger,
20,
);

const elapsedMs = Date.now() - start;

expect(result).toEqual({ data: 'loop finished on its own' });
expect(elapsedMs).toBeGreaterThanOrEqual(150);
});

// process.exit() can't be run inside this same Jest process — it would
// actually terminate the test runner. This spawns local-execution.process-exit.fixture.ts as its
// own Jest process (real transform, real module resolution, real executeScriptLocally/
// runScriptLocally code path — not a hand-rolled emulation of it) to test the relevant claim:
// does the try/finally runScriptLocally wraps around the customer's function call offer any
// protection against process.exit()? It doesn't — process.exit() is immediate and unconditional
// at the OS level, so no JS-level exception handling in this in-process design can intercept it.
// A customer function calling process.exit() takes the whole dev server down with it, not just
// its own execution. `--runInBand` is required so the fixture runs in the spawned process itself
// rather than a Jest worker — otherwise Jest would report a worker crash instead of surfacing
// exit code 7 on the process this test observes.
test("Should confirm process.exit() inside the customer function crashes the whole process, bypassing runScriptLocally's own try/finally cleanup — known, real risk, not a safely-contained failure", () => {
const fixturePath = path.join(__dirname, 'local-execution.process-exit.fixture.ts');
const jestBinPath = path.join(
path.dirname(require.resolve('jest/package.json')),
'bin/jest.js',
);
const jestConfigPath = path.join(__dirname, '../../../../tests/jest.config.ts');

const result = spawnSync(
process.execPath,
[
jestBinPath,
'--config',
jestConfigPath,
// jest-cli's --testMatch is a bare glob string (yargs `type: 'array'` collects one
// occurrence per flag) — a JSON-stringified array is taken literally as a single
// glob containing "[" and "]" characters and matches nothing.
'--testMatch',
`**/${path.basename(fixturePath)}`,
'--runInBand',
],
{ encoding: 'utf8', timeout: 30000 },
);

expect(result.status).toBe(7);
expect(result.stdout).toContain('FIXTURE_STARTED');
expect(result.stdout).not.toContain('FIXTURE_CLEANUP_RAN');
}, 30000);
});
Loading