From 126e79084dfefc0a54b14ba30161c061f417a821 Mon Sep 17 00:00:00 2001 From: Forbes Lindesay Date: Fri, 19 Jul 2019 15:33:29 +0000 Subject: [PATCH 1/7] feat: allow persistent data in postgres test --- packages/pg-config/src/PgConfig.ts | 8 +++++++ packages/pg-config/src/PgConfig.validator.ts | 5 ++++ packages/pg-test/src/.cli.ts.swp | Bin 0 -> 12288 bytes packages/pg-test/src/index.ts | 23 ++++++++++++++++++- packages/with-container/src/index.ts | 16 +++++++++++++ 5 files changed, 51 insertions(+), 1 deletion(-) create mode 100644 packages/pg-test/src/.cli.ts.swp diff --git a/packages/pg-config/src/PgConfig.ts b/packages/pg-config/src/PgConfig.ts index 9dbf4326..b1a069a1 100644 --- a/packages/pg-config/src/PgConfig.ts +++ b/packages/pg-config/src/PgConfig.ts @@ -18,6 +18,14 @@ export interface TestConfig { * @default "postgres:10.6-alpine" */ image: string; + /** + * Set this to a volume name to automatically + * persist data to a docker volume of that name. + * + * N.B. this will automatically remove "-ram" + * from the end of any image name. + */ + persistVolume?: string; /** * The default name to give the docker * containers run by @database/pg-test diff --git a/packages/pg-config/src/PgConfig.validator.ts b/packages/pg-config/src/PgConfig.validator.ts index ad7e0c32..5d225a74 100644 --- a/packages/pg-config/src/PgConfig.validator.ts +++ b/packages/pg-config/src/PgConfig.validator.ts @@ -61,6 +61,11 @@ export const PgConfigSchema = { description: 'Optional script to run after the database\nhas been started but before running tests', }, + persistVolume: { + description: + 'Set this to a volume name to automatically\npersist data to a docker volume of that name.\n\nN.B. this will automatically remove "-ram"\nfrom the end of any image name.', + type: 'string', + }, pgDb: { default: 'test-db', description: 'The db to create in the test docker container', diff --git a/packages/pg-test/src/.cli.ts.swp b/packages/pg-test/src/.cli.ts.swp new file mode 100644 index 0000000000000000000000000000000000000000..093965dc739bf31e31c65e4f59ac24337ee8daf4 GIT binary patch literal 12288 zcmeI%v2MaJ5P;!>&Q%f2NG=Ra#6T-`=o2t7!fuijA)twDmyy0&m%a<_Q#2OI!hm3m z`df0&mz=HKw?$cw7Sow}iJqkxt#CPw@2-f=GGFDFVv_EM<4XnQvqLyg8=n|&md2${ z=O*5mRamTsK6PF@8|!$xj=a0#JW3Ei;1+@V`O{=P3Oo9}N7ZT1Z`n*O0tg_000Iag zfB*sr)Lg*l4f!m3XqJ84m9cRa%anis0tg_000IagfB*srAb { pgUser: string; pgDb: string; + persistVolume?: string; } export default async function getDatabase(options: Partial = {}) { - const {pgUser, pgDb, environment, ...rawOptions}: Options = { + const {pgUser, pgDb, environment, persistVolume, ...rawOptions}: Options = { debug: DEFAULT_PG_DEBUG, image: DEFAULT_IMAGE, containerName: DEFAULT_CONTAINER_NAME, @@ -43,11 +44,31 @@ export default async function getDatabase(options: Partial = {}) { pgDb: DEFAULT_PG_DB, defaultExternalPort: DEFAULT_PG_PORT, externalPort: config.test.port, + persistVolume: config.test.persistVolume, ...options, }; + if (persistVolume) { + rawOptions.image = rawOptions.image.replace(/\-ram$/, ''); + } + if (options.persistVolume) { + console.info(`Using ${options.persistVolume} to store sql data`); + console.info(`Run "docker volume rm ${options.persistVolume}" to clear data`); + } const {proc, externalPort, kill} = await startContainer({ ...rawOptions, + mount: [ + ...(options.mount || []), + ...(options.persistVolume + ? [ + { + type: 'volume', + src: options.persistVolume, + dst: '/var/lib/postgresql/data', + } as const, + ] + : []), + ], internalPort: DEFAULT_PG_PORT, environment: {...environment, POSTGRES_USER: pgUser, POSTGRES_DB: pgDb}, }); diff --git a/packages/with-container/src/index.ts b/packages/with-container/src/index.ts index 7ae9faf9..cf37d391 100644 --- a/packages/with-container/src/index.ts +++ b/packages/with-container/src/index.ts @@ -23,6 +23,12 @@ export interface Options { */ refreshImage?: boolean; detached?: boolean; + mount?: ReadonlyArray<{ + type: 'bind' | 'volume' | 'tmpfs'; + src: string; + dst: string; + readonly?: boolean; + }>; } export interface NormalizedOptions @@ -82,6 +88,15 @@ export function startDockerContainer(options: NormalizedOptions) { envArgs.push('--env'); envArgs.push(`${key}=${env[key]}`); }); + const mounts: string[] = []; + (options.mount || []).forEach(mount => { + mounts.push('--mount'); + mounts.push( + `type=${mount.type},src=${mount.src},dst=${mount.dst}${ + mount.readonly ? `,readonly` : `` + }`, + ); + }); return spawn( 'docker', [ @@ -93,6 +108,7 @@ export function startDockerContainer(options: NormalizedOptions) { '-p', // forward appropriate port `${options.externalPort}:${options.internalPort}`, ...(options.detached ? ['--detach'] : []), + ...mounts, // set enviornment variables ...envArgs, options.image, From 90066290e6d3685753528a263611df6adda643a1 Mon Sep 17 00:00:00 2001 From: Forbes Lindesay Date: Fri, 4 Oct 2019 16:03:57 +0100 Subject: [PATCH 2/7] feat: allow migrationsScript to be a string --- packages/pg-test/src/.cli.ts.swp | Bin 12288 -> 0 bytes packages/pg-test/src/index.ts | 4 +++- packages/pg-test/src/jest/globalSetup.ts | 14 +++++++------- 3 files changed, 10 insertions(+), 8 deletions(-) delete mode 100644 packages/pg-test/src/.cli.ts.swp diff --git a/packages/pg-test/src/.cli.ts.swp b/packages/pg-test/src/.cli.ts.swp deleted file mode 100644 index 093965dc739bf31e31c65e4f59ac24337ee8daf4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 12288 zcmeI%v2MaJ5P;!>&Q%f2NG=Ra#6T-`=o2t7!fuijA)twDmyy0&m%a<_Q#2OI!hm3m z`df0&mz=HKw?$cw7Sow}iJqkxt#CPw@2-f=GGFDFVv_EM<4XnQvqLyg8=n|&md2${ z=O*5mRamTsK6PF@8|!$xj=a0#JW3Ei;1+@V`O{=P3Oo9}N7ZT1Z`n*O0tg_000Iag zfB*sr)Lg*l4f!m3XqJ84m9cRa%anis0tg_000IagfB*srAb = {}) { if (options.persistVolume) { console.info(`Using ${options.persistVolume} to store sql data`); - console.info(`Run "docker volume rm ${options.persistVolume}" to clear data`); + console.info( + `Run "docker volume rm ${options.persistVolume}" to clear data`, + ); } const {proc, externalPort, kill} = await startContainer({ ...rawOptions, diff --git a/packages/pg-test/src/jest/globalSetup.ts b/packages/pg-test/src/jest/globalSetup.ts index f0c44bf5..5494caba 100644 --- a/packages/pg-test/src/jest/globalSetup.ts +++ b/packages/pg-test/src/jest/globalSetup.ts @@ -1,6 +1,10 @@ // @public +<<<<<<< HEAD import getDatabase, {Options} from '../'; +======= +import getDatabase, {run, runExec, Options} from '../'; +>>>>>>> feat: allow migrationsScript to be a string import {getPgConfigSync} from '@databases/pg-config'; import {spawnBuffered, execBuffered} from 'modern-spawn'; @@ -20,7 +24,7 @@ export default async function setup( const migrationsScript = opts.migrationsScript || (process.env.PG_TEST_MIGRATIONS_SCRIPT - ? process.env.PG_TEST_MIGRATIONS_SCRIPT.split(' ') + ? process.env.PG_TEST_MIGRATIONS_SCRIPT : config.test.migrationsScript); if (process.env[envVar]) { console.info(`Using existing pg database from: ${envVar}`); @@ -34,16 +38,12 @@ export default async function setup( if (typeof migrationsScript === 'string') { await execBuffered(migrationsScript, { debug: - opts.debug || - (opts.debug === undefined && config.test.debug) || - false, + opts.debug || (opts.debug === undefined && config.test.debug) || false, }).getResult(); } else { await spawnBuffered(migrationsScript[0], migrationsScript.slice(1), { debug: - opts.debug || - (opts.debug === undefined && config.test.debug) || - false, + opts.debug || (opts.debug === undefined && config.test.debug) || false, }).getResult(); } } From 0674b5b65fd4b5eb4f74fea13676e34dd1c5f907 Mon Sep 17 00:00:00 2001 From: Forbes Lindesay Date: Fri, 4 Oct 2019 16:36:16 +0100 Subject: [PATCH 3/7] feat: add CLI to pg-test --- packages/pg-test/jest/globalSetup.d.ts | 6 -- packages/pg-test/jest/globalSetup.js | 3 - packages/pg-test/jest/globalTeardown.d.ts | 5 -- packages/pg-test/jest/globalTeardown.js | 3 - packages/pg-test/package.json | 3 + packages/pg-test/src/cli.ts | 82 +++++++++++++++++++++++ packages/pg-test/src/index.ts | 29 +++++++- 7 files changed, 111 insertions(+), 20 deletions(-) delete mode 100644 packages/pg-test/jest/globalSetup.d.ts delete mode 100644 packages/pg-test/jest/globalSetup.js delete mode 100644 packages/pg-test/jest/globalTeardown.d.ts delete mode 100644 packages/pg-test/jest/globalTeardown.js create mode 100644 packages/pg-test/src/cli.ts diff --git a/packages/pg-test/jest/globalSetup.d.ts b/packages/pg-test/jest/globalSetup.d.ts deleted file mode 100644 index 45315881..00000000 --- a/packages/pg-test/jest/globalSetup.d.ts +++ /dev/null @@ -1,6 +0,0 @@ -// @autogenerated - -import def from '../lib/jest/globalSetup'; - -export default def; -export * from '../lib/jest/globalSetup'; diff --git a/packages/pg-test/jest/globalSetup.js b/packages/pg-test/jest/globalSetup.js deleted file mode 100644 index 6f3c7114..00000000 --- a/packages/pg-test/jest/globalSetup.js +++ /dev/null @@ -1,3 +0,0 @@ -// @autogenerated - -module.exports = require('../lib/jest/globalSetup'); \ No newline at end of file diff --git a/packages/pg-test/jest/globalTeardown.d.ts b/packages/pg-test/jest/globalTeardown.d.ts deleted file mode 100644 index 0ada2f27..00000000 --- a/packages/pg-test/jest/globalTeardown.d.ts +++ /dev/null @@ -1,5 +0,0 @@ -// @autogenerated - -import def from '../lib/jest/globalTeardown'; - -export default def; diff --git a/packages/pg-test/jest/globalTeardown.js b/packages/pg-test/jest/globalTeardown.js deleted file mode 100644 index a75753b6..00000000 --- a/packages/pg-test/jest/globalTeardown.js +++ /dev/null @@ -1,3 +0,0 @@ -// @autogenerated - -module.exports = require('../lib/jest/globalTeardown'); \ No newline at end of file diff --git a/packages/pg-test/package.json b/packages/pg-test/package.json index aa4922a6..6b303f47 100644 --- a/packages/pg-test/package.json +++ b/packages/pg-test/package.json @@ -4,6 +4,9 @@ "description": "", "main": "./lib/index.js", "types": "./lib/index.d.ts", + "bin": { + "pg-test": "./lib/cli.js" + }, "dependencies": { "@databases/pg-config": "^0.0.0", "@databases/with-container": "^0.0.0", diff --git a/packages/pg-test/src/cli.ts b/packages/pg-test/src/cli.ts new file mode 100644 index 00000000..175272a2 --- /dev/null +++ b/packages/pg-test/src/cli.ts @@ -0,0 +1,82 @@ +#!/usr/bin/env node + +import getDatabase, {stopDatabase} from '.'; +import {readFileSync, writeFileSync} from 'fs'; + +if ( + process.argv[2] === 'help' || + process.argv.includes('--help') || + process.argv.includes('-h') +) { + usage(); + process.exit(0); +} + +const debug = process.argv.includes('--debug') ? true : undefined; +const containerName = process.argv.includes('--containerName') + ? process.argv[process.argv.indexOf('--containerName') + 1] + : undefined; + +function get(key: string) { + return process.argv.includes(key) + ? process.argv[process.argv.indexOf(key) + 1] + : undefined; +} +const command = process.argv[2]; +switch (command) { + case 'start': + getDatabase({ + detached: true, + debug, + containerName, + pgUser: get('--pgUser'), + pgDb: get('--pgDb'), + persistVolume: get('--persistVolume'), + refreshImage: process.argv.includes('--refreshImage') ? true : undefined, + }) + .then(result => { + const envLine = 'DATABASE_URL=' + result.databaseURL; + if (process.argv.includes('--writeEnv')) { + console.info('Writing to .env'); + console.info(''); + console.info(envLine); + try { + let dotenv = readFileSync('.env', 'utf8'); + + if (/^DATABASE_URL *=.*$/gm.test(dotenv)) { + dotenv = dotenv.replace(/^DATABASE_URL *=.*$/gm, envLine); + } else { + dotenv += '\n' + envLine + '\n'; + } + writeFileSync('.env', dotenv); + } catch (ex) { + if (ex.code !== 'ENOENT') { + throw ex; + } + writeFileSync('.env', envLine + '\n'); + } + } else { + console.info(envLine); + } + }) + .catch(ex => { + console.error(ex); + process.exit(1); + }); + break; + case 'stop': + stopDatabase({debug, containerName}).catch(ex => { + console.error(ex); + process.exit(1); + }); + break; + default: + usage(); + process.exit(1); + break; +} + +function usage() { + console.info('Usage: pg-test start'); + console.info('Usage: pg-test stop'); +} diff --git a/packages/pg-test/src/index.ts b/packages/pg-test/src/index.ts index c642549a..e584737c 100644 --- a/packages/pg-test/src/index.ts +++ b/packages/pg-test/src/index.ts @@ -1,5 +1,6 @@ import startContainer, { Options as WithContainerOptions, + killOldContainers, } from '@databases/with-container'; import {getPgConfigSync} from '@databases/pg-config'; @@ -34,8 +35,31 @@ export interface Options persistVolume?: string; } +export async function stopDatabase( + options: Partial> = {}, +) { + await killOldContainers({ + debug: options.debug !== undefined ? options.debug : DEFAULT_PG_DEBUG, + containerName: options.containerName || DEFAULT_CONTAINER_NAME, + }); +} +function withDefaults(overrides: Partial, defaults: T): T { + const result = {...defaults}; + Object.keys(overrides).forEach(key => { + if ((overrides as any)[key] !== undefined) { + (result as any)[key] = (overrides as any)[key]; + } + }); + return result; +} export default async function getDatabase(options: Partial = {}) { - const {pgUser, pgDb, environment, persistVolume, ...rawOptions}: Options = { + const { + pgUser, + pgDb, + environment, + persistVolume, + ...rawOptions + }: Options = withDefaults(options, { debug: DEFAULT_PG_DEBUG, image: DEFAULT_IMAGE, containerName: DEFAULT_CONTAINER_NAME, @@ -45,8 +69,7 @@ export default async function getDatabase(options: Partial = {}) { defaultExternalPort: DEFAULT_PG_PORT, externalPort: config.test.port, persistVolume: config.test.persistVolume, - ...options, - }; + }); if (persistVolume) { rawOptions.image = rawOptions.image.replace(/\-ram$/, ''); } From 9e27f81d7bc8206da2faa950d2bd7dc794954f35 Mon Sep 17 00:00:00 2001 From: Forbes Lindesay Date: Fri, 13 Dec 2019 17:49:06 +0000 Subject: [PATCH 4/7] fix: use non-circleci docker image for pg This means it's no longer RAM based --- packages/pg-migrations/build-package.d.ts | 6 ------ packages/pg-migrations/build-package.js | 3 --- packages/pg-migrations/getID.d.ts | 5 ----- packages/pg-migrations/getID.js | 3 --- packages/pg-test/src/jest/globalSetup.ts | 12 ++++++------ 5 files changed, 6 insertions(+), 23 deletions(-) delete mode 100644 packages/pg-migrations/build-package.d.ts delete mode 100644 packages/pg-migrations/build-package.js delete mode 100644 packages/pg-migrations/getID.d.ts delete mode 100644 packages/pg-migrations/getID.js diff --git a/packages/pg-migrations/build-package.d.ts b/packages/pg-migrations/build-package.d.ts deleted file mode 100644 index 894a718a..00000000 --- a/packages/pg-migrations/build-package.d.ts +++ /dev/null @@ -1,6 +0,0 @@ -// @autogenerated - -import def from './lib/build-package'; - -export default def; -export * from './lib/build-package'; diff --git a/packages/pg-migrations/build-package.js b/packages/pg-migrations/build-package.js deleted file mode 100644 index bbc16a24..00000000 --- a/packages/pg-migrations/build-package.js +++ /dev/null @@ -1,3 +0,0 @@ -// @autogenerated - -module.exports = require('./lib/build-package'); \ No newline at end of file diff --git a/packages/pg-migrations/getID.d.ts b/packages/pg-migrations/getID.d.ts deleted file mode 100644 index e3ea7463..00000000 --- a/packages/pg-migrations/getID.d.ts +++ /dev/null @@ -1,5 +0,0 @@ -// @autogenerated - -import def from './lib/getID'; - -export default def; diff --git a/packages/pg-migrations/getID.js b/packages/pg-migrations/getID.js deleted file mode 100644 index a3fb7d77..00000000 --- a/packages/pg-migrations/getID.js +++ /dev/null @@ -1,3 +0,0 @@ -// @autogenerated - -module.exports = require('./lib/getID'); \ No newline at end of file diff --git a/packages/pg-test/src/jest/globalSetup.ts b/packages/pg-test/src/jest/globalSetup.ts index 5494caba..7292e18a 100644 --- a/packages/pg-test/src/jest/globalSetup.ts +++ b/packages/pg-test/src/jest/globalSetup.ts @@ -1,10 +1,6 @@ // @public -<<<<<<< HEAD import getDatabase, {Options} from '../'; -======= -import getDatabase, {run, runExec, Options} from '../'; ->>>>>>> feat: allow migrationsScript to be a string import {getPgConfigSync} from '@databases/pg-config'; import {spawnBuffered, execBuffered} from 'modern-spawn'; @@ -38,12 +34,16 @@ export default async function setup( if (typeof migrationsScript === 'string') { await execBuffered(migrationsScript, { debug: - opts.debug || (opts.debug === undefined && config.test.debug) || false, + opts.debug || + (opts.debug === undefined && config.test.debug) || + false, }).getResult(); } else { await spawnBuffered(migrationsScript[0], migrationsScript.slice(1), { debug: - opts.debug || (opts.debug === undefined && config.test.debug) || false, + opts.debug || + (opts.debug === undefined && config.test.debug) || + false, }).getResult(); } } From 25d0ac2df7f6baf8f3093901b1b80c7ee470ab9b Mon Sep 17 00:00:00 2001 From: Forbes Lindesay Date: Fri, 13 Dec 2019 17:50:40 +0000 Subject: [PATCH 5/7] fix: wait longer for container to be ready --- packages/with-container/src/index.ts | 28 ++++++++++++++++++++++------ 1 file changed, 22 insertions(+), 6 deletions(-) diff --git a/packages/with-container/src/index.ts b/packages/with-container/src/index.ts index cf37d391..1a0c3ccf 100644 --- a/packages/with-container/src/index.ts +++ b/packages/with-container/src/index.ts @@ -92,7 +92,7 @@ export function startDockerContainer(options: NormalizedOptions) { (options.mount || []).forEach(mount => { mounts.push('--mount'); mounts.push( - `type=${mount.type},src=${mount.src},dst=${mount.dst}${ + `type=${mount.type},source=${mount.src},target=${mount.dst}${ mount.readonly ? `,readonly` : `` }`, ); @@ -134,16 +134,32 @@ export async function waitForDatabaseToStart(options: NormalizedOptions) { console.info( `Waiting for ${options.containerName} on port ${options.externalPort}...`, ); + let currentTestFinished = false; const connection = connect(options.externalPort) .on('error', () => { - if (finished) return; + if (finished || currentTestFinished) return; + currentTestFinished = true; + setTimeout(test, 500); + }) + .on('end', () => { + if (finished || currentTestFinished) return; + currentTestFinished = true; + setTimeout(test, 500); + }) + .on('close', () => { + if (finished || currentTestFinished) return; + currentTestFinished = true; setTimeout(test, 500); }) .on('connect', () => { - finished = true; - clearTimeout(timeout); - connection.end(); - setTimeout(resolve, 1000); + setTimeout(() => { + if (finished || currentTestFinished) return; + finished = true; + currentTestFinished = true; + clearTimeout(timeout); + connection.end(); + resolve(); + }, 2000); }); } test(); From a55ddbf54a37b83cfdc0d084faa27c1f8c5d09e7 Mon Sep 17 00:00:00 2001 From: Forbes Lindesay Date: Fri, 13 Dec 2019 17:52:47 +0000 Subject: [PATCH 6/7] feat: automatically generate ports from volumes and add tests --- packages/pg-test/jest/globalSetup.d.ts | 6 ++ packages/pg-test/jest/globalSetup.js | 3 + packages/pg-test/jest/globalTeardown.d.ts | 5 + packages/pg-test/jest/globalTeardown.js | 3 + packages/pg-test/package.json | 4 +- packages/pg-test/src/__tests__/.gitignore | 1 + .../__snapshots__/integration.test.ts.snap | 5 + .../pg-test/src/__tests__/integration.test.ts | 70 ++++++++++++++ .../src/__tests__/numberToValidPort.test.ts | 10 ++ .../src/__tests__/stringToValidPort.test.ts | 10 ++ packages/pg-test/src/cli-function.ts | 96 +++++++++++++++++++ packages/pg-test/src/cli.ts | 84 ++-------------- packages/pg-test/src/numberToValidPort.ts | 9 ++ packages/pg-test/src/stringToValidPort.ts | 10 ++ yarn.lock | 5 + 15 files changed, 242 insertions(+), 79 deletions(-) create mode 100644 packages/pg-test/jest/globalSetup.d.ts create mode 100644 packages/pg-test/jest/globalSetup.js create mode 100644 packages/pg-test/jest/globalTeardown.d.ts create mode 100644 packages/pg-test/jest/globalTeardown.js create mode 100644 packages/pg-test/src/__tests__/.gitignore create mode 100644 packages/pg-test/src/__tests__/__snapshots__/integration.test.ts.snap create mode 100644 packages/pg-test/src/__tests__/integration.test.ts create mode 100644 packages/pg-test/src/__tests__/numberToValidPort.test.ts create mode 100644 packages/pg-test/src/__tests__/stringToValidPort.test.ts create mode 100644 packages/pg-test/src/cli-function.ts create mode 100644 packages/pg-test/src/numberToValidPort.ts create mode 100644 packages/pg-test/src/stringToValidPort.ts diff --git a/packages/pg-test/jest/globalSetup.d.ts b/packages/pg-test/jest/globalSetup.d.ts new file mode 100644 index 00000000..45315881 --- /dev/null +++ b/packages/pg-test/jest/globalSetup.d.ts @@ -0,0 +1,6 @@ +// @autogenerated + +import def from '../lib/jest/globalSetup'; + +export default def; +export * from '../lib/jest/globalSetup'; diff --git a/packages/pg-test/jest/globalSetup.js b/packages/pg-test/jest/globalSetup.js new file mode 100644 index 00000000..6f3c7114 --- /dev/null +++ b/packages/pg-test/jest/globalSetup.js @@ -0,0 +1,3 @@ +// @autogenerated + +module.exports = require('../lib/jest/globalSetup'); \ No newline at end of file diff --git a/packages/pg-test/jest/globalTeardown.d.ts b/packages/pg-test/jest/globalTeardown.d.ts new file mode 100644 index 00000000..0ada2f27 --- /dev/null +++ b/packages/pg-test/jest/globalTeardown.d.ts @@ -0,0 +1,5 @@ +// @autogenerated + +import def from '../lib/jest/globalTeardown'; + +export default def; diff --git a/packages/pg-test/jest/globalTeardown.js b/packages/pg-test/jest/globalTeardown.js new file mode 100644 index 00000000..a75753b6 --- /dev/null +++ b/packages/pg-test/jest/globalTeardown.js @@ -0,0 +1,3 @@ +// @autogenerated + +module.exports = require('../lib/jest/globalTeardown'); \ No newline at end of file diff --git a/packages/pg-test/package.json b/packages/pg-test/package.json index 6b303f47..4342ccfb 100644 --- a/packages/pg-test/package.json +++ b/packages/pg-test/package.json @@ -10,6 +10,7 @@ "dependencies": { "@databases/pg-config": "^0.0.0", "@databases/with-container": "^0.0.0", + "generate-alphabetic-name": "^1.0.0", "modern-spawn": "^1.0.0" }, "scripts": {}, @@ -19,7 +20,8 @@ "access": "public" }, "devDependencies": { - "@types/node": "^13.13.4" + "@types/node": "^13.13.4", + "@databases/pg": "^0.0.0" }, "bugs": "https://github.com/ForbesLindesay/atdatabases/issues", "homepage": "https://www.atdatabases.org/docs/pg-test", diff --git a/packages/pg-test/src/__tests__/.gitignore b/packages/pg-test/src/__tests__/.gitignore new file mode 100644 index 00000000..c4618616 --- /dev/null +++ b/packages/pg-test/src/__tests__/.gitignore @@ -0,0 +1 @@ +env-example-* \ No newline at end of file diff --git a/packages/pg-test/src/__tests__/__snapshots__/integration.test.ts.snap b/packages/pg-test/src/__tests__/__snapshots__/integration.test.ts.snap new file mode 100644 index 00000000..c305f4ff --- /dev/null +++ b/packages/pg-test/src/__tests__/__snapshots__/integration.test.ts.snap @@ -0,0 +1,5 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`integration: DATABASE_URL for atdatabases-test-volume-1 1`] = `"postgres://test-user@localhost:47936/test-db"`; + +exports[`integration: DATABASE_URL for atdatabases-test-volume-2 1`] = `"postgres://test-user@localhost:9833/test-db"`; diff --git a/packages/pg-test/src/__tests__/integration.test.ts b/packages/pg-test/src/__tests__/integration.test.ts new file mode 100644 index 00000000..724b0221 --- /dev/null +++ b/packages/pg-test/src/__tests__/integration.test.ts @@ -0,0 +1,70 @@ +import connect, {sql} from '@databases/pg'; +import {readFileSync} from 'fs'; +import {run} from '../'; +import call from '../cli-function'; + +jest.setTimeout(60_000); +if (!process.env.CI) { + const cleanup = async () => { + for (const volume of [ + 'atdatabases-test-volume-1', + 'atdatabases-test-volume-2', + ]) { + await run('docker', ['kill', volume], { + debug: false, + name: 'docker kill', + allowFailure: true, + }); + await run('docker', ['volume', 'rm', volume], { + debug: false, + name: 'docker volume rm', + allowFailure: true, + }); + } + }; + afterAll(cleanup); + beforeAll(cleanup); + test('integration', async () => { + for (const volume of [ + 'atdatabases-test-volume-1', + 'atdatabases-test-volume-2', + ]) { + await run('docker', ['volume', 'create', volume], { + debug: true, + name: 'docker volume create', + allowFailure: false, + }); + await call([ + 'start', + '--persistVolume', + volume, + '--writeEnv', + `${__dirname}/env-example-1`, + ]); + + const dbURL = readFileSync(`${__dirname}/env-example-1`, 'utf8') + .trim() + .replace(/DATABASE_URL=/, ''); + expect(dbURL).toMatchSnapshot(`DATABASE_URL for ${volume}`); + const db = connect(dbURL); + await db.query(sql`CREATE TABLE entries (id INT NOT NULL PRIMARY KEY)`); + await db.query(sql`INSERT INTO entries (id) VALUES (1), (2)`); + await db.dispose(); + + await call(['stop', '--persistVolume', volume]); + + await call(['start', '--persistVolume', volume]); + + const db2 = connect(dbURL); + expect(await db2.query(sql`SELECT * FROM entries`)).toEqual([ + {id: 1}, + {id: 2}, + ]); + await db2.dispose(); + + await call(['stop', '--persistVolume', volume]); + } + }); +} else { + test.skip('Cannot run in CI because we need to control docker images'); +} diff --git a/packages/pg-test/src/__tests__/numberToValidPort.test.ts b/packages/pg-test/src/__tests__/numberToValidPort.test.ts new file mode 100644 index 00000000..681df1b7 --- /dev/null +++ b/packages/pg-test/src/__tests__/numberToValidPort.test.ts @@ -0,0 +1,10 @@ +import numberToValidPort from '../numberToValidPort'; + +test('numberToValidPort', () => { + expect(numberToValidPort(0, 5, 7)).toBe(5); + expect(numberToValidPort(1, 5, 7)).toBe(6); + expect(numberToValidPort(2, 5, 7)).toBe(7); + expect(numberToValidPort(3, 5, 7)).toBe(5); + expect(numberToValidPort(4, 5, 7)).toBe(6); + expect(numberToValidPort(5, 5, 7)).toBe(7); +}); diff --git a/packages/pg-test/src/__tests__/stringToValidPort.test.ts b/packages/pg-test/src/__tests__/stringToValidPort.test.ts new file mode 100644 index 00000000..95a0270c --- /dev/null +++ b/packages/pg-test/src/__tests__/stringToValidPort.test.ts @@ -0,0 +1,10 @@ +import stringToValidPort from '../stringToValidPort'; + +test('numberToValidPort', () => { + expect(stringToValidPort('volume-a', 1000, 2000)).toMatchInlineSnapshot( + `1980`, + ); + expect(stringToValidPort('volume-b', 1000, 2000)).toMatchInlineSnapshot( + `1224`, + ); +}); diff --git a/packages/pg-test/src/cli-function.ts b/packages/pg-test/src/cli-function.ts new file mode 100644 index 00000000..302e1589 --- /dev/null +++ b/packages/pg-test/src/cli-function.ts @@ -0,0 +1,96 @@ +#!/usr/bin/env node + +import getDatabase, {stopDatabase} from '.'; +import {readFileSync, writeFileSync} from 'fs'; +import stringToValidPort from './stringToValidPort'; + +export default async function call(args: string[]): Promise { + if (args[0] === 'help' || args.includes('--help') || args.includes('-h')) { + usage(); + return 0; + } + + const PORT_RANGE = [1024, 49151] as const; + const debug = args.includes('--debug') ? true : undefined; + function get(key: string) { + return args.includes(key) ? args[args.indexOf(key) + 1] : undefined; + } + + const persistVolume = get('--persistVolume'); + const containerName = + get('--containerName') || + (persistVolume ? `pg-test-${persistVolume}` : undefined); + + const portStr = get('port'); + if ( + portStr && + (!/^\d+$/.test(portStr) || + portStr.length > PORT_RANGE[1].toString().length || + parseInt(portStr, 10) < PORT_RANGE[0] || + parseInt(portStr, 10) > PORT_RANGE[1]) + ) { + console.error( + `The port must be a valid integer between ${PORT_RANGE[0]} and ${PORT_RANGE[1]} (inclusive)`, + ); + return 1; + } + + const defaultExternalPort = portStr + ? parseInt(portStr, 10) + : persistVolume + ? stringToValidPort(persistVolume, PORT_RANGE[0], PORT_RANGE[1]) + : undefined; + + const command = args[0]; + switch (command) { + case 'start': + const result = await getDatabase({ + detached: true, + debug, + containerName, + pgUser: get('--pgUser'), + pgDb: get('--pgDb'), + persistVolume, + externalPort: portStr ? defaultExternalPort : undefined, + defaultExternalPort, + refreshImage: args.includes('--refreshImage') ? true : undefined, + }); + const envLine = 'DATABASE_URL=' + result.databaseURL; + const writeEnv = get('--writeEnv'); + if (args.includes('--writeEnv')) { + const envFile = writeEnv && writeEnv[0] !== '-' ? writeEnv : '.env'; + console.info('Writing to .env'); + console.info(''); + console.info(envLine); + try { + let dotenv = readFileSync(envFile, 'utf8'); + + if (/^DATABASE_URL *=.*$/gm.test(dotenv)) { + dotenv = dotenv.replace(/^DATABASE_URL *=.*$/gm, envLine); + } else { + dotenv += '\n' + envLine + '\n'; + } + writeFileSync(envFile, dotenv); + } catch (ex) { + if (ex.code !== 'ENOENT') { + throw ex; + } + writeFileSync(envFile, envLine + '\n'); + } + } else { + console.info(envLine); + } + return 0; + case 'stop': + await stopDatabase({debug, containerName}); + return 0; + default: + usage(); + return 1; + } + + function usage() { + console.info('Usage: pg-test start'); + console.info('Usage: pg-test stop'); + } +} diff --git a/packages/pg-test/src/cli.ts b/packages/pg-test/src/cli.ts index 175272a2..0424f5d3 100644 --- a/packages/pg-test/src/cli.ts +++ b/packages/pg-test/src/cli.ts @@ -1,82 +1,10 @@ #!/usr/bin/env node -import getDatabase, {stopDatabase} from '.'; -import {readFileSync, writeFileSync} from 'fs'; +import call from './cli-function'; -if ( - process.argv[2] === 'help' || - process.argv.includes('--help') || - process.argv.includes('-h') -) { - usage(); - process.exit(0); -} - -const debug = process.argv.includes('--debug') ? true : undefined; -const containerName = process.argv.includes('--containerName') - ? process.argv[process.argv.indexOf('--containerName') + 1] - : undefined; - -function get(key: string) { - return process.argv.includes(key) - ? process.argv[process.argv.indexOf(key) + 1] - : undefined; -} -const command = process.argv[2]; -switch (command) { - case 'start': - getDatabase({ - detached: true, - debug, - containerName, - pgUser: get('--pgUser'), - pgDb: get('--pgDb'), - persistVolume: get('--persistVolume'), - refreshImage: process.argv.includes('--refreshImage') ? true : undefined, - }) - .then(result => { - const envLine = 'DATABASE_URL=' + result.databaseURL; - if (process.argv.includes('--writeEnv')) { - console.info('Writing to .env'); - console.info(''); - console.info(envLine); - try { - let dotenv = readFileSync('.env', 'utf8'); - - if (/^DATABASE_URL *=.*$/gm.test(dotenv)) { - dotenv = dotenv.replace(/^DATABASE_URL *=.*$/gm, envLine); - } else { - dotenv += '\n' + envLine + '\n'; - } - writeFileSync('.env', dotenv); - } catch (ex) { - if (ex.code !== 'ENOENT') { - throw ex; - } - writeFileSync('.env', envLine + '\n'); - } - } else { - console.info(envLine); - } - }) - .catch(ex => { - console.error(ex); - process.exit(1); - }); - break; - case 'stop': - stopDatabase({debug, containerName}).catch(ex => { - console.error(ex); - process.exit(1); - }); - break; - default: - usage(); +call(process.argv.slice(2)) + .then(code => process.exit(code)) + .catch(ex => { + console.error(ex); process.exit(1); - break; -} - -function usage() { - console.info('Usage: pg-test start'); - console.info('Usage: pg-test stop'); -} + }); diff --git a/packages/pg-test/src/numberToValidPort.ts b/packages/pg-test/src/numberToValidPort.ts new file mode 100644 index 00000000..bde8cdaa --- /dev/null +++ b/packages/pg-test/src/numberToValidPort.ts @@ -0,0 +1,9 @@ +export default function numberToValidPort( + value: number, + minPort: number, + maxPort: number, +) { + const range = maxPort + 1 - minPort; + const index = value % range; + return minPort + index; +} diff --git a/packages/pg-test/src/stringToValidPort.ts b/packages/pg-test/src/stringToValidPort.ts new file mode 100644 index 00000000..9be366bc --- /dev/null +++ b/packages/pg-test/src/stringToValidPort.ts @@ -0,0 +1,10 @@ +import {hash} from 'generate-alphabetic-name'; +import numberToValidPort from './numberToValidPort'; + +export default function stringToValidPort( + value: string, + minPort: number, + maxPort: number, +) { + return numberToValidPort(hash(value), minPort, maxPort); +} diff --git a/yarn.lock b/yarn.lock index 55424fe4..f7c4a94d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2505,6 +2505,11 @@ gauge@~2.7.3: strip-ansi "^3.0.1" wide-align "^1.1.0" +generate-alphabetic-name@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/generate-alphabetic-name/-/generate-alphabetic-name-1.0.0.tgz#20390ac8dc8325caf9edb4b5643beb5385210e5a" + integrity sha512-/0o6Y1wZ2NgvF9Ev8mYEqi2TGQjyQM0Z9BuyQ8ulZ8ObkDV9inRDrLFTH9nFmMIRswDTavyWz60L4tRndgFyFA== + generate-function@^2.3.1: version "2.3.1" resolved "https://registry.yarnpkg.com/generate-function/-/generate-function-2.3.1.tgz#f069617690c10c868e73b8465746764f97c3479f" From 59faf074fe895deb401761b6ea636c5a914ade06 Mon Sep 17 00:00:00 2001 From: Forbes Lindesay Date: Sat, 2 May 2020 17:36:46 +0100 Subject: [PATCH 7/7] test: fix test case --- packages/pg-migrations/build-package.d.ts | 6 ++++++ packages/pg-migrations/build-package.js | 3 +++ packages/pg-migrations/getID.d.ts | 5 +++++ packages/pg-migrations/getID.js | 3 +++ .../pg-test/src/__tests__/integration.test.ts | 17 ++++++----------- 5 files changed, 23 insertions(+), 11 deletions(-) create mode 100644 packages/pg-migrations/build-package.d.ts create mode 100644 packages/pg-migrations/build-package.js create mode 100644 packages/pg-migrations/getID.d.ts create mode 100644 packages/pg-migrations/getID.js diff --git a/packages/pg-migrations/build-package.d.ts b/packages/pg-migrations/build-package.d.ts new file mode 100644 index 00000000..894a718a --- /dev/null +++ b/packages/pg-migrations/build-package.d.ts @@ -0,0 +1,6 @@ +// @autogenerated + +import def from './lib/build-package'; + +export default def; +export * from './lib/build-package'; diff --git a/packages/pg-migrations/build-package.js b/packages/pg-migrations/build-package.js new file mode 100644 index 00000000..bbc16a24 --- /dev/null +++ b/packages/pg-migrations/build-package.js @@ -0,0 +1,3 @@ +// @autogenerated + +module.exports = require('./lib/build-package'); \ No newline at end of file diff --git a/packages/pg-migrations/getID.d.ts b/packages/pg-migrations/getID.d.ts new file mode 100644 index 00000000..e3ea7463 --- /dev/null +++ b/packages/pg-migrations/getID.d.ts @@ -0,0 +1,5 @@ +// @autogenerated + +import def from './lib/getID'; + +export default def; diff --git a/packages/pg-migrations/getID.js b/packages/pg-migrations/getID.js new file mode 100644 index 00000000..a3fb7d77 --- /dev/null +++ b/packages/pg-migrations/getID.js @@ -0,0 +1,3 @@ +// @autogenerated + +module.exports = require('./lib/getID'); \ No newline at end of file diff --git a/packages/pg-test/src/__tests__/integration.test.ts b/packages/pg-test/src/__tests__/integration.test.ts index 724b0221..ff367877 100644 --- a/packages/pg-test/src/__tests__/integration.test.ts +++ b/packages/pg-test/src/__tests__/integration.test.ts @@ -1,7 +1,7 @@ import connect, {sql} from '@databases/pg'; import {readFileSync} from 'fs'; -import {run} from '../'; import call from '../cli-function'; +import {spawnBuffered} from 'modern-spawn'; jest.setTimeout(60_000); if (!process.env.CI) { @@ -10,15 +10,12 @@ if (!process.env.CI) { 'atdatabases-test-volume-1', 'atdatabases-test-volume-2', ]) { - await run('docker', ['kill', volume], { + // these may fail without harm, so we don't check the exit code + await spawnBuffered('docker', ['kill', volume], { debug: false, - name: 'docker kill', - allowFailure: true, }); - await run('docker', ['volume', 'rm', volume], { + await spawnBuffered('docker', ['volume', 'rm', volume], { debug: false, - name: 'docker volume rm', - allowFailure: true, }); } }; @@ -29,11 +26,9 @@ if (!process.env.CI) { 'atdatabases-test-volume-1', 'atdatabases-test-volume-2', ]) { - await run('docker', ['volume', 'create', volume], { + await spawnBuffered('docker', ['volume', 'create', volume], { debug: true, - name: 'docker volume create', - allowFailure: false, - }); + }).getResult(); await call([ 'start', '--persistVolume',