diff --git a/README.md b/README.md index e97c5ba8b..02e430bcd 100644 --- a/README.md +++ b/README.md @@ -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 @@ -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. ``` @@ -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 diff --git a/package.json b/package.json index 19892be7b..bd25bd6d0 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/util/MongoBinary.js b/src/util/MongoBinary.js index 9099ae209..da2fe9984 100644 --- a/src/util/MongoBinary.js +++ b/src/util/MongoBinary.js @@ -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 = { @@ -22,6 +24,83 @@ export type MongoBinaryOpts = { export default class MongoBinary { static cache: MongoBinaryCache = {}; + static debug: Function; + + static async getSystemPath(systemBinary: string): Promise { + 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 { + 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 { const legacyDLDir = path.resolve(os.homedir(), '.mongodb-binaries'); @@ -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) { 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 { diff --git a/src/util/__tests__/MongoBinary-test.js b/src/util/__tests__/MongoBinary-test.js index 755c9b462..663897cf4 100644 --- a/src/util/__tests__/MongoBinary-test.js +++ b/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(); + }); }); });