From e8803c6b365669eabb76b9cc04cf0de10b43e738 Mon Sep 17 00:00:00 2001 From: David Herman Date: Mon, 8 Mar 2021 21:51:23 -0800 Subject: [PATCH] - 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 --- .gitignore | 2 + create-neon/data/templates/.gitignore.hbs | 1 + create-neon/dev/expect.ts | 90 ++++++++++++ create-neon/package.json | 6 +- create-neon/src/bin/create-neon.ts | 2 +- create-neon/test/create-neon.ts | 165 ++++++---------------- create-neon/tsconfig.json | 1 + 7 files changed, 140 insertions(+), 127 deletions(-) create mode 100644 create-neon/dev/expect.ts diff --git a/.gitignore b/.gitignore index c5e4c4a..7fd5ffd 100644 --- a/.gitignore +++ b/.gitignore @@ -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 diff --git a/create-neon/data/templates/.gitignore.hbs b/create-neon/data/templates/.gitignore.hbs index db22c6f..6ca71fb 100644 --- a/create-neon/data/templates/.gitignore.hbs +++ b/create-neon/data/templates/.gitignore.hbs @@ -2,3 +2,4 @@ target index.node **/node_modules **/.DS_Store +npm-debug.log* diff --git a/create-neon/dev/expect.ts b/create-neon/dev/expect.ts new file mode 100644 index 0000000..e21a302 --- /dev/null +++ b/create-neon/dev/expect.ts @@ -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, 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 { + let resolve: (code: number | null) => void; + let result: Promise = new Promise(res => { resolve = res; }); + child.on('exit', code => { + resolve(code); + }); + return result; +} + +export default async function expect(child: ChildProcess, script: Record): Promise { + for await (let _ of run(script, child.stdin!, child.stdout!)) { } + + return await exit(child); +} diff --git a/create-neon/package.json b/create-neon/package.json index a303c8a..11cd7b4 100644 --- a/create-neon/package.json +++ b/create-neon/package.json @@ -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", diff --git a/create-neon/src/bin/create-neon.ts b/create-neon/src/bin/create-neon.ts index 04de7d6..20dad67 100644 --- a/create-neon/src/bin/create-neon.ts +++ b/create-neon/src/bin/create-neon.ts @@ -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"); diff --git a/create-neon/test/create-neon.ts b/create-neon/test/create-neon.ts index 6c455b8..a3d873d 100644 --- a/create-neon/test/create-neon.ts +++ b/create-neon/test/create-neon.ts @@ -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, 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 { - await rmdir(PROJECT, { recursive: true }); - return spawn(NODE, [CREATE_NEON, PROJECT]); -} - -/* -function timeout(ms: number): Promise { - let resolve: () => void; - let result: Promise = new Promise(res => { resolve = res; }); - setTimeout(() => { resolve() }, ms); - return result; -} -*/ - -function exit(child: ChildProcess): Promise { - let resolve: (code: number | null) => void; - let result: Promise = 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" ', + '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" '); + + 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" ']); + }); }); diff --git a/create-neon/tsconfig.json b/create-neon/tsconfig.json index f5bb0e7..21bee49 100644 --- a/create-neon/tsconfig.json +++ b/create-neon/tsconfig.json @@ -21,6 +21,7 @@ }, "include": [ "src/**/*", + "dev/**/*", "test/**/*" ], "exclude": [