Skip to content

Commit c61aa31

Browse files
authored
fix(NODE-6606): install bson libraries to tmp directory (#24)
1 parent 1a0e347 commit c61aa31

File tree

7 files changed

+204
-73
lines changed

7 files changed

+204
-73
lines changed

packages/bson-bench/src/base.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import * as BSON from 'bson';
22
import { readFileSync } from 'fs';
3+
import { join } from 'path';
34
import { performance } from 'perf_hooks';
45
import * as process from 'process';
56

@@ -116,10 +117,12 @@ function run(bson: BSONLib | ConstructibleBSON, config: BenchmarkSpecification)
116117

117118
function listener(message: RunBenchmarkMessage) {
118119
if (message.type === 'runBenchmark') {
119-
const packageSpec = new Package(message.benchmark.library);
120+
const packageSpec = new Package(message.benchmark.library, message.benchmark.installLocation);
120121
let bson: BSONLib;
121122
try {
122-
bson = require(packageSpec.computedModuleName);
123+
bson = require(
124+
join(message.benchmark.installLocation, 'node_modules', packageSpec.computedModuleName)
125+
);
123126
} catch (error) {
124127
reportErrorAndQuit(error as Error);
125128
return;

packages/bson-bench/src/common.ts

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import * as cp from 'child_process';
22
import { once } from 'events';
3-
import * as path from 'path';
3+
import { join } from 'path';
44

55
import { exists } from './utils';
66

@@ -26,8 +26,10 @@ export class Package {
2626
gitCommitish?: string;
2727
// path to local library
2828
localPath?: string;
29+
installPath: string;
2930

30-
constructor(libSpec: string) {
31+
constructor(libSpec: string, installPath: string) {
32+
this.installPath = installPath;
3133
let match: RegExpExecArray | null;
3234
if ((match = NPM_PACKAGE_REGEX.exec(libSpec))) {
3335
this.type = 'npm';
@@ -44,7 +46,8 @@ export class Package {
4446
this.library = match[1] as 'bson' | 'bson-ext';
4547

4648
this.localPath = match[2];
47-
this.computedModuleName = `${this.library}-local-${this.localPath.replaceAll(path.sep, '_')}`;
49+
const cleanedLocalPath = this.localPath.replaceAll('/', '_').replaceAll('\\', '_');
50+
this.computedModuleName = `${this.library}-local-${cleanedLocalPath}`;
4851
} else {
4952
throw new Error('unknown package specifier');
5053
}
@@ -55,7 +58,7 @@ export class Package {
5558
*/
5659
check<B extends BSONLib>(): B | undefined {
5760
try {
58-
return require(this.computedModuleName);
61+
return require(join(this.installPath, 'node_modules', this.computedModuleName));
5962
} catch {
6063
return undefined;
6164
}
@@ -90,10 +93,10 @@ export class Package {
9093
break;
9194
}
9295

93-
const npmInstallProcess = cp.exec(
94-
`npm install ${this.computedModuleName}@${source} --no-save`,
95-
{ encoding: 'utf8', cwd: __dirname }
96-
);
96+
const npmInstallProcess = cp.exec(`npm install ${this.computedModuleName}@${source}`, {
97+
encoding: 'utf8',
98+
cwd: this.installPath
99+
});
97100

98101
const exitCode: number = (await once(npmInstallProcess, 'exit'))[0];
99102
if (exitCode !== 0) {
@@ -130,11 +133,12 @@ export type BenchmarkSpecification = {
130133
/** Specifier of the bson or bson-ext library to be used. Can be an npm package, git repository or
131134
* local package */
132135
library: string;
136+
installLocation?: string;
133137
};
134138

135139
export interface RunBenchmarkMessage {
136140
type: 'runBenchmark';
137-
benchmark: BenchmarkSpecification;
141+
benchmark: Omit<BenchmarkSpecification, 'installLocation'> & { installLocation: string };
138142
}
139143

140144
export interface ResultMessage {

packages/bson-bench/src/task.ts

Lines changed: 42 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { type ChildProcess, fork } from 'child_process';
22
import { once } from 'events';
3-
import { writeFile } from 'fs/promises';
3+
import { mkdir, rm, writeFile } from 'fs/promises';
4+
import { tmpdir } from 'os';
45
import * as path from 'path';
56

67
import {
@@ -11,25 +12,28 @@ import {
1112
type PerfSendResult,
1213
type ResultMessage
1314
} from './common';
15+
import { exists } from './utils';
1416

1517
/**
1618
* An individual benchmark task that runs in its own Node.js process
1719
*/
1820
export class Task {
1921
result: BenchmarkResult | undefined;
20-
benchmark: BenchmarkSpecification;
22+
benchmark: Omit<BenchmarkSpecification, 'installLocation'> & { installLocation: string };
2123
taskName: string;
2224
testName: string;
2325
/** @internal */
2426
children: ChildProcess[];
2527
/** @internal */
2628
hasRun: boolean;
2729

30+
static packageInstallLocation: string = path.join(tmpdir(), 'bsonBench');
31+
2832
constructor(benchmarkSpec: BenchmarkSpecification) {
2933
this.result = undefined;
30-
this.benchmark = benchmarkSpec;
3134
this.children = [];
3235
this.hasRun = false;
36+
this.benchmark = { ...benchmarkSpec, installLocation: Task.packageInstallLocation };
3337

3438
this.taskName = `${path.basename(this.benchmark.documentPath, 'json')}_${
3539
this.benchmark.operation
@@ -174,31 +178,41 @@ export class Task {
174178

175179
// install required modules before running child process as new Node processes need to know that
176180
// it exists before they can require it.
177-
const pack = new Package(this.benchmark.library);
178-
if (!pack.check()) await pack.install();
179-
// spawn child process
180-
const child = fork(`${__dirname}/base`, {
181-
stdio: ['inherit', 'inherit', 'inherit', 'ipc'],
182-
serialization: 'advanced'
183-
});
184-
child.send({ type: 'runBenchmark', benchmark: this.benchmark });
185-
this.children.push(child);
186-
187-
// listen for results or error
188-
const resultOrError: ResultMessage | ErrorMessage = (await once(child, 'message'))[0];
189-
190-
// wait for child to close
191-
await once(child, 'exit');
192-
193-
this.hasRun = true;
194-
switch (resultOrError.type) {
195-
case 'returnResult':
196-
this.result = resultOrError.result;
197-
return resultOrError.result;
198-
case 'returnError':
199-
throw resultOrError.error;
200-
default:
201-
throw new Error('Unexpected result returned from child process');
181+
if (!(await exists(Task.packageInstallLocation))) {
182+
await mkdir(Task.packageInstallLocation);
183+
}
184+
185+
try {
186+
const pack = new Package(this.benchmark.library, Task.packageInstallLocation);
187+
if (!pack.check()) await pack.install();
188+
// spawn child process
189+
const child = fork(`${__dirname}/base`, {
190+
stdio: ['inherit', 'inherit', 'inherit', 'ipc'],
191+
serialization: 'advanced'
192+
});
193+
child.send({ type: 'runBenchmark', benchmark: this.benchmark });
194+
this.children.push(child);
195+
196+
// listen for results or error
197+
const resultOrErrorPromise = once(child, 'message');
198+
// Wait for process to exit
199+
const exit = once(child, 'exit');
200+
201+
const resultOrError: ResultMessage | ErrorMessage = (await resultOrErrorPromise)[0];
202+
await exit;
203+
204+
this.hasRun = true;
205+
switch (resultOrError.type) {
206+
case 'returnResult':
207+
this.result = resultOrError.result;
208+
return resultOrError.result;
209+
case 'returnError':
210+
throw resultOrError.error;
211+
default:
212+
throw new Error('Unexpected result returned from child process');
213+
}
214+
} finally {
215+
await rm(Task.packageInstallLocation, { recursive: true, force: true });
202216
}
203217
}
204218
}

packages/bson-bench/test/unit/common.test.ts

Lines changed: 52 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,42 @@
11
import { expect } from 'chai';
2-
import { sep } from 'path';
2+
import { mkdir, rm } from 'fs/promises';
3+
import { tmpdir } from 'os';
4+
import { join, sep } from 'path';
35

46
import { Package } from '../../lib/common';
5-
import { clearTestedDeps } from '../utils';
7+
import { clearTestedDeps, exists } from '../utils';
68

79
describe('common functionality', function () {
810
const BSON_PATH = process.env.BSON_PATH;
911

1012
context('Package', function () {
11-
beforeEach(clearTestedDeps);
12-
after(clearTestedDeps);
13+
let installDir: string;
14+
15+
after(async function () {
16+
await rm(installDir, { recursive: true, force: true });
17+
});
18+
19+
beforeEach(async function () {
20+
await clearTestedDeps(installDir);
21+
});
22+
23+
before(async function () {
24+
installDir = join(tmpdir(), 'bsonBenchTest');
25+
await mkdir(installDir);
26+
});
1327

1428
context('constructor()', function () {
29+
//github.com/mongodb-js/dbx-js-tools/pull/24/files
1530
context('when given a correctly formatted npm package', function () {
1631
it('sets computedModuleName correctly', function () {
17-
const pack = new Package('[email protected]');
32+
const pack = new Package('[email protected]', installDir);
1833
expect(pack).to.haveOwnProperty('computedModuleName', 'bson-6.0.0');
1934
});
2035
});
2136

2237
context('when given a correctly formatted git repository', function () {
2338
it('sets computedModuleName correctly', function () {
24-
const pack = new Package('bson#eb98b8c39d6d5ba4ce7231ab9e0f29495d74b994');
39+
const pack = new Package('bson#eb98b8c39d6d5ba4ce7231ab9e0f29495d74b994', installDir);
2540
expect(pack).to.haveOwnProperty(
2641
'computedModuleName',
2742
'bson-git-eb98b8c39d6d5ba4ce7231ab9e0f29495d74b994'
@@ -31,13 +46,16 @@ describe('common functionality', function () {
3146

3247
context('when trying to install an npm package apart from bson or bson-ext', function () {
3348
it('throws an error', function () {
34-
expect(() => new Package('[email protected]')).to.throw(Error, /unknown package specifier/);
49+
expect(() => new Package('[email protected]', installDir)).to.throw(
50+
Error,
51+
/unknown package specifier/
52+
);
3553
});
3654
});
3755

3856
context('when trying to install a git package apart from bson or bson-ext', function () {
3957
it('throws an error', function () {
40-
expect(() => new Package('notBson#abcdabcdabcd')).to.throw(
58+
expect(() => new Package('notBson#abcdabcdabcd', installDir)).to.throw(
4159
Error,
4260
/unknown package specifier/
4361
);
@@ -50,7 +68,7 @@ describe('common functionality', function () {
5068
console.log('Skipping since BSON_PATH is undefined');
5169
this.skip();
5270
}
53-
const pack = new Package(`bson:${BSON_PATH}`);
71+
const pack = new Package(`bson:${BSON_PATH}`, installDir);
5472
expect(pack).to.haveOwnProperty(
5573
'computedModuleName',
5674
`bson-local-${BSON_PATH.replaceAll(sep, '_')}`
@@ -62,14 +80,14 @@ describe('common functionality', function () {
6280
context('#check()', function () {
6381
context('when package is not installed', function () {
6482
it('returns undefined', function () {
65-
const pack = new Package('bson@6');
83+
const pack = new Package('bson@6', installDir);
6684
expect(pack.check()).to.be.undefined;
6785
});
6886
});
6987

7088
context('when package is installed', function () {
7189
it('returns the module', async function () {
72-
const pack = new Package('[email protected]');
90+
const pack = new Package('[email protected]', installDir);
7391
await pack.install();
7492
expect(pack.check()).to.not.be.undefined;
7593
});
@@ -79,26 +97,31 @@ describe('common functionality', function () {
7997
context('#install()', function () {
8098
context('when given a correctly formatted npm package that exists', function () {
8199
for (const lib of ['[email protected]', '[email protected]', 'bson@latest', 'bson-ext@latest']) {
82-
it(`installs ${lib} successfully`, async function () {
83-
const pack = new Package(lib);
100+
it(`installs ${lib} successfully to the specified install directory`, async function () {
101+
const pack = new Package(lib, installDir);
84102
await pack.install();
103+
104+
expect(await exists(join(installDir, 'node_modules', pack.computedModuleName))).to.be
105+
.true;
85106
});
86107
}
87108
});
88109

89110
context('when given a correctly formatted npm package that does not exist', function () {
90111
it('throws an error', async function () {
91-
const bson9000 = new Package('bson@9000');
112+
const bson9000 = new Package('bson@9000', installDir);
92113
const error = await bson9000.install().catch(error => error);
93114
expect(error).to.be.instanceOf(Error);
94115
});
95116
});
96117

97118
context('when given a correctly formatted git package using commit that exists', function () {
98-
it('installs successfully', async function () {
99-
const bson6Git = new Package('bson#58c002d');
119+
it('installs successfully to specified install directory', async function () {
120+
const bson6Git = new Package('bson#58c002d', installDir);
100121
const maybeError = await bson6Git.install().catch(error => error);
101122
expect(maybeError).to.be.undefined;
123+
expect(await exists(join(installDir, 'node_modules', bson6Git.computedModuleName))).to.be
124+
.true;
102125
});
103126
});
104127

@@ -107,7 +130,10 @@ describe('common functionality', function () {
107130
function () {
108131
// TODO: NODE-6361: Unskip and fix this test.
109132
it.skip('throws an error', async function () {
110-
const bson6Git = new Package('bson#58c002d87bca9bbe7c7001cc6acae54e90a951bcf');
133+
const bson6Git = new Package(
134+
'bson#58c002d87bca9bbe7c7001cc6acae54e90a951bcf',
135+
installDir
136+
);
111137
const maybeError = await bson6Git.install().catch(error => error);
112138
expect(maybeError).to.be.instanceOf(Error);
113139
});
@@ -118,9 +144,11 @@ describe('common functionality', function () {
118144
'when given a correctly formatted git package using git tag that exists',
119145
function () {
120146
it('installs successfully', async function () {
121-
const bson6Git = new Package('bson#v6.0.0');
147+
const bson6Git = new Package('bson#v6.0.0', installDir);
122148
const maybeError = await bson6Git.install().catch(error => error);
123149
expect(maybeError).to.be.undefined;
150+
expect(await exists(join(installDir, 'node_modules', bson6Git.computedModuleName))).to
151+
.be.true;
124152
});
125153
}
126154
);
@@ -129,7 +157,7 @@ describe('common functionality', function () {
129157
'when given a correctly formatted git package using git tag that does not exist',
130158
function () {
131159
it('throws an error', async function () {
132-
const bson6Git = new Package('bson#v999.999.9');
160+
const bson6Git = new Package('bson#v999.999.9', installDir);
133161
const maybeError = await bson6Git.install().catch(error => error);
134162
expect(maybeError).to.be.instanceOf(Error);
135163
});
@@ -143,16 +171,19 @@ describe('common functionality', function () {
143171
this.skip();
144172
}
145173

146-
const bsonLocal = new Package(`bson:${BSON_PATH}`);
174+
const bsonLocal = new Package(`bson:${BSON_PATH}`, installDir);
147175
const maybeError = await bsonLocal.install().catch(error => error);
148176
expect(maybeError).to.not.be.instanceOf(Error, maybeError.message);
177+
expect(await exists(join(installDir, 'node_modules', bsonLocal.computedModuleName))).to.be
178+
.true;
149179
});
150180
});
151181

152182
context('when given a path that does not exist', function () {
153183
it('throws an error', async function () {
154184
const bsonLocal = new Package(
155-
`bson:/highly/unlikely/path/to/exist/that/should/point/to/bson`
185+
`bson:/highly/unlikely/path/to/exist/that/should/point/to/bson`,
186+
installDir
156187
);
157188
const maybeError = await bsonLocal.install().catch(error => error);
158189
expect(maybeError).to.be.instanceOf(Error);

0 commit comments

Comments
 (0)