All files / src/tools/shared pebble.ts

92.3% Statements 12/13
93.33% Branches 14/15
100% Functions 1/1
92.3% Lines 12/13

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68                                                                        9x       9x 1x       8x 1x     7x       7x 1x         6x 1x     5x 1x      
/**
 * Running pebble from a tool, and turning a failed run into a message and an exit code.
 *
 * paf build and the dev plugin's tap walk both drive pebble, and a failure has to read and exit the
 * same way from either, whether pebble failed, was killed, ran out of time, or never started.
 */
import fs from 'node:fs';
import path from 'node:path';
import { ToolError, exitCodeOf } from './tool-error.ts';
 
/** What a pebble run reports back, as far as the tools read it. */
export type PebbleRun = {
  /** The exit code, or null when a signal stopped it. */
  status: number | null;
  /** The signal that stopped it, or null when it exited on its own. */
  signal: NodeJS.Signals | null;
  /** Set when pebble could not run at all, such as when it is not installed, ran past its timeout, or printed past the buffer. */
  error?: NodeJS.ErrnoException;
};
 
/** Runs pebble with the given arguments in a folder. */
export type PebbleRunner = (args: string[], cwd: string) => PebbleRun;
 
/**
 * Runs one pebble command in a folder and stops when it fails.
 *
 * A failure ends with pebble's own exit code. A pebble stopped by a signal ends with 128 plus the
 * signal's number, a pebble that ran past its timeout says so, and a pebble that is not installed says
 * the SDK has to be. Any other failure to run gives its own message.
 *
 * @param args The pebble command and its arguments, such as build or emu-tap.
 * @param cwd The folder pebble runs in, such as a build sandbox.
 * @param run What runs pebble, which is the real one outside a spec.
 * @param where What a failure names, the folder's name unless given.
 */
export function runPebble(args: string[], cwd: string, run: PebbleRunner, where: string = path.basename(cwd)): void {
  const result = run(args, cwd);
 
  // spawnSync reports a run that hit its timeout as an error too, and blaming a missing SDK for it
  // would send the developer looking in the wrong place
  if (result.error?.code === 'ETIMEDOUT') {
    throw new ToolError(`pebble ${args[0]} did not finish in time in ${where}, so it was stopped`);
  }
 
  // spawnSync says ENOENT for a folder that is not there as well as for a pebble that is not
  if (result.error?.code === 'ENOENT' && !fs.existsSync(cwd)) {
    throw new ToolError(`pebble ${args[0]} could not run in ${where}, since ${cwd} is not there`);
  }
 
  Iif (result.error?.code === 'ENOENT') {
    throw new ToolError(`pebble could not run: ${result.error.message}. The Pebble SDK has to be installed, which on Windows means WSL`);
  }
 
  if (result.error) {
    throw new ToolError(`pebble ${args[0]} could not run in ${where}: ${result.error.message}`);
  }
 
  // a signal ends the run the way a shell reports it, 128 plus the signal's number, so CI reads
  // an out of memory kill as a kill rather than as an ordinary failure
  if (result.signal) {
    throw new ToolError(`pebble ${args[0]} was stopped by ${result.signal} in ${where}`, exitCodeOf(result));
  }
 
  if (result.status !== 0) {
    throw new ToolError(`pebble ${args[0]} failed in ${where}`, exitCodeOf(result));
  }
}