Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Cycles prop tests #2178

Merged
merged 15 commits into from
Nov 13, 2024
4 changes: 4 additions & 0 deletions tests/property/ic_api/cycles/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
.azle
.dfx
dfx_generated
node_modules
23 changes: 23 additions & 0 deletions tests/property/ic_api/cycles/dfx.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"canisters": {
"cycles": {
"type": "azle",
"main": "src/cycles/index.ts",
"declarations": {
"output": "test/dfx_generated/cycles",
"node_compatibility": true
}
},
"intermediary": {
"type": "azle",
"main": "src/intermediary/index.ts",
"declarations": {
"output": "test/dfx_generated/intermediary",
"node_compatibility": true
},
"custom": {
"env": ["CYCLES_PRINCIPAL"]
}
}
}
}
11 changes: 11 additions & 0 deletions tests/property/ic_api/cycles/jest.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
/** @type {import('ts-jest').JestConfigWithTsJest} */
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
testTimeout: 100_000_000,
transform: {
'^.+\\.ts$': ['ts-jest', { isolatedModules: true }],
'^.+\\.js$': 'ts-jest'
},
transformIgnorePatterns: ['/node_modules/(?!(azle)/)'] // Make sure azle is transformed
};
6,020 changes: 6,020 additions & 0 deletions tests/property/ic_api/cycles/package-lock.json

Large diffs are not rendered by default.

16 changes: 16 additions & 0 deletions tests/property/ic_api/cycles/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"scripts": {
"pretest": "tsx test/pretest.ts",
"test": "jest"
},
"dependencies": {
"azle": "0.24.1"
},
"devDependencies": {
"@dfinity/agent": "^0.19.2",
"jest": "^29.7.0",
"ts-jest": "^29.1.4",
"tsx": "^4.15.7",
"typescript": "^5.2.2"
}
}
65 changes: 65 additions & 0 deletions tests/property/ic_api/cycles/src/cycles/index.ts
lastmjs marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import {
canisterBalance,
IDL,
msgCyclesAccept,
msgCyclesAvailable,
reply,
update
} from 'azle';

import { CyclesResult, CyclesResultIDL } from '../types';

export default class {
@update([], CyclesResultIDL)
receiveAllCycles(): CyclesResult {
return acceptCycles((available) => available);
}

@update([], CyclesResultIDL)
receiveHalfCycles(): CyclesResult {
lastmjs marked this conversation as resolved.
Show resolved Hide resolved
return acceptCycles((available) => available / 2n);
}

@update([], CyclesResultIDL)
receiveNoCycles(): CyclesResult {
return acceptCycles(() => 0n);
}

// TODO we could have fun playing with the constraints of msgCyclesAccept
lastmjs marked this conversation as resolved.
Show resolved Hide resolved
// TODO like making sure it doesn't trap, or that it can be called multiple times
// TODO or what happens if it accepts more than is available, or it it tries to accept more than the max_amount

@update([], IDL.Nat64, { manual: true })
lastmjs marked this conversation as resolved.
Show resolved Hide resolved
receiveCyclesManual(): void {
const msgCyclesAvailableResult = msgCyclesAvailable();
reply({ data: msgCyclesAvailableResult, idlType: IDL.Nat64 });
// const remainingCycles = msgCyclesAvailable();
// if (remainingCycles !== 0n) {
// // TODO what will happen if we throw after we already replied?
// // TODO we probably don't need to tests this as part of the property tests
// // TODO but if we do should we do this same test but with reject?
// throw new Error(
// `msgCyclesAvailable() !== 0. Actual value: ${remainingCycles}. Initial msgCyclesAvailable result: ${msgCyclesAvailableResult}`
// );
// }
}
}

function acceptCycles(
lastmjs marked this conversation as resolved.
Show resolved Hide resolved
lastmjs marked this conversation as resolved.
Show resolved Hide resolved
getAcceptAmount: (available: bigint) => bigint
): CyclesResult {
const startingCanisterBalance = canisterBalance();
const initialAvailable = msgCyclesAvailable();
const accepted = msgCyclesAccept(getAcceptAmount(initialAvailable));
const finalAvailable = msgCyclesAvailable();
const endingCanisterBalance = canisterBalance();
const cyclesRefunded = 0n; // This will always be 0 in the cycles canister
return {
initialAvailable,
accepted,
finalAvailable,
startingCanisterBalance,
endingCanisterBalance,
cyclesRefunded
};
}
52 changes: 52 additions & 0 deletions tests/property/ic_api/cycles/src/intermediary/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { call, IDL, msgCyclesRefunded, trap, update } from 'azle';

import { CyclesResult, CyclesResultIDL } from '../types';

export default class {
@update([IDL.Nat64], CyclesResultIDL)
async sendAllCycles(amount: bigint): Promise<CyclesResult> {
const result = await call(getCyclesPrincipal(), 'receiveAllCycles', {
returnIdlType: CyclesResultIDL,
cycles: amount
});
return { ...result, cyclesRefunded: msgCyclesRefunded() };
}

@update([IDL.Nat64], CyclesResultIDL)
async sendHalfCycles(amount: bigint): Promise<CyclesResult> {
const result = await call(getCyclesPrincipal(), 'receiveHalfCycles', {
returnIdlType: CyclesResultIDL,
cycles: amount
});
return { ...result, cyclesRefunded: msgCyclesRefunded() };
}

@update([IDL.Nat64], CyclesResultIDL)
async sendNoCycles(amount: bigint): Promise<CyclesResult> {
const result = await call(getCyclesPrincipal(), 'receiveNoCycles', {
returnIdlType: CyclesResultIDL,
cycles: amount
});
return { ...result, cyclesRefunded: msgCyclesRefunded() };
}

@update([IDL.Nat64], IDL.Nat64)
async sendCyclesManual(amount: bigint): Promise<bigint> {
lastmjs marked this conversation as resolved.
Show resolved Hide resolved
console.log(
'sendCyclesManual And its for sure this unquie one from 1:17',
amount
);
const result = await call(getCyclesPrincipal(), 'receiveCyclesManual', {
returnIdlType: IDL.Nat64,
cycles: amount
});
return result;
}
}

function getCyclesPrincipal(): string {
lastmjs marked this conversation as resolved.
Show resolved Hide resolved
return (
process.env.CYCLES_PRINCIPAL ??
trap('process.env.CYCLES_PRINCIPAL is undefined')
lastmjs marked this conversation as resolved.
Show resolved Hide resolved
);
}
19 changes: 19 additions & 0 deletions tests/property/ic_api/cycles/src/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { IDL } from 'azle';

export type CyclesResult = {
initialAvailable: bigint;
accepted: bigint;
finalAvailable: bigint;
startingCanisterBalance: bigint;
endingCanisterBalance: bigint;
cyclesRefunded: bigint;
};

export const CyclesResultIDL = IDL.Record({
lastmjs marked this conversation as resolved.
Show resolved Hide resolved
initialAvailable: IDL.Nat64,
accepted: IDL.Nat64,
finalAvailable: IDL.Nat64,
startingCanisterBalance: IDL.Nat64,
endingCanisterBalance: IDL.Nat64,
cyclesRefunded: IDL.Nat64
});
35 changes: 35 additions & 0 deletions tests/property/ic_api/cycles/test/pretest.ts
lastmjs marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { getCanisterId } from 'azle/dfx';
import { linkAndInstallPatch } from 'azle/test/jest_link';
import { execSync } from 'child_process';
import { join } from 'path';

function pretest(): void {
linkAndInstallPatch(
lastmjs marked this conversation as resolved.
Show resolved Hide resolved
join('tests', 'end_to_end', 'candid_rpc', 'functional_syntax', 'cycles')
);

execSync(`dfx canister uninstall-code cycles || true`, {
stdio: 'inherit'
});

execSync(`dfx canister uninstall-code intermediary || true`, {
stdio: 'inherit'
});

execSync(`dfx deploy cycles`, {
stdio: 'inherit'
});

execSync(
`CYCLES_PRINCIPAL=${getCanisterId('cycles')} dfx deploy intermediary`,
{
stdio: 'inherit'
}
);

execSync(`dfx generate`, {
stdio: 'inherit'
});
}

pretest();
13 changes: 13 additions & 0 deletions tests/property/ic_api/cycles/test/test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { getCanisterId } from 'azle/dfx';
import { runTests } from 'azle/test';

import { createActor } from './dfx_generated/intermediary';
import { getTests } from './tests';

const intermediaryCanister = createActor(getCanisterId('intermediary'), {
lastmjs marked this conversation as resolved.
Show resolved Hide resolved
agentOptions: {
host: 'http://127.0.0.1:8000'
}
});

runTests(getTests(intermediaryCanister));
83 changes: 83 additions & 0 deletions tests/property/ic_api/cycles/test/tests.ts
lastmjs marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import { ActorSubclass } from '@dfinity/agent';
import { defaultPropTestParams, expect, it, Test } from 'azle/test';
import fc from 'fast-check';

import { CyclesResult } from '../src/types';
// @ts-ignore this path may not exist when these tests are imported into other test projects
lastmjs marked this conversation as resolved.
Show resolved Hide resolved
import { _SERVICE } from './dfx_generated/intermediary/intermediary.did';
lastmjs marked this conversation as resolved.
Show resolved Hide resolved

export function getTests(intermediaryCanister: ActorSubclass<_SERVICE>): Test {
return () => {
it('should handle sendAllCycles correctly', async () => {
await fc.assert(
fc.asyncProperty(fc.bigInt(1n, 10_000_000n), async (amount) => {
const result =
await intermediaryCanister.sendAllCycles(amount);
validateCyclesResult(result, amount, 'all');
}),
defaultPropTestParams
);
});

it('should handle sendHalfCycles correctly', async () => {
await fc.assert(
fc.asyncProperty(fc.bigInt(1n, 10_000_000n), async (amount) => {
const result =
await intermediaryCanister.sendHalfCycles(amount);
validateCyclesResult(result, amount, 'half');
}),
defaultPropTestParams
);
});

it('should handle sendNoCycles correctly', async () => {
await fc.assert(
fc.asyncProperty(fc.bigInt(1n, 10_000_000n), async (amount) => {
const result =
await intermediaryCanister.sendNoCycles(amount);
validateCyclesResult(result, amount, 'none');
}),
defaultPropTestParams
);
});

it('should handle sendCyclesManual correctly', async () => {
lastmjs marked this conversation as resolved.
Show resolved Hide resolved
await fc.assert(
fc.asyncProperty(fc.bigInt(1n, 10_000_000n), async (amount) => {
const result =
await intermediaryCanister.sendCyclesManual(amount);
expect(typeof result).toBe('bigint');
expect(result).toBeGreaterThanOrEqual(0n);
expect(result).toBeLessThanOrEqual(amount);
}),
defaultPropTestParams
);
});
};
}

function validateCyclesResult(
result: CyclesResult,
amount: bigint,
mode: 'all' | 'half' | 'none'
): void {
expect(result.initialAvailable).toBe(amount);

if (mode === 'all') {
expect(result.accepted).toBe(result.initialAvailable);
}
if (mode === 'half') {
expect(result.accepted).toBe(result.initialAvailable / 2n);
}
if (mode === 'none') {
expect(result.accepted).toBe(0n);
}

expect(result.finalAvailable).toBe(
result.initialAvailable - result.accepted
);
expect(result.cyclesRefunded).toBe(result.finalAvailable);
expect(result.endingCanisterBalance - result.startingCanisterBalance).toBe(
result.accepted
);
}
10 changes: 10 additions & 0 deletions tests/property/ic_api/cycles/tsconfig.json
lastmjs marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"compilerOptions": {
"strict": true,
"target": "ES2020",
"module": "ES2020",
"moduleResolution": "node",
"allowJs": true,
"outDir": "HACK_BECAUSE_OF_ALLOW_JS"
}
}