diff --git a/README.md b/README.md index 376184b..8563492 100644 --- a/README.md +++ b/README.md @@ -52,7 +52,6 @@ Establishing this initial prototype as a singular flexible engine foundation tha ### Weval AOT Compilation - ## Platform APIs The following APIs are available: @@ -330,21 +329,20 @@ imports. Direct component analysis should be used to correctly infer the real im ## Contributing -### Pre-requisites +To contribute, you'll need to set up the following: * `git submodule update --init --recursive` to update the submodules. * Stable Rust with the `wasm32-unknown-unknown` and `wasm32-wasi` targets installed. * `wasi-sdk-20.0` installed at `/opt/wasi-sdk/` -### Building and testing +## Building Building and testing the project can be performed via NPM scripts (see [`package.json`](./package.json)): ```console npm install npm run build -npm run test ``` Before being able to use `componetize-js` (ex. via `npm link`, from `jco`), you'll need to run: @@ -361,12 +359,30 @@ To clean up a local installation (i.e. remove the installation of StarlingMonkey npm run clean ``` +## Testing + +To run all tests: + +```console +npm run test +``` + +### Running a specific test + +To run a specific test suite, you can pass an argument to [`vitest`][vitest]: + +```console +npm run test -- test/wasi.js +``` + +[vitest]: https://vitest.dev + # License This project is licensed under the Apache 2.0 license with the LLVM exception. See [LICENSE](LICENSE) for more details. -### Contribution +## Contribution Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in this project by you, as defined in the Apache-2.0 license, diff --git a/test/bindings.js b/test/bindings.js new file mode 100644 index 0000000..74d984b --- /dev/null +++ b/test/bindings.js @@ -0,0 +1,147 @@ +import { fileURLToPath, URL } from 'node:url'; +import { readFile, readdir, mkdir, writeFile } from 'node:fs/promises'; + +import { componentize } from '@bytecodealliance/componentize-js'; +import { transpile } from '@bytecodealliance/jco'; + +import { suite, test } from 'vitest'; + +import { + DEBUG_TRACING_ENABLED, + WEVAL_TEST_ENABLED, + DEBUG_TEST_ENABLED, + maybeLogging, +} from './util.js'; + +suite('Bindings', async () => { + const bindingsCases = await readdir(new URL('./cases', import.meta.url)); + + for (const name of bindingsCases) { + const testFn = WEVAL_TEST_ENABLED ? test : test.concurrent; + testFn(name, async () => { + const source = await readFile( + new URL(`./cases/${name}/source.js`, import.meta.url), + 'utf8', + ); + + const test = await import(`./cases/${name}/test.js`); + + // Determine the relevant WIT world to use + let witWorld, + witPath, + worldName, + isWasiTarget = false; + if (test.worldName) { + witPath = fileURLToPath(new URL('./wit', import.meta.url)); + worldName = test.worldName; + isWasiTarget = true; + } else { + try { + witWorld = await readFile( + new URL(`./cases/${name}/world.wit`, import.meta.url), + 'utf8', + ); + } catch (e) { + if (e?.code == 'ENOENT') { + try { + isWasiTarget = true; + witPath = fileURLToPath( + new URL(`./cases/${name}/wit`, import.meta.url), + ); + await readdir(witPath); + } catch (e) { + if (e?.code === 'ENOENT') { + witPath = fileURLToPath(new URL('./wit', import.meta.url)); + worldName = 'test2'; + } else { + throw e; + } + } + } else { + throw e; + } + } + } + + const enableFeatures = test.enableFeatures || ['http']; + const disableFeatures = + test.disableFeatures || + (isWasiTarget ? [] : ['random', 'clocks', 'http', 'stdio']); + + let testArg; + try { + const { component, imports } = await componentize(source, { + sourceName: `${name}.js`, + witWorld, + witPath, + worldName, + enableFeatures, + disableFeatures: maybeLogging(disableFeatures), + enableAot: WEVAL_TEST_ENABLED, + debugBuild: DEBUG_TEST_ENABLED, + }); + const map = { + 'wasi:cli-base/*': '@bytecodealliance/preview2-shim/cli-base#*', + 'wasi:clocks/*': '@bytecodealliance/preview2-shim/clocks#*', + 'wasi:filesystem/*': '@bytecodealliance/preview2-shim/filesystem#*', + 'wasi:http/*': '@bytecodealliance/preview2-shim/http#*', + 'wasi:io/*': '@bytecodealliance/preview2-shim/io#*', + 'wasi:logging/*': '@bytecodealliance/preview2-shim/logging#*', + 'wasi:poll/*': '@bytecodealliance/preview2-shim/poll#*', + 'wasi:random/*': '@bytecodealliance/preview2-shim/random#*', + 'wasi:sockets/*': '@bytecodealliance/preview2-shim/sockets#*', + }; + for (let [impt] of imports) { + if (impt.startsWith('wasi:')) continue; + if (impt.startsWith('[')) impt = impt.slice(impt.indexOf(']') + 1); + let importName = impt.split('/').pop(); + if (importName === 'test') importName = 'imports'; + map[impt] = `../../cases/${name}/${importName}.js`; + } + + const { + files, + imports: componentImports, + exports: componentExports, + } = await transpile(component, { + name, + map, + wasiShim: true, + validLiftingOptimization: false, + tracing: DEBUG_TRACING_ENABLED, + }); + + testArg = { imports, componentImports, componentExports }; + + await mkdir(new URL(`./output/${name}/interfaces`, import.meta.url), { + recursive: true, + }); + + await writeFile( + new URL(`./output/${name}.component.wasm`, import.meta.url), + component, + ); + + for (const file of Object.keys(files)) { + let source = files[file]; + await writeFile( + new URL(`./output/${name}/${file}`, import.meta.url), + source, + ); + } + + const outputPath = fileURLToPath( + new URL(`./output/${name}/${name}.js`, import.meta.url), + ); + var instance = await import(outputPath); + } catch (e) { + if (test.err) { + test.err(e); + return; + } + throw e; + } + await test.test(instance, testArg); + }); + } +}); diff --git a/test/builtins.js b/test/builtins.js new file mode 100644 index 0000000..b312cad --- /dev/null +++ b/test/builtins.js @@ -0,0 +1,130 @@ +import { readFile, readdir, mkdir, writeFile } from 'node:fs/promises'; +import { spawn } from 'node:child_process'; +import { fileURLToPath, URL } from 'node:url'; + +import { componentize } from '@bytecodealliance/componentize-js'; +import { transpile } from '@bytecodealliance/jco'; + +import { suite, test, assert } from 'vitest'; + +import { + DEBUG_TRACING_ENABLED, + WEVAL_TEST_ENABLED, + DEBUG_TEST_ENABLED, + maybeLogging, +} from './util.js'; + +suite('Builtins', async () => { + const builtins = await readdir(new URL('./builtins', import.meta.url)); + + for (const filename of builtins) { + const name = filename.slice(0, -3); + const testFn = WEVAL_TEST_ENABLED ? test : test.concurrent; + testFn(name, async () => { + const { + source, + test: runTest, + disableFeatures, + enableFeatures, + } = await import(`./builtins/${name}.js`); + + const { component } = await componentize( + source, + ` + package local:runworld; + world runworld { + export run: func(); + } + `, + { + sourceName: `${name}.js`, + // also test the debug build while we are about it (unless testing Weval) + debugBuild: DEBUG_TEST_ENABLED, + enableFeatures, + disableFeatures: maybeLogging(disableFeatures), + enableAot: WEVAL_TEST_ENABLED, + }, + ); + + const { files } = await transpile(component, { + name, + wasiShim: true, + tracing: DEBUG_TRACING_ENABLED, + }); + + await mkdir(new URL(`./output/${name}/interfaces`, import.meta.url), { + recursive: true, + }); + + await writeFile( + new URL(`./output/${name}.component.wasm`, import.meta.url), + component, + ); + + for (const file of Object.keys(files)) { + await writeFile( + new URL(`./output/${name}/${file}`, import.meta.url), + files[file], + ); + } + + await writeFile( + new URL(`./output/${name}/run.js`, import.meta.url), + ` + import { run } from './${name}.js'; + run(); + `, + ); + + try { + await runTest(async function run() { + let stdout = '', + stderr = '', + timeout; + try { + await new Promise((resolve, reject) => { + const cp = spawn( + process.argv[0], + [ + fileURLToPath( + new URL(`./output/${name}/run.js`, import.meta.url), + ), + ], + { stdio: 'pipe' }, + ); + cp.stdout.on('data', (chunk) => { + stdout += chunk; + }); + cp.stderr.on('data', (chunk) => { + stderr += chunk; + }); + cp.on('error', reject); + cp.on('exit', (code) => + code === 0 ? resolve() : reject(new Error(stderr || stdout)), + ); + timeout = setTimeout(() => { + reject( + new Error( + 'test timed out with output:\n' + + stdout + + '\n\nstderr:\n' + + stderr, + ), + ); + }, 10_000); + }); + } catch (err) { + throw { err, stdout, stderr }; + } finally { + clearTimeout(timeout); + } + + return { stdout, stderr }; + }); + } catch (err) { + if (err.stderr) console.error(err.stderr); + throw err.err || err; + } + }); + } +}); diff --git a/test/cases/fetch-event-server/test.js b/test/cases/fetch-event-server/test.js index 57e3725..f4ca8f7 100644 --- a/test/cases/fetch-event-server/test.js +++ b/test/cases/fetch-event-server/test.js @@ -8,15 +8,17 @@ export const enableFeatures = ['http', 'fetch-event']; export const worldName = 'test3'; export async function test(instance) { - const server = new HTTPServer(instance.incomingHandler); - let port = await getRandomPort(); - server.listen(port); - + let server; try { + server = new HTTPServer(instance.incomingHandler); + let port = await getRandomPort(); + server.listen(port); const resp = await fetch(`http://localhost:${port}`); const text = await resp.text(); strictEqual(text, 'Hello World'); } finally { - server.stop(); + if (server) { + server.stop(); + } } } diff --git a/test/cases/http-server/test.js b/test/cases/http-server/test.js index 3f81da8..6f2a1fb 100644 --- a/test/cases/http-server/test.js +++ b/test/cases/http-server/test.js @@ -8,15 +8,17 @@ export const enableFeatures = ['http']; export const worldName = 'test3'; export async function test(instance) { - const server = new HTTPServer(instance.incomingHandler); - let port = await getRandomPort(); - server.listen(port); - + let server; try { + server = new HTTPServer(instance.incomingHandler); + let port = await getRandomPort(); + server.listen(port); const resp = await fetch(`http://localhost:${port}`); const text = await resp.text(); strictEqual(text, 'Hello world!'); } finally { - server.stop(); + if (server) { + server.stop(); + } } } diff --git a/test/test.js b/test/test.js deleted file mode 100644 index 0bdc68d..0000000 --- a/test/test.js +++ /dev/null @@ -1,357 +0,0 @@ -import { readFile, readdir, mkdir, writeFile } from 'node:fs/promises'; -import { spawn } from 'node:child_process'; -import { fileURLToPath, URL } from 'node:url'; - -import { componentize } from '@bytecodealliance/componentize-js'; -import { transpile } from '@bytecodealliance/jco'; - -import { suite, test, assert } from 'vitest'; - -const DEBUG_TRACING = false; -const LOG_DEBUGGING = false; - -const enableAot = process.env.WEVAL_TEST == '1'; -const debugBuild = process.env.DEBUG_TEST == '1'; - -function maybeLogging(disableFeatures) { - if (!LOG_DEBUGGING) return disableFeatures; - if (disableFeatures && disableFeatures.includes('stdio')) { - disableFeatures.splice(disableFeatures.indexOf('stdio'), 1); - } - return disableFeatures; -} - -const builtinsCases = await readdir(new URL('./builtins', import.meta.url)); -suite('Builtins', () => { - for (const filename of builtinsCases) { - const name = filename.slice(0, -3); - test(name, async () => { - const { - source, - test: runTest, - disableFeatures, - enableFeatures, - } = await import(`./builtins/${name}.js`); - - const { component } = await componentize( - source, - ` - package local:runworld; - world runworld { - export run: func(); - } - `, - { - sourceName: `${name}.js`, - // also test the debug build while we are about it (unless testing Weval) - debugBuild, - enableFeatures, - disableFeatures: maybeLogging(disableFeatures), - enableAot, - }, - ); - - const { files } = await transpile(component, { - name, - wasiShim: true, - tracing: DEBUG_TRACING, - }); - - await mkdir(new URL(`./output/${name}/interfaces`, import.meta.url), { - recursive: true, - }); - - await writeFile( - new URL(`./output/${name}.component.wasm`, import.meta.url), - component, - ); - - for (const file of Object.keys(files)) { - await writeFile( - new URL(`./output/${name}/${file}`, import.meta.url), - files[file], - ); - } - - await writeFile( - new URL(`./output/${name}/run.js`, import.meta.url), - ` - import { run } from './${name}.js'; - run(); - `, - ); - - try { - await runTest(async function run() { - let stdout = '', - stderr = '', - timeout; - try { - await new Promise((resolve, reject) => { - const cp = spawn( - process.argv[0], - [ - fileURLToPath( - new URL(`./output/${name}/run.js`, import.meta.url), - ), - ], - { stdio: 'pipe' }, - ); - cp.stdout.on('data', (chunk) => { - stdout += chunk; - }); - cp.stderr.on('data', (chunk) => { - stderr += chunk; - }); - cp.on('error', reject); - cp.on('exit', (code) => - code === 0 ? resolve() : reject(new Error(stderr || stdout)), - ); - timeout = setTimeout(() => { - reject( - new Error( - 'test timed out with output:\n' + - stdout + - '\n\nstderr:\n' + - stderr, - ), - ); - }, 10_000); - }); - } catch (err) { - throw { err, stdout, stderr }; - } finally { - clearTimeout(timeout); - } - - return { stdout, stderr }; - }); - } catch (err) { - if (err.stderr) console.error(err.stderr); - throw err.err || err; - } - }); - } -}); - -const bindingsCases = await readdir(new URL('./cases', import.meta.url)); -suite('Bindings', () => { - for (const name of bindingsCases) { - test(name, async () => { - const source = await readFile( - new URL(`./cases/${name}/source.js`, import.meta.url), - 'utf8', - ); - - const test = await import(`./cases/${name}/test.js`); - - // Determine the relevant WIT world to use - let witWorld, - witPath, - worldName, - isWasiTarget = false; - if (test.worldName) { - witPath = fileURLToPath(new URL('./wit', import.meta.url)); - worldName = test.worldName; - isWasiTarget = true; - } else { - try { - witWorld = await readFile( - new URL(`./cases/${name}/world.wit`, import.meta.url), - 'utf8', - ); - } catch (e) { - if (e?.code == 'ENOENT') { - try { - isWasiTarget = true; - witPath = fileURLToPath( - new URL(`./cases/${name}/wit`, import.meta.url), - ); - await readdir(witPath); - } catch (e) { - if (e?.code === 'ENOENT') { - witPath = fileURLToPath(new URL('./wit', import.meta.url)); - worldName = 'test2'; - } else { - throw e; - } - } - } else { - throw e; - } - } - } - - const enableFeatures = test.enableFeatures || ['http']; - const disableFeatures = - test.disableFeatures || - (isWasiTarget ? [] : ['random', 'clocks', 'http', 'stdio']); - - let testArg; - try { - const { component, imports } = await componentize(source, { - sourceName: `${name}.js`, - witWorld, - witPath, - worldName, - enableFeatures, - disableFeatures: maybeLogging(disableFeatures), - enableAot, - debugBuild, - }); - const map = { - 'wasi:cli-base/*': '@bytecodealliance/preview2-shim/cli-base#*', - 'wasi:clocks/*': '@bytecodealliance/preview2-shim/clocks#*', - 'wasi:filesystem/*': '@bytecodealliance/preview2-shim/filesystem#*', - 'wasi:http/*': '@bytecodealliance/preview2-shim/http#*', - 'wasi:io/*': '@bytecodealliance/preview2-shim/io#*', - 'wasi:logging/*': '@bytecodealliance/preview2-shim/logging#*', - 'wasi:poll/*': '@bytecodealliance/preview2-shim/poll#*', - 'wasi:random/*': '@bytecodealliance/preview2-shim/random#*', - 'wasi:sockets/*': '@bytecodealliance/preview2-shim/sockets#*', - }; - for (let [impt] of imports) { - if (impt.startsWith('wasi:')) continue; - if (impt.startsWith('[')) impt = impt.slice(impt.indexOf(']') + 1); - let importName = impt.split('/').pop(); - if (importName === 'test') importName = 'imports'; - map[impt] = `../../cases/${name}/${importName}.js`; - } - - const { - files, - imports: componentImports, - exports: componentExports, - } = await transpile(component, { - name, - map, - wasiShim: true, - validLiftingOptimization: false, - tracing: DEBUG_TRACING, - }); - - testArg = { imports, componentImports, componentExports }; - - await mkdir(new URL(`./output/${name}/interfaces`, import.meta.url), { - recursive: true, - }); - - await writeFile( - new URL(`./output/${name}.component.wasm`, import.meta.url), - component, - ); - - for (const file of Object.keys(files)) { - let source = files[file]; - await writeFile( - new URL(`./output/${name}/${file}`, import.meta.url), - source, - ); - } - - const instancePath = fileURLToPath( - new URL(`./output/${name}/${name}.js`, import.meta.url), - ); - var instance = await import(instancePath); - } catch (e) { - if (test.err) { - test.err(e); - return; - } - throw e; - } - await test.test(instance, testArg); - }); - } -}); - -suite('WASI', () => { - test('basic app (old API)', async () => { - const { component } = await componentize( - ` - import { now } from 'wasi:clocks/wall-clock@0.2.3'; - import { getRandomBytes } from 'wasi:random/random@0.2.3'; - - let result; - export const run = { - run () { - result = \`NOW: \${now().seconds}, RANDOM: \${getRandomBytes(2n)}\`; - return { tag: 'ok' }; - } - }; - - export const getResult = () => result; - `, - { - witPath: fileURLToPath(new URL('./wit', import.meta.url)), - worldName: 'test1', - enableAot, - debugBuild, - }, - ); - - await writeFile( - new URL(`./output/wasi.component.wasm`, import.meta.url), - component, - ); - - const { files } = await transpile(component, { tracing: DEBUG_TRACING }); - - await mkdir(new URL(`./output/wasi/interfaces`, import.meta.url), { - recursive: true, - }); - - for (const file of Object.keys(files)) { - await writeFile( - new URL(`./output/wasi/${file}`, import.meta.url), - files[file], - ); - } - - var instance = await import(`./output/wasi/component.js`); - instance.run.run(); - const result = instance.getResult(); - assert.strictEqual( - result.slice(0, 10), - `NOW: ${String(Date.now()).slice(0, 5)}`, - ); - assert.strictEqual(result.split(',').length, 3); - }); - - test('basic app (OriginalSourceFile API)', async () => { - const { component } = await componentize({ - sourcePath: './test/api/index.js', - witPath: fileURLToPath(new URL('./wit', import.meta.url)), - worldName: 'test1', - enableAot, - debugBuild, - }); - - await writeFile( - new URL(`./output/wasi.component.wasm`, import.meta.url), - component, - ); - - const { files } = await transpile(component, { tracing: DEBUG_TRACING }); - - await mkdir(new URL(`./output/wasi/interfaces`, import.meta.url), { - recursive: true, - }); - - for (const file of Object.keys(files)) { - await writeFile( - new URL(`./output/wasi/${file}`, import.meta.url), - files[file], - ); - } - - var instance = await import(`./output/wasi/component.js`); - instance.run.run(); - const result = instance.getResult(); - assert.strictEqual( - result.slice(0, 10), - `NOW: ${String(Date.now()).slice(0, 5)}`, - ); - assert.strictEqual(result.split(',').length, 3); - }); -}); diff --git a/test/util.js b/test/util.js index 366c309..162e317 100644 --- a/test/util.js +++ b/test/util.js @@ -1,5 +1,17 @@ +import { env } from 'node:process'; import { createServer } from 'node:net'; +export const DEBUG_TRACING_ENABLED = isEnabledEnvVar(env.DEBUG_TRACING); +export const LOG_DEBUGGING_ENABLED = isEnabledEnvVar(env.LOG_DEBUGGING); +export const WEVAL_TEST_ENABLED = isEnabledEnvVar(env.WEVAL_TEST); +export const DEBUG_TEST_ENABLED = isEnabledEnvVar(env.DEBUG_TEST); + +function isEnabledEnvVar(v) { + return ( + typeof v === 'string' && ['1', 'yes', 'true'].includes(v.toLowerCase()) + ); +} + // Utility function for getting a random port export async function getRandomPort() { return await new Promise((resolve) => { @@ -11,3 +23,11 @@ export async function getRandomPort() { }); }); } + +export function maybeLogging(disableFeatures) { + if (!LOG_DEBUGGING_ENABLED) return disableFeatures; + if (disableFeatures && disableFeatures.includes('stdio')) { + disableFeatures.splice(disableFeatures.indexOf('stdio'), 1); + } + return disableFeatures; +} diff --git a/test/vitest.ts b/test/vitest.ts index fb8769c..5c86885 100644 --- a/test/vitest.ts +++ b/test/vitest.ts @@ -1,17 +1,52 @@ import { defineConfig } from 'vitest/config'; -const DEFAULT_TIMEOUT_MS = 120_000; +/** + * Under high concurrency, tests (in partticular bindings generation) can take + * much longer than expected. + * + * This issues primarily happen in CI and not locally on a sufficiently powerful machine. + */ +const TIMEOUT_MS = process.env.CI ? 240_000 : 120_000; + +/** + * On relatively modest machines in CI when weval is enabled, + * tests that use it (i.e. any test that does a build w/ enableAot) can + * suffer from CPU resource contention. + */ +const MAX_CONCURRENT_SUITES = + process.env.CI && process.env.WEVAL_TEST ? 1 : undefined; const REPORTERS = process.env.GITHUB_ACTIONS ? ['verbose', 'github-actions'] : ['verbose']; +/** + * + * Retry is set because there are issues that can randomly happen under high test concurrency: + * - file systems issues (weval[.exe] is busy) + * - performance under concurrency issues (see `builtins.performance.js` test) + * + * These issues primarily happen in CI and not locally, on a sufficiently powerful machine. + */ +const RETRY = process.env.CI ? 3 : 0; + export default defineConfig({ test: { reporters: REPORTERS, disableConsoleIntercept: true, + retry: RETRY, printConsoleTrace: true, passWithNoTests: false, + /** + * We use only one concurrent suite because tools like Weval + * are very sensitive to other operations, and during tests + * we will blow through timeouts and performance + * + * Inside individual suites, test.concurrent will still enable tests + * to be run side-by-side (see TIMEOUT_MS and RETRY which enable + * those tests to eventually pass despite increased load). + */ + maxConcurrentSuites: MAX_CONCURRENT_SUITES, include: ['test/**/*.js'], setupFiles: ['test/meta-resolve-stub.ts'], exclude: [ @@ -21,8 +56,8 @@ export default defineConfig({ 'test/output/*', 'test/util.js', ], - testTimeout: DEFAULT_TIMEOUT_MS, - hookTimeout: DEFAULT_TIMEOUT_MS, - teardownTimeout: DEFAULT_TIMEOUT_MS, + testTimeout: TIMEOUT_MS, + hookTimeout: TIMEOUT_MS, + teardownTimeout: TIMEOUT_MS, }, }); diff --git a/test/wasi.js b/test/wasi.js new file mode 100644 index 0000000..375f565 --- /dev/null +++ b/test/wasi.js @@ -0,0 +1,129 @@ +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import { spawn } from 'node:child_process'; +import { fileURLToPath, URL } from 'node:url'; +import { readFile, readdir, mkdir, writeFile, mkdtemp } from 'node:fs/promises'; + +import { componentize } from '@bytecodealliance/componentize-js'; +import { transpile } from '@bytecodealliance/jco'; + +import { suite, test, assert } from 'vitest'; + +import { + DEBUG_TRACING_ENABLED, + WEVAL_TEST_ENABLED, + DEBUG_TEST_ENABLED, +} from './util.js'; + +suite('WASI', () => { + test('basic app (old API)', async () => { + const { instance } = await setupComponent({ + componentize: { + src: ` + import { now } from 'wasi:clocks/wall-clock@0.2.3'; + import { getRandomBytes } from 'wasi:random/random@0.2.3'; + + let result; + export const run = { + run () { + result = \`NOW: \${now().seconds}, RANDOM: \${getRandomBytes(2n)}\`; + return { tag: 'ok' }; + } + }; + + export const getResult = () => result; + `, + opts: { + witPath: fileURLToPath(new URL('./wit', import.meta.url)), + worldName: 'test1', + enableAot: WEVAL_TEST_ENABLED, + debugBuild: DEBUG_TEST_ENABLED, + }, + }, + transpile: { + opts: { + tracing: DEBUG_TRACING_ENABLED, + }, + }, + }); + + instance.run.run(); + + const result = instance.getResult(); + + assert.strictEqual( + result.slice(0, 10), + `NOW: ${String(Date.now()).slice(0, 5)}`, + ); + assert.strictEqual(result.split(',').length, 3); + }); + + test('basic app (OriginalSourceFile API)', async () => { + const { instance } = await setupComponent({ + componentize: { + opts: { + sourcePath: './test/api/index.js', + witPath: fileURLToPath(new URL('./wit', import.meta.url)), + worldName: 'test1', + enableAot: WEVAL_TEST_ENABLED, + debugBuild: DEBUG_TEST_ENABLED, + }, + }, + transpile: { + opts: { + tracing: DEBUG_TRACING_ENABLED, + }, + }, + }); + + instance.run.run(); + + const result = instance.getResult(); + + assert.strictEqual( + result.slice(0, 10), + `NOW: ${String(Date.now()).slice(0, 5)}`, + ); + assert.strictEqual(result.split(',').length, 3); + }); +}); + +async function setupComponent(opts) { + const componentizeSrc = opts?.componentize?.src; + const componentizeOpts = opts?.componentize?.opts; + const transpileOpts = opts?.transpile?.opts; + + let component; + if (componentizeSrc) { + const srcBuild = await componentize(componentizeSrc, componentizeOpts); + component = srcBuild.component; + } else if (!componentizeSrc && componentizeOpts) { + const optsBuild = await componentize(componentizeOpts); + component = optsBuild.component; + } else { + throw new Error('no componentize options or src provided'); + } + + const outputDir = join('./out', 'wasi-test'); + await mkdir(outputDir, { recursive: true }); + + await writeFile(join(outputDir, 'wasi.component.wasm'), component); + + const { files } = await transpile(component, transpileOpts); + + const wasiDir = join(outputDir, 'wasi'); + const interfacesDir = join(wasiDir, 'interfaces'); + await mkdir(interfacesDir, { recursive: true }); + + for (const file of Object.keys(files)) { + await writeFile(join(wasiDir, file), files[file]); + } + + const componentJsPath = join(wasiDir, 'component.js'); + var instance = await import(componentJsPath); + + return { + instance, + outputDir, + }; +}