- 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:
parent
7840161021
commit
e8803c6b36
2
.gitignore
vendored
2
.gitignore
vendored
@ -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
|
||||
|
||||
@ -2,3 +2,4 @@ target
|
||||
index.node
|
||||
**/node_modules
|
||||
**/.DS_Store
|
||||
npm-debug.log*
|
||||
|
||||
90
create-neon/dev/expect.ts
Normal file
90
create-neon/dev/expect.ts
Normal 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);
|
||||
}
|
||||
@ -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",
|
||||
|
||||
@ -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");
|
||||
|
||||
@ -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>']);
|
||||
});
|
||||
});
|
||||
|
||||
@ -21,6 +21,7 @@
|
||||
},
|
||||
"include": [
|
||||
"src/**/*",
|
||||
"dev/**/*",
|
||||
"test/**/*"
|
||||
],
|
||||
"exclude": [
|
||||
|
||||
Loading…
Reference in New Issue
Block a user