- Move test helpers into dev/ directory

- Delete test output directory after every test
- Add test output directories to gitignore
- Add npm-debug.log to gitignore template
This commit is contained in:
David Herman 2021-03-08 21:51:23 -08:00
parent 7840161021
commit e8803c6b36
7 changed files with 140 additions and 127 deletions

2
.gitignore vendored
View File

@ -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

View File

@ -2,3 +2,4 @@ target
index.node
**/node_modules
**/.DS_Store
npm-debug.log*

90
create-neon/dev/expect.ts Normal file
View File

@ -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);
}

View File

@ -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",

View File

@ -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");

View File

@ -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');
@ -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);
@ -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);
});
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!)) { }
afterEach(async () => {
await rmdir(PROJECT, { recursive: true });
});
let code = await exit(child);
it('succeeds with all default answers', async () => {
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);
@ -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>']);
});
});

View File

@ -21,6 +21,7 @@
},
"include": [
"src/**/*",
"dev/**/*",
"test/**/*"
],
"exclude": [