Skip to content

Commit

Permalink
fix: 100 test coverage (#38)
Browse files Browse the repository at this point in the history
  • Loading branch information
lukekarrys authored Dec 15, 2022
1 parent 9f2b9aa commit 3c113db
Show file tree
Hide file tree
Showing 16 changed files with 159 additions and 61 deletions.
63 changes: 24 additions & 39 deletions lib/index.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
const fs = require('fs/promises')
const vm = require('vm')
const util = require('util')
const crypto = require('crypto')
const { runInThisContext } = require('vm')
const { promisify } = require('util')
const { randomBytes } = require('crypto')
const { Module } = require('module')
const path = require('path')
const { dirname, basename } = require('path')
const read = require('read')

const files = {}
Expand All @@ -12,7 +12,7 @@ class PromZard {
#file = null
#backupFile = null
#ctx = null
#unique = crypto.randomBytes(8).toString('hex')
#unique = randomBytes(8).toString('hex')
#prompts = []

constructor (file, ctx = {}, options = {}) {
Expand Down Expand Up @@ -43,8 +43,7 @@ class PromZard {
}

try {
const d = await fs.readFile(this.#file, 'utf8')
files[this.#file] = d
files[this.#file] = await fs.readFile(this.#file, 'utf8')
} catch (er) {
if (er && this.#backupFile) {
this.#file = this.#backupFile
Expand All @@ -62,34 +61,22 @@ class PromZard {
mod.loaded = true
mod.filename = this.#file
mod.id = this.#file
mod.paths = Module._nodeModulePaths(path.dirname(this.#file))
mod.paths = Module._nodeModulePaths(dirname(this.#file))

this.#ctx.prompt = this.#makePrompt()
this.#ctx.__filename = this.#file
this.#ctx.__dirname = path.dirname(this.#file)
this.#ctx.__basename = path.basename(this.#file)
this.#ctx.__dirname = dirname(this.#file)
this.#ctx.__basename = basename(this.#file)
this.#ctx.module = mod
this.#ctx.require = (p) => mod.require(p)
this.#ctx.require.resolve = (p) => Module._resolveFilename(p, mod)
this.#ctx.exports = mod.exports

const body = util.format(
'(function( %s ) { %s\n })',
Object.keys(this.#ctx).join(', '),
files[this.#file]
)
const fn = vm.runInThisContext(body, this.#file)
const args = Object.keys(this.#ctx).map((k) => this.#ctx[k])

const output = fn.apply(this.#ctx, args)
const res = (
output &&
typeof output === 'object' &&
exports === mod.exports &&
Object.keys(exports).length === 1
) ? output : mod.exports

return this.#walk(res)
const body = `(function(${Object.keys(this.#ctx).join(', ')}) { ${files[this.#file]}\n })`
runInThisContext(body, this.#file).apply(this.#ctx, Object.values(this.#ctx))
this.#ctx.res = mod.exports

return this.#walk()
}

#makePrompt () {
Expand Down Expand Up @@ -119,7 +106,7 @@ class PromZard {
}
}

async #walk (o) {
async #walk (o = this.#ctx.res) {
const keys = Object.keys(o)

const len = keys.length
Expand All @@ -137,15 +124,10 @@ class PromZard {

if (v && typeof v === 'string' && v.startsWith(this.#unique)) {
const n = +v.slice(this.#unique.length + 1)
const promptParts = this.#prompts[n]

if (!promptParts) {
continue
}

// default to the key
// default to the ctx value, if there is one
const [prompt = k, def = this.#ctx[k], tx] = promptParts
const [prompt = k, def = this.#ctx[k], tx] = this.#prompts[n]

try {
o[k] = await this.#prompt(prompt, def, tx)
Expand All @@ -161,7 +143,11 @@ class PromZard {
}

if (typeof v === 'function') {
const fn = v.length ? util.promisify(v) : v
// XXX: remove v.length check to remove cb from functions
// would be a breaking change for `npm init`
// XXX: if cb is no longer an argument then this.#ctx should
// be passed in to allow arrow fns to be used and still access ctx
const fn = v.length ? promisify(v) : v
o[k] = await fn.call(this.#ctx)
// back up so that we process this one again.
// this is because it might return a prompt() call in the cb.
Expand All @@ -174,10 +160,9 @@ class PromZard {
}

async #prompt (prompt, def, tx) {
let res = await read({ prompt: prompt + ':', default: def })
if (tx) {
res = tx(res)
}
const res = await read({ prompt: prompt + ':', default: def }).then((r) => tx ? tx(r) : r)
// XXX: remove this to require throwing an error instead of
// returning it. would be a breaking change for `npm init`
if (res instanceof Error && res.notValid) {
throw res
}
Expand Down
4 changes: 0 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,10 +38,6 @@
"version": "4.11.0"
},
"tap": {
"statements": 93,
"branches": 72,
"functions": 87,
"lines": 94,
"jobs": 1,
"test-ignore": "fixtures/",
"nyc-arg": [
Expand Down
3 changes: 2 additions & 1 deletion test/backup-file.js → test/backup-file copy.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ if (isChild()) {
}

t.test('backup file', async (t) => {
const output = await setup(__filename, ['', '55'])
const output = await setup(__filename, ['', '55', 'no'])

t.same(JSON.parse(output), {
a: 3,
Expand All @@ -15,5 +15,6 @@ t.test('backup file', async (t) => {
x: 55,
y: '/tmp/y/file.txt',
},
error: 'no',
})
})
1 change: 1 addition & 0 deletions test/basic.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ t.test('run the example', async (t) => {
version: '0.0.0',
description: 'testing description',
main: 'test-entry.js',
resolved: 'index.js',
directories: {
example: 'example',
test: 'test',
Expand Down
1 change: 1 addition & 0 deletions test/buffer.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ t.test('run the example', async (t) => {
version: '0.0.0',
description: 'testing description',
main: 'test-entry.js',
resolved: 'error',
directories: {
example: 'example',
test: 'test',
Expand Down
27 changes: 27 additions & 0 deletions test/conditional.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@

const t = require('tap')
const { setup: _setup, child, isChild } = require('./fixtures/setup')

if (isChild()) {
return child(__filename)
}

const setup = (...args) => _setup(__filename, args).then(JSON.parse)

t.test('conditional', async (t) => {
t.same(await setup(''), {})
t.same(await setup('a'), {})
t.same(await setup('git', ''), {})
t.same(await setup('git', 'http'), {
repository: {
type: 'git',
url: 'http',
},
})
t.same(await setup('svn', 'http'), {
repository: {
type: 'svn',
url: 'http',
},
})
})
10 changes: 10 additions & 0 deletions test/error-file.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
const t = require('tap')
const { setup, child, isChild } = require('./fixtures/setup')

if (isChild()) {
return child('file does not exist')
}

t.test('backup file', async (t) => {
t.match(await setup(__filename), 'ENOENT')
})
24 changes: 14 additions & 10 deletions test/fixtures/basic.fixture.js
Original file line number Diff line number Diff line change
@@ -1,25 +1,29 @@
/* globals prompt, basename */

const fs = require('fs/promises')
const path = require('path')

module.exports = {
name: basename.replace(/^node-/, ''),
version: '0.0.0',
description: (function () {
let value
try {
const src = fs.readFileSync('README.markdown', 'utf8')
value = src.split('\n')
description: async () => {
const value = await fs.readFile('README.markdown', 'utf8')
.then((src) => src.split('\n')
.find((l) => /\s+/.test(l) && l.trim() !== basename.replace(/^node-/, ''))
.trim()
.replace(/^./, c => c.toLowerCase())
.replace(/\.$/, '')
} catch {
// no value
}
.replace(/\.$/, ''))
.catch(() => null)
return prompt('description', value)
})(),
},
main: prompt('entry point', 'index.js'),
resolved: () => {
try {
return path.basename(require.resolve('../../'))
} catch {
return 'error'
}
},
bin: async function () {
const exists = await fs.stat('bin/cmd.js')
.then(() => true)
Expand Down
18 changes: 18 additions & 0 deletions test/fixtures/conditional.fixture.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/* globals prompt */

module.exports = {
repository: {
type: prompt('repo type'),
url () {
if (['git', 'svn'].includes(this.res.repository.type)) {
return prompt(`${this.res.repository.type} url`)
}
},
},
// this name of this doesnt matter, just that it comes last
'' () {
if (!this.res.repository.type || !this.res.repository.url) {
delete this.res.repository
}
},
}
17 changes: 17 additions & 0 deletions test/fixtures/prompts.fixture.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/* globals prompt */

const transform = (a) => a

module.exports = {
a: prompt({
prompt: 'a',
default: 'a',
transform,
}),
b: prompt('b', 'b', () => 'b', {
prompt: 'a',
default: 'a',
transform,
}),
c: prompt('c', 'c', transform, {}),
}
2 changes: 1 addition & 1 deletion test/fixtures/setup.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ const CHILD = 'child'

const isChild = () => process.argv[2] === CHILD

const setup = async (file, writes) => {
const setup = async (file, writes = []) => {
const proc = spawn(process.execPath, [file, CHILD])
const entries = Array.isArray(writes) ? writes : Object.entries(writes)

Expand Down
6 changes: 6 additions & 0 deletions test/fixtures/simple.fixture.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,10 @@ module.exports = {
x: prompt(),
y: tmpdir + '/y/file.txt',
},
error: prompt('error', (v) => {
if (v === 'throw') {
throw new Error('this is unexpected')
}
return v
}),
}
8 changes: 8 additions & 0 deletions test/fixtures/validate.fixture.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,12 @@ module.exports = {
notValid: true,
})
}),
name2: prompt('name2', (data) => {
if (data === 'cool') {
return data
}
throw Object.assign(new Error('name must be cool'), {
notValid: true,
})
}),
}
16 changes: 16 additions & 0 deletions test/prompts.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
const t = require('tap')
const { setup, child, isChild } = require('./fixtures/setup')

if (isChild()) {
return child(__filename)
}

t.test('prompts', async (t) => {
const output = await setup(__filename, ['', '', ''])

t.same(JSON.parse(output), {
a: 'a',
b: 'a',
c: 'c',
})
})
11 changes: 7 additions & 4 deletions test/simple.js
Original file line number Diff line number Diff line change
@@ -1,19 +1,22 @@
const t = require('tap')
const { setup, child, isChild } = require('./fixtures/setup')
const { setup: _setup, child, isChild } = require('./fixtures/setup')

if (isChild()) {
return child(__filename, { tmpdir: '/tmp' })
}

t.test('simple', async (t) => {
const output = await setup(__filename, ['', '55'])
const setup = (...args) => _setup(__filename, args)

t.same(JSON.parse(output), {
t.test('simple', async (t) => {
t.same(await setup('', '55', 'no error').then(JSON.parse), {
a: 3,
b: '!2b',
c: {
x: 55,
y: '/tmp/y/file.txt',
},
error: 'no error',
})

t.match(await setup('', '55', 'throw'), /Error: this is unexpected/)
})
9 changes: 7 additions & 2 deletions test/validate.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,12 @@ if (isChild()) {
}

t.test('validate', async (t) => {
const output = await setup(__filename, [[/name: $/, 'not cool'], [/name: $/, 'cool']])
const output = await setup(__filename, [
[/name: $/, 'not cool'],
[/name: $/, 'cool'],
[/name2: $/, 'not cool'],
[/name2: $/, 'cool'],
])

t.same(JSON.parse(output), { name: 'cool' })
t.same(JSON.parse(output), { name: 'cool', name2: 'cool' })
})

0 comments on commit 3c113db

Please sign in to comment.