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

Add discard functionality for conditional tests #40

Merged
merged 21 commits into from
Nov 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
05363bd
Add preliminary function prototype
BowTiedRadone Nov 1, 2024
3c72d66
Use the `preliminary function` for the test function early return
BowTiedRadone Nov 1, 2024
9413b6b
Retrieve property preliminary functions
BowTiedRadone Nov 1, 2024
88faca2
Pair test functions with preliminary functions
BowTiedRadone Nov 1, 2024
902442e
Add ultimate check for preliminary functions
BowTiedRadone Nov 1, 2024
f5858b2
Run preliminary function before the test function
BowTiedRadone Nov 1, 2024
7bbde3a
Adjust test output logs
BowTiedRadone Nov 1, 2024
2c1f32b
Conditionally run properties based on preliminary functions' response
BowTiedRadone Nov 2, 2024
cf248e7
Apply suggestions from code review
BowTiedRadone Nov 4, 2024
af4c8c9
Add new line in reporter logs
BowTiedRadone Nov 4, 2024
fbf8286
Tweak logging ansicolors
BowTiedRadone Nov 4, 2024
756bdd9
Remove unnecessary line
BowTiedRadone Nov 5, 2024
cb3f927
Rename `preliminary` to `discard` across the app
BowTiedRadone Nov 5, 2024
d154005
Refactor discard prototype comment
BowTiedRadone Nov 6, 2024
f0d8daf
Refactor test-discard pairing comment
BowTiedRadone Nov 6, 2024
ada9bc7
Refactor discard rules verification comment
BowTiedRadone Nov 6, 2024
3d12d0b
Refactor discard functions validation control flow
BowTiedRadone Nov 6, 2024
96130cf
Refactor control flow for test discarding
BowTiedRadone Nov 6, 2024
7eabfaf
Add property-based tests for discard utility functions
BowTiedRadone Nov 7, 2024
a1d8c70
Correct grammar in comments
moodmosaic Nov 8, 2024
34060ef
Remove extra spaces in log messages and misplaced `green` function call
moodmosaic Nov 8, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 34 additions & 25 deletions example/contracts/slice.tests.clar
Original file line number Diff line number Diff line change
Expand Up @@ -4,33 +4,42 @@
(define-constant ERR_ASSERTION_FAILED_3 (err 3))

(define-public (test-slice-list-int (seq (list 127 int)) (skip int) (n int))
(if
;; Early return if the input is invalid.
(or
(not (and (<= 0 n) (<= n 127)))
(not (and (<= 0 skip) (<= skip 127))))
(ok true)
(let (
(result (contract-call? .slice slice seq skip n))
)
(let (
(result (contract-call? .slice slice seq skip n))
)
(if
;; Case 1: If skip > length of seq, result should be an empty list.
(> (to-uint skip) (len seq))
(asserts! (is-eq result (list )) ERR_ASSERTION_FAILED_1)
(if
;; Case 1: If skip > length of seq, result should be an empty list.
(> (to-uint skip) (len seq))
(asserts! (is-eq result (list )) ERR_ASSERTION_FAILED_1)
(if
;; Case 2: If n > length of seq - skip, result length should be
;; length of seq - skip.
(>
(to-uint n)
;; Case 2: If n > length of seq - skip, result length should be
;; length of seq - skip.
(>
(to-uint n)
(- (len seq) (to-uint skip)))
(asserts!
(is-eq
(len result)
(- (len seq) (to-uint skip)))
(asserts!
(is-eq
(len result)
(- (len seq) (to-uint skip)))
ERR_ASSERTION_FAILED_2)
;; Case 3: If n <= length of seq - skip, result length should be n.
(asserts! (is-eq (len result) (to-uint n)) ERR_ASSERTION_FAILED_3)))
(ok true))))
ERR_ASSERTION_FAILED_2)
;; Case 3: If n <= length of seq - skip, result length should be n.
(asserts! (is-eq (len result) (to-uint n)) ERR_ASSERTION_FAILED_3)))
(ok true)))

;; Some tests, like 'test-slice-list-int', are valid only for specific inputs.
;; Rendezvous generates a wide range of inputs, which may include values that
;; are unsuitable for those tests.
;; To skip the test when inputs are invalid, define a 'discard' function:
BowTiedRadone marked this conversation as resolved.
Show resolved Hide resolved
;; - Must be read-only.
;; - Name should match the property test function's, prefixed with "can-".
;; - Parameters should mirror those of the property test.
;; - Returns true only if inputs are valid, allowing the test to run.
(define-read-only (can-test-slice-list-int (seq (list 127 int))
(skip int)
(n int))
(and
(and (<= 0 n) (<= n 127))
(and (<= 0 skip) (<= skip 127))))

(define-public (test-slice-list-uint (seq (list 127 uint)) (skip int) (n int))
(if
Expand Down
8 changes: 4 additions & 4 deletions heatstroke.tests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ describe("Custom reporter logging", () => {

// Assert
const expectedMessages = [
`Error: Property failed after ${r.numRuns} tests.`,
`\nError: Property failed after ${r.numRuns} tests.`,
`Seed : ${r.seed}`,
`\nCounterexample:`,
`- Contract : ${getContractNameFromRendezvousId(
Expand Down Expand Up @@ -231,7 +231,7 @@ describe("Custom reporter logging", () => {

// Assert
const expectedMessages = [
`Error: Property failed after ${r.numRuns} tests.`,
`\nError: Property failed after ${r.numRuns} tests.`,
`Seed : ${r.seed}`,
`Path : ${r.path}`,
`\nCounterexample:`,
Expand Down Expand Up @@ -441,7 +441,7 @@ describe("Custom reporter logging", () => {

// Assert
const expectedMessages = [
`Error: Property failed after ${r.numRuns} tests.`,
`\nError: Property failed after ${r.numRuns} tests.`,
`Seed : ${r.seed}`,
`\nCounterexample:`,
`- Test Contract : ${testContractId.split(".")[1]}`,
Expand Down Expand Up @@ -543,7 +543,7 @@ describe("Custom reporter logging", () => {

// Assert
const expectedMessages = [
`Error: Property failed after ${r.numRuns} tests.`,
`\nError: Property failed after ${r.numRuns} tests.`,
`Seed : ${r.seed}`,
`Path : ${r.path}`,
`\nCounterexample:`,
Expand Down
6 changes: 2 additions & 4 deletions heatstroke.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ export function reporter(

radio.emit(
"logFailure",
`Error: Property failed after ${runDetails.numRuns} tests.`
`\nError: Property failed after ${runDetails.numRuns} tests.`
);
radio.emit("logFailure", `Seed : ${runDetails.seed}`);
if (runDetails.path) {
Expand Down Expand Up @@ -138,11 +138,9 @@ export function reporter(
} else {
radio.emit(
"logMessage",
green(
`\nOK, ${
type === "invariant" ? "invariants" : "properties"
} passed after ${runDetails.numRuns} runs.`
)
} passed after ${runDetails.numRuns} runs.\n`
);
}
}
205 changes: 205 additions & 0 deletions property.tests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,19 @@ import {
deriveTestContractName,
filterTestContractsInterfaces,
getTestsContractSource,
isParamsMatch,
isReturnTypeBoolean,
} from "./property";
import { getSimnetDeployerContractsInterfaces } from "./shared";
import { resolve } from "path";
import fs from "fs";
import fc from "fast-check";
import {
ContractInterfaceFunction,
ContractInterfaceFunctionAccess,
ContractInterfaceFunctionArg,
ContractInterfaceFunctionOutput,
} from "@hirosystems/clarinet-sdk/dist/esm/contractInterface";

describe("File stream operations", () => {
it("retrieves the test contract source", async () => {
Expand Down Expand Up @@ -165,3 +173,200 @@ describe("Simnet contracts operations", () => {
expect(actualTestContractsList).toEqual(expectedTestContractsList);
});
});

describe("Test discarding related operations", () => {
it("boolean output checker returns true when the function's output is boolean", () => {
fc.assert(
fc.property(
fc.record({
fnName: fc.string(),
access: fc.constant("read_only"),
args: fc.array(
fc.record({
name: fc.string(),
type: fc.constantFrom("int128", "uint128", "bool", "principal"),
})
),
outputs: fc.record({ type: fc.constant("bool") }),
}),
(r: {
fnName: string;
access: string;
args: { name: string; type: string }[];
outputs: { type: string };
}) => {
const discardFunctionInterface: ContractInterfaceFunction = {
name: r.fnName,
access: r.access as ContractInterfaceFunctionAccess,
args: r.args as ContractInterfaceFunctionArg[],
outputs: r.outputs as ContractInterfaceFunctionOutput,
};
const actual = isReturnTypeBoolean(discardFunctionInterface);
expect(actual).toBe(true);
}
)
);
});

it("boolean output checker returns false when the function's output is non-boolean", () => {
fc.assert(
fc.property(
fc.record({
fnName: fc.string(),
access: fc.constant("read_only"),
args: fc.array(
fc.record({
name: fc.string(),
type: fc.constantFrom("int128", "uint128", "bool", "principal"),
})
),
outputs: fc.record({
type: fc.constantFrom("int128", "uint128", "principal"),
}),
}),
(r: {
fnName: string;
access: string;
args: { name: string; type: string }[];
outputs: { type: string };
}) => {
const discardFunctionInterface: ContractInterfaceFunction = {
name: r.fnName,
access: r.access as ContractInterfaceFunctionAccess,
args: r.args as ContractInterfaceFunctionArg[],
outputs: r.outputs as ContractInterfaceFunctionOutput,
};
const actual = isReturnTypeBoolean(discardFunctionInterface);
expect(actual).toBe(false);
}
)
);
});

it("param matcher returns true when two functions have the same parameters", () => {
fc.assert(
fc.property(
fc.record({
fnName: fc.string(),
access: fc.constant("read_only"),
args: fc.array(
fc.record({
name: fc.string(),
type: fc.constantFrom("int128", "uint128", "bool", "principal"),
})
),
outputs: fc.record({ type: fc.constant("bool") }),
}),
(r: {
fnName: string;
access: string;
args: { name: string; type: string }[];
outputs: { type: string };
}) => {
const testFunctionInterface: ContractInterfaceFunction = {
name: r.fnName,
access: r.access as ContractInterfaceFunctionAccess,
args: r.args as ContractInterfaceFunctionArg[],
outputs: r.outputs as ContractInterfaceFunctionOutput,
};
const discardFunctionInterface: ContractInterfaceFunction = {
name: r.fnName,
access: r.access as ContractInterfaceFunctionAccess,
args: r.args as ContractInterfaceFunctionArg[],
outputs: r.outputs as ContractInterfaceFunctionOutput,
};
const actual = isParamsMatch(
testFunctionInterface,
discardFunctionInterface
);
expect(actual).toBe(true);
}
)
);
});

it("param matcher returns false when two functions have different parameters", () => {
fc.assert(
fc.property(
fc
.record({
testFn: fc.record({
fnName: fc.string(),
access: fc.constant("read_only"),
args: fc.array(
fc.record({
name: fc.string(),
type: fc.constantFrom(
"int128",
"uint128",
"bool",
"principal"
),
})
),
outputs: fc.record({ type: fc.constant("bool") }),
}),
discardFn: fc.record({
fnName: fc.string(),
access: fc.constant("read_only"),
args: fc.array(
fc.record({
name: fc.string(),
type: fc.constantFrom(
"int128",
"uint128",
"bool",
"principal"
),
})
),
outputs: fc.record({ type: fc.constant("bool") }),
}),
})
.filter(
moodmosaic marked this conversation as resolved.
Show resolved Hide resolved
(r) =>
JSON.stringify(
[...r.testFn.args].sort((a, b) => a.name.localeCompare(b.name))
) !==
JSON.stringify(
[...r.discardFn.args].sort((a, b) =>
a.name.localeCompare(b.name)
)
)
),
(r: {
testFn: {
fnName: string;
access: string;
args: { name: string; type: string }[];
outputs: { type: string };
};
discardFn: {
fnName: string;
access: string;
args: { name: string; type: string }[];
outputs: { type: string };
};
}) => {
const testFunctionInterface: ContractInterfaceFunction = {
name: r.testFn.fnName,
access: r.testFn.access as ContractInterfaceFunctionAccess,
args: r.testFn.args as ContractInterfaceFunctionArg[],
outputs: r.testFn.outputs as ContractInterfaceFunctionOutput,
};
const discardFunctionInterface: ContractInterfaceFunction = {
name: r.discardFn.fnName,
access: r.discardFn.access as ContractInterfaceFunctionAccess,
args: r.discardFn.args as ContractInterfaceFunctionArg[],
outputs: r.discardFn.outputs as ContractInterfaceFunctionOutput,
};
const actual = isParamsMatch(
testFunctionInterface,
discardFunctionInterface
);
expect(actual).toBe(false);
}
)
);
});
});
Loading