diff --git a/src/common/runtime/rn/README.md b/src/common/runtime/rn/README.md new file mode 100644 index 000000000000..f7df28a2609b --- /dev/null +++ b/src/common/runtime/rn/README.md @@ -0,0 +1,308 @@ +# React Native Runtime for WebGPU CTS + +This directory contains a React Native compatible runtime for running WebGPU CTS tests. + +## Why a Separate Runtime? + +React Native (via Metro bundler) doesn't support dynamic `import()` statements, which the default CTS runtime relies on. This runtime uses pre-generated static imports instead. + +## Setup + +### 1. Generate Static Imports + +Before using the runtime, you need to generate a file that statically imports all spec files. Create a build script or use the example below: + +```typescript +// gen_rn_specs.ts - Run with ts-node or similar +import * as fs from 'fs'; +import * as path from 'path'; + +const webgpuDir = path.join(__dirname, '../../webgpu'); +const outputFile = path.join(__dirname, 'generated/all_specs.ts'); + +// Find all spec files +function findSpecs(dir: string, base: string = ''): string[] { + const specs: string[] = []; + for (const entry of fs.readdirSync(dir, { withFileTypes: true })) { + const relPath = base ? `${base}/${entry.name}` : entry.name; + if (entry.isDirectory()) { + specs.push(...findSpecs(path.join(dir, entry.name), relPath)); + } else if (entry.name.endsWith('.spec.ts')) { + specs.push(relPath.replace(/\.spec\.ts$/, '')); + } + } + return specs; +} + +const specs = findSpecs(webgpuDir); + +// Generate imports +let output = `/** + * Auto-generated - DO NOT EDIT + */ +import { AllSpecs, SpecEntry } from '../loader.js'; + +`; + +specs.forEach((spec, i) => { + output += `import * as spec${i} from '../../../webgpu/${spec}.spec.js';\n`; +}); + +output += `\nconst webgpuSpecs: SpecEntry[] = [\n`; +specs.forEach((spec, i) => { + const parts = spec.split('/').map(p => `'${p}'`).join(', '); + output += ` { path: [${parts}], spec: spec${i} },\n`; +}); +output += `];\n\n`; + +output += `export const allSpecs: AllSpecs = new Map([ + ['webgpu', webgpuSpecs], +]); + +export default allSpecs; +`; + +fs.mkdirSync(path.dirname(outputFile), { recursive: true }); +fs.writeFileSync(outputFile, output); +console.log(`Generated ${outputFile} with ${specs.length} specs`); +``` + +### 2. Polyfills Required + +React Native doesn't have some Web APIs that the CTS uses. You'll need to polyfill: + +- `Event` +- `EventTarget` +- `MessageEvent` + +Example polyfill: + +```typescript +// event-target-polyfill.ts - Import before using CTS +if (typeof globalThis.Event === 'undefined') { + globalThis.Event = class Event { + readonly type: string; + readonly bubbles: boolean = false; + readonly cancelable: boolean = false; + readonly defaultPrevented: boolean = false; + readonly timeStamp: number; + + constructor(type: string, init?: EventInit) { + this.type = type; + this.bubbles = init?.bubbles ?? false; + this.cancelable = init?.cancelable ?? false; + this.timeStamp = Date.now(); + } + preventDefault() {} + stopPropagation() {} + stopImmediatePropagation() {} + }; +} + +if (typeof globalThis.EventTarget === 'undefined') { + globalThis.EventTarget = class EventTarget { + private listeners = new Map(); + + addEventListener(type: string, listener: Function) { + if (!this.listeners.has(type)) this.listeners.set(type, []); + this.listeners.get(type)!.push(listener); + } + + removeEventListener(type: string, listener: Function) { + const arr = this.listeners.get(type); + if (arr) { + const idx = arr.indexOf(listener); + if (idx >= 0) arr.splice(idx, 1); + } + } + + dispatchEvent(event: Event): boolean { + const arr = this.listeners.get(event.type); + if (arr) arr.forEach(fn => fn.call(this, event)); + return true; + } + }; +} + +if (typeof globalThis.MessageEvent === 'undefined') { + globalThis.MessageEvent = class MessageEvent { + readonly type: string; + readonly data: T; + readonly defaultPrevented = false; + readonly timeStamp: number; + + constructor(type: string, init?: { data?: T }) { + this.type = type; + this.data = init?.data as T; + this.timeStamp = Date.now(); + } + preventDefault() {} + stopPropagation() {} + }; +} +``` + +## Usage + +### Basic Usage + +```typescript +import { CTSRunner } from 'webgpu-cts/src/common/runtime/rn'; +import { allSpecs } from 'webgpu-cts/src/common/runtime/rn/generated/all_specs'; + +const runner = new CTSRunner(allSpecs, { + debug: false, + compatibility: false, +}); + +const { summary, results } = await runner.runTests('webgpu:api,operation,adapter,*'); + +console.log(`Passed: ${summary.passed}/${summary.total}`); +``` + +### With Progress Callbacks + +```typescript +const { summary, results } = await runner.runTests( + 'webgpu:api,operation,*', + { + onTestStart: (name, index, total) => { + console.log(`[${index + 1}/${total}] Running: ${name}`); + }, + onTestComplete: (result, index, total) => { + console.log(` ${result.status} (${result.timems.toFixed(1)}ms)`); + }, + onRunComplete: (summary) => { + console.log(`Done! ${summary.passed} passed, ${summary.failed} failed`); + }, + } +); +``` + +### Stopping a Test Run + +```typescript +const runner = new CTSRunner(allSpecs); + +// Start tests +const promise = runner.runTests('webgpu:*', { + shouldStop: () => runner.isStopRequested(), +}); + +// Later, request stop +runner.requestStop(); + +// Remaining tests will be marked as 'skip' +const { summary } = await promise; +``` + +### Standalone Functions + +For simpler use cases without creating a runner instance: + +```typescript +import { listTests, runTests, runSingleTest } from 'webgpu-cts/src/common/runtime/rn'; +import { allSpecs } from 'webgpu-cts/src/common/runtime/rn/generated/all_specs'; + +// List matching tests +const tests = await listTests(allSpecs, 'webgpu:api,operation,adapter,*'); +console.log(`Found ${tests.length} tests`); + +// Run tests +const { summary } = await runTests(allSpecs, 'webgpu:api,operation,adapter,info:*'); + +// Run a single test +const result = await runSingleTest(allSpecs, 'webgpu:api,operation,adapter,info:*'); +``` + +## Query Syntax + +The CTS uses a hierarchical query syntax: + +| Query | Description | +|-------|-------------| +| `webgpu:*` | All WebGPU tests | +| `webgpu:api,*` | All API tests | +| `webgpu:api,operation,*` | All operation tests | +| `webgpu:api,operation,adapter,*` | All adapter tests | +| `webgpu:api,operation,adapter,info:*` | Single test file | + +## Configuration Options + +```typescript +interface CTSConfig { + /** Run in WebGPU compatibility mode */ + compatibility?: boolean; + + /** Force fallback adapter */ + forceFallbackAdapter?: boolean; + + /** Enforce default limits */ + enforceDefaultLimits?: boolean; + + /** Enable debug logging */ + debug?: boolean; + + /** Unroll const eval loops */ + unrollConstEvalLoops?: boolean; + + /** Custom GPU provider function */ + gpuProvider?: () => GPU; + + /** Power preference for adapter */ + powerPreference?: GPUPowerPreference; +} +``` + +## API Reference + +### CTSRunner + +Main class for running tests with full control. + +```typescript +class CTSRunner { + constructor(allSpecs: AllSpecs, config?: CTSConfig); + + listTests(query: string): Promise; + runTests(query: string, callbacks?: TestRunCallbacks): Promise<{ summary, results }>; + + requestStop(): void; + isStopRequested(): boolean; + resetStop(): void; + + getResultsJSON(space?: number): string; +} +``` + +### TestResult + +```typescript +interface TestResult { + name: string; + status: 'pass' | 'fail' | 'skip' | 'warn'; + timems: number; + logs?: string[]; +} +``` + +### TestRunSummary + +```typescript +interface TestRunSummary { + total: number; + passed: number; + failed: number; + skipped: number; + warned: number; + timems: number; +} +``` + +## Integration Example + +See the [[react-native-webgpu](https://github.com/wcandillon/react-native-webgpu)](https://github.com/wcandillon/react-native-webgpu/pull/306) example app for a complete integration including: + +- Sync script to copy CTS files for Metro bundler +- UI component for running tests +- Full polyfill implementations diff --git a/src/common/runtime/rn/index.ts b/src/common/runtime/rn/index.ts new file mode 100644 index 000000000000..b6b85e82218a --- /dev/null +++ b/src/common/runtime/rn/index.ts @@ -0,0 +1,36 @@ +/** + * React Native runtime for the WebGPU CTS + * + * Usage: + * import { CTSRunner, runTests, listTests } from './rn/index.js'; + * import { allSpecs } from './rn/generated/all_specs.js'; + * + * // Option 1: Use the runner class + * const runner = new CTSRunner(allSpecs, { compatibility: false }); + * const { summary, results } = await runner.runTests('webgpu:api,operation,*'); + * + * // Option 2: Use standalone functions + * const tests = await listTests(allSpecs, 'webgpu:*'); + * const { summary, results } = await runTests(allSpecs, 'webgpu:api,operation,adapter,*', { + * onTestStart: (name, i, total) => console.log(`Running ${i + 1}/${total}: ${name}`), + * onTestComplete: (result, i, total) => console.log(` ${result.status}`), + * }); + */ + +export { + ReactNativeTestFileLoader, + AllSpecs, + SpecEntry, +} from './loader.js'; + +export { + CTSRunner, + CTSConfig, + TestResult, + TestRunSummary, + TestRunCallbacks, + applyConfig, + listTests, + runTests, + runSingleTest, +} from './runtime.js'; diff --git a/src/common/runtime/rn/loader.ts b/src/common/runtime/rn/loader.ts new file mode 100644 index 000000000000..3899040a54c6 --- /dev/null +++ b/src/common/runtime/rn/loader.ts @@ -0,0 +1,74 @@ +import { SpecFile, TestFileLoader } from '../../internal/file_loader.js'; +import { TestSuiteListing, TestSuiteListingEntry } from '../../internal/test_suite_listing.js'; + +/** + * Entry for a pre-imported spec file. + * Generated at build time by gen_rn_specs.ts + */ +export interface SpecEntry { + /** The path parts, e.g. ['api', 'operation', 'adapter', 'info'] */ + readonly path: string[]; + /** The pre-imported spec module */ + readonly spec: SpecFile; +} + +/** + * All specs for a suite, keyed by suite name (e.g., 'webgpu') + */ +export type AllSpecs = Map; + +/** + * React Native compatible TestFileLoader. + * + * Unlike DefaultTestFileLoader which uses dynamic imports, + * this loader uses pre-imported spec modules that are bundled + * at build time. + * + * Usage: + * import { allSpecs } from './generated/all_specs.js'; + * const loader = new ReactNativeTestFileLoader(allSpecs); + * const tree = await loader.loadTree(parseQuery('webgpu:*')); + */ +export class ReactNativeTestFileLoader extends TestFileLoader { + private readonly specs: AllSpecs; + + constructor(specs: AllSpecs) { + super(); + this.specs = specs; + } + + async listing(suite: string): Promise { + const entries = this.specs.get(suite); + if (!entries) { + throw new Error(`Unknown test suite: ${suite}`); + } + + // Build listing from the pre-imported spec entries + const listing: TestSuiteListingEntry[] = entries.map(entry => ({ + file: entry.path, + })); + + return listing; + } + + protected async import(path: string): Promise { + // path is like "webgpu/api/operation/adapter/info.spec.js" + const parts = path.replace(/\.spec\.js$/, '').split('/'); + const suite = parts[0]; + const filePath = parts.slice(1); + + const entries = this.specs.get(suite); + if (!entries) { + throw new Error(`Unknown test suite: ${suite}`); + } + + const pathStr = filePath.join('/'); + const entry = entries.find(e => e.path.join('/') === pathStr); + + if (!entry) { + throw new Error(`Spec file not found: ${path}`); + } + + return entry.spec; + } +} diff --git a/src/common/runtime/rn/runtime.ts b/src/common/runtime/rn/runtime.ts new file mode 100644 index 000000000000..1c634180be0f --- /dev/null +++ b/src/common/runtime/rn/runtime.ts @@ -0,0 +1,348 @@ +import { globalTestConfig } from '../../framework/test_config.js'; +import { Logger } from '../../internal/logging/logger.js'; +import { LiveTestCaseResult, Status } from '../../internal/logging/result.js'; +import { parseQuery } from '../../internal/query/parseQuery.js'; +import { TestQueryWithExpectation } from '../../internal/query/query.js'; +import { TestTreeLeaf } from '../../internal/tree.js'; +import { setDefaultRequestAdapterOptions, setGPUProvider } from '../../util/navigator_gpu.js'; + +import { AllSpecs, ReactNativeTestFileLoader } from './loader.js'; + +/** + * Configuration options for the React Native CTS runner + */ +export interface CTSConfig { + /** Run in WebGPU compatibility mode */ + compatibility?: boolean; + /** Force fallback adapter */ + forceFallbackAdapter?: boolean; + /** Enforce default limits */ + enforceDefaultLimits?: boolean; + /** Enable debug logging */ + debug?: boolean; + /** Unroll const eval loops */ + unrollConstEvalLoops?: boolean; + /** Custom GPU provider function */ + gpuProvider?: () => GPU; + /** Power preference for adapter */ + powerPreference?: GPUPowerPreference; +} + +/** + * Result of a single test case + */ +export interface TestResult { + name: string; + status: Status; + timems: number; + logs?: string[]; +} + +/** + * Summary of a test run + */ +export interface TestRunSummary { + total: number; + passed: number; + failed: number; + skipped: number; + warned: number; + timems: number; +} + +/** + * Callbacks for test run progress + */ +export interface TestRunCallbacks { + /** Called when a test starts */ + onTestStart?: (name: string, index: number, total: number) => void; + /** Called when a test completes */ + onTestComplete?: (result: TestResult, index: number, total: number) => void; + /** Called when the entire run completes */ + onRunComplete?: (summary: TestRunSummary, results: TestResult[]) => void; + /** Check if the run should be stopped */ + shouldStop?: () => boolean; +} + +/** + * Apply configuration to global test config + */ +export function applyConfig(config: CTSConfig): void { + if (config.compatibility !== undefined) { + globalTestConfig.compatibility = config.compatibility; + } + if (config.forceFallbackAdapter !== undefined) { + globalTestConfig.forceFallbackAdapter = config.forceFallbackAdapter; + } + if (config.enforceDefaultLimits !== undefined) { + globalTestConfig.enforceDefaultLimits = config.enforceDefaultLimits; + } + if (config.debug !== undefined) { + globalTestConfig.enableDebugLogs = config.debug; + } + if (config.unrollConstEvalLoops !== undefined) { + globalTestConfig.unrollConstEvalLoops = config.unrollConstEvalLoops; + } + + // Set adapter options + if (config.compatibility || config.forceFallbackAdapter || config.powerPreference) { + setDefaultRequestAdapterOptions({ + ...(config.powerPreference && { powerPreference: config.powerPreference }), + ...(config.compatibility && { featureLevel: 'compatibility' as const }), + ...(config.forceFallbackAdapter && { forceFallbackAdapter: true }), + }); + } + + // Set GPU provider + if (config.gpuProvider) { + setGPUProvider(config.gpuProvider); + } +} + +/** + * List all test cases matching a query + */ +export async function listTests( + allSpecs: AllSpecs, + query: string +): Promise { + const loader = new ReactNativeTestFileLoader(allSpecs); + const parsedQuery = parseQuery(query); + const testcases = await loader.loadCases(parsedQuery); + + const names: string[] = []; + for (const testcase of testcases) { + names.push(testcase.query.toString()); + } + return names; +} + +/** + * Run tests matching a query + */ +export async function runTests( + allSpecs: AllSpecs, + query: string, + callbacks?: TestRunCallbacks, + expectations?: TestQueryWithExpectation[] +): Promise<{ summary: TestRunSummary; results: TestResult[] }> { + const loader = new ReactNativeTestFileLoader(allSpecs); + const parsedQuery = parseQuery(query); + const testcases = await loader.loadCases(parsedQuery); + const log = new Logger(); + + // Collect all test cases first to get total count + const testcaseArray: TestTreeLeaf[] = []; + for (const testcase of testcases) { + testcaseArray.push(testcase); + } + + const total = testcaseArray.length; + const results: TestResult[] = []; + const summary: TestRunSummary = { + total, + passed: 0, + failed: 0, + skipped: 0, + warned: 0, + timems: 0, + }; + + const startTime = performance.now(); + + for (let i = 0; i < testcaseArray.length; i++) { + // Check if we should stop + if (callbacks?.shouldStop?.()) { + // Mark remaining tests as skipped + for (let j = i; j < testcaseArray.length; j++) { + const name = testcaseArray[j].query.toString(); + results.push({ name, status: 'skip', timems: 0 }); + summary.skipped++; + } + break; + } + + const testcase = testcaseArray[i]; + const name = testcase.query.toString(); + + callbacks?.onTestStart?.(name, i, total); + + const [rec, res] = log.record(name); + await testcase.run(rec, expectations ?? []); + + const result: TestResult = { + name, + status: res.status, + timems: res.timems, + logs: res.logs?.map(l => l.toJSON()), + }; + + results.push(result); + + switch (res.status) { + case 'pass': + summary.passed++; + break; + case 'fail': + summary.failed++; + break; + case 'skip': + summary.skipped++; + break; + case 'warn': + summary.warned++; + break; + } + + callbacks?.onTestComplete?.(result, i, total); + } + + summary.timems = performance.now() - startTime; + + callbacks?.onRunComplete?.(summary, results); + + return { summary, results }; +} + +/** + * Run a single test by name + */ +export async function runSingleTest( + allSpecs: AllSpecs, + testName: string, + expectations?: TestQueryWithExpectation[] +): Promise { + const { results } = await runTests(allSpecs, testName, undefined, expectations); + if (results.length === 0) { + throw new Error(`Test not found: ${testName}`); + } + return results[0]; +} + +/** + * Create a test runner instance for more control + */ +export class CTSRunner { + private readonly loader: ReactNativeTestFileLoader; + private readonly logger: Logger; + private stopRequested = false; + + constructor(allSpecs: AllSpecs, config?: CTSConfig) { + this.loader = new ReactNativeTestFileLoader(allSpecs); + this.logger = new Logger(); + if (config) { + applyConfig(config); + } + } + + /** Request to stop the current run */ + requestStop(): void { + this.stopRequested = true; + } + + /** Check if stop was requested */ + isStopRequested(): boolean { + return this.stopRequested; + } + + /** Reset stop flag */ + resetStop(): void { + this.stopRequested = false; + } + + /** List tests matching a query */ + async listTests(query: string): Promise { + const parsedQuery = parseQuery(query); + const testcases = await this.loader.loadCases(parsedQuery); + const names: string[] = []; + for (const testcase of testcases) { + names.push(testcase.query.toString()); + } + return names; + } + + /** Run tests with callbacks */ + async runTests( + query: string, + callbacks?: TestRunCallbacks, + expectations?: TestQueryWithExpectation[] + ): Promise<{ summary: TestRunSummary; results: TestResult[] }> { + this.resetStop(); + + const parsedQuery = parseQuery(query); + const testcases = await this.loader.loadCases(parsedQuery); + + const testcaseArray: TestTreeLeaf[] = []; + for (const testcase of testcases) { + testcaseArray.push(testcase); + } + + const total = testcaseArray.length; + const results: TestResult[] = []; + const summary: TestRunSummary = { + total, + passed: 0, + failed: 0, + skipped: 0, + warned: 0, + timems: 0, + }; + + const startTime = performance.now(); + + for (let i = 0; i < testcaseArray.length; i++) { + if (this.stopRequested || callbacks?.shouldStop?.()) { + for (let j = i; j < testcaseArray.length; j++) { + const name = testcaseArray[j].query.toString(); + results.push({ name, status: 'skip', timems: 0 }); + summary.skipped++; + } + break; + } + + const testcase = testcaseArray[i]; + const name = testcase.query.toString(); + + callbacks?.onTestStart?.(name, i, total); + + const [rec, res] = this.logger.record(name); + await testcase.run(rec, expectations ?? []); + + const result: TestResult = { + name, + status: res.status, + timems: res.timems, + logs: res.logs?.map(l => l.toJSON()), + }; + + results.push(result); + + switch (res.status) { + case 'pass': + summary.passed++; + break; + case 'fail': + summary.failed++; + break; + case 'skip': + summary.skipped++; + break; + case 'warn': + summary.warned++; + break; + } + + callbacks?.onTestComplete?.(result, i, total); + } + + summary.timems = performance.now() - startTime; + callbacks?.onRunComplete?.(summary, results); + + return { summary, results }; + } + + /** Get logger results as JSON */ + getResultsJSON(space?: number): string { + return this.logger.asJSON(space); + } +} diff --git a/src/common/tools/gen_rn_specs.ts b/src/common/tools/gen_rn_specs.ts new file mode 100644 index 000000000000..fdea589733a0 --- /dev/null +++ b/src/common/tools/gen_rn_specs.ts @@ -0,0 +1,171 @@ +/** + * Generates a TypeScript file that statically imports all spec files + * for use in React Native (which cannot use dynamic imports). + * + * Usage: + * npx ts-node src/common/tools/gen_rn_specs.ts [--suite webgpu] [--out path/to/output.ts] + * + * This will generate a file like: + * + * import * as spec0 from '../../webgpu/api/operation/adapter/info.spec.js'; + * import * as spec1 from '../../webgpu/api/operation/adapter/requestAdapter.spec.js'; + * ... + * export const allSpecs = new Map([ + * ['webgpu', [ + * { path: ['api', 'operation', 'adapter', 'info'], spec: spec0 }, + * { path: ['api', 'operation', 'adapter', 'requestAdapter'], spec: spec1 }, + * ... + * ]], + * ]); + */ + +import * as fs from 'fs'; +import * as path from 'path'; + +const specFileSuffix = '.spec.ts'; + +async function crawlSpecFiles(dir: string): Promise { + const results: string[] = []; + + async function crawl(currentDir: string): Promise { + const entries = await fs.promises.readdir(currentDir, { withFileTypes: true }); + + for (const entry of entries) { + const fullPath = path.join(currentDir, entry.name); + + if (entry.isDirectory()) { + await crawl(fullPath); + } else if (entry.isFile() && entry.name.endsWith(specFileSuffix)) { + results.push(fullPath); + } + } + } + + await crawl(dir); + return results.sort(); +} + +interface GenerateOptions { + /** Suite name (e.g., 'webgpu') */ + suite: string; + /** Root directory of the CTS source */ + rootDir: string; + /** Output file path */ + outputPath: string; + /** Import path prefix (for adjusting relative imports) */ + importPrefix?: string; +} + +async function generateSpecsFile(options: GenerateOptions): Promise { + const { suite, rootDir, outputPath, importPrefix = '../..' } = options; + + const suiteDir = path.join(rootDir, 'src', suite); + if (!fs.existsSync(suiteDir)) { + throw new Error(`Suite directory not found: ${suiteDir}`); + } + + const specFiles = await crawlSpecFiles(suiteDir); + + console.log(`Found ${specFiles.length} spec files in ${suite}`); + + // Generate imports and entries + const imports: string[] = []; + const entries: string[] = []; + + specFiles.forEach((filePath, index) => { + // Get path relative to suite dir + const relativePath = path.relative(suiteDir, filePath); + // Convert to path parts (without .spec.ts extension) + const pathWithoutExt = relativePath.replace(/\.spec\.ts$/, ''); + const pathParts = pathWithoutExt.split(path.sep); + + // Import path (use .js extension for ESM compatibility) + const importPath = `${importPrefix}/${suite}/${pathWithoutExt}.spec.js`; + + imports.push(`import * as spec${index} from '${importPath}';`); + entries.push(` { path: [${pathParts.map(p => `'${p}'`).join(', ')}], spec: spec${index} },`); + }); + + const output = `/** + * Auto-generated file - DO NOT EDIT + * Generated by: npx tsx src/common/tools/gen_rn_specs.ts + * + * This file statically imports all CTS spec files for React Native. + */ + +import { AllSpecs, SpecEntry } from '../loader.js'; + +${imports.join('\n')} + +const ${suite}Specs: SpecEntry[] = [ +${entries.join('\n')} +]; + +export const allSpecs: AllSpecs = new Map([ + ['${suite}', ${suite}Specs], +]); + +export default allSpecs; +`; + + // Ensure output directory exists + const outputDir = path.dirname(outputPath); + if (!fs.existsSync(outputDir)) { + fs.mkdirSync(outputDir, { recursive: true }); + } + + fs.writeFileSync(outputPath, output); + console.log(`Generated: ${outputPath}`); +} + +// CLI handling +async function main(): Promise { + const args = process.argv.slice(2); + + let suite = 'webgpu'; + let outputPath = 'src/common/runtime/rn/generated/all_specs.ts'; + let importPrefix = '../../..'; + + for (let i = 0; i < args.length; i++) { + if (args[i] === '--suite' && args[i + 1]) { + suite = args[++i]; + } else if (args[i] === '--out' && args[i + 1]) { + outputPath = args[++i]; + } else if (args[i] === '--import-prefix' && args[i + 1]) { + importPrefix = args[++i]; + } else if (args[i] === '--help') { + console.log(` +Usage: npx ts-node src/common/tools/gen_rn_specs.ts [options] + +Options: + --suite Suite to generate imports for (default: webgpu) + --out Output file path (default: src/common/runtime/rn/generated/all_specs.ts) + --import-prefix Prefix for import paths (default: ../../..) + --help Show this help message +`); + process.exit(0); + } + } + + // Find root directory (where src/ is) + let rootDir = process.cwd(); + if (!fs.existsSync(path.join(rootDir, 'src', suite))) { + // Try going up one level + rootDir = path.dirname(rootDir); + if (!fs.existsSync(path.join(rootDir, 'src', suite))) { + throw new Error(`Cannot find src/${suite} directory. Run from CTS root.`); + } + } + + await generateSpecsFile({ + suite, + rootDir, + outputPath: path.join(rootDir, outputPath), + importPrefix, + }); +} + +main().catch(err => { + console.error(err); + process.exit(1); +});