Skip to content

Commit

Permalink
Merge branch 'main' into update_package_json
Browse files Browse the repository at this point in the history
  • Loading branch information
bdemann committed Dec 13, 2024
2 parents a8f9a1d + fc3a16a commit 65a4477
Show file tree
Hide file tree
Showing 82 changed files with 5,659 additions and 27,211 deletions.
12 changes: 8 additions & 4 deletions .github/workflows/fuzz.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,12 @@ on:
call-delay:
description: 'Length of time (in seconds) to wait between canister method fuzz test calls'
required: false
type: number
default: .1
type: string
default: '.1'
timeout:
description: 'Length of time (in minutes) after which the fuzz tests will automatically succeed'
required: false
type: number
type: string
default: '300'

jobs:
Expand All @@ -29,7 +29,11 @@ jobs:
name: 'Property IC API',
directories: './tests/property/ic_api'
}
uses: ./.github/workflows/get_and_run_tests.yml
uses: ./.github/workflows/run_test.yml
with:
directories: ${{ matrix.test_group.directories }}
exclude-dirs: class_syntax/new
fuzz: true
call-delay: ${{ inputs.call-delay }}
timeout: ${{ inputs.timeout }}
link-azle: true
9 changes: 5 additions & 4 deletions .github/workflows/run_test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,12 @@ on:
default: false
call-delay:
required: false
type: number
default: .1
type: string
default: '.1'
timeout:
required: false
type: number
default: 300
type: string
default: '300'

jobs:
get-test-infos:
Expand Down Expand Up @@ -167,6 +167,7 @@ jobs:
exit $exit_code
fi
working-directory: ${{ matrix.test.path }}
shell: bash -l {0}
env:
AZLE_PROPTEST_NUM_RUNS: ${{ steps.calc-runs.outputs.runs }}
AZLE_PROPTEST_VERBOSE: true
Expand Down
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Binary file modified canister_templates/experimental.wasm
Binary file not shown.
Binary file modified canister_templates/stable.wasm
Binary file not shown.
28 changes: 25 additions & 3 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
"type": "module",
"bin": {
"azle": "./src/build/index.ts",
"cuzz": "./node_modules/.bin/cuzz",
"tsc": "./node_modules/.bin/tsc"
},
"main": "./src/lib/stable/index.ts",
Expand All @@ -36,6 +37,7 @@
"class-transformer": "^0.5.1",
"class-validator": "^0.14.1",
"crypto-browserify": "^3.12.0",
"cuzz": "github:demergent-labs/cuzz#70f95363c64674ec2cf19b9eccfc1a773c3bf28f",
"deep-is": "^0.1.4",
"esbuild": "^0.24.0",
"esbuild-plugin-tsc": "^0.4.0",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,3 @@
// TODO the plan is to integrate rquickjs for stable
// TODO and at that time create two crates
// TODO we should place each crate at src/build/stable/commands/compile/rust
// TODO and src/build/experimental/commands/compile/rust respectively

use std::cell::RefCell;

#[allow(unused)]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ ic-cdk-timers = "0.6.0"
ic-stable-structures = "0.6.5"
ic-wasi-polyfill = "0.6.1"
rquickjs = { version = "0.6.2", features = ["array-buffer"] }
scopeguard = "1.2.0"
serde = "1.0.202"
serde_json = "1.0.107"
slotmap = "=1.0.6"
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,24 @@ use rquickjs::{
Ctx, Exception, Function, IntoJs, Object, Result as QuickJsResult, TypedArray, Value,
};

use crate::{error::quickjs_call_with_error_handling, ic::throw_error, quickjs_with_ctx};
use crate::{error::quickjs_call_with_error_handling, ic::throw_error};

pub fn get_function(ctx: Ctx) -> QuickJsResult<Function> {
// We use a raw pointer here to deal with already borrowed issues
// using quickjs_with_ctx after a cross-canister call (await point)
// Sometimes the await point won't actually behave like a full await point
// because the cross-canister call returns an error where the callback
// is never queued and executed by the IC
// In this case the outer quickjs_with_ctx for the original call
// will still have borrowed something having to do with the ctx
// whereas in a full cross-canister call the outer quickjs_with_ctx
// will have completed.
// Using a raw pointer overcomes the issue, and I believe it is safe
// because we only ever have one Runtime, one Context, we never destroy them
// and we are in a single-threaded environment. We will of course
// engage in intense testing and review to ensure safety
let ctx_ptr = ctx.as_raw();

Function::new(
ctx.clone(),
move |promise_id: String,
Expand All @@ -33,13 +48,25 @@ pub fn get_function(ctx: Ctx) -> QuickJsResult<Function> {
.map_err(|e| throw_error(ctx.clone(), e))?;

spawn(async move {
let call_result = call_raw128(canister_id, &method, args_raw, payment).await;
let ctx = unsafe { Ctx::from_raw(ctx_ptr) };

// My understanding of how this works
// scopeguard will execute its closure at the end of the scope
// After a successful or unsuccessful cross-canister call (await point)
// the closure will run, cleaning up the global promise callbacks
// Even during a trap, the IC will ensure that the closure runs in its own call
// thus allowing us to recover from a trap and persist that state
let _cleanup = scopeguard::guard((), |_| {
let result = cleanup(ctx.clone(), &promise_id);

if let Err(e) = result {
trap(&format!("Azle CallRawCleanupError: {e}"));
}
});

let result = quickjs_with_ctx(|ctx| {
resolve_or_reject(ctx.clone(), &call_result, &promise_id)?;
let call_result = call_raw128(canister_id, &method, args_raw, payment).await;

Ok(())
});
let result = resolve_or_reject(ctx.clone(), &call_result, &promise_id);

if let Err(e) = result {
trap(&format!("Azle CallRawError: {e}"));
Expand All @@ -51,6 +78,21 @@ pub fn get_function(ctx: Ctx) -> QuickJsResult<Function> {
)
}

fn cleanup(ctx: Ctx, promise_id: &str) -> Result<(), Box<dyn Error>> {
let reject_id = format!("_reject_{}", promise_id);
let resolve_id = format!("_resolve_{}", promise_id);

let globals = ctx.clone().globals();

let reject_ids = globals.get::<_, Object>("_azleRejectIds")?;
let resolve_ids = globals.get::<_, Object>("_azleResolveIds")?;

reject_ids.remove(&reject_id)?;
resolve_ids.remove(&resolve_id)?;

Ok(())
}

fn resolve_or_reject<'a>(
ctx: Ctx<'a>,
call_result: &Result<Vec<u8>, (RejectionCode, String)>,
Expand Down
48 changes: 16 additions & 32 deletions src/lib/stable/ic_apis/call.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,6 @@ export async function call<Args extends any[] | undefined, Return = any>(
const returnTypeIdl = options?.returnIdlType;
const raw = options?.raw;

// TODO perhaps we should be more robust
// TODO for example, we can keep the time with these
// TODO if they are over a certain amount old we can delete them
globalThis._azleResolveIds[globalResolveId] = (
result: Uint8Array | ArrayBuffer
): void => {
Expand All @@ -46,16 +43,10 @@ export async function call<Args extends any[] | undefined, Return = any>(
idlDecode(idlType, new Uint8Array(result))[0] as Return
);
}

delete globalThis._azleResolveIds[globalResolveId];
delete globalThis._azleRejectIds[globalRejectId];
};

globalThis._azleRejectIds[globalRejectId] = (error: any): void => {
reject(error);

delete globalThis._azleResolveIds[globalResolveId];
delete globalThis._azleRejectIds[globalRejectId];
};

const paramIdlTypes = options?.paramIdlTypes ?? [];
Expand All @@ -71,29 +62,22 @@ export async function call<Args extends any[] | undefined, Return = any>(
raw === undefined ? idlEncode(paramIdlTypes, args) : raw;
const cyclesString = cycles.toString();

// TODO consider finally, what if deletion goes wrong
try {
if (globalThis._azleIcExperimental !== undefined) {
globalThis._azleIcExperimental.callRaw(
promiseId,
canisterIdBytes.buffer,
method,
argsRaw.buffer,
cyclesString
);
} else {
globalThis._azleIcStable.callRaw(
promiseId,
canisterIdBytes,
method,
argsRaw,
cyclesString
);
}
} catch (error) {
delete globalThis._azleResolveIds[globalResolveId];
delete globalThis._azleRejectIds[globalRejectId];
throw error;
if (globalThis._azleIcExperimental !== undefined) {
globalThis._azleIcExperimental.callRaw(
promiseId,
canisterIdBytes.buffer,
method,
argsRaw.buffer,
cyclesString
);
} else {
globalThis._azleIcStable.callRaw(
promiseId,
canisterIdBytes,
method,
argsRaw,
cyclesString
);
}
});
}
77 changes: 77 additions & 0 deletions test/fuzz.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import { spawn } from 'child_process';
import { CuzzConfig } from 'cuzz';
import { readFile } from 'fs-extra';
import { join } from 'path';

import { DfxJson } from '../src/build/stable/utils/types';

export async function runFuzzTests(): Promise<void> {
const dfxJson = await getDfxJson();
const cuzzConfig = await getCuzzConfig();
const canisterNames = getCanisterNames(dfxJson);
const callDelay = getCallDelay(cuzzConfig);

for (const canisterName of canisterNames) {
fuzzTestCanister(canisterName, callDelay);
}
}

async function getDfxJson(): Promise<DfxJson> {
const dfxFile = await readFile(join(process.cwd(), 'dfx.json'), 'utf-8');

return JSON.parse(dfxFile);
}

async function getCuzzConfig(): Promise<CuzzConfig> {
try {
const cuzzFile = await readFile(
join(process.cwd(), 'cuzz.json'),
'utf-8'
);

return JSON.parse(cuzzFile);
} catch {
return {};
}
}

function getCanisterNames(dfxJson: DfxJson): string[] {
if (dfxJson.canisters === undefined) {
throw new Error('No canisters found in dfx.json');
}

return Object.keys(dfxJson.canisters);
}

function getCallDelay(cuzzConfig: CuzzConfig): string {
return (
process.env.AZLE_FUZZ_CALL_DELAY ??
cuzzConfig.callDelay?.toString() ??
'.1'
);
}

function fuzzTestCanister(canisterName: string, callDelay: string): void {
const baseCuzzArgs = [
'--canister-name',
canisterName,
'--skip-deploy',
'--call-delay',
callDelay
];

const cuzzArgs =
process.env.AZLE_FUZZ_TERMINAL === 'true'
? [...baseCuzzArgs, '--terminal']
: baseCuzzArgs;

let cuzzProcess = spawn('node_modules/.bin/cuzz', cuzzArgs, {
stdio: 'inherit'
});

cuzzProcess.on('exit', (code) => {
if (code !== 0) {
process.exit(code ?? 1);
}
});
}
Loading

0 comments on commit 65a4477

Please sign in to comment.