Skip to content

Commit

Permalink
feat: automatically generate ports from volumes and add tests
Browse files Browse the repository at this point in the history
  • Loading branch information
ForbesLindesay committed May 2, 2020
1 parent b62ab62 commit d75d821
Show file tree
Hide file tree
Showing 15 changed files with 242 additions and 79 deletions.
6 changes: 6 additions & 0 deletions packages/pg-test/jest/globalSetup.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
// @autogenerated

import def from '../lib/jest/globalSetup';

export default def;
export * from '../lib/jest/globalSetup';
3 changes: 3 additions & 0 deletions packages/pg-test/jest/globalSetup.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
// @autogenerated

module.exports = require('../lib/jest/globalSetup');
5 changes: 5 additions & 0 deletions packages/pg-test/jest/globalTeardown.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
// @autogenerated

import def from '../lib/jest/globalTeardown';

export default def;
3 changes: 3 additions & 0 deletions packages/pg-test/jest/globalTeardown.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
// @autogenerated

module.exports = require('../lib/jest/globalTeardown');
4 changes: 3 additions & 1 deletion packages/pg-test/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {},
Expand All @@ -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",
Expand Down
1 change: 1 addition & 0 deletions packages/pg-test/src/__tests__/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
env-example-*
Original file line number Diff line number Diff line change
@@ -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"`;
70 changes: 70 additions & 0 deletions packages/pg-test/src/__tests__/integration.test.ts
Original file line number Diff line number Diff line change
@@ -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');
}
10 changes: 10 additions & 0 deletions packages/pg-test/src/__tests__/numberToValidPort.test.ts
Original file line number Diff line number Diff line change
@@ -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);
});
10 changes: 10 additions & 0 deletions packages/pg-test/src/__tests__/stringToValidPort.test.ts
Original file line number Diff line number Diff line change
@@ -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`,
);
});
96 changes: 96 additions & 0 deletions packages/pg-test/src/cli-function.ts
Original file line number Diff line number Diff line change
@@ -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<number> {
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');
}
}
84 changes: 6 additions & 78 deletions packages/pg-test/src/cli.ts
Original file line number Diff line number Diff line change
@@ -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');
}
});
9 changes: 9 additions & 0 deletions packages/pg-test/src/numberToValidPort.ts
Original file line number Diff line number Diff line change
@@ -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;
}
10 changes: 10 additions & 0 deletions packages/pg-test/src/stringToValidPort.ts
Original file line number Diff line number Diff line change
@@ -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);
}
5 changes: 5 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down

0 comments on commit d75d821

Please sign in to comment.