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 13 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 these tests.
;; To skip the test when inputs are invalid, define a 'discard' function:
BowTiedRadone marked this conversation as resolved.
Show resolved Hide resolved
;; - It must be read-only.
BowTiedRadone marked this conversation as resolved.
Show resolved Hide resolved
;; - Its name should match the property test function's, prefixed with "can-".
BowTiedRadone marked this conversation as resolved.
Show resolved Hide resolved
;; - Its parameters should mirror those of the property test.
BowTiedRadone marked this conversation as resolved.
Show resolved Hide resolved
;; - It must return a boolean indicating if the inputs are invalid.
BowTiedRadone marked this conversation as resolved.
Show resolved Hide resolved
(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
4 changes: 2 additions & 2 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 @@ -141,7 +141,7 @@ export function reporter(
green(
`\nOK, ${
type === "invariant" ? "invariants" : "properties"
} passed after ${runDetails.numRuns} runs.`
} passed after ${runDetails.numRuns} runs.\n`
)
);
}
Expand Down
175 changes: 151 additions & 24 deletions property.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
getFunctionsListForContract,
getSimnetDeployerContractsInterfaces,
} from "./shared";
import { dim, green, red, underline, yellow } from "ansicolor";

export const checkProperties = (
simnet: Simnet,
Expand Down Expand Up @@ -59,6 +60,93 @@ export const checkProperties = (
`\nStarting property testing type for the ${sutContractName} contract...`
);

// Search for discard functions, for each test function. This map will
// be used to pair the test functions with their corresponding discard
// functions.
const testContractsDiscardFunctions = new Map(
Array.from(testContractsAllFunctions, ([contractId, functions]) => [
contractId,
functions.filter(
(f) => f.access === "read_only" && f.name.startsWith("can-")
),
])
);

// Pair each test function with its corresponding discard function. When a
// test function is selected, Rendezvous will first call its discard
// function, if available, to validate that the generated test arguments are
BowTiedRadone marked this conversation as resolved.
Show resolved Hide resolved
// meaningful. This way, we are reducing the risk of false positives in test
BowTiedRadone marked this conversation as resolved.
Show resolved Hide resolved
// results.
BowTiedRadone marked this conversation as resolved.
Show resolved Hide resolved
const testContractsPairedFunctions = new Map(
moodmosaic marked this conversation as resolved.
Show resolved Hide resolved
Array.from(testContractsTestFunctions, ([contractId, functions]) => [
contractId,
new Map(
functions.map((f) => {
const discardFunction = testContractsDiscardFunctions
.get(contractId)
?.find((pf) => pf.name === `can-${f.name}`);

return [f.name, discardFunction?.name];
})
),
])
);

let discardFunctionError = false;
BowTiedRadone marked this conversation as resolved.
Show resolved Hide resolved

// If discard functions are available, verify they follow the remaining
BowTiedRadone marked this conversation as resolved.
Show resolved Hide resolved
// rules:
BowTiedRadone marked this conversation as resolved.
Show resolved Hide resolved
// - Their parameters must match those of the test function.
BowTiedRadone marked this conversation as resolved.
Show resolved Hide resolved
// - Their return type must be boolean.
BowTiedRadone marked this conversation as resolved.
Show resolved Hide resolved
testContractsPairedFunctions.forEach((pairedMap, contractId) => {
BowTiedRadone marked this conversation as resolved.
Show resolved Hide resolved
pairedMap.forEach((discardFunctionName, testFunctionName) => {
if (discardFunctionName) {
const testFunction = testContractsTestFunctions
.get(contractId)
?.find((f) => f.name === testFunctionName);
const discardFunction = testContractsDiscardFunctions
.get(contractId)
?.find((f) => f.name === discardFunctionName);

if (testFunction && discardFunction) {
const paramsMatch =
JSON.stringify(testFunction.args) ===
JSON.stringify(discardFunction.args);
const returnTypeIsBoolean = discardFunction.outputs.type === "bool";

if (!paramsMatch) {
radio.emit(
"logFailure",
red(
`\n[FAIL] Parameter mismatch for discard function "${discardFunctionName}" in contract "${deriveTestContractName(
sutContractIds[0]
)}".`
)
);
discardFunctionError = true;
return;
}
if (!returnTypeIsBoolean) {
radio.emit(
"logFailure",
red(
`\n[FAIL] Return type must be boolean for discard function "${discardFunctionName}" in contract "${deriveTestContractName(
sutContractIds[0]
)}".`
)
);
discardFunctionError = true;
return;
}
}
}
});
});

if (discardFunctionError) {
return;
}
BowTiedRadone marked this conversation as resolved.
Show resolved Hide resolved

const radioReporter = (runDetails: any) => {
reporter(runDetails, radio, "test");
};
Expand Down Expand Up @@ -129,37 +217,76 @@ export const checkProperties = (
.join(" ");

const [testCallerWallet, testCallerAddress] = r.testCaller;
const { result: testFunctionCallResult } = simnet.callPublicFn(
r.testContractId,
r.selectedTestFunction.name,
selectedTestFunctionArgs,
testCallerAddress
);

const testFunctionCallResultJson = cvToJSON(testFunctionCallResult);
const discardFunctionName = testContractsPairedFunctions
.get(r.testContractId)!
.get(r.selectedTestFunction.name);

if (
testFunctionCallResultJson.success &&
testFunctionCallResultJson.value.value === true
) {
radio.emit(
"logMessage",
` ✔ ${testCallerWallet} ${r.testContractId.split(".")[1]} ${
r.selectedTestFunction.name
} ${printedTestFunctionArgs}`
let discarded = false;
BowTiedRadone marked this conversation as resolved.
Show resolved Hide resolved

// If a discard function is defined, call it first to determine if the
// test function can be executed.
if (discardFunctionName !== undefined) {
const { result: discardFunctionCallResult } = simnet.callReadOnlyFn(
r.testContractId,
discardFunctionName,
selectedTestFunctionArgs,
testCallerAddress
);
} else {

const jsonDiscardFunctionCallResult = cvToJSON(
discardFunctionCallResult
);

discarded = jsonDiscardFunctionCallResult.value === false;
}

if (discarded) {
radio.emit(
"logMessage",
` ✗ ${testCallerWallet} ${r.testContractId.split(".")[1]} ${
r.selectedTestFunction.name
} ${printedTestFunctionArgs}`
` ${yellow("[WARN]")} ${dim(testCallerWallet)} ${
r.testContractId.split(".")[1]
} ${underline(r.selectedTestFunction.name)} ${dim(
printedTestFunctionArgs
)}`
);
throw new Error(
`Test failed for ${r.testContractId.split(".")[1]} contract: "${
r.selectedTestFunction.name
}" returned ${testFunctionCallResultJson.value.value}`
} else {
const { result: testFunctionCallResult } = simnet.callPublicFn(
r.testContractId,
r.selectedTestFunction.name,
selectedTestFunctionArgs,
testCallerAddress
);

const testFunctionCallResultJson = cvToJSON(testFunctionCallResult);

if (
testFunctionCallResultJson.success &&
testFunctionCallResultJson.value.value === true
) {
radio.emit(
"logMessage",
` ${green("[PASS]")} ${dim(testCallerWallet)} ${
r.testContractId.split(".")[1]
} ${underline(
r.selectedTestFunction.name
)} ${printedTestFunctionArgs}`
);
} else {
radio.emit(
"logMessage",
` ${red("[FAIL]")} ${dim(testCallerWallet)} ${
r.testContractId.split(".")[1]
} ${underline(
r.selectedTestFunction.name
)} ${printedTestFunctionArgs}`
);
throw new Error(
`Test failed for ${r.testContractId.split(".")[1]} contract: "${
r.selectedTestFunction.name
}" returned ${testFunctionCallResultJson.value.value}`
);
}
}
}
),
Expand Down