File size: 3,173 Bytes
5cb63c1
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
import { vi } from "vitest";
import type { ProcessSupervisor, SpawnInput, SpawnProcessAdapter } from "./types.js";

type ChildSpawnOptions = Omit<Extract<SpawnInput, { mode: "child" }>, "mode">;

type StubOutputSubscriber = {
  listener: (chunk: string) => void;
  onRaw?: (chunk: Buffer) => void;
};

export type StubChildAdapter = SpawnProcessAdapter<NodeJS.Signals | null> & {
  emitStdout: (chunk: string) => void;
  emitStdoutRaw: (chunk: Buffer) => void;
  emitStderr: (chunk: string) => void;
  settle: (code: number | null, signal?: NodeJS.Signals | null) => void;
  killMock: ReturnType<typeof vi.fn>;
  disposeMock: ReturnType<typeof vi.fn>;
};

export function createWriteStdoutArgv(output: string): string[] {
  if (process.platform === "win32") {
    return [process.execPath, "-e", `process.stdout.write(${JSON.stringify(output)})`];
  }
  return ["/usr/bin/printf", "%s", output];
}

export function createSilentIdleArgv(): string[] {
  return [process.execPath, "-e", "setInterval(() => {}, 1_000)"];
}

export function createStubChildAdapter(options?: {
  pid?: number;
  onKill?: (signal: NodeJS.Signals | undefined, adapter: StubChildAdapter) => void;
}): StubChildAdapter {
  const stdoutSubscribers: StubOutputSubscriber[] = [];
  const stderrSubscribers: StubOutputSubscriber[] = [];
  // Mirror onDecodedOutput: one stream chunk reaches the raw subscriber before
  // its decoded sibling, so a single emit exercises both supervisor paths.
  const emitOutput = (subscribers: StubOutputSubscriber[], chunk: string) => {
    for (const subscriber of subscribers) {
      subscriber.onRaw?.(Buffer.from(chunk));
      subscriber.listener(chunk);
    }
  };
  let resolveWait:
    | ((value: { code: number | null; signal: NodeJS.Signals | null }) => void)
    | null = null;
  const waitPromise = new Promise<{ code: number | null; signal: NodeJS.Signals | null }>(
    (resolve) => {
      resolveWait = resolve;
    },
  );
  const killMock = vi.fn();
  const disposeMock = vi.fn();
  const adapter: StubChildAdapter = {
    pid: options?.pid ?? 1234,
    stdin: undefined,
    supportsRawOutput: true,
    onStdout: (listener, onRaw) => {
      stdoutSubscribers.push({ listener, ...(onRaw ? { onRaw } : {}) });
    },
    onStderr: (listener, onRaw) => {
      stderrSubscribers.push({ listener, ...(onRaw ? { onRaw } : {}) });
    },
    wait: async () => await waitPromise,
    kill: (signal) => {
      killMock(signal);
      options?.onKill?.(signal, adapter);
    },
    dispose: () => {
      disposeMock();
    },
    emitStdout: (chunk) => {
      emitOutput(stdoutSubscribers, chunk);
    },
    emitStdoutRaw: (chunk) => {
      for (const subscriber of stdoutSubscribers) {
        subscriber.onRaw?.(chunk);
      }
    },
    emitStderr: (chunk) => {
      emitOutput(stderrSubscribers, chunk);
    },
    settle: (code, signal = null) => {
      resolveWait?.({ code, signal });
      resolveWait = null;
    },
    killMock,
    disposeMock,
  };

  return adapter;
}

export async function spawnChild(supervisor: ProcessSupervisor, options: ChildSpawnOptions) {
  return supervisor.spawn({
    ...options,
    mode: "child",
  });
}