All files / tests/utils serial-test-helper.ts

36.36% Statements 16/44
23.52% Branches 8/34
46.66% Functions 7/15
38.09% Lines 16/42

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 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 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227                                                    18x 18x                                                                                                                                                                                                                                                                                       18x 18x 18x   18x   18x 18x   18x     1309x               18x 18x 18x                   18x                   18x                             18x    
/**
 * Serial Test Helper Utilities
 * 
 * Provides robust utilities for testing Arduino serial output in integration tests.
 * These helpers handle the asynchronous nature of sketch compilation, execution,
 * and output processing with proper timeout handling and error reporting.
 */
 
import { SandboxRunner } from '../../server/services/sandbox-runner';
 
/**
 * Extract plain text from serial outputs.
 * 
 * With the new SerialOutputBatcher system, all serial data comes as plain text
 * (batched and rate-limited by baudrate). No more JSON wrapping.
 * 
 * @param outputs - Array of output lines or single output string
 * @returns Plain text content
 * 
 * @example
 * ```ts
 * const outputs = ['Hello', 'World'];
 * const text = extractPlainText(outputs); // Returns: "HelloWorld"
 * ```
 */
export function extractPlainText(outputs: string[] | string): string {
  const lines = Array.isArray(outputs) ? outputs : [outputs];
  return lines.join('');
}
 
/**
 * Wait for SandboxRunner to reach RUNNING state.
 * 
 * WHY THIS IS NECESSARY:
 * - Docker container startup has latency (image pull, container init)
 * - Local compilation can take 1-3 seconds for g++ to process Arduino code
 * - Registry manager has 1.5s wait mode before sending initial pin state
 * 
 * Polling ensures we don't start checking for output before the sketch
 * has even started executing.
 * 
 * @param runner - SandboxRunner instance to monitor
 * @param timeout - Maximum time to wait in milliseconds (default: 15000ms)
 * @throws Error if runner doesn't reach RUNNING state within timeout
 */
export async function waitForRunning(runner: SandboxRunner, timeout = 15000): Promise<void> {
  const start = Date.now();
  
  while (Date.now() - start < timeout) {
    if (runner.simulationState === 'running') {
      return;
    }
    await new Promise(r => setTimeout(r, 50));
  }
  
  throw new Error(
    `Timeout waiting for runner to reach RUNNING state after ${timeout}ms. ` +
    `Current state: ${runner.simulationState}`
  );
}
 
/**
 * Wait for specific serial output to appear.
 * 
 * CRITICAL DESIGN NOTES:
 * 1. WHY POLLING: Serial output arrives asynchronously via callbacks. We can't
 *    use Promises directly because multiple output chunks may arrive over time.
 * 
 * 2. WHY GENEROUS TIMEOUT: Docker scenarios need time for:
 *    - Container startup: ~1-2s
 *    - Compilation: ~2-5s (can be 10s+ in CI)
 *    - Execution: varies by sketch
 *    - Serial batching: 50ms tick interval for SerialOutputBatcher
 * 
 * 3. WHY CONTENT-BASED: The SerialOutputBatcher uses 50ms ticks, so we can't
 *    predict chunk counts. We only check if the expected content exists.
 * 
 * @param outputs - Reference to array that will be populated by onOutput callback
 * @param target - String to search for in the output
 * @param timeout - Maximum time to wait in milliseconds (default: 30000ms)
 * @param debug - Enable debug logging for long waits (default: true)
 * @throws Error if target string doesn't appear within timeout
 * 
 * @example
 * ```ts
 * const outputs: string[] = [];
 * runner.runSketch(sketch, (line) => outputs.push(line), ...);
 * await waitForSerialOutput(outputs, 'Hello', 10000);
 * expect(extractPlainText(outputs)).toContain('Hello');
 * ```
 */
export async function waitForSerialOutput(
  outputs: string[],
  target: string,
  timeout = 30000,
  debug = true
): Promise<void> {
  const start = Date.now();
  let lastLog = start;
  
  while (Date.now() - start < timeout) {
    const currentOutput = extractPlainText(outputs);
    
    if (currentOutput.includes(target)) {
      return;
    }
    
    // Debug logging if taking longer than 5s
    if (debug) {
      const elapsed = Date.now() - start;
      if (elapsed > 5000 && Date.now() - lastLog > 2000) {
        const preview = currentOutput.substring(0, 100);
        console.log(
          `[waitForSerialOutput] Still waiting for "${target}" after ${(elapsed / 1000).toFixed(1)}s. ` +
          `Current buffer (${currentOutput.length} chars): "${preview}${currentOutput.length > 100 ? '...' : ''}"`
        );
        lastLog = Date.now();
      }
    }
    
    await new Promise(r => setTimeout(r, 50));
  }
  
  throw new Error(
    `Timeout waiting for serial output: "${target}" after ${timeout}ms. ` +
    `Current output: "${extractPlainText(outputs)}"`
  );
}
 
/**
 * Run an Arduino sketch and wait for it to complete.
 * 
 * CRITICAL: This helper ensures the message queue is properly flushed.
 * 
 * WHY MESSAGE QUEUE FLUSH IS CRITICAL:
 * The SandboxRunner queues output and pin state messages while waiting for
 * the I/O registry to be collected (1.5s wait mode). If a sketch executes
 * and exits quickly (< 1.5s), queued messages were historically lost.
 * 
 * The fix (added to SandboxRunner.ts close handler) calls flushMessageQueue()
 * before process exit, ensuring all queued messages are delivered even for
 * fast-completing sketches.
 * 
 * @param runner - SandboxRunner instance
 * @param sketch - Arduino sketch code to execute
 * @param options - Configuration options
 * @returns Promise resolving to collected outputs and success status
 * 
 * @example
 * ```ts
 * const result = await runSketchWithOutput(runner, 'void setup() { Serial.println("Hi"); }');
 * expect(result.success).toBe(true);
 * expect(extractPlainText(result.outputs)).toContain('Hi');
 * ```
 */
export async function runSketchWithOutput(
  runner: SandboxRunner,
  sketch: string,
  options: {
    timeout?: number;
    fallbackTimeout?: number;
  } = {}
): Promise<{
  outputs: string[];
  success: boolean;
  error?: string;
}> {
  const outputs: string[] = [];
  const timeout = options.timeout ?? 15;
  const fallbackTimeout = options.fallbackTimeout ?? 25000;
  
  const result = await new Promise<{ outputs: string[]; success: boolean; error?: string }>(
    (resolve) => {
      let compiled = false;
      let exited = false;
      
      runner.runSketch(
        sketch,
        (line: string) => {
          outputs.push(line);
        },
        (error: string) => {
          // onError - compilation or runtime errors
          resolve({ outputs, success: false, error });
        },
        (code: number | null) => {
          // onExit
          exited = true;
          Eif (compiled || outputs.length > 0) {
            resolve({ outputs, success: true });
          }
          // If neither condition met, wait for fallback timer
        },
        (error: string) => {
          // onCompileError
          resolve({ outputs, success: false, error: `Compile: ${error}` });
        },
        () => {
          // onCompileSuccess
          compiled = true;
        },
        () => {}, // onPinState
        timeout, // timeoutSec
        (registry, baudrate) => {
          // onIORegistry - triggers message queue flush
        }
      );
      
      // Fallback timeout - resolve with whatever we have
      setTimeout(() => {
        if (!exited && !compiled) {
          resolve({
            outputs,
            success: false,
            error: `Never started compiling. Runner state: ${runner.simulationState}`,
          });
        } else if (compiled && !exited) {
          // Compiled but process didn't exit yet - resolve with current outputs
          resolve({ outputs, success: outputs.length > 0 });
        }
      }, fallbackTimeout);
    }
  );
  
  return result;
}