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

System binary #119

Merged
merged 12 commits into from
Dec 19, 2018
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ const mongod = new MongoMemoryServer({
arch?: string, // by default os.arch()
debug?: boolean, // by default false
skipMD5?: boolean, // by default false OR process.env.MONGOMS_SKIP_MD5_CHECK
systemBinary?: string, // by default undefined or process.env.MONGOMS_SYSTEM_BINARY
},
debug?: boolean, // by default false
autoStart?: boolean, // by default true
Expand All @@ -77,6 +78,7 @@ MONGOMS_VERSION=3
MONGOMS_DEBUG=1 # also available case-insensitive values: "on" "yes" "true"
MONGOMS_DOWNLOAD_MIRROR=url # your mirror url to download the mongodb binary
MONGOMS_DISABLE_POSTINSTALL=1 # if you want to skip download binaries on `npm i` command
MONGOMS_SYSTEM_BINARY=/usr/local/bin/mongod # if you want to use an existing binary already on your system.
MONGOMS_SKIP_MD5_CHECK=1 # if you want to skip MD5 check of downloaded binary.
# Passed constructor parameter `binary.skipMD5` has higher priority.
```
Expand Down Expand Up @@ -333,6 +335,11 @@ Additional examples of Jest tests:
### AVA test runner
For AVA written [detailed tutorial](https://github.com/zellwk/ava/blob/8b7ccba1d80258b272ae7cae6ba4967cd1c13030/docs/recipes/endpoint-testing-with-mongoose.md) how to test mongoose models by @zellwk.

### Docker Alpine
There isn't currently an official MongoDB release for alpine linux. This means that we can't pull binaries for Alpine
(or any other platform that isn't officially supported by MongoDB), but you can use a Docker image that already has mongod
built in and then set the MONGOMS_SYSTEM_BINARY variable to point at that binary. This should allow you to use
mongodb-memory-server on any system on which you can install mongod.

## Travis

Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@
"@babel/runtime": "^7.2.0",
"debug": "^4.1.0",
"decompress": "^4.2.0",
"dedent": "^0.7.0",
"find-cache-dir": "^2.0.0",
"get-port": "^4.0.0",
"getos": "^3.1.1",
Expand Down
164 changes: 109 additions & 55 deletions src/util/MongoBinary.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import path from 'path';
import LockFile from 'lockfile';
import mkdirp from 'mkdirp';
import findCacheDir from 'find-cache-dir';
import { execSync } from 'child_process';
import dedent from 'dedent';
import MongoBinaryDownload from './MongoBinaryDownload';

export type MongoBinaryCache = {
Expand All @@ -22,6 +24,83 @@ export type MongoBinaryOpts = {

export default class MongoBinary {
static cache: MongoBinaryCache = {};
static debug: Function;

static async getSystemPath(systemBinary: string): Promise<string> {
let binaryPath: string = '';

try {
await fs.access(systemBinary);

this.debug(`MongoBinary: found sytem binary path at ${systemBinary}`);
binaryPath = systemBinary;
} catch (err) {
this.debug(`MongoBinary: can't find system binary at ${systemBinary}`);
}

return binaryPath;
}

static async getCachePath(version: string) {
this.debug(`MongoBinary: found cached binary path for ${version}`);
return this.cache[version];
}

static async getDownloadPath(options: any): Promise<string> {
const { downloadDir, platform, arch, version } = options;

// create downloadDir if not exists
await new Promise((resolve, reject) => {
mkdirp(downloadDir, err => {
if (err) reject(err);
else resolve();
});
});

const lockfile = path.resolve(downloadDir, `${version}.lock`);

// wait lock
await new Promise((resolve, reject) => {
LockFile.lock(
lockfile,
{
wait: 120000,
pollPeriod: 100,
stale: 110000,
retries: 3,
retryWait: 100,
},
err => {
if (err) reject(err);
else resolve();
}
);
});

// again check cache, maybe other instance resolve it
if (!this.cache[version]) {
const downloader = new MongoBinaryDownload({
downloadDir,
platform,
arch,
version,
});

downloader.debug = this.debug;
this.cache[version] = await downloader.getMongodPath();
}

// remove lock
LockFile.unlock(lockfile, err => {
this.debug(
err
? `MongoBinary: Error when removing download lock ${err}`
: `MongoBinary: Download lock removed`
);
});

return this.cache[version];
}

static async getPath(opts?: MongoBinaryOpts = {}): Promise<string> {
const legacyDLDir = path.resolve(os.homedir(), '.mongodb-binaries');
Expand All @@ -43,84 +122,59 @@ export default class MongoBinary {
platform: process.env?.MONGOMS_PLATFORM || os.platform(),
arch: process.env?.MONGOMS_ARCH || os.arch(),
version: process.env?.MONGOMS_VERSION || 'latest',
systemBinary: process.env?.MONGOMS_SYSTEM_BINARY,
debug:
typeof process.env.MONGOMS_DEBUG === 'string'
? ['1', 'on', 'yes', 'true'].indexOf(process.env.MONGOMS_DEBUG.toLowerCase()) !== -1
: false,
};

let debug;
if (opts.debug) {
if (typeof opts.debug === 'function' && opts.debug.apply) {
debug = opts.debug;
} else {
debug = console.log.bind(null);
this.debug = console.log.bind(null);
}
} else {
debug = (msg: string) => {}; // eslint-disable-line
this.debug = (msg: string) => {}; // eslint-disable-line
}

const options = { ...defaultOptions, ...opts };
debug(`MongoBinary options: ${JSON.stringify(options)}`);
this.debug(`MongoBinary options: ${JSON.stringify(options)}`);

const { downloadDir, platform, arch, version } = options;
const { version, systemBinary } = options;

if (this.cache[version]) {
debug(`MongoBinary: found cached binary path for ${version}`);
} else {
// create downloadDir if not exists
await new Promise((resolve, reject) => {
mkdirp(downloadDir, err => {
if (err) reject(err);
else resolve();
});
});
let binaryPath: string = '';

const lockfile = path.resolve(downloadDir, `${version}.lock`);

// wait lock
await new Promise((resolve, reject) => {
LockFile.lock(
lockfile,
{
wait: 120000,
pollPeriod: 100,
stale: 110000,
retries: 3,
retryWait: 100,
},
err => {
if (err) reject(err);
else resolve();
}
);
});
if (systemBinary) {
binaryPath = await this.getSystemPath(systemBinary);
const binaryVersion = execSync('mongod --version')
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should be

const binaryVersion = execSync(`${binaryPath} --version`)

And need to add check that binaryPath is not empty ;)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oh yeah, good catch totally missed that.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've just merged with the master (there was a conflict).
Pull changes before commit.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There were updated packages in master.
Maybe it helps to resolve #120

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👌 I made that fix and pushed.

.toString()
.split('\n')[0]
.split(' ')[2];

// again check cache, maybe other instance resolve it
if (!this.cache[version]) {
const downloader = new MongoBinaryDownload({
downloadDir,
platform,
arch,
version,
});

downloader.debug = debug;
this.cache[version] = await downloader.getMongodPath();
if (version !== 'latest' && version !== binaryVersion) {
// we will log the version number of the system binary and the version requested so the user can see the difference
this.debug(dedent`
MongoMemoryServer: Possible version conflict
SystemBinary version: ${binaryVersion}
Requested version: ${version}

Using SystemBinary!
`);
}
}

// remove lock
LockFile.unlock(lockfile, err => {
debug(
err
? `MongoBinary: Error when removing download lock ${err}`
: `MongoBinary: Download lock removed`
);
});
if (!binaryPath) {
binaryPath = await this.getCachePath(version);
}

debug(`MongoBinary: Mongod binary path: ${this.cache[version]}`);
return this.cache[version];
if (!binaryPath) {
binaryPath = await this.getDownloadPath(options);
}

this.debug(`MongoBinary: Mongod binary path: ${binaryPath}`);
return binaryPath;
}

static hasValidBinPath(files: string[]): boolean {
Expand Down
96 changes: 73 additions & 23 deletions src/util/__tests__/MongoBinary-test.js
Original file line number Diff line number Diff line change
@@ -1,39 +1,89 @@
/* @flow */

import tmp from 'tmp';
import fs from 'fs';
import os from 'os';
import MongoBinary from '../MongoBinary';

const MongoBinaryDownload: any = require('../MongoBinaryDownload');

tmp.setGracefulCleanup();
jasmine.DEFAULT_TIMEOUT_INTERVAL = 600000;

const mockGetMongodPath = jest.fn().mockResolvedValue('/temp/path');

jest.mock('../MongoBinaryDownload', () => {
return jest.fn().mockImplementation(() => {
return { getMongodPath: mockGetMongodPath };
});
});

describe('MongoBinary', () => {
it('should download binary and keep it in cache', async () => {
const tmpDir = tmp.dirSync({ prefix: 'mongo-mem-bin-', unsafeCleanup: true });

// download
const version = 'latest';
const binPath = await MongoBinary.getPath({
downloadDir: tmpDir.name,
version,
let tmpDir;

beforeEach(() => {
tmpDir = tmp.dirSync({ prefix: 'mongo-mem-bin-', unsafeCleanup: true });
});

// cleanup
afterEach(() => {
tmpDir.removeCallback();
MongoBinaryDownload.mockClear();
mockGetMongodPath.mockClear();
MongoBinary.cache = {};
});

describe('getPath', () => {
it('should get system binary from the environment', async () => {
const accessSpy = jest.spyOn(fs, 'access');
process.env.MONGOMS_SYSTEM_BINARY = '/usr/local/bin/mongod';
await MongoBinary.getPath();

expect(accessSpy).toHaveBeenCalledWith('/usr/local/bin/mongod');

accessSpy.mockClear();
});
// eg. /tmp/mongo-mem-bin-33990ScJTSRNSsFYf/mongodb-download/a811facba94753a2eba574f446561b7e/mongodb-macOS-x86_64-3.5.5-13-g00ee4f5/
expect(binPath).toMatch(/mongo-mem-bin-.*\/.*\/mongod$/);

// reuse cache
expect(MongoBinary.cache[version]).toBeDefined();
expect(MongoBinary.cache[version]).toEqual(binPath);
const binPathAgain = await MongoBinary.getPath({
downloadDir: tmpDir.name,
version,
});

describe('getDownloadPath', () => {
it('should download binary and keep it in cache', async () => {
// download
const version = 'latest';
const binPath = await MongoBinary.getPath({
downloadDir: tmpDir.name,
version,
});

// eg. /tmp/mongo-mem-bin-33990ScJTSRNSsFYf/mongodb-download/a811facba94753a2eba574f446561b7e/mongodb-macOS-x86_64-3.5.5-13-g00ee4f5/
expect(MongoBinaryDownload).toHaveBeenCalledWith({
downloadDir: tmpDir.name,
platform: os.platform(),
arch: os.arch(),
version,
});

expect(mockGetMongodPath).toHaveBeenCalledTimes(1);

expect(MongoBinary.cache[version]).toBeDefined();
expect(MongoBinary.cache[version]).toEqual(binPath);
});
expect(binPathAgain).toEqual(binPath);
});

// cleanup
tmpDir.removeCallback();
describe('getCachePath', () => {
it('should get the cache', async () => {
MongoBinary.cache['3.4.2'] = '/bin/mongod';
await expect(MongoBinary.getCachePath('3.4.2')).resolves.toEqual('/bin/mongod');
});
});

it('should use cache', async () => {
MongoBinary.cache['3.4.2'] = '/bin/mongod';
await expect(MongoBinary.getPath({ version: '3.4.2' })).resolves.toEqual('/bin/mongod');
describe('getSystemPath', () => {
it('should use system binary if option is passed.', async () => {
const accessSpy = jest.spyOn(fs, 'access');
await MongoBinary.getSystemPath('/usr/bin/mongod');

expect(accessSpy).toHaveBeenCalledWith('/usr/bin/mongod');

accessSpy.mockClear();
});
});
});