-
Notifications
You must be signed in to change notification settings - Fork 1.8k
WIP [SPIKE] - test(NODE-7345): custom vm.Context runner for mocha #4844
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 1 commit
3f33ad8
e267e34
8f98008
a7b383a
38ff598
3bf91cd
626e2a5
1c0af0c
b5d497e
72896ad
1ea5d92
7393381
dda52f2
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||
|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,207 @@ | ||||||||
| /* eslint-disable no-restricted-globals */ | ||||||||
|
tadjik1 marked this conversation as resolved.
Outdated
|
||||||||
|
|
||||||||
| const fs = require('node:fs'); | ||||||||
| const path = require('node:path'); | ||||||||
| const vm = require('node:vm'); | ||||||||
| const ts = require('typescript'); | ||||||||
| const Mocha = require('mocha'); | ||||||||
|
|
||||||||
| require('ts-node/register'); | ||||||||
| require('source-map-support/register'); | ||||||||
|
|
||||||||
| const mocha = new Mocha({ | ||||||||
| extension: ['js', 'ts'], | ||||||||
| ui: 'test/tools/runner/metadata_ui.js', | ||||||||
| recursive: true, | ||||||||
| timeout: 60000, | ||||||||
| failZero: true, | ||||||||
| reporter: 'test/tools/reporter/mongodb_reporter.js', | ||||||||
| sort: true, | ||||||||
| color: true, | ||||||||
| ignore: [ | ||||||||
|
tadjik1 marked this conversation as resolved.
Outdated
|
||||||||
| 'test/integration/node-specific/examples/handler.js', | ||||||||
| 'test/integration/node-specific/examples/handler.test.js', | ||||||||
| 'test/integration/node-specific/examples/aws_handler.js', | ||||||||
| 'test/integration/node-specific/examples/aws_handler.test.js', | ||||||||
| 'test/integration/node-specific/examples/setup.js', | ||||||||
| 'test/integration/node-specific/examples/transactions.test.js', | ||||||||
| 'test/integration/node-specific/examples/versioned_api.js' | ||||||||
| ] | ||||||||
| }); | ||||||||
| mocha.suite.emit('pre-require', global, 'host-context', mocha); | ||||||||
|
|
||||||||
| require('./throw_rejections.cjs'); | ||||||||
| require('./chai_addons.ts'); | ||||||||
| require('./ee_checker.ts'); | ||||||||
|
|
||||||||
| for (const path of ['./hooks/leak_checker.ts', './hooks/configuration.ts']) { | ||||||||
| const mod = require(path); | ||||||||
| const hooks = mod.mochaHooks; | ||||||||
| const register = (hookName, globalFn) => { | ||||||||
| if (hooks[hookName]) { | ||||||||
| const list = Array.isArray(hooks[hookName]) ? hooks[hookName] : [hooks[hookName]]; | ||||||||
| list.forEach(fn => globalFn(fn)); | ||||||||
| } | ||||||||
| }; | ||||||||
|
|
||||||||
| register('beforeAll', global.before); | ||||||||
| register('afterAll', global.after); | ||||||||
| register('beforeEach', global.beforeEach); | ||||||||
| register('afterEach', global.afterEach); | ||||||||
| } | ||||||||
|
|
||||||||
| let compilerOptions = { module: ts.ModuleKind.CommonJS }; | ||||||||
| const tsConfigPath = path.join(__dirname, '../../tsconfig.json'); | ||||||||
| const configFile = ts.readConfigFile(tsConfigPath, ts.sys.readFile); | ||||||||
| if (!configFile.error) { | ||||||||
|
tadjik1 marked this conversation as resolved.
Outdated
|
||||||||
| const parsedConfig = ts.parseJsonConfigFileContent( | ||||||||
| configFile.config, | ||||||||
| ts.sys, | ||||||||
| path.dirname(tsConfigPath) | ||||||||
| ); | ||||||||
| compilerOptions = { | ||||||||
| ...parsedConfig.options, | ||||||||
| module: ts.ModuleKind.CommonJS, | ||||||||
| sourceMap: false, | ||||||||
| inlineSourceMap: false | ||||||||
| }; | ||||||||
| } | ||||||||
|
|
||||||||
| const moduleCache = new Map(); | ||||||||
|
|
||||||||
| function createSandboxContext(filename) { | ||||||||
| const exportsContainer = {}; | ||||||||
| return { | ||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
Just so none of the property lookups for built-ins fall back to the global one unintentionally
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. thanks, included! |
||||||||
| console: console, | ||||||||
| AbortController: AbortController, | ||||||||
| AbortSignal: AbortSignal, | ||||||||
|
|
||||||||
| context: global.context, | ||||||||
| describe: global.describe, | ||||||||
| xdescribe: global.xdescribe, | ||||||||
| it: global.it, | ||||||||
| xit: global.xit, | ||||||||
| before: global.before, | ||||||||
| after: global.after, | ||||||||
| beforeEach: global.beforeEach, | ||||||||
| afterEach: global.afterEach, | ||||||||
|
|
||||||||
| exports: exportsContainer, | ||||||||
| module: { exports: exportsContainer }, | ||||||||
| __filename: filename, | ||||||||
| __dirname: path.dirname(filename), | ||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'd try to emulate the behavior of the built-in CommonJS evaluator as closely as possible. None of these are typically globals, and modules do share a single global – all of (you should generally be able to use that file as a reference for a lot of this)
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That is awesome, thanks! I haven't seen this file before, this is huge help indeed.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Added the same style wrapper around the script and define "globals" (context) just once so it's shared. |
||||||||
|
|
||||||||
| // Buffer: Buffer, | ||||||||
| queueMicrotask: queueMicrotask | ||||||||
| }; | ||||||||
| } | ||||||||
|
|
||||||||
| function createProxiedRequire(parentPath) { | ||||||||
| const parentDir = path.dirname(parentPath); | ||||||||
|
|
||||||||
| return function sandboxRequire(moduleIdentifier) { | ||||||||
| if (!moduleIdentifier.startsWith('.')) { | ||||||||
| return require(moduleIdentifier); | ||||||||
| } | ||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is this intentional? I figured we'd want to require almost everything inside the sandbox except for Node.js built-ins and maybe a list of modules that are explicit exceptions to this rule
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is temporary solution to be able to import all dev dependencies and to highlight the more general idea (this ticket and PR is spike), so I was focusing mostly on "running existing integration tests inside vm.Context sandbox". But you are absolutely right, ideally all prod dependencies should be required from the inside, otherwise it's pointless (if driver is browser-compatible but, say bson not - we can't run application). |
||||||||
|
|
||||||||
| const absolutePath = path.resolve(parentDir, moduleIdentifier); | ||||||||
|
|
||||||||
| let fullPath; | ||||||||
| try { | ||||||||
| fullPath = require.resolve(absolutePath); | ||||||||
| } catch (e) { | ||||||||
| if (e.code === 'MODULE_NOT_FOUND') { | ||||||||
| const alternatives = [absolutePath + '.ts', path.join(absolutePath, 'index.ts')]; | ||||||||
|
|
||||||||
| for (const alt of alternatives) { | ||||||||
| try { | ||||||||
| fullPath = require.resolve(alt); | ||||||||
| break; | ||||||||
| } catch {} | ||||||||
| } | ||||||||
|
|
||||||||
| if (!fullPath) { | ||||||||
| return require(moduleIdentifier); | ||||||||
| } | ||||||||
| } else { | ||||||||
| throw e; | ||||||||
| } | ||||||||
| } | ||||||||
|
|
||||||||
| if (fullPath.includes('node_modules')) { | ||||||||
| return require(fullPath); | ||||||||
| } | ||||||||
|
|
||||||||
| if (fullPath.endsWith('.ts') || fullPath.endsWith('.js')) { | ||||||||
| return loadInSandbox(fullPath); | ||||||||
| } | ||||||||
|
|
||||||||
| return require(fullPath); | ||||||||
| }; | ||||||||
| } | ||||||||
|
|
||||||||
| function loadInSandbox(filepath) { | ||||||||
| const realPath = fs.realpathSync(filepath); | ||||||||
|
|
||||||||
| if (moduleCache.has(realPath)) { | ||||||||
| return moduleCache.get(realPath); | ||||||||
| } | ||||||||
|
|
||||||||
| const content = fs.readFileSync(realPath, 'utf8'); | ||||||||
|
|
||||||||
| const transpiled = ts.transpileModule(content, { | ||||||||
| compilerOptions: compilerOptions, | ||||||||
| filename: realPath | ||||||||
| }); | ||||||||
|
|
||||||||
| const sandbox = createSandboxContext(realPath); | ||||||||
| sandbox.require = createProxiedRequire(realPath); | ||||||||
|
|
||||||||
| moduleCache.set(realPath, sandbox.module.exports); | ||||||||
|
|
||||||||
| try { | ||||||||
| const script = new vm.Script(transpiled.outputText, { filename: realPath }); | ||||||||
| script.runInNewContext(sandbox); | ||||||||
| } catch (err) { | ||||||||
| console.error(`Error running ${realPath} in sandbox:`, err.message); | ||||||||
| throw err; | ||||||||
| } | ||||||||
|
|
||||||||
| moduleCache.set(realPath, sandbox.module.exports); | ||||||||
| return sandbox.module.exports; | ||||||||
| } | ||||||||
|
|
||||||||
| // use it similar to regular mocha: | ||||||||
| // mocha --config test/mocha_mongodb.js test/integration | ||||||||
| // node test/runner/vm_context.js test/integration | ||||||||
| const userArgs = process.argv.slice(2); | ||||||||
| const searchTargets = userArgs.length > 0 ? userArgs : ['test']; | ||||||||
| const testFiles = searchTargets.flatMap(target => { | ||||||||
| try { | ||||||||
| const stats = fs.statSync(target); | ||||||||
| if (stats.isDirectory()) { | ||||||||
| const pattern = path.join(target, '**/*.test.{ts,js}').replace(/\\/g, '/'); | ||||||||
| return fs.globSync(pattern); | ||||||||
| } | ||||||||
| if (stats.isFile()) { | ||||||||
| return [target]; | ||||||||
| } | ||||||||
| } catch { | ||||||||
| console.error(`Error: Could not find path "${target}"`); | ||||||||
| } | ||||||||
| return []; | ||||||||
| }); | ||||||||
|
|
||||||||
| if (testFiles.length === 0) { | ||||||||
| console.log('No test files found.'); | ||||||||
| process.exit(0); | ||||||||
| } | ||||||||
|
|
||||||||
| testFiles.forEach(file => { | ||||||||
| loadInSandbox(path.resolve(file)); | ||||||||
| }); | ||||||||
|
|
||||||||
| console.log('Running Tests...'); | ||||||||
| mocha.run(failures => { | ||||||||
| process.exitCode = failures ? 1 : 0; | ||||||||
| }); | ||||||||
Uh oh!
There was an error while loading. Please reload this page.