Iām happy to announce PicJS, a canister testing library with TypeScript/JavaScript support for PocketIC!
PicJS allows you to write integration tests for your canisters using your favorite JavaScript test runners and runtimes. If youād like to try it out, you can:
PocketIC is a canister testing platform that supports deterministic, programmatic canister testing. If you want to learn more about it, you can check out the previous posts from the Testing & Verification team that have put together this wonderful tool:
Writing tests with PicJS is as easy as this:
import { resolve } from 'node:path';
import { Principal } from '@dfinity/principal';
import { PocketIc } from '@hadronous/pic';
import { Actor, idlFactory, _SERVICE } from '../declarations/counter';
const wasmPath = resolve('..', '..', 'counter.wasm');
describe('Counter', () => {
let pic: PocketIc;
let actor: Actor<_SERVICE>;
beforeEach(async () => {
pic = await PocketIc.create();
const fixture = await pic.setupCanister<_SERVICE>(
idlFactory,
WASM_PATH,
);
actor = fixture.actor;
});
afterEach(async () => {
await pic.tearDown();
});
it('should increment the counter', async () => {
const initialCount = await actor.get();
await actor.inc();
const countAfterFirstInc = await actor.get();
await actor.inc();
const finalCount = await actor.get();
expect(initialCount).toEqual(0n);
expect(countAfterFirstInc).toEqual(1n);
expect(finalCount).toEqual(2n);
});
});
Iām happy to hear any feedback or answer any questions that you might have.
Disclaimer: I work for DFINITY, but this is not a DFINITY project. Iām posting in my capacity as a part-time community member.
24 Likes
great work! i think this is worth adding to awesome icp (maybe together with pocket ic)
4 Likes
Thank you!
I agree, expect a PR tomorrow
3 Likes
Version 0.3.0
of PicJS has been released.
The main changes include:
- Support for PocketIC server version
3.0.0
- Support for multiple subnets
- Support for cross-subnet calls
- Added
updateCanisterSettings
to allow updating canister settings after creation (controllers etcā¦)
tick
now accepts an optional number
to tick multiple times in a single call
- many public methods accept a single object instead of a list of parameters (ex:
installCode
)
- Added a multicanister Motoko example with composite query calls and cross subnet update calls
I look forward to seeing how developers continue to improve the quality of their testing infrastructure using PocketIC
8 Likes
I seem to have some trouble getting and keeping pocket ic running. I start it from the command line:
/Users/afat/.cache/dfinity/pocket-ic/pocket-ic
everything looks good:
2024-04-02T00:31:24.975295Z INFO pocket_ic_server: The PocketIC server is listening on port 60626
but after a bit I get:
2024-04-02T00:32:25.087578Z INFO pocket_ic_server: The PocketIC server will terminate
How do I keep it running? Do I need to specify a port to PicJS anywhere?
Iām getting an error on create canister that it canāt decode some text value:
Canister ryjl3-tyaaa-aaaaa-aaaba-cai trapped explicitly: failed to decode call arguments: Custom(Fail to decode argument 0
Caused by:
Subtyping error: text)
64 |
65 |
> 66 | nnsledger = await pic.setupCanister<_NNSLedgerService>({
| ^
67 | wasm : "packages/pic/wasms/ic-icrc1-ledger.wasm",
68 | idlFactory : nnsIdlFactory,
69 | targetCanisterId: Principal.fromText("ryjl3-tyaaa-aaaaa-aaaba-cai"),
I may be doing something horribly wrong because Iām using the nns-ledger wasm I found in my /Users/afat/.cache/dfinity/versions/0.18.0/wasms folder and a did file I pulled from someone elseās project. Iāve found it all super hard to debug so far.
1 Like
When you start PocketIC manually, pass a time-to-live (the default is 60s):
./pocket-ic --ttl 600
Whenever PocketIC receives an HTTP request, it bumps its TTL.
wasm : āpackages/pic/wasms/ic-icrc1-ledger.wasmā,
I donāt know the JS API, but is it possible that you should pass wasm bytes as the first argument, rather than the path of the wasm file?
PicJS includes and runs the PocketIC server for you, you donāt need to run it manually. Thats a distinction from the Rust and Python versions.
So the server youāre running manually isnāt being used and thatās why it just terminates.
The issue with the argument is harder to tell. Does this canister expect any init args? The path to your WASM looks relative, maybe try an absolute path.
That is super convenient! Nice!
The wasm is being pulled in correctly.
The failure is happening on the server ise after the post. It doesnāt like the args even though they were successfully parsed with the IDL. Iām guessing there some discrepancy between the IDL that I have and the wasm delivered by dfx extension install nns.
I guess generally it would be awesome to have some way to have PocketIC install all the NNS canisters similar to dfx nns install.
I guess generally it would be awesome to have some way to have PocketIC install all the NNS canisters similar to dfx nns install.
This will be possible in the next release of PocketIC, because tools like ic-admin will work out of the box. @mraszyk there is no workaround in the current version, right?
Itās possible, but a slight pain to setup. I wrote a guide on how to do that here: Working with the NNS | PicJS
Oh wowā¦ok. So I need to pull the subnet ID because it will be different every time. Do we know who it will give the ICP tokens to? I guess Iāll need that identity in PicJs. What would be the best way to load it? So far Iāve only used the createIdentity function but Iām guessing Iāll need to load the test_nns pem file to access those ICP?
I also got these errors as I was running dfx nns install:
2024-04-02 14:07:37.033346 UTC: [Canister rkp4c-7iaaa-aaaaa-aaaca-cai] Panicked at 'Deserialization Failed: "Cannot parse header 286f7074207265636f7264207b206379636c65735f6c65646765725f63616e69737465725f6964203d206f7074207072696e636970616c2022756d3569772d72716161612d61616161712d71616162612d63616922207d29"', rs/rust_canisters/dfn_core/src/endpoint.rs:49:41
2024-04-02 14:07:37.097886 UTC: [Canister r7inp-6aaaa-aaaaa-aaabq-cai] [Root Canister] start_canister call successful. Ok(())
2024-04-02 14:07:37.097886 UTC: [Canister r7inp-6aaaa-aaaaa-aaabq-cai] Panicked at 'called `Result::unwrap()` on an `Err` value: (CanisterError, "Canister rkp4c-7iaaa-aaaaa-aaaca-cai trapped explicitly: Panicked at 'Deserialization Failed: \"Cannot parse header 286f7074207265636f7264207b206379636c65735f6c65646765725f63616e69737465725f6964203d206f7074207072696e636970616c2022756d3569772d72716161612d61616161712d71616162612d63616922207d29\"', rs/rust_canisters/dfn_core/src/endpoint.rs:49:41")', rs/nervous_system/root/src/change_canister.rs:246:9
The donāt look innocent.
The subnet Id assignment is deterministic in PocketIc, Iām not sure about DFX. The main point is that you need to know what subnet Id was assigned by DFX so you can use the same one in PocketIc.
There are some pre-assigned accounts that have some ICP that you can use, thatās documented in the DFX docs: sdk/docs/cli-reference/dfx-nns.mdx at master Ā· dfinity/sdk Ā· GitHub. And the example project referenced by the guide I posted above has some example usage of transferring ICP, setting up a neuron, staking and then creating proposals.
This will be possible in the next release of PocketIC, because tools like ic-admin will work out of the box. @mraszyk there is no workaround in the current version, right?
Thereās no easy workaround in the current version.
The most relevant code for getting control of an Identity that has ICP already loaded is here: pic-js/examples/nns_proxy/tests/src/support/identity.ts at main Ā· hadronous/pic-js Ā· GitHub
1 Like
Awesomeā¦I found all the minter identity stuff and am up and running now. Sorry for not RTFM. :(. Thank you for writing it up!
1 Like
What is the likely hood of future dfx versions braking this state?
Iāve no idea what changes may occur in the state folder, so Iād definitely encourage making a dedicated copy for your tests and keeping it separate from the .dfx directory so that thereās no unexpected mutations by DFX.
1 Like
Three years of debugging motoko with debug statments have left me relying on the cruch of using them when things go sideways and I canāt track down what is going wrong.
Can pic give me the equivalent of the dfx output that has these logs in it?
Edit:
I was able to get these in my console by editing pocket-ic-server.js:
serverProcess.on('error', error => {
if ((0, util_1.isArm)() && (0, util_1.isDarwin)()) {
throw new error_1.BinStartMacOSArmError(error);
}
throw new error_1.BinStartError(error);
});
serverProcess.stdout.on('data', data => {
//console.log(`stdout server: ${data}`);
});
serverProcess.stderr.on('data', data => {
stderrBuffer += data.toString();
// Split based on newline characters
let lines = stderrBuffer.split('\n');
// Keep the last incomplete line in the buffer
stderrBuffer = lines.pop();
// Process complete lines
lines.forEach((line) => {
console.error(`debug: ${line}`);
});
});
serverProcess.stderr.on('close', () => {
if (stderrBuffer.length > 0) {
console.error(`debug final: ${stderrBuffer}`);
stderrBuffer = ''; // Clear the buffer
}
});
return await (0, util_1.poll)(async () => {
const isPocketIcReady = await (0, util_1.exists)(readyFilePath);
if (isPocketIcReady) {
const portString = await (0, util_1.readFileAsString)(portFilePath);
const port = parseInt(portString);
return new PocketIcServer(serverProcess, port);
}
throw new error_1.BinTimeoutError();
});
}
My process isnāt stoping properly so Iāve likely messed something up, but I can see my debug statements now!