/ scripts / save-test-results.js
save-test-results.js
  1  #!/usr/bin/env node
  2  /**
  3   * TAP-to-JSON converter for test results
  4   * Parses TAP output and saves to JSON format for dashboard consumption
  5   */
  6  
  7  import { readFileSync, writeFileSync, mkdirSync } from 'fs';
  8  import { dirname } from 'path';
  9  
 10  function parseTapToJson(tapContent) {
 11    const lines = tapContent.split('\n');
 12    const result = {
 13      total: 0,
 14      pass: 0,
 15      fail: 0,
 16      skip: 0,
 17      duration_ms: 0,
 18      tests: [],
 19      timestamp: new Date().toISOString(),
 20    };
 21  
 22    let currentSuite = null;
 23    const suiteStack = [];
 24  
 25    for (let i = 0; i < lines.length; i++) {
 26      const line = lines[i];
 27  
 28      // Parse summary lines
 29      if (line.startsWith('# tests ')) {
 30        result.total = parseInt(line.split(' ')[2], 10);
 31      } else if (line.startsWith('# pass ')) {
 32        result.pass = parseInt(line.split(' ')[2], 10);
 33      } else if (line.startsWith('# fail ')) {
 34        result.fail = parseInt(line.split(' ')[2], 10);
 35      } else if (line.startsWith('# skip ')) {
 36        result.skip = parseInt(line.split(' ')[2], 10);
 37      } else if (line.startsWith('# duration_ms ')) {
 38        result.duration_ms = parseFloat(line.split(' ')[2]);
 39      }
 40      // Track test suites
 41      else if (line.includes('# Subtest:')) {
 42        const indent = line.search(/\S/);
 43        const suiteName = line.replace('# Subtest:', '').trim();
 44  
 45        // Manage suite hierarchy based on indentation
 46        while (suiteStack.length > 0 && suiteStack[suiteStack.length - 1].indent >= indent) {
 47          suiteStack.pop();
 48        }
 49  
 50        suiteStack.push({ name: suiteName, indent });
 51        currentSuite = suiteStack.map(s => s.name).join(' > ');
 52      }
 53      // Parse individual test results
 54      else if (line.trim().startsWith('ok ') || line.trim().startsWith('not ok ')) {
 55        // Skip suite-level results
 56        const nextLine = i + 1 < lines.length ? lines[i + 1] : '';
 57        if (nextLine.includes("type: 'suite'")) {
 58          continue;
 59        }
 60  
 61        const parts = line.trim().split(' ');
 62        const status = parts[0] === 'ok' ? 'pass' : 'fail';
 63        const testNum = parts[1];
 64        let testName = parts.slice(2).join(' ').replace(/^- /, '');
 65  
 66        // Check for SKIP directive
 67        const isSkipped = testName.includes('# SKIP') || testName.includes('# TODO');
 68        if (isSkipped) {
 69          testName = testName.replace(/# (SKIP|TODO).*$/, '').trim();
 70        }
 71  
 72        // Extract duration from following lines
 73        let duration_ms = null;
 74        for (let j = i + 1; j < Math.min(i + 5, lines.length); j++) {
 75          if (lines[j].includes('duration_ms:')) {
 76            const match = lines[j].match(/duration_ms:\s*([\d.]+)/);
 77            if (match) {
 78              duration_ms = parseFloat(match[1]);
 79              break;
 80            }
 81          }
 82        }
 83  
 84        result.tests.push({
 85          suite: currentSuite || 'Root',
 86          name: testName,
 87          status: isSkipped ? 'skip' : status,
 88          duration_ms,
 89        });
 90      }
 91    }
 92  
 93    return result;
 94  }
 95  
 96  // Main execution
 97  const args = process.argv.slice(2);
 98  if (args.length < 2) {
 99    console.error('Usage: save-test-results.js <input-tap-file> <output-json-file>');
100    process.exit(1);
101  }
102  
103  const [inputFile, outputFile] = args;
104  
105  try {
106    // Read TAP input
107    const tapContent = readFileSync(inputFile, 'utf8');
108  
109    // Parse to JSON
110    const jsonResult = parseTapToJson(tapContent);
111  
112    // Ensure output directory exists
113    mkdirSync(dirname(outputFile), { recursive: true });
114  
115    // Write JSON output
116    writeFileSync(outputFile, JSON.stringify(jsonResult, null, 2));
117  
118    console.log(`✅ Saved test results to ${outputFile}`);
119    console.log(
120      `   Tests: ${jsonResult.total} total, ${jsonResult.pass} passed, ${jsonResult.fail} failed, ${jsonResult.skip} skipped`
121    );
122    console.log(`   Duration: ${(jsonResult.duration_ms / 1000).toFixed(2)}s`);
123  } catch (error) {
124    console.error(`❌ Error processing test results: ${error.message}`);
125    process.exit(1);
126  }