From d75d821a208ea4067acda767e32dd60ca9386356 Mon Sep 17 00:00:00 2001 From: Forbes Lindesay Date: Fri, 13 Dec 2019 17:52:47 +0000 Subject: [PATCH] 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 5b8eec1e..8d78c47f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2465,6 +2465,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"