Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion CONTEXT.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,5 +17,9 @@ One invocation of the Bin as a subprocess against a Project.
_Avoid_: execution, spawn, call

**Debug mode**:
A Run where the Bin starts under the Node inspector, either attaching or breaking on the first line, and whose Project is preserved rather than removed at teardown. Set by `runBinDebug` or `BINTASTIC_DEBUG`; unset, empty, `0`, and `false` mean off.
A Run where the Bin starts under the Node inspector, either attaching or breaking on the first line. Set per Run by `runBinDebug` or `BINTASTIC_DEBUG`, with a Run's own `env` taking precedence over the process env; unset, empty, `0`, and `false` mean off.
_Avoid_: inspect mode, debugging

**Preserved Project**:
A Project that is kept on disk at teardown instead of removed. A Project is preserved when any Run against it was in Debug mode, or when `BINTASTIC_DEBUG` is on in the process env at teardown. A Run's own `env` cannot stop its Project being preserved.
_Avoid_: kept fixture, leftover dir
2 changes: 2 additions & 0 deletions docs/guide/debugging.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ The value is read the same way everywhere:
await runBin('--flag', { env: { BINTASTIC_DEBUG: 'break' } });
```

Turning debugging on for one run also keeps that run's fixture. Turning it off for one run only skips the inspector: if `BINTASTIC_DEBUG` is on in the process environment, the directory stays on disk even when a run passes `BINTASTIC_DEBUG: 'false'`.

`bintastic` reads `BINTASTIC_DEBUG` itself; it does not add the variable to your CLI's environment.

## `runBinDebug`
Expand Down
2 changes: 2 additions & 0 deletions docs/guide/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ const project = await setupProject(); // creates temp directory
teardownProject(); // removes temp directory
```

In debug mode, `teardownProject` keeps the directory on disk so you can inspect it. See [Debugging](./debugging).

`teardownProject` also returns to the working directory you were in before `project.chdir()`, and it's safe to call when no project is active, so an `afterEach` can't mask a failure in `beforeEach`. Calling `setupProject` again without tearing down first tears the previous project down for you.

To leave a project's directory without removing it, call `project.restoreCwd()`.
Expand Down
98 changes: 69 additions & 29 deletions src/create-bintastic.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { execaNode, type Options, type ResultPromise } from 'execa';
import BintasticProject from './project';
import { createProjectLifecycle } from './project-lifecycle';
import { planRun, type RunBinArgs } from './run-plan';
import { planRun, resolveDebugMode, type RunBinArgs } from './run-plan';

/**
* Options shared by every way of configuring bintastic.
Expand Down Expand Up @@ -119,52 +118,93 @@ export interface CreateBintasticResult<TProject extends BintasticProject> {

/**
* Creates the bintastic API functions to use within tests.
*
* The returned functions share one active Project. It is idle until `setupProject`,
* active until `teardownProject`, and preserved at teardown when any Run against it
* was in Debug mode or `BINTASTIC_DEBUG` is on in the process env.
* @param {BintasticOptions<TProject>} options - An object of bintastic options
* @returns {CreateBintasticResult<TProject>} - A project instance.
*/
export function createBintastic<TProject extends BintasticProject>(
options: BintasticOptions<TProject>
): CreateBintasticResult<TProject> {
const lifecycle = createProjectLifecycle<TProject>({
createProject:
typeof options.createProject === 'function'
? options.createProject
: () => new BintasticProject() as TProject,
env: process.env,
});
const createProject =
typeof options.createProject === 'function'
? options.createProject
: () => new BintasticProject() as TProject;

let active: TProject | undefined;
let preserved = false;

/**
* Returns the active Project, or throws if setupProject() hasn't been called.
* @param {string} caller - The public function name used in the error message.
* @returns {TProject} The active Project.
* Releases the active Project: restores cwd, then removes the directory unless the
* Project is preserved. A no-op when idle.
*/
function requireProject(caller: string): TProject {
const project = lifecycle.active;
if (!project) throw new Error(`[bintastic] setupProject() must be called before ${caller}()`);
function teardownProject(): void {
const project = active;
const keep = preserved || resolveDebugMode({ env: process.env }) !== undefined;
active = undefined;
preserved = false;

if (!project) {
return;
}

if (keep) {
project.restoreCwd();
console.log(`[bintastic] Fixture preserved: ${project.baseDir}`);
return;
}

project.dispose();
}

/**
* Tears down any active Project, then creates, writes, and activates a new one.
* @returns {Promise<TProject>} The new active Project.
*/
async function setupProject(): Promise<TProject> {
teardownProject();

const project = await createProject();

try {
await project.write();
} catch (error) {
project.dispose();
throw error;
}

active = project;
return project;
}

/**
* Plans and starts a Run against the active Project. Debug mode marks the Project
* as preserved so teardownProject leaves it on disk.
* @param {TProject} activeProject - The Project to run against.
* Plans and starts a Run against the active Project. The first Debug mode Run marks
* the Project as preserved and logs its path.
* @param {string} caller - The public function name used in the error message.
* @param {RunBinArgs} args - Arguments or execa options.
* @param {boolean} forceDebug - True for runBinDebug.
* @returns {ResultPromise} An instance of execa's result promise.
*/
function run(activeProject: TProject, args: RunBinArgs, forceDebug: boolean): ResultPromise {
function run(caller: string, args: RunBinArgs, forceDebug: boolean): ResultPromise {
const project = active;
if (!project) {
throw new Error(`[bintastic] setupProject() must be called before ${caller}()`);
}

const plan = planRun({
config: options,
project: activeProject,
project,
args,
forceDebug,
env: process.env,
execArgv: process.execArgv,
});

if (plan.debugMode) {
lifecycle.preserve();
console.log(`[bintastic] Debugging enabled. Fixture: ${activeProject.baseDir}`);
if (!preserved && plan.debugMode) {
preserved = true;
console.log(`[bintastic] Debugging enabled. Fixture: ${project.baseDir}`);
}

return execaNode(plan.file, plan.args, plan.options);
Expand All @@ -175,31 +215,31 @@ export function createBintastic<TProject extends BintasticProject>(
* @returns {ResultPromise} An instance of execa's result promise.
*/
function runBin(...args: RunBinArgs): ResultPromise {
return run(requireProject('runBin'), args, false);
return run('runBin', args, false);
}

/**
* Runs the configured bin with Node inspector enabled in attach mode (--inspect).
* @param {...RunBinArgs} args Arguments identical to runBin
*/
function runBinDebug(...args: RunBinArgs): ResultPromise {
return run(requireProject('runBinDebug'), args, true);
return run('runBinDebug', args, true);
}

/**
* Sets up a tmp directory for use within tests.
*/
async function setupTmpDir() {
const activeProject = lifecycle.active ?? (await lifecycle.setup());
const project = active ?? (await setupProject());

return activeProject.baseDir;
return project.baseDir;
}

return {
runBin,
runBinDebug,
setupProject: () => lifecycle.setup(),
teardownProject: () => lifecycle.teardown(),
setupProject,
teardownProject,
setupTmpDir,
};
}
107 changes: 0 additions & 107 deletions src/project-lifecycle.ts

This file was deleted.

Loading
Loading