From 4b853d6f90c57c01af482287785ab37903986499 Mon Sep 17 00:00:00 2001 From: deucalioncodes Date: Sat, 7 Mar 2026 21:03:38 +0100 Subject: [PATCH 1/2] Remove transient feature from ic-wasi-polyfill for persistent filesystem (#18) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Remove transient feature from ic-wasi-polyfill in Cargo.toml so WASI filesystem is backed by IC stable memory instead of heap memory - Add write_file/read_file canister methods to filesystem example - Add persistence CI test: write file → upgrade canister → read file → assert content survived [rebuild] --- .../cpython_canister_template/Cargo.toml | 2 +- examples/filesystem/src/main.py | 24 +++++++++ examples/filesystem/test/tests.ts | 49 +++++++++++++++++++ 3 files changed, 74 insertions(+), 1 deletion(-) diff --git a/basilisk/compiler/cpython_canister_template/Cargo.toml b/basilisk/compiler/cpython_canister_template/Cargo.toml index d8b63be8..88a8290e 100644 --- a/basilisk/compiler/cpython_canister_template/Cargo.toml +++ b/basilisk/compiler/cpython_canister_template/Cargo.toml @@ -28,7 +28,7 @@ serde_json = "1.0" async-recursion = "1.0.0" ic-stable-structures = "0.5.2" slotmap = "1.0.6" -ic-wasi-polyfill = { version = "0.6.1", features = ["transient"] } +ic-wasi-polyfill = { version = "0.6.1" } [patch.crates-io] num-bigint = { git = "https://github.com/rust-num/num-bigint" } diff --git a/examples/filesystem/src/main.py b/examples/filesystem/src/main.py index 52d396ac..15e8dd9c 100644 --- a/examples/filesystem/src/main.py +++ b/examples/filesystem/src/main.py @@ -125,3 +125,27 @@ def test_fs_nested_mkdir() -> Vec[str]: results.append(str(os.path.exists("/test_nested/a/b/c"))) results.append(str(os.path.isdir("/test_nested/a/b/c"))) return results + + +@update +def write_file(path: str, content: str) -> str: + """Write content to a file. Returns 'OK' on success or error message.""" + try: + parent = os.path.dirname(path) + if parent and parent != "/": + os.makedirs(parent, exist_ok=True) + with open(path, "w") as f: + f.write(content) + return "OK" + except Exception as e: + return f"ERROR: {e}" + + +@query +def read_file(path: str) -> str: + """Read content from a file. Returns file content or error message.""" + try: + with open(path, "r") as f: + return f.read() + except Exception as e: + return f"ERROR: {e}" diff --git a/examples/filesystem/test/tests.ts b/examples/filesystem/test/tests.ts index bd5ca087..ed5461f3 100644 --- a/examples/filesystem/test/tests.ts +++ b/examples/filesystem/test/tests.ts @@ -1,6 +1,7 @@ import { Test } from 'azle/test'; import { _SERVICE } from './dfx_generated/filesystem/filesystem.did'; import { ActorSubclass } from '@dfinity/agent'; +import { execSync } from 'child_process'; export function getTests(actor: ActorSubclass<_SERVICE>): Test[] { return [ @@ -76,6 +77,54 @@ export function getTests(actor: ActorSubclass<_SERVICE>): Test[] { Ok: result.every((v: string) => v === 'True') }; } + }, + { + name: 'test_fs_persistence: write file before upgrade', + test: async () => { + const result = await actor.write_file( + '/persist_test/hello.txt', + 'Hello from before upgrade!' + ); + console.log('write_file result:', result); + return { + Ok: result === 'OK' + }; + } + }, + { + name: 'test_fs_persistence: verify file readable before upgrade', + test: async () => { + const result = await actor.read_file('/persist_test/hello.txt'); + console.log('read_file before upgrade:', result); + return { + Ok: result === 'Hello from before upgrade!' + }; + } + }, + { + name: 'test_fs_persistence: upgrade canister', + test: async () => { + try { + execSync( + `dfx deploy filesystem --upgrade-unchanged`, + { stdio: 'inherit' } + ); + return { Ok: true }; + } catch (e) { + console.error('upgrade failed:', e); + return { Ok: false }; + } + } + }, + { + name: 'test_fs_persistence: read file after upgrade', + test: async () => { + const result = await actor.read_file('/persist_test/hello.txt'); + console.log('read_file after upgrade:', result); + return { + Ok: result === 'Hello from before upgrade!' + }; + } } ]; } From eef335515697ea260e04b6f940aa7bddc247733e Mon Sep 17 00:00:00 2001 From: deucalioncodes Date: Sat, 7 Mar 2026 21:26:21 +0100 Subject: [PATCH 2/2] Update filesystem test to use HttpAgent with fetchRootKey; simplify CI test command --- .github/workflows/test.yml | 2 +- .../dfx_generated/filesystem/filesystem.did | 8 +++ .../filesystem/filesystem.did.d.ts | 14 ++++++ .../filesystem/filesystem.did.js | 11 ++++ .../test/dfx_generated/filesystem/index.d.ts | 50 +++++++++++++++++++ .../test/dfx_generated/filesystem/index.js | 40 +++++++++++++++ examples/filesystem/test/test.ts | 18 ++++--- 7 files changed, 136 insertions(+), 7 deletions(-) create mode 100644 examples/filesystem/test/dfx_generated/filesystem/filesystem.did create mode 100644 examples/filesystem/test/dfx_generated/filesystem/filesystem.did.d.ts create mode 100644 examples/filesystem/test/dfx_generated/filesystem/filesystem.did.js create mode 100644 examples/filesystem/test/dfx_generated/filesystem/index.d.ts create mode 100644 examples/filesystem/test/dfx_generated/filesystem/index.js diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index d79deda2..c311056c 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -422,7 +422,7 @@ jobs: timeout-minutes: 35 run: | source ../venv/bin/activate - BASILISK_REBUILD=true npx ts-node --transpile-only --ignore=false test.ts 2>&1 || { + npm test 2>&1 || { echo "::error::Test failed for ${{ matrix.example_directories }}" exit 1 } diff --git a/examples/filesystem/test/dfx_generated/filesystem/filesystem.did b/examples/filesystem/test/dfx_generated/filesystem/filesystem.did new file mode 100644 index 00000000..8f53676b --- /dev/null +++ b/examples/filesystem/test/dfx_generated/filesystem/filesystem.did @@ -0,0 +1,8 @@ +service : { + "test_fs_diagnostics" : () -> (vec text); + "test_fs_mkdir" : () -> (vec text); + "test_fs_path_exists" : () -> (vec text); + "test_fs_rename" : () -> (vec text); + "test_fs_rmdir" : () -> (vec text); + "test_fs_nested_mkdir" : () -> (vec text); +} diff --git a/examples/filesystem/test/dfx_generated/filesystem/filesystem.did.d.ts b/examples/filesystem/test/dfx_generated/filesystem/filesystem.did.d.ts new file mode 100644 index 00000000..ccab9b8e --- /dev/null +++ b/examples/filesystem/test/dfx_generated/filesystem/filesystem.did.d.ts @@ -0,0 +1,14 @@ +import type { Principal } from '@dfinity/principal'; +import type { ActorMethod } from '@dfinity/agent'; +import type { IDL } from '@dfinity/candid'; + +export interface _SERVICE { + 'test_fs_diagnostics' : ActorMethod<[], Array>, + 'test_fs_mkdir' : ActorMethod<[], Array>, + 'test_fs_nested_mkdir' : ActorMethod<[], Array>, + 'test_fs_path_exists' : ActorMethod<[], Array>, + 'test_fs_rename' : ActorMethod<[], Array>, + 'test_fs_rmdir' : ActorMethod<[], Array>, +} +export declare const idlFactory: IDL.InterfaceFactory; +export declare const init: (args: { IDL: typeof IDL }) => IDL.Type[]; diff --git a/examples/filesystem/test/dfx_generated/filesystem/filesystem.did.js b/examples/filesystem/test/dfx_generated/filesystem/filesystem.did.js new file mode 100644 index 00000000..28b5c4cd --- /dev/null +++ b/examples/filesystem/test/dfx_generated/filesystem/filesystem.did.js @@ -0,0 +1,11 @@ +export const idlFactory = ({ IDL }) => { + return IDL.Service({ + 'test_fs_diagnostics' : IDL.Func([], [IDL.Vec(IDL.Text)], []), + 'test_fs_mkdir' : IDL.Func([], [IDL.Vec(IDL.Text)], []), + 'test_fs_nested_mkdir' : IDL.Func([], [IDL.Vec(IDL.Text)], []), + 'test_fs_path_exists' : IDL.Func([], [IDL.Vec(IDL.Text)], []), + 'test_fs_rename' : IDL.Func([], [IDL.Vec(IDL.Text)], []), + 'test_fs_rmdir' : IDL.Func([], [IDL.Vec(IDL.Text)], []), + }); +}; +export const init = ({ IDL }) => { return []; }; diff --git a/examples/filesystem/test/dfx_generated/filesystem/index.d.ts b/examples/filesystem/test/dfx_generated/filesystem/index.d.ts new file mode 100644 index 00000000..501bad49 --- /dev/null +++ b/examples/filesystem/test/dfx_generated/filesystem/index.d.ts @@ -0,0 +1,50 @@ +import type { + ActorSubclass, + HttpAgentOptions, + ActorConfig, + Agent, +} from "@dfinity/agent"; +import type { Principal } from "@dfinity/principal"; +import type { IDL } from "@dfinity/candid"; + +import { _SERVICE } from './filesystem.did'; + +export declare const idlFactory: IDL.InterfaceFactory; +export declare const canisterId: string; + +export declare interface CreateActorOptions { + /** + * @see {@link Agent} + */ + agent?: Agent; + /** + * @see {@link HttpAgentOptions} + */ + agentOptions?: HttpAgentOptions; + /** + * @see {@link ActorConfig} + */ + actorOptions?: ActorConfig; +} + +/** + * Intializes an {@link ActorSubclass}, configured with the provided SERVICE interface of a canister. + * @constructs {@link ActorSubClass} + * @param {string | Principal} canisterId - ID of the canister the {@link Actor} will talk to + * @param {CreateActorOptions} options - see {@link CreateActorOptions} + * @param {CreateActorOptions["agent"]} options.agent - a pre-configured agent you'd like to use. Supercedes agentOptions + * @param {CreateActorOptions["agentOptions"]} options.agentOptions - options to set up a new agent + * @see {@link HttpAgentOptions} + * @param {CreateActorOptions["actorOptions"]} options.actorOptions - options for the Actor + * @see {@link ActorConfig} + */ +export declare const createActor: ( + canisterId: string | Principal, + options?: CreateActorOptions +) => ActorSubclass<_SERVICE>; + +/** + * Intialized Actor using default settings, ready to talk to a canister using its candid interface + * @constructs {@link ActorSubClass} + */ +export declare const filesystem: ActorSubclass<_SERVICE>; diff --git a/examples/filesystem/test/dfx_generated/filesystem/index.js b/examples/filesystem/test/dfx_generated/filesystem/index.js new file mode 100644 index 00000000..fecd0b2a --- /dev/null +++ b/examples/filesystem/test/dfx_generated/filesystem/index.js @@ -0,0 +1,40 @@ +import { Actor, HttpAgent } from "@dfinity/agent"; + +// Imports and re-exports candid interface +import { idlFactory } from "./filesystem.did.js"; +export { idlFactory } from "./filesystem.did.js"; + +/* CANISTER_ID is replaced by webpack based on node environment + * Note: canister environment variable will be standardized as + * process.env.CANISTER_ID_ + * beginning in dfx 0.15.0 + */ +export const canisterId = + process.env.CANISTER_ID_FILESYSTEM; + +export const createActor = (canisterId, options = {}) => { + const agent = options.agent || new HttpAgent({ ...options.agentOptions }); + + if (options.agent && options.agentOptions) { + console.warn( + "Detected both agent and agentOptions passed to createActor. Ignoring agentOptions and proceeding with the provided agent." + ); + } + + // Fetch root key for certificate validation during development + if (process.env.DFX_NETWORK !== "ic") { + agent.fetchRootKey().catch((err) => { + console.warn( + "Unable to fetch root key. Check to ensure that your local replica is running" + ); + console.error(err); + }); + } + + // Creates an actor with using the candid interface and the HttpAgent + return Actor.createActor(idlFactory, { + agent, + canisterId, + ...options.actorOptions, + }); +}; diff --git a/examples/filesystem/test/test.ts b/examples/filesystem/test/test.ts index a3c3352d..8a04589a 100644 --- a/examples/filesystem/test/test.ts +++ b/examples/filesystem/test/test.ts @@ -1,11 +1,17 @@ import { getCanisterId, runTests } from 'azle/test'; import { createActor } from './dfx_generated/filesystem'; import { getTests } from './tests'; +import { HttpAgent } from '@dfinity/agent'; -const filesystemCanister = createActor(getCanisterId('filesystem'), { - agentOptions: { - host: 'http://127.0.0.1:8000' - } -}); +async function main() { + const agent = new HttpAgent({ host: 'http://127.0.0.1:8000' }); + await agent.fetchRootKey(); -runTests(getTests(filesystemCanister)); + const filesystemCanister = createActor(getCanisterId('filesystem'), { + agent + }); + + runTests(getTests(filesystemCanister)); +} + +main();