#!/usr/bin/env node /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { existsSync } from 'node:fs'; import { readdir, readFile, writeFile } from 'node:fs/promises'; import { dirname, join, relative, resolve } from 'node:path'; import { fileURLToPath } from 'node:url'; const SCRIPT_PATH = fileURLToPath(import.meta.url); const REPO_ROOT = resolve(dirname(SCRIPT_PATH), '..'); const OUTPUT_PATH = join(REPO_ROOT, 'docs', 'windows-test-inventory.md'); const SEARCH_ROOTS = ['apps', 'packages', 'scripts']; export async function collectWindowsTestSkips(root = REPO_ROOT) { const files = []; for (const searchRoot of SEARCH_ROOTS) { const absolute = join(root, searchRoot); if (existsSync(absolute)) await collectTestFiles(absolute, files); } const entries = []; for (const file of files.sort()) { const path = relative(root, file).replaceAll('\\', '/'); const sourceText = await readFile(file, 'utf8'); const sourceLines = sourceText.split(/\r?\n/u); for (const skip of findSkipExpressions(sourceText)) { const expression = skip.expression.replaceAll(/\s+/gu, ' '); if (!excludesWindows(expression)) continue; const index = sourceText.slice(0, skip.offset).split(/\r?\n/u).length - 1; const title = nearbyTestTitle(sourceLines, index); entries.push({ path, line: index + 1, title, expression, classification: classifySkip(path, title, expression), }); } } return entries.sort( (left, right) => left.path.localeCompare(right.path) || left.line - right.line, ); } export function findSkipExpressions(sourceText) { const expressions = []; let index = 0; while (index < sourceText.length) { const next = skipTriviaOrLiteral(sourceText, index); if (next !== index) { index = next; continue; } if (!sourceText.startsWith('skip', index) || isIdentifierPart(sourceText[index - 1])) { index += 1; continue; } const afterIdentifier = index + 4; if (isIdentifierPart(sourceText[afterIdentifier])) { index = afterIdentifier; continue; } const colon = skipWhitespaceAndComments(sourceText, afterIdentifier); if (sourceText[colon] !== ':') { index = afterIdentifier; continue; } const expressionStart = skipWhitespaceAndComments(sourceText, colon + 1); const expressionEnd = findExpressionEnd(sourceText, expressionStart); const expression = sourceText.slice(expressionStart, expressionEnd).trim(); if (expression) expressions.push({ offset: index, expression }); index = Math.max(expressionEnd, afterIdentifier); } return expressions; } function findExpressionEnd(sourceText, start) { const closing = []; let index = start; while (index < sourceText.length) { const next = skipTriviaOrLiteral(sourceText, index); if (next !== index) { index = next; continue; } const char = sourceText[index]; if (char === '(') closing.push(')'); else if (char === '[') closing.push(']'); else if (char === '{') closing.push('}'); else if (closing.at(-1) === char) closing.pop(); else if (closing.length === 0 && (char === ',' || char === '}')) return index; index += 1; } return index; } function skipTriviaOrLiteral(sourceText, index) { const char = sourceText[index]; if (char === '/' && sourceText[index + 1] === '/') { const newline = sourceText.indexOf('\n', index + 2); return newline === -1 ? sourceText.length : newline + 1; } if (char === '/' && sourceText[index + 1] === '*') { const end = sourceText.indexOf('*/', index + 2); return end === -1 ? sourceText.length : end + 2; } if (char === "'" || char === '"' || char === '`') return skipQuotedLiteral(sourceText, index, char); return index; } function skipQuotedLiteral(sourceText, start, quote) { let index = start + 1; while (index < sourceText.length) { if (sourceText[index] === '\\') { index += 2; continue; } if (sourceText[index] === quote) return index + 1; index += 1; } return index; } function skipWhitespaceAndComments(sourceText, start) { let index = start; while (index < sourceText.length) { if (/\s/u.test(sourceText[index])) { index += 1; continue; } const next = skipTriviaOrLiteral(sourceText, index); if (next === index || !sourceText.startsWith('/', index)) return index; index = next; } return index; } function isIdentifierPart(char) { return char !== undefined && /[$\w]/u.test(char); } export function renderWindowsTestInventory(entries) { const counts = new Map(); for (const entry of entries) { counts.set(entry.classification, (counts.get(entry.classification) ?? 0) + 1); } const lines = [ '# Windows test skip inventory', '', '', '', 'This inventory covers test declarations whose `skip` expression excludes Windows. It is a Phase 0 baseline, not permission to retain the skips indefinitely.', 'Locations intentionally omit line numbers so unrelated edits do not invalidate the semantic inventory.', '', '## Classifications', '', '- `windows-backend-gap`: the product contract is relevant on Windows, but the Windows transport, process, crash, or sandbox evidence is missing.', '- `portable-candidate`: the behavior appears platform-neutral; remove the skip or document the concrete Windows blocker.', '- `platform-contract`: the test intentionally exercises a POSIX/macOS primitive with no direct Windows equivalent.', '', '## Summary', '', '| Classification | Count |', '|---|---:|', ]; for (const classification of ['windows-backend-gap', 'portable-candidate', 'platform-contract']) { lines.push(`| ${classification} | ${counts.get(classification) ?? 0} |`); } lines.push( '', `Total Windows-excluded declarations: **${entries.length}**`, '', '## Inventory', '', ); lines.push('| Classification | Test | Skip expression |'); lines.push('|---|---|---|'); for (const entry of entries) { lines.push( `| ${entry.classification} | \`${escapeTable(entry.path)}\` ${escapeTable(entry.title)} | \`${escapeTable(entry.expression)}\` |`, ); } return `${lines.join('\n')}\n`; } export function windowsTestInventoriesMatch(current, rendered) { return normalizeLineEndings(current) === normalizeLineEndings(rendered); } function normalizeLineEndings(value) { return value.replaceAll(/\r\n?|\n/gu, '\n'); } function classifySkip(path, title, expression) { const value = `${path} ${title} ${expression}`.toLowerCase(); if ( /process\.platform\s*!==\s*['"]darwin['"]/u.test(expression) || value.includes('posix process discovery') || value.includes('posix detached process-group') || value.includes('posix snapshot') || value.includes('posix process snapshot') || value.includes('graceful sigterm') || value.includes('publishes private posix endpoint') || value.includes('open sqlite') || value.includes('fifo') || value.includes('non-utf-8 git path') || value.includes('permissions') || path.endsWith('/shell-env.test.ts') ) { return 'platform-contract'; } if ( path.includes('/runtime-host/') || value.includes('process death') || value.includes('real-process crash') || value.includes('sqlite-runtime-crash') || value.includes('sqlite-long-term-memory-crash') || value.includes('sandbox') ) { return 'windows-backend-gap'; } return 'portable-candidate'; } export function excludesWindows(expression) { const compact = stripBalancedOuterParentheses(expression.replaceAll(/\s+/gu, ' ').trim()); // A Windows-only regression commonly uses `false` on Windows and a skip // reason elsewhere. Exempt only when that ternary is the complete skip // expression: a surrounding boolean expression may still skip on Windows. if ( /^process\.platform\s*===\s*(['"])win32\1\s*\?\s*false\s*:\s*(?:'(?:\\.|[^'\\])*'|"(?:\\.|[^"\\])*")$/u.test( compact, ) ) { return false; } return ( /process\.platform\s*===\s*['"]win32['"]/u.test(compact) || /process\.platform\s*!==\s*['"]darwin['"]/u.test(compact) ); } function stripBalancedOuterParentheses(expression) { let compact = expression; while (hasCompleteOuterParentheses(compact)) compact = compact.slice(1, -1).trim(); return compact; } function hasCompleteOuterParentheses(expression) { if (!expression.startsWith('(') || !expression.endsWith(')')) return false; let depth = 0; let quote; let escaped = false; for (let index = 0; index < expression.length; index += 1) { const char = expression[index]; if (quote) { if (escaped) escaped = false; else if (char === '\\') escaped = true; else if (char === quote) quote = undefined; continue; } if (char === "'" || char === '"' || char === '`') { quote = char; continue; } if (char === '(') depth += 1; else if (char === ')') { depth -= 1; if (depth === 0) return index === expression.length - 1; if (depth < 0) return false; } } return false; } function nearbyTestTitle(lines, skipIndex) { const context = lines.slice(Math.max(0, skipIndex - 12), skipIndex + 1).join('\n'); const matches = [...context.matchAll(/\b(?:test|it|describe)\s*\(\s*(['"`])([\s\S]+?)\1/gu)]; return matches.at(-1)?.[2].replaceAll(/\s+/gu, ' ').trim() ?? ''; } async function collectTestFiles(directory, output) { for (const entry of await readdir(directory, { withFileTypes: true })) { if (entry.name === 'node_modules' || entry.name === 'dist') continue; const path = join(directory, entry.name); if (entry.isDirectory()) await collectTestFiles(path, output); else if (/\.test\.(?:[cm]?js|tsx?)$/u.test(entry.name)) output.push(path); } } function escapeTable(value) { return String(value).replaceAll('|', '\\|').replaceAll('\n', ' '); } async function main(args) { const entries = await collectWindowsTestSkips(); const rendered = renderWindowsTestInventory(entries); if (args.includes('--write')) { await writeFile(OUTPUT_PATH, rendered); console.log(`wrote ${relative(REPO_ROOT, OUTPUT_PATH)} (${entries.length} declarations)`); return; } if (args.includes('--check')) { const current = existsSync(OUTPUT_PATH) ? await readFile(OUTPUT_PATH, 'utf8') : ''; if (!windowsTestInventoriesMatch(current, rendered)) { throw new Error( 'Windows test skip inventory is stale; run `npm run windows:inventory:write`', ); } console.log(`Windows test skip inventory is current (${entries.length} declarations)`); return; } process.stdout.write(rendered); } if (process.argv[1] && resolve(process.argv[1]) === SCRIPT_PATH) { main(process.argv.slice(2)).catch((error) => { console.error(error.message); process.exitCode = 1; }); }