Skip to content
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

test: run interfering tests in their own process #1325

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
8 changes: 7 additions & 1 deletion test/addon.cc
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ class TestAddon : public Napi::Addon<TestAddon> {
{InstanceMethod("decrement", &TestAddon::Decrement)}))});
}

~TestAddon() { fprintf(stderr, "TestAddon::~TestAddon\n"); }

private:
Napi::Value Increment(const Napi::CallbackInfo& info) {
return Napi::Number::New(info.Env(), ++value);
Expand All @@ -29,10 +31,14 @@ class TestAddon : public Napi::Addon<TestAddon> {
uint32_t value = 42;
};

Napi::Value CreateAddon(const Napi::CallbackInfo& info) {
return TestAddon::Init(info.Env(), Napi::Object::New(info.Env()));
}

} // end of anonymous namespace

Napi::Object InitAddon(Napi::Env env) {
return TestAddon::Init(env, Napi::Object::New(env));
return Napi::Function::New<CreateAddon>(env, "CreateAddon");
}

#endif // (NAPI_VERSION > 5)
14 changes: 5 additions & 9 deletions test/addon.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,7 @@
'use strict';

const assert = require('assert');

module.exports = require('./common').runTest(test);

function test (binding) {
assert.strictEqual(binding.addon.increment(), 43);
assert.strictEqual(binding.addon.increment(), 44);
assert.strictEqual(binding.addon.subObject.decrement(), 43);
}
module.exports = require('./common').runTestInChildProcess({
suite: 'addon',
testName: 'workingCode',
expectedStderr: ['TestAddon::~TestAddon']
});
6 changes: 3 additions & 3 deletions test/addon_data.cc
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@
#include "test_helper.h"

// An overly elaborate way to get/set a boolean stored in the instance data:
// 0. A boolean named "verbose" is stored in the instance data. The constructor
// for JS `VerboseIndicator` instances is also stored in the instance data.
// 0. The constructor for JS `VerboseIndicator` instances, which have a private
// member named "verbose", is stored in the instance data.
// 1. Add a property named "verbose" onto exports served by a getter/setter.
// 2. The getter returns a object of type VerboseIndicator, which itself has a
// 2. The getter returns an object of type VerboseIndicator, which itself has a
// property named "verbose", also served by a getter/setter:
// * The getter returns a boolean, indicating whether "verbose" is set.
// * The setter sets "verbose" on the instance data.
Expand Down
54 changes: 16 additions & 38 deletions test/addon_data.js
Original file line number Diff line number Diff line change
@@ -1,46 +1,24 @@
'use strict';

const assert = require('assert');
const { spawn } = require('child_process');
const readline = require('readline');
const common = require('./common');

module.exports = require('./common').runTestWithBindingPath(test);
module.exports = common.runTest(test);

// Make sure the instance data finalizer is called at process exit. If the hint
// is non-zero, it will be printed out by the child process.
function testFinalizer (bindingName, hint, expected) {
return new Promise((resolve) => {
bindingName = bindingName.split('\\').join('\\\\');
const child = spawn(process.execPath, [
'-e',
`require('${bindingName}').addon_data(${hint}).verbose = true;`
]);
const actual = [];
readline
.createInterface({ input: child.stderr })
.on('line', (line) => {
if (expected.indexOf(line) >= 0) {
actual.push(line);
}
})
.on('close', () => {
assert.deepStrictEqual(expected, actual);
resolve();
});
async function test () {
await common.runTestInChildProcess({
suite: 'addon_data',
testName: 'workingCode'
});
}

async function test (bindingName) {
const binding = require(bindingName).addon_data(0);

// Make sure it is possible to get/set instance data.
assert.strictEqual(binding.verbose.verbose, false);
binding.verbose = true;
assert.strictEqual(binding.verbose.verbose, true);
binding.verbose = false;
assert.strictEqual(binding.verbose.verbose, false);
await common.runTestInChildProcess({
suite: 'addon_data',
testName: 'cleanupWithoutHint',
expectedStderr: ['addon_data: Addon::~Addon']
});

await testFinalizer(bindingName, 0, ['addon_data: Addon::~Addon']);
await testFinalizer(bindingName, 42,
['addon_data: Addon::~Addon', 'hint: 42']);
await common.runTestInChildProcess({
suite: 'addon_data',
testName: 'cleanupWithHint',
expectedStderr: ['addon_data: Addon::~Addon', 'hint: 42']
});
}
11 changes: 11 additions & 0 deletions test/child_processes/addon.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
'use strict';
const assert = require('assert');

module.exports = {
workingCode: binding => {
const addon = binding.addon();
assert.strictEqual(addon.increment(), 43);
assert.strictEqual(addon.increment(), 44);
assert.strictEqual(addon.subObject.decrement(), 43);
}
};
24 changes: 24 additions & 0 deletions test/child_processes/addon_data.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
'use strict';

const assert = require('assert');

// Make sure the instance data finalizer is called at process exit. If the hint
// is non-zero, it will be printed out by the child process.
const cleanupTest = (binding, hint) => {
binding.addon_data(hint).verbose = true;
};

module.exports = {
workingCode: binding => {
const addonData = binding.addon_data(0);

// Make sure it is possible to get/set instance data.
assert.strictEqual(addonData.verbose.verbose, false);
addonData.verbose = true;
assert.strictEqual(addonData.verbose.verbose, true);
addonData.verbose = false;
assert.strictEqual(addonData.verbose.verbose, false);
},
cleanupWithHint: binding => cleanupTest(binding, 42),
cleanupWithoutHint: binding => cleanupTest(binding, 0)
};
22 changes: 22 additions & 0 deletions test/child_processes/objectwrap_function.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
'use strict';

const assert = require('assert');
const testUtil = require('../testUtil');

module.exports = {
runTest: function (binding) {
return testUtil.runGCTests([
'objectwrap function',
() => {
const { FunctionTest } = binding.objectwrap_function();
const newConstructed = new FunctionTest();
const functionConstructed = FunctionTest();
assert(newConstructed instanceof FunctionTest);
assert(functionConstructed instanceof FunctionTest);
assert.throws(() => (FunctionTest(true)), /an exception/);
},
// Do one gc before returning.
() => {}
]);
}
};
55 changes: 54 additions & 1 deletion test/common/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,11 @@
const assert = require('assert');
const path = require('path');
const { access } = require('node:fs/promises');
const { spawn } = require('child_process');
const { EOL } = require('os');
const readline = require('readline');

const escapeBackslashes = (pathString) => pathString.split('\\').join('\\\\');

const noop = () => {};

Expand All @@ -26,7 +31,7 @@ function runCallChecks (exitCode) {
context.name,
context.messageSegment,
context.actual);
console.log(context.stack.split('\n').slice(2).join('\n'));
console.log(context.stack.split(EOL).slice(2).join(EOL));
});

if (failed.length) process.exit(1);
Expand Down Expand Up @@ -190,3 +195,51 @@ exports.runTestWithBuildType = async function (test, buildType) {
await Promise.resolve(test(buildType))
.finally(exports.mustCall());
};

// Some tests have to run in their own process, otherwise they would interfere
// with each other. Such tests export a factory function rather than the test
// itself so as to avoid automatic instantiation, and therefore interference,
// in the main process. Two examples are addon and addon_data, both of which
// use Napi::Env::SetInstanceData(). This helper function provides a common
// approach for running such tests.
exports.runTestInChildProcess = function ({ suite, testName, expectedStderr }) {
return exports.runTestWithBindingPath((bindingName) => {
return new Promise((resolve) => {
bindingName = escapeBackslashes(bindingName);
// Test suites are assumed to be located here.
const suitePath = escapeBackslashes(path.join(__dirname, '..', 'child_processes', suite));
const child = spawn(process.execPath, [
'--expose-gc',
'-e',
`require('${suitePath}').${testName}(require('${bindingName}'))`
]);
const resultOfProcess = { stderr: [] };

// Capture the exit code and signal.
child.on('close', (code, signal) => resolve(Object.assign(resultOfProcess, { code, signal })));

// Capture the stderr as an array of lines.
readline
.createInterface({ input: child.stderr })
.on('line', (line) => {
resultOfProcess.stderr.push(line);
});
}).then(actual => {
// Back up the stderr in case the assertion fails.
const fullStderr = actual.stderr.map(item => `from child process: ${item}`);
const expected = { stderr: expectedStderr, code: 0, signal: null };

if (!expectedStderr) {
// If we don't care about stderr, delete it.
delete actual.stderr;
delete expected.stderr;
} else {
// Otherwise we only care about expected lines in the actual stderr, so
// filter out everything else.
actual.stderr = actual.stderr.filter(line => expectedStderr.includes(line));
}

assert.deepStrictEqual(actual, expected, `Assertion for child process test ${suite}.${testName} failed:${EOL}` + fullStderr.join(EOL));
});
});
};
1 change: 1 addition & 0 deletions test/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ function loadTestModules (currentDirectory = __dirname, pre = '') {
file === 'binding.gyp' ||
file === 'build' ||
file === 'common' ||
file === 'child_processes' ||
file === 'napi_child.js' ||
file === 'testUtil.js' ||
file === 'thunking_manual.cc' ||
Expand Down
22 changes: 10 additions & 12 deletions test/objectwrap_function.cc
Original file line number Diff line number Diff line change
Expand Up @@ -18,28 +18,26 @@ class FunctionTest : public Napi::ObjectWrap<FunctionTest> {
return MaybeUnwrap(GetConstructor(info.Env()).New(args));
}

// Constructor-per-env map in a static member because env.SetInstanceData()
// would interfere with Napi::Addon<T>
static std::unordered_map<napi_env, Napi::FunctionReference> constructors;

static void Initialize(Napi::Env env, Napi::Object exports) {
const char* name = "FunctionTest";
Napi::Function func = DefineClass(env, name, {});
constructors[env] = Napi::Persistent(func);
env.AddCleanupHook([env] { constructors.erase(env); });
Napi::FunctionReference* ctor = new Napi::FunctionReference();
*ctor = Napi::Persistent(func);
env.SetInstanceData(ctor);
exports.Set(name, func);
}

static Napi::Function GetConstructor(Napi::Env env) {
return constructors[env].Value();
return env.GetInstanceData<Napi::FunctionReference>()->Value();
}
};

std::unordered_map<napi_env, Napi::FunctionReference>
FunctionTest::constructors;
Napi::Value ObjectWrapFunctionFactory(const Napi::CallbackInfo& info) {
Napi::Object exports = Napi::Object::New(info.Env());
FunctionTest::Initialize(info.Env(), exports);
return exports;
}

Napi::Object InitObjectWrapFunction(Napi::Env env) {
Napi::Object exports = Napi::Object::New(env);
FunctionTest::Initialize(env, exports);
return exports;
return Napi::Function::New<ObjectWrapFunctionFactory>(env, "FunctionFactory");
}
24 changes: 4 additions & 20 deletions test/objectwrap_function.js
Original file line number Diff line number Diff line change
@@ -1,22 +1,6 @@
'use strict';

const assert = require('assert');
const testUtil = require('./testUtil');

function test (binding) {
return testUtil.runGCTests([
'objectwrap function',
() => {
const { FunctionTest } = binding.objectwrap_function;
const newConstructed = new FunctionTest();
const functionConstructed = FunctionTest();
assert(newConstructed instanceof FunctionTest);
assert(functionConstructed instanceof FunctionTest);
assert.throws(() => (FunctionTest(true)), /an exception/);
},
// Do on gc before returning.
() => {}
]);
}

module.exports = require('./common').runTest(test);
module.exports = require('./common').runTestInChildProcess({
suite: 'objectwrap_function',
testName: 'runTest'
});