Skip to content

Commit

Permalink
- Move test helpers into dev/ directory
Browse files Browse the repository at this point in the history
- Delete test output directory after every test
- Add test output directories to gitignore
- Add npm-debug.log to gitignore template
  • Loading branch information
dherman committed Mar 9, 2021
1 parent 7840161 commit e8803c6
Show file tree
Hide file tree
Showing 7 changed files with 139 additions and 126 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ Cargo.lock
**/artifacts.json
cli/lib
create-neon/dist
create-neon/create-neon-test-project
create-neon/create-neon-manual-test-project
test/cli/lib
npm-debug.log
rls*.log
1 change: 1 addition & 0 deletions create-neon/data/templates/.gitignore.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ target
index.node
**/node_modules
**/.DS_Store
npm-debug.log*
90 changes: 90 additions & 0 deletions create-neon/dev/expect.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import { ChildProcess } from 'child_process';
import { PassThrough, Readable, Writable } from 'stream';
import { StringDecoder } from 'string_decoder';

function readChunks(input: Readable): Readable {
let output = new PassThrough({ objectMode: true });
let decoder = new StringDecoder('utf8');
input.on('data', data => {
output.write(decoder.write(data));
});
input.on('close', () => {
output.write(decoder.end());
output.destroy();
});
return output;
}

function splitLines(s: string): string[] {
return s.split(/([^\n]*\r?\n)/).filter(x => x);
}

function isCompleteLine(s: string): boolean {
return s.endsWith('\n');
}

class LinesBuffer {

// INVARIANT: (this.buffer.length > 0) &&
// !isCompleteLine(this.buffer[this.buffer.length - 1])
// In other words, the last line in the buffer is always incomplete.
private buffer: string[];

constructor() {
this.buffer = [""];
}

add(lines: string[]) {
if (isCompleteLine(lines[lines.length - 1])) {
lines.push("");
}
this.buffer[this.buffer.length - 1] += lines.shift();
this.buffer = this.buffer.concat(lines);
}

find(p: (s: string) => boolean): string[] | null {
let index = this.buffer.findIndex(p);
if (index === -1) {
return null;
}
let extracted = this.buffer.splice(0, index + 1);
if (this.buffer.length === 0) {
this.buffer.push("");
}
return extracted;
}
}

async function* run(script: Record<string, string>, stdin: Writable, stdout: Readable) {
let lines = new LinesBuffer();

let keys = Object.keys(script);
let i = 0;
for await (let chunk of readChunks(stdout)) {
lines.add(splitLines(chunk));
let found = lines.find(line => line.startsWith(keys[i]));
if (found) {
stdin.write(script[keys[i]] + "\n");
yield found;
i++;
if (i >= keys.length) {
break;
}
}
}
}

function exit(child: ChildProcess): Promise<number | null> {
let resolve: (code: number | null) => void;
let result: Promise<number | null> = new Promise(res => { resolve = res; });
child.on('exit', code => {
resolve(code);
});
return result;
}

export default async function expect(child: ChildProcess, script: Record<string, string>): Promise<number | null> {
for await (let _ of run(script, child.stdin!, child.stdout!)) { }

return await exit(child);
}
6 changes: 4 additions & 2 deletions create-neon/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,15 @@
"create-neon": "dist/src/bin/create-neon.js"
},
"files": [
"dist/**/*"
"dist/src/**/*",
"dist/data/**/*"
],
"scripts": {
"build": "tsc && cp -r data/templates dist/data",
"prepublishOnly": "npm run build",
"pretest": "npm run build",
"test": "mocha",
"manual-test": "npm run build && rm -rf throwaway-test && node ./dist/src/bin/create-neon.js throwaway-test"
"manual-test": "npm run build && rm -rf create-neon-manual-test-project && node ./dist/src/bin/create-neon.js create-neon-manual-test-project"
},
"repository": {
"type": "git",
Expand Down
2 changes: 1 addition & 1 deletion create-neon/src/bin/create-neon.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ async function main(name: string) {
console.log(`✨ Created Neon project \`${name}\`. Happy 🦀 hacking! ✨`);
}

if (process.argv.length !== 3) {
if (process.argv.length < 3) {
console.error("✨ create-neon: Create a new Neon project with zero configuration. ✨");
console.error();
console.error("Usage: npm init neon name");
Expand Down
163 changes: 40 additions & 123 deletions create-neon/test/create-neon.ts
Original file line number Diff line number Diff line change
@@ -1,84 +1,10 @@
import { assert } from 'chai';
import { Readable, PassThrough, Writable } from 'stream';
//import * as readline from 'readline';
import { ChildProcess, spawn } from 'child_process';
import { spawn } from 'child_process';
import execa from 'execa';
import * as path from 'path';
import { readFile, rmdir } from 'fs/promises';
import { StringDecoder } from 'string_decoder';
import * as TOML from 'toml';

function readChunks(input: Readable): Readable {
let output = new PassThrough({ objectMode: true });
let decoder = new StringDecoder('utf8');
input.on('data', data => {
output.write(decoder.write(data));
});
input.on('close', () => {
output.write(decoder.end());
output.destroy();
});
return output;
}

function splitLines(s: string): string[] {
return s.split(/([^\n]*\r?\n)/).filter(x => x);
}

function isCompleteLine(s: string): boolean {
return s.endsWith('\n');
}

class LinesBuffer {

// INVARIANT: (this.buffer.length > 0) &&
// !isCompleteLine(this.buffer[this.buffer.length - 1])
// In other words, the last line in the buffer is always incomplete.
private buffer: string[];

constructor() {
this.buffer = [""];
}

add(lines: string[]) {
if (isCompleteLine(lines[lines.length - 1])) {
lines.push("");
}
this.buffer[this.buffer.length - 1] += lines.shift();
this.buffer = this.buffer.concat(lines);
}

find(p: (s: string) => boolean): string[] | null {
let index = this.buffer.findIndex(p);
if (index === -1) {
return null;
}
let extracted = this.buffer.splice(0, index + 1);
if (this.buffer.length === 0) {
this.buffer.push("");
}
return extracted;
}
}

async function* dialog(script: Record<string, string>, stdin: Writable, stdout: Readable) {
let lines = new LinesBuffer();

let keys = Object.keys(script);
let i = 0;
for await (let chunk of readChunks(stdout)) {
lines.add(splitLines(chunk));
let found = lines.find(line => line.startsWith(keys[i]));
if (found) {
stdin.write(script[keys[i]] + "\n");
yield found;
i++;
if (i >= keys.length) {
break;
}
}
}
}
import expect from '../dev/expect';

const NODE: string = process.execPath;
const CREATE_NEON = path.join(__dirname, '..', 'dist', 'src', 'bin', 'create-neon.js');
Expand All @@ -93,18 +19,9 @@ describe('Command-line argument validation', () => {
}
});

it('rejects extra arguments', async () => {
try {
await(execa(NODE, [CREATE_NEON, 'name', 'ohnoanextraargument']));
assert.fail("should fail when too many arguments are supplied");
} catch (expected) {
assert.isTrue(true);
}
});

it('fails if the directory already exists', async () => {
try {
await execa(NODE, [CREATE_NEON, 'dist']);
await execa(NODE, [CREATE_NEON, 'src']);
assert.fail("should fail when directory exists");
} catch (expected) {
assert.isTrue(true);
Expand All @@ -114,46 +31,22 @@ describe('Command-line argument validation', () => {

const PROJECT = 'create-neon-test-project';

async function start(): Promise<ChildProcess> {
await rmdir(PROJECT, { recursive: true });
return spawn(NODE, [CREATE_NEON, PROJECT]);
}

/*
function timeout(ms: number): Promise<void> {
let resolve: () => void;
let result: Promise<void> = new Promise(res => { resolve = res; });
setTimeout(() => { resolve() }, ms);
return result;
}
*/

function exit(child: ChildProcess): Promise<number | null> {
let resolve: (code: number | null) => void;
let result: Promise<number | null> = new Promise(res => { resolve = res; });
child.on('exit', code => {
resolve(code);
describe('Project creation', () => {
afterEach(async () => {
await rmdir(PROJECT, { recursive: true });
});
return result;
}

const DEFAULTS_SCRIPT = {
'package name:': '',
'version:': '',
'description:': '',
'git repository:': '',
'keywords:': '',
'author:': '',
'license:': '',
'Is this OK?': ''
};

describe('Project creation', () => {
it('succeeds with all default answers', async () => {
let child = await start();
for await (let _ of dialog(DEFAULTS_SCRIPT, child.stdin!, child.stdout!)) { }

let code = await exit(child);
let code = await expect(spawn(NODE, [CREATE_NEON, PROJECT]), {
'package name:': '',
'version:': '',
'description:': '',
'git repository:': '',
'keywords:': '',
'author:': '',
'license:': '',
'Is this OK?': ''
});

assert.strictEqual(code, 0);

Expand All @@ -175,4 +68,28 @@ describe('Project creation', () => {
assert.deepEqual(toml.lib['crate-type'], ['cdylib']);
});

it('handles quotation marks in author and description', async () => {
let code = await expect(spawn(NODE, [CREATE_NEON, PROJECT]), {
'package name:': '',
'version:': '',
'description:': 'the "hello world" of examples',
'git repository:': '',
'keywords:': '',
'author:': '"Dave Herman" <dherman@example.com>',
'license:': '',
'Is this OK?': ''
});

assert.strictEqual(code, 0);

let json = JSON.parse(await readFile(path.join(PROJECT, 'package.json'), { encoding: 'utf8' }));

assert.strictEqual(json.description, 'the "hello world" of examples');
assert.strictEqual(json.author, '"Dave Herman" <dherman@example.com>');

let toml = TOML.parse(await readFile(path.join(PROJECT, 'Cargo.toml'), { encoding: 'utf8' }));

assert.strictEqual(toml.package.description, 'the "hello world" of examples');
assert.deepEqual(toml.package.authors, ['"Dave Herman" <dherman@example.com>']);
});
});
1 change: 1 addition & 0 deletions create-neon/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
},
"include": [
"src/**/*",
"dev/**/*",
"test/**/*"
],
"exclude": [
Expand Down

0 comments on commit e8803c6

Please sign in to comment.