Skip to content

Commit

Permalink
chore(endomoat): better scenario test running
Browse files Browse the repository at this point in the history
- Eschew use of `console.log` in lieu of `t.log`
- Unwrap scenarios into their own tests!
  • Loading branch information
boneskull committed Feb 5, 2024
1 parent 22ed657 commit 2e74e61
Show file tree
Hide file tree
Showing 3 changed files with 195 additions and 128 deletions.
10 changes: 10 additions & 0 deletions packages/endomoat/.eslintrc.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,14 @@ module.exports = {
'@endo/no-nullish-coalescing': 'off',
},
extends: ['plugin:@endo/recommended'],
overrides: [
{
files: ['./test/**/*.js'],
rules: {
'no-console': 'error',
// of dubious value
'ava/use-t-well': 'off',
},
},
],
}
211 changes: 126 additions & 85 deletions packages/endomoat/test/helpers.js
Original file line number Diff line number Diff line change
@@ -1,48 +1,61 @@
import capcon from 'capture-console'
import { mergePolicy } from 'lavamoat-core'
import util from 'node:util'
// @ts-expect-error - needs types
import { prepareScenarioOnDisk } from 'lavamoat-core/test/util.js'
import { Volume, createFsFromVolume } from 'memfs'
import { memfs } from 'memfs'
import path from 'node:path'
import { run, toEndoPolicy } from '../src/index.js'

/**
* Dumps a bunch of information about an error and the virtual FS volume.
* Optionally, policies
*
* @param {unknown} err
* @param {import('memfs').Volume} vol
* @param {import('lavamoat-core').LavaMoatPolicy} [lavamoatPolicy]
* @param {import('../src/policy-converter.js').LavaMoatEndoPolicy} [endoPolicy]
* @param {object} options
* @param {import('lavamoat-core').LavaMoatPolicy} [options.lavamoatPolicy]
* @param {import('../src/policy-converter.js').LavaMoatEndoPolicy} [options.endoPolicy]
* @param {(...args: any) => void} [options.log]
*/
function dumpError(err, vol, lavamoatPolicy, endoPolicy) {
console.debug()
console.debug(err)
function dumpError(
err,
vol,
// eslint-disable-next-line no-console
{ lavamoatPolicy, endoPolicy, log = console.error.bind(console) } = {}
) {
log()
log(err)
if (endoPolicy) {
console.debug('Endo policy:')
console.dir(endoPolicy, { depth: null })
log('Endo policy:')
log(util.inspect(endoPolicy, { depth: null }))
}
if (lavamoatPolicy) {
console.debug('Lavamoat policy:')
console.dir(lavamoatPolicy, { depth: null })
log('Lavamoat policy:')
log(util.inspect(lavamoatPolicy, { depth: null }))
}
console.debug('FS:')
console.debug(vol.toTree())
console.debug()
log('Filesystem:')
log(vol.toTree())
log()
}

/**
* Reads a policy and policy overrides using a `ReadFn`, then merges the result.
*
* @param {import('@endo/compartment-mapper').ReadFn} readPower
* @param {string} policyDir
* @param {import('@endo/compartment-mapper').ReadFn} readPower - File read
* power
* @param {string} policyDir - Path to the policy directory. If relative, will
* be resolved to cwd
* @returns {Promise<import('lavamoat-core').LavaMoatPolicy>}
* @todo Make the stuff in `lavamoat-core`'s `loadPolicy` accept a `readPower`,
* and get rid of this.
*/
async function readPolicy(readPower, policyDir) {
// console.debug('Reading policy from %s', policyDir)
let [lavamoatPolicy, lavamoatPolicyOverrides] = await Promise.all(
['policy.json', 'policy-override.json'].map((filename) =>
readPower(path.join(policyDir, filename))
.then((bytes) => `${bytes}`)
.then(JSON.parse)
readPower(path.resolve(policyDir, filename))
.then((bytes) => JSON.parse(`${bytes}`))
.catch((err) => {
if (err.code !== 'ENOENT') {
throw err
Expand All @@ -54,17 +67,20 @@ async function readPolicy(readPower, policyDir) {
if (!lavamoatPolicy) {
throw new Error(`LavaMoat - policy not found in ${policyDir}`)
}

if (lavamoatPolicyOverrides) {
// console.debug('Applying policy overrides; before:')
// console.dir(lavamoatPolicy, { depth: null })
lavamoatPolicy = mergePolicy(lavamoatPolicy, lavamoatPolicyOverrides)
// console.debug('after:')
// console.dir(lavamoatPolicy, { depth: null })
}

return lavamoatPolicy
}

/**
* Captures `process.stderr` & `process.stdout` and returns a function to stop
* capturing them.
*
* @returns {() => { stdout: string; stderr: string }}
*/
function capture() {
let stdout = ''
let stderr = ''
Expand All @@ -83,79 +99,104 @@ function capture() {
return { stdout, stderr }
}
}
// @ts-expect-error - needs types
export async function runScenario({ scenario }) {
const vol = new Volume()
const fs = createFsFromVolume(vol)

const readFile = /** @type {import('@endo/compartment-mapper').ReadFn} */ (
fs.promises.readFile
)
/**
* Bootstraps the scenario runner.
*
* Return value should be provided to `lavamoat-core`'s `runAndTestScenario`
*
* @param {(...args: any) => void} log Logger
* @returns
*/
// eslint-disable-next-line no-console
export function createScenarioRunner(log = console.error.bind(console)) {
/**
* Runs a scenario from `lavamoat-core`.
*
* The idea here is to establishe feature-compatibility with `lavamoat-node`.
*
* @remarks
* The runner in e.g., `lavamoat-node` spawns a child process for each
* scenario; I don't feel that is necessary for our purposes. It _is_ useful
* and necessary to test the CLI, but I'm not convinced there's added value in
* doing so for _every scenario_.
* @param {{ scenario: any }} opts
* @returns {Promise<unknown>} Result of the stdout of the scenario parsed as
* JSON
* @todo Scenario needs a type definition
*/
return async ({ scenario }) => {
const { fs, vol } = memfs()

const readFile = /** @type {import('@endo/compartment-mapper').ReadFn} */ (
fs.promises.readFile
)

/** @type {string} */
let projectDir
/** @type {string} */
let policyDir

// for eslint
await Promise.resolve()

try {
;({ projectDir, policyDir } = await prepareScenarioOnDisk({
fs: fs.promises,
scenario,
policyName: 'endomoat',
}))
} catch (e) {
dumpError(e, vol)
throw e
}
/** @type {string} */
let projectDir
/** @type {string} */
let policyDir

/** @type {import('lavamoat-core').LavaMoatPolicy} */
let lavamoatPolicy
try {
lavamoatPolicy = await readPolicy(readFile, policyDir)
} catch (e) {
dumpError(e, vol)
throw e
}
// for eslint
await Promise.resolve()

const endoPolicy = toEndoPolicy(lavamoatPolicy)
try {
;({ projectDir, policyDir } = await prepareScenarioOnDisk({
fs: fs.promises,
scenario,
policyName: 'endomoat',
}))
} catch (e) {
dumpError(e, vol, { log })
throw e
}

const entryPath = new URL(
`file://` + path.join(projectDir, scenario.entries[0])
)
/** @type {import('lavamoat-core').LavaMoatPolicy} */
let lavamoatPolicy
try {
lavamoatPolicy = await readPolicy(readFile, policyDir)
} catch (e) {
dumpError(e, vol, { log })
throw e
}

const endoPolicy = toEndoPolicy(lavamoatPolicy)

/** @type {import('@endo/compartment-mapper').ReadFn} */
const readPower = async (location) => readFile(new URL(location).pathname)
const entryPath = new URL(
`file://` + path.join(projectDir, scenario.entries[0])
)

const stopCapture = capture()
/** @type {import('@endo/compartment-mapper').ReadFn} */
const readPower = async (location) => readFile(new URL(location).pathname)

/** @type {string} */
let stderr
/** @type {string} */
let stdout
try {
await run(entryPath, { readPower, endoPolicy })
;({ stderr, stdout } = stopCapture())
} catch (err) {
// this must happen prior to dumping the error
;({ stderr, stdout } = stopCapture())
const stopCapture = capture()

if (!scenario.expectedFailure) {
dumpError(err, vol, lavamoatPolicy, endoPolicy)
/** @type {string} */
let stderr
/** @type {string} */
let stdout
try {
await run(entryPath, { readPower, endoPolicy })
;({ stderr, stdout } = stopCapture())
} catch (err) {
// this must happen prior to dumping the error
;({ stderr, stdout } = stopCapture())

if (!scenario.expectedFailure) {
dumpError(err, vol, { lavamoatPolicy, endoPolicy, log })
}
throw err
}
throw err
}

let result
if (stderr) {
throw new Error(`Unexpected output in standard err: \n${stderr}`)
}
try {
result = JSON.parse(stdout)
} catch (e) {
throw new Error(`Unexpected output in standard out: \n${stdout}`)
let result
if (stderr) {
throw new Error(`Unexpected output in standard err: \n${stderr}`)
}
try {
result = JSON.parse(stdout)
} catch (e) {
throw new Error(`Unexpected output in standard out: \n${stdout}`)
}
return result
}
return result
}

0 comments on commit 2e74e61

Please sign in to comment.