Skip to content

Commit

Permalink
chore(loader): fix to support new metering formats + test cases
Browse files Browse the repository at this point in the history
  • Loading branch information
PeterFarber committed Oct 18, 2024
1 parent 4d846f7 commit 0d4a977
Show file tree
Hide file tree
Showing 19 changed files with 303 additions and 13 deletions.
10 changes: 8 additions & 2 deletions loader/src/index.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,9 @@ module.exports = async function (binary, options) {
if (typeof binary === "function") {
options.instantiateWasm = binary
} else {
binary = metering.meterWASM(binary, { meterType })
if (options.format === "wasm32-unknown-emscripten-metering" || options.format === "wasm64-unknown-emscripten-draft_2024_10_16-metering") {
binary = metering.meterWASM(binary, { meterType })
}
options.wasmBinary = binary
}

Expand Down Expand Up @@ -194,7 +196,11 @@ module.exports = async function (binary, options) {
if (instance.cleanupListeners) {
instance.cleanupListeners()
}
if (options.format !== "wasm64-unknown-emscripten-draft_2024_02_15" && options.format !== "wasm32-unknown-emscripten4") {

if (options.format !== "wasm64-unknown-emscripten-draft_2024_02_15" &&
options.format !== "wasm32-unknown-emscripten4" &&
options.format !== "wasm32-unknown-emscripten-metering" &&
options.format !== "wasm64-unknown-emscripten-draft_2024_10_16-metering") {
doHandle = instance.cwrap('handle', 'string', ['string', 'string'])
}

Expand Down
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
10 changes: 5 additions & 5 deletions loader/test/emscripten2.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ const MODULE_PATH = process.env.MODULE_PATH || '../src/index.cjs'
console.log(`${MODULE_PATH}`)

const { default: AoLoader } = await import(MODULE_PATH)
const wasmBinary = fs.readFileSync('./test/process/process.wasm')
const wasmBinary = fs.readFileSync('./test/emscripten2/process.wasm')

describe('loader', async () => {
it('load wasm and evaluate message', async () => {
Expand Down Expand Up @@ -53,10 +53,10 @@ describe('loader', async () => {
* that any Response will do
*/
new Response(
Readable.toWeb(createReadStream('./test/process/process.wasm')),
Readable.toWeb(createReadStream('./test/emscripten2/process.wasm')),
{ headers: { 'Content-Type': 'application/wasm' } }
),
{ format: 'wasm32-unknown-emscripten2', applyMetering: false }
{ format: 'wasm32-unknown-emscripten2' }
)

const handle = await AoLoader((info, receiveInstance) => {
Expand Down Expand Up @@ -197,14 +197,14 @@ describe('loader', async () => {
{ Process: { Id: '1', Tags: [] } }
)

assert.equal(result.Output, '1970-01-01')
assert.ok(result.Output === '1970-01-01' || result.Output === '1969-12-31')

const result2 = await handle(null,
{ Owner: 'tom', Target: '1', Tags: [{ name: 'Action', value: 'Date' }], Data: '' },
{ Process: { Id: '1', Tags: [] } }
)

assert.equal(result2.Output, '1970-01-01')
assert.ok(result2.Output === '1970-01-01' || result2.Output === '1969-12-31')

// console.log(result.GasUsed)
assert.ok(true)
Expand Down
File renamed without changes.
File renamed without changes.
File renamed without changes.
Binary file added loader/test/emscripten4/process.wasm
Binary file not shown.
10 changes: 5 additions & 5 deletions loader/test/index.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ const MODULE_PATH = process.env.MODULE_PATH || '../src/index.cjs'
console.log(`${MODULE_PATH}`)

const { default: AoLoader } = await import(MODULE_PATH)
const wasmBinary = fs.readFileSync('./test/legacy/process.wasm')
const wasmBinary = fs.readFileSync('./test/emscripten/process.wasm')

describe('loader', async () => {
it('load wasm and evaluate message', async () => {
Expand Down Expand Up @@ -53,10 +53,10 @@ describe('loader', async () => {
* that any Response will do
*/
new Response(
Readable.toWeb(createReadStream('./test/legacy/process.wasm')),
Readable.toWeb(createReadStream('./test/emscripten/process.wasm')),
{ headers: { 'Content-Type': 'application/wasm' } }
),
{ format: 'wasm32-unknown-emscripten', applyMetering: false }
{ format: 'wasm32-unknown-emscripten' }
)

const handle = await AoLoader((info, receiveInstance) => {
Expand Down Expand Up @@ -145,14 +145,14 @@ describe('loader', async () => {
{ Process: { Id: '1', Tags: [] } }
)

assert.equal(result.Output, '1970-01-01')
assert.ok(result.Output === '1970-01-01' || result.Output === '1969-12-31')

const result2 = await handle(null,
{ Owner: 'tom', Target: '1', Tags: [{ name: 'Action', value: 'Date' }], Data: '' },
{ Process: { Id: '1', Tags: [] } }
)

assert.equal(result2.Output, '1970-01-01')
assert.ok(result2.Output === '1970-01-01' || result2.Output === '1969-12-31')

// console.log(result.GasUsed)
assert.ok(true)
Expand Down
149 changes: 149 additions & 0 deletions loader/test/loader.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
/* eslint-disable no-prototype-builtins */

import { describe, it } from 'node:test'
import * as assert from 'node:assert'
import fs from 'fs'
import { Readable } from 'node:stream'
import { createReadStream } from 'node:fs'

/**
* dynamic import, so we can run unit tests against the source
* and integration tests against the bundled distribution
*/
const MODULE_PATH = process.env.MODULE_PATH || '../src/index.cjs'

console.log(`${MODULE_PATH}`)

const { default: AoLoader } = await import(MODULE_PATH)

/**
* Test cases for both 32-bit and 64-bit versions.
* Each test case includes the binary path and options specific to that architecture.
*/
const testCases = [
{
name: 'Emscripten4 (32-bit)', // Test for the 32-bit WASM
binaryPath: './test/emscripten4/process.wasm', // Path to the 32-bit WASM binary
options: { format: 'wasm32-unknown-emscripten4' } // Format for 32-bit
},
{
name: 'Wasm64-Emscripten (64-bit)', // Test for the 64-bit WASM
binaryPath: './test/wasm64-emscripten/process.wasm', // Path to the 64-bit WASM binary
options: { format: 'wasm64-unknown-emscripten-draft_2024_02_15' } // Format for 64-bit
},
{
name: 'Emscripten4 Metering (32-bit)', // Test for the 32-bit WASM
binaryPath: './test/emscripten4/process.wasm', // Path to the 32-bit WASM binary
options: { format: 'wasm32-unknown-emscripten-metering' } // Format for 32-bit metering
},
{
name: 'Wasm64-Emscripten Metering (64-bit)', // Test for the 64-bit WASM
binaryPath: './test/wasm64-emscripten/process.wasm', // Path to the 64-bit WASM binary
options: { format: 'wasm64-unknown-emscripten-draft_2024_10_16-metering' } // Format for 64-bit metering
}
]

/* Helper function to generate test messages */
function getMsg (Data, Action = 'Eval') {
return {
Target: '1',
From: 'FOOBAR',
Owner: 'FOOBAR',
Module: 'FOO',
Id: '1',
'Block-Height': '1000',
Timestamp: Date.now(),
Tags: [{ name: 'Action', value: Action }],
Data
}
}

/* Helper function to generate test environment variables */
function getEnv () {
return {
Process: {
Id: '1',
Owner: 'FOOBAR',
Tags: [{ name: 'Name', value: 'TEST_PROCESS_OWNER' }]
}
}
}

describe('AoLoader Functionality Tests', () => {
// Iterate over each test case (32-bit and 64-bit)
for (const testCase of testCases) {
const { name, binaryPath, options } = testCase
const wasmBinary = fs.readFileSync(binaryPath) // Load the WASM binary

describe(`${name}`, () => {
it('load wasm and evaluate message', async () => {
const handle = await AoLoader(wasmBinary, options)
const mainResult = await handle(null, getMsg('return \'Hello World\''), getEnv())

// Check basic properties of the result
assert.ok(mainResult.Memory)
assert.ok(mainResult.hasOwnProperty('Messages'))
assert.ok(mainResult.hasOwnProperty('Spawns'))
assert.ok(mainResult.hasOwnProperty('Error'))
assert.equal(mainResult.Output.data, 'Hello World') // Expect 'Hello World' in output
})

it('should use separately instantiated WebAssembly.Instance', async () => {
// Compile and instantiate a separate WebAssembly instance
const wasmModuleP = WebAssembly.compileStreaming(
new Response(
Readable.toWeb(createReadStream(binaryPath)),
{ headers: { 'Content-Type': 'application/wasm' } }
),
options
)

// Set up a loader and check the separate instance
const handle = await AoLoader((info, receiveInstance) => {
assert.ok(info)
assert.ok(receiveInstance)
wasmModuleP
.then((mod) => WebAssembly.instantiate(mod, info))
.then((instance) => receiveInstance(instance))
}, options)

// Evaluate the message and verify the result
const result = await handle(null, getMsg('count = 1\nreturn count'), getEnv())
assert.equal(result.Output.data, 1)

const result2 = await handle(result.Memory, getMsg('count = count + 1\nreturn count'), getEnv())
assert.equal(result2.Output.data, 2)
})

/* TODO: Add tests to make sure the loader:
1. ) Creates the correct instance based on the format
2. ) Creates a /data directory using FS_createPath
3. ) Correctly determines the doHandle function based on the format
4. ) If a buffer is passed, it is used as the memory and resizes the HEAP
5. ) Checks to make sure Memory, Messages, Spawns, GasUsed, Assignments, Output, and Error are returned
*/

// TODO: This test should not be part of loader tests
it('should get deterministic date', async () => {
const handle = await AoLoader(wasmBinary, options)

// Verify that the date returned is deterministic
const result = await handle(null, getMsg('return os.date("%Y-%m-%d")'), getEnv())
assert.ok(result.Output.data === '1970-01-01' || result.Output.data === '1969-12-31')
})

// TODO: This test should not be part of loader tests
it('should get deterministic random numbers', async () => {
let handle = await AoLoader(wasmBinary, options)

// Verify deterministic random number generation
const result = await handle(null, getMsg('return math.random(1, 10)'), getEnv())
assert.equal(result.Output.data, 4)

handle = await AoLoader(wasmBinary, options)
const result2 = await handle(null, getMsg('return math.random(1, 10)'), getEnv())
assert.equal(result2.Output.data, 4)
})
})
}
})
135 changes: 135 additions & 0 deletions loader/test/metering.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
/* eslint-disable no-prototype-builtins */

import { describe, it } from 'node:test'
import * as assert from 'node:assert'
import fs from 'fs'

/**
* dynamic import, so we can run unit tests against the source
* and integration tests against the bundled distribution
*/
const MODULE_PATH = process.env.MODULE_PATH || '../src/index.cjs'

console.log(`${MODULE_PATH}`)

const { default: AoLoader } = await import(MODULE_PATH)

/**
* Test cases for both 32-bit and 64-bit versions.
* Each test case includes the binary path and options specific to that architecture.
*/
const testCases = [
{
name: 'Emscripten4 Metering (32-bit)', // Test for the 32-bit WASM
binaryPath: './test/emscripten4/process.wasm', // Path to the 32-bit WASM binary
options: { format: 'wasm32-unknown-emscripten-metering' }, // Format for 32-bit metering
standardOptions: { format: 'wasm32-unknown-emscripten4' } // Format for 32-bit standard
},
{
name: 'Wasm64-Emscripten Metering (64-bit)', // Test for the 64-bit WASM
binaryPath: './test/wasm64-emscripten/process.wasm', // Path to the 64-bit WASM binary
options: { format: 'wasm64-unknown-emscripten-draft_2024_10_16-metering' }, // Format for 64-bit metering
standardOptions: { format: 'wasm64-unknown-emscripten-draft_2024_02_15' } // Format for 64-bit standard
}
]

/* Helper function to generate test messages */
function getMsg (Data, Action = 'Eval') {
return {
Target: '1',
From: 'FOOBAR',
Owner: 'FOOBAR',
Module: 'FOO',
Id: '1',
'Block-Height': '1000',
Timestamp: Date.now(),
Tags: [{ name: 'Action', value: Action }],
Data
}
}

/* Helper function to generate test environment variables */
function getEnv () {
return {
Process: {
Id: '1',
Owner: 'FOOBAR',
Tags: [{ name: 'Name', value: 'TEST_PROCESS_OWNER' }]
}
}
}

describe('AoLoader Metering Tests', () => {
// Iterate over each test case (32-bit and 64-bit)
for (const testCase of testCases) {
const { name, binaryPath, options, standardOptions } = testCase
const wasmBinary = fs.readFileSync(binaryPath) // Load the WASM binary

describe(`${name}`, () => {
it('should use gas for computation', async () => {
const handle = await AoLoader(wasmBinary, options)

const initial = await handle(null, getMsg('return "OK"'), getEnv())

// Test that gas is used for computation
const result = await handle(initial.Memory, getMsg('return 1+1'), getEnv())
assert.ok(result.GasUsed > 0)

const result2 = await handle(initial.Memory, getMsg('return 1+1+1'), getEnv())
assert.ok(result2.GasUsed > result.GasUsed)
})

it('should load previous memory and refill gas', async () => {
const handle = await AoLoader(wasmBinary, options)

// Test loading previous memory and ensuring gas is refilled on each invocation
// The first invocation always uses alot more gas than subsequent invocations due to setup
const initial = await handle(null, getMsg('count = 1\nreturn count'), getEnv())
assert.equal(initial.Output.data, 1)
assert.ok(initial.GasUsed > 0)

// Thats why we need to run the function twice to get a more accurate gas usage comparison
const result = await handle(initial.Memory, getMsg('count = count + 1\nreturn count'), getEnv())
assert.equal(result.Output.data, 2)
assert.ok(result.GasUsed > 0)

const result2 = await handle(result.Memory, getMsg('count = count + 1\nreturn count'), getEnv())
assert.equal(result2.Output.data, 3)
assert.ok(result2.GasUsed > 0)

// Check that gas usage difference is within an acceptable range
const gasDiff = Math.abs(result.GasUsed - result2.GasUsed)
assert.ok(gasDiff < 900000)
})

it('should run out of gas', async () => {
// Test that the function will run out of gas when exceeding the compute limit
const handle = await AoLoader(wasmBinary, { format: options.format, computeLimit: 10_750_000_000 })
try {
await handle(null, getMsg(`
count = 0
for i = 1, 1000 do
count = count + 1
end
return count
`), getEnv())
assert.ok(false) // Should not reach here
} catch (e) {
assert.equal(e.message, 'out of gas!') // Expect an out-of-gas error
}
})

it('metering should only apply to metering formats', async () => {
// Verify that metering only applies to specific formats
const handle = await AoLoader(wasmBinary, standardOptions)
const result = await handle(null, getMsg('return \'Hello World\''), getEnv())
assert.equal(result.GasUsed, 0) // No gas usage in non-metered format
})

/* TODO: Some other possible tests for metering:
1. ) Confirm that WebAssembly.compile is being overridden
2. ) Confirm that WebAssembly.compileStreaming is being overridden
*/
})
}
})
2 changes: 1 addition & 1 deletion loader/test/sqlite.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ const msg = (cmd) => ({
})

const { default: AoLoader } = await import(MODULE_PATH)
const wasm = fs.readFileSync('./test/sqlite/process.wasm')
const wasm = fs.readFileSync('./test/emscripten2_sqlite/process.wasm')

test('create sqlite db, run insert & select', async () => {
const handle = await AoLoader(wasm, { format: 'wasm32-unknown-emscripten2' })
Expand Down
Binary file added loader/test/wasm64-emscripten/process.wasm
Binary file not shown.

0 comments on commit 0d4a977

Please sign in to comment.