Skip to content

Commit

Permalink
feat: add SYSTEM_BINARY option
Browse files Browse the repository at this point in the history
* test on CI

* fix unit test

* fix prettier

* check that system binary exists

* fix unit tests

* refactor getPath method to consume several smaller methods

* update tests to excersize each smaller method individually

this also updates tests to use a mocked version of the
MongoBinaryDownloader so that we have a truelly issolated unit test...

* fix flow typing issues

* check system binary version against requested version

this is a fairly simple check and doesn't attempt to do any complex
version matching or resolution.  It simply takes the value provided by
the system and checks strict equality with the version requested.

* update the README.md to include new option

also added a section explaining how to use mongodb-memory-server on a
system not officially supported by mongodb

* check that systemBinary exists before checking versions
  • Loading branch information
jtbairdsr authored and nodkz committed Dec 19, 2018
1 parent ff9d7b0 commit 89a6b73
Show file tree
Hide file tree
Showing 4 changed files with 196 additions and 82 deletions.
7 changes: 7 additions & 0 deletions README.md
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
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
174 changes: 115 additions & 59 deletions src/util/MongoBinary.js
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,61 @@ 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 && opts.debug.call) {
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)}`);

const { downloadDir, platform, arch, version } = 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();
});
});

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 = debug;
this.cache[version] = await downloader.getMongodPath();
this.debug(`MongoBinary options: ${JSON.stringify(options)}`);

const { version, systemBinary } = options;

let binaryPath: string = '';

if (systemBinary) {
binaryPath = await this.getSystemPath(systemBinary);
if (binaryPath) {
const binaryVersion = execSync('mongod --version')
.toString()
.split('\n')[0]
.split(' ')[2];

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
@@ -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();
});
});
});

0 comments on commit 89a6b73

Please sign in to comment.