diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 4fff681887..1c0b33fa0e 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -135,6 +135,7 @@ jobs: "property_tests/tests/bool", "property_tests/tests/canister_methods/init", "property_tests/tests/canister_methods/post_upgrade", + "property_tests/tests/canister_methods/pre_upgrade", "property_tests/tests/canister_methods/query", "property_tests/tests/canister_methods/update", "property_tests/tests/float32", diff --git a/property_tests/arbitraries/canister_arb.ts b/property_tests/arbitraries/canister_arb.ts index bf6c0487f0..613a98e4fe 100644 --- a/property_tests/arbitraries/canister_arb.ts +++ b/property_tests/arbitraries/canister_arb.ts @@ -5,6 +5,7 @@ import { CorrespondingJSType } from './candid/corresponding_js_type'; import { InitMethod } from './canister_methods/init_method_arb'; import { PostUpgradeMethod } from './canister_methods/post_upgrade_arb'; import { UpdateMethod } from './canister_methods/update_method_arb'; +import { PreUpgradeMethod } from './canister_methods/pre_upgrade_method_arb'; export type Canister = { deployArgs: string[] | undefined; @@ -19,7 +20,8 @@ export type CanisterMethod< | QueryMethod | UpdateMethod | InitMethod - | PostUpgradeMethod; + | PostUpgradeMethod + | PreUpgradeMethod; export type CanisterConfig< ParamAgentArgumentValue extends CorrespondingJSType = undefined, @@ -31,6 +33,7 @@ export type CanisterConfig< ParamAgentArgumentValue, ParamAgentResponseValue >; + preUpgradeMethod?: PreUpgradeMethod; queryMethods?: QueryMethod[]; updateMethods?: UpdateMethod[]; }; @@ -51,6 +54,7 @@ export function CanisterArb< >[] = [ ...(config.initMethod ? [config.initMethod] : []), ...(config.postUpgradeMethod ? [config.postUpgradeMethod] : []), + ...(config.preUpgradeMethod ? [config.preUpgradeMethod] : []), ...(config.queryMethods ?? []), ...(config.updateMethods ?? []) ]; diff --git a/property_tests/arbitraries/canister_methods/index.ts b/property_tests/arbitraries/canister_methods/index.ts index b3d11ab16a..f923a77703 100644 --- a/property_tests/arbitraries/canister_methods/index.ts +++ b/property_tests/arbitraries/canister_methods/index.ts @@ -1,3 +1,5 @@ +import fc from 'fast-check'; + import { CandidValueAndMeta } from '../candid/candid_value_and_meta_arb'; import { CandidReturnType } from '../candid/candid_return_type_arb'; import { CorrespondingJSType } from '../candid/corresponding_js_type'; @@ -5,8 +7,8 @@ import { Named } from '../..'; import { Test } from '../../../test'; export type BodyGenerator< - ParamAgentArgumentValue extends CorrespondingJSType, - ParamAgentResponseValue, + ParamAgentArgumentValue extends CorrespondingJSType = undefined, + ParamAgentResponseValue = undefined, ReturnTypeAgentArgumentValue extends CorrespondingJSType = undefined, ReturnTypeAgentResponseValue = undefined > = ( @@ -20,8 +22,8 @@ export type BodyGenerator< ) => string; export type TestsGenerator< - ParamAgentArgumentValue extends CorrespondingJSType, - ParamAgentResponseValue, + ParamAgentArgumentValue extends CorrespondingJSType = undefined, + ParamAgentResponseValue = undefined, ReturnTypeAgentArgumentValue extends CorrespondingJSType = undefined, ReturnTypeAgentResponseValue = undefined > = ( @@ -37,6 +39,11 @@ export type TestsGenerator< export type CallbackLocation = 'INLINE' | 'STANDALONE'; +export const CallbackLocationArb = fc.constantFrom( + 'INLINE', + 'STANDALONE' +); + export function isDefined(value: T | undefined): value is T { return value !== undefined; } diff --git a/property_tests/arbitraries/canister_methods/init_method_arb.ts b/property_tests/arbitraries/canister_methods/init_method_arb.ts index 102e81daf3..cdf6c70279 100644 --- a/property_tests/arbitraries/canister_methods/init_method_arb.ts +++ b/property_tests/arbitraries/canister_methods/init_method_arb.ts @@ -9,7 +9,8 @@ import { TestsGenerator, CallbackLocation, isDefined, - generateCallback + generateCallback, + CallbackLocationArb } from '.'; import { Test } from '../../../test'; import { VoidArb } from '../candid/primitive/void'; @@ -51,7 +52,7 @@ export function InitMethodArb< UniqueIdentifierArb('canisterMethod'), paramTypeArrayArb, VoidArb(), - fc.constantFrom('INLINE', 'STANDALONE'), + CallbackLocationArb, UniqueIdentifierArb('typeDeclaration') // TODO: This unique id would be better named globalScope or something // But needs to match the same scope as typeDeclarations so I'm using diff --git a/property_tests/arbitraries/canister_methods/pre_upgrade_method_arb.ts b/property_tests/arbitraries/canister_methods/pre_upgrade_method_arb.ts new file mode 100644 index 0000000000..e3705577d3 --- /dev/null +++ b/property_tests/arbitraries/canister_methods/pre_upgrade_method_arb.ts @@ -0,0 +1,77 @@ +import fc from 'fast-check'; + +import { UniqueIdentifierArb } from '../unique_identifier_arb'; +import { + BodyGenerator, + TestsGenerator, + CallbackLocation, + generateCallback, + CallbackLocationArb +} from '.'; +import { Test } from '../../../test'; +import { VoidArb } from '../candid/primitive/void'; + +export type PreUpgradeMethod = { + imports: Set; + globalDeclarations: string[]; + sourceCode: string; + tests: Test[][]; +}; + +export function PreUpgradeMethodArb(constraints: { + generateBody: BodyGenerator; + generateTests: TestsGenerator; + callbackLocation?: CallbackLocation; +}) { + return fc + .tuple( + UniqueIdentifierArb('canisterMethod'), + VoidArb(), + CallbackLocationArb, + UniqueIdentifierArb('typeDeclaration') + // TODO: This unique id would be better named globalScope or something + // But needs to match the same scope as typeDeclarations so I'm using + // that for now. + ) + .map( + ([ + functionName, + returnType, + defaultCallbackLocation, + callbackName + ]): PreUpgradeMethod => { + const callbackLocation = + constraints.callbackLocation ?? defaultCallbackLocation; + + const imports = new Set(['preUpgrade']); + + const callback = generateCallback( + [], + returnType, + constraints.generateBody, + callbackLocation, + callbackName + ); + + const globalDeclarations = + callbackLocation === 'STANDALONE' ? [callback] : []; + + const sourceCode = `${functionName}: preUpgrade(${ + callbackLocation === 'STANDALONE' ? callbackName : callback + })`; + + const tests = constraints.generateTests( + functionName, + [], + returnType + ); + + return { + imports, + globalDeclarations, + sourceCode, + tests + }; + } + ); +} diff --git a/property_tests/arbitraries/canister_methods/query_method_arb.ts b/property_tests/arbitraries/canister_methods/query_method_arb.ts index caf2ccd7da..a7595f04b2 100644 --- a/property_tests/arbitraries/canister_methods/query_method_arb.ts +++ b/property_tests/arbitraries/canister_methods/query_method_arb.ts @@ -10,7 +10,8 @@ import { TestsGenerator, CallbackLocation, isDefined, - generateCallback + generateCallback, + CallbackLocationArb } from '.'; import { Test } from '../../../test'; @@ -57,7 +58,7 @@ export function QueryMethodArb< UniqueIdentifierArb('canisterMethod'), paramTypeArrayArb, returnTypeArb, - fc.constantFrom('INLINE', 'STANDALONE'), + CallbackLocationArb, UniqueIdentifierArb('typeDeclaration') // TODO: This unique id would be better named globalScope or something // But needs to match the same scope as typeDeclarations so I'm using diff --git a/property_tests/tests/canister_methods/pre_upgrade/test/test.ts b/property_tests/tests/canister_methods/pre_upgrade/test/test.ts new file mode 100644 index 0000000000..6a2c8912d7 --- /dev/null +++ b/property_tests/tests/canister_methods/pre_upgrade/test/test.ts @@ -0,0 +1,116 @@ +import fc from 'fast-check'; + +import { deepEqual, getActor, runPropTests } from 'azle/property_tests'; +import { CandidValueAndMetaArbWithoutFuncs as CandidValueAndMetaArb } from 'azle/property_tests/arbitraries/candid/candid_value_and_meta_arb'; +import { CandidReturnTypeArb } from 'azle/property_tests/arbitraries/candid/candid_return_type_arb'; +import { CorrespondingJSType } from 'azle/property_tests/arbitraries/candid/corresponding_js_type'; +import { + CanisterArb, + CanisterConfig +} from 'azle/property_tests/arbitraries/canister_arb'; +import { UpdateMethodArb } from 'azle/property_tests/arbitraries/canister_methods/update_method_arb'; +import { + QueryMethod, + QueryMethodArb +} from 'azle/property_tests/arbitraries/canister_methods/query_method_arb'; +import { PreUpgradeMethodArb } from 'azle/property_tests/arbitraries/canister_methods/pre_upgrade_method_arb'; + +const SimplePreUpgradeArb = PreUpgradeMethodArb({ + generateBody: () => + /*TS*/ `stable.insert(PRE_UPGRADE_HOOK_EXECUTED, true);`, + generateTests: () => [] +}); + +const HeterogeneousQueryMethodArb = QueryMethodArb( + fc.array(CandidValueAndMetaArb()), + CandidReturnTypeArb(), + { + generateBody: (_, returnType) => + `return ${returnType.src.valueLiteral}`, + generateTests: () => [] + } +); + +const HeterogeneousUpdateMethodArb = UpdateMethodArb( + fc.array(CandidValueAndMetaArb()), + CandidReturnTypeArb(), + { + generateBody: (_, returnType) => + `return ${returnType.src.valueLiteral}`, + generateTests: () => [] + } +); + +const small = { + minLength: 0, + maxLength: 20 +}; + +const CanisterConfigArb = fc + .tuple( + SimplePreUpgradeArb, + fc + .array(HeterogeneousQueryMethodArb, small) + .chain((queryMethods) => + fc.constant([ + generateGetPreUpgradeExecutedCanisterMethod(), + ...queryMethods + ]) + ), + fc.array(HeterogeneousUpdateMethodArb, small) + ) + + .map( + ([preUpgradeMethod, queryMethods, updateMethods]): CanisterConfig< + CorrespondingJSType, + CorrespondingJSType + > => { + const globalDeclarations = [ + /*TS*/ `const PRE_UPGRADE_HOOK_EXECUTED = 'PRE_UPGRADE_HOOK_EXECUTED';`, + /*TS*/ `let stable = StableBTreeMap(0);` + ]; + + return { + globalDeclarations, + preUpgradeMethod, + queryMethods, + updateMethods + }; + } + ); + +runPropTests(CanisterArb(CanisterConfigArb)); + +function generateGetPreUpgradeExecutedCanisterMethod(): QueryMethod { + return { + imports: new Set(['bool', 'query', 'StableBTreeMap']), + globalDeclarations: [], + sourceCode: /*TS*/ `getPreUpgradeExecuted: query([], bool,() => { + return stable.get(PRE_UPGRADE_HOOK_EXECUTED).Some === true + })`, + tests: [ + [ + { + name: `pre upgrade was not called after first deploy`, + test: async () => { + const actor = getActor(__dirname); + const result = await actor.getPreUpgradeExecuted(); + + return { Ok: deepEqual(result, false) }; + } + } + ], + [ + { + name: `pre upgrade was called after second deploy`, + test: async () => { + const actor = getActor(__dirname); + const result = await actor.getPreUpgradeExecuted(); + + return { Ok: deepEqual(result, false) }; + } + } + ] + ] + }; +}