@stacks/rendezvous 0.13.1 → 0.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/app.js CHANGED
@@ -22,7 +22,6 @@ const package_json_1 = require("./package.json");
22
22
  const ansicolor_1 = require("ansicolor");
23
23
  const fs_1 = require("fs");
24
24
  const util_1 = require("util");
25
- const dialer_1 = require("./dialer");
26
25
  const logger = (log, logLevel = "log") => {
27
26
  console[logLevel](log);
28
27
  };
@@ -45,19 +44,22 @@ exports.getManifestFileName = getManifestFileName;
45
44
  const helpMessage = `
46
45
  rv v${package_json_1.version}
47
46
 
48
- Usage: rv <path-to-clarinet-project> <contract-name> <type> [--seed=<seed>] [--runs=<runs>] [--dial=<path-to-dialers-file>] [--help]
47
+ Usage: rv <path> <contract> <type> [OPTIONS]
49
48
 
50
- Positional arguments:
51
- path-to-clarinet-project - The path to the Clarinet project.
52
- contract-name - The name of the contract to be fuzzed.
53
- type - The type to use for exercising the contracts. Possible values: test, invariant.
49
+ Arguments:
50
+ <path> Path to the Clarinet project
51
+ <contract> Contract name to fuzz
52
+ <type> Test type: test | invariant
54
53
 
55
54
  Options:
56
- --seed - The seed to use for the replay functionality.
57
- --runs - The runs to use for iterating over the tests. Default: 100.
58
- --bail - Stop after the first failure.
59
- --dial The path to a JavaScript file containing custom pre- and post-execution functions (dialers).
60
- --help - Show the help message.
55
+ --seed=<n> Seed for replay functionality
56
+ --runs=<n> Number of test iterations [default: 100]
57
+ --dial=<f> Path to custom dialers file
58
+ --regr Run regression tests only
59
+ --bail Stop on first failure
60
+ -h, --help Show this message
61
+
62
+ Learn more: https://stacks-network.github.io/rendezvous/
61
63
  `;
62
64
  function main() {
63
65
  return __awaiter(this, void 0, void 0, function* () {
@@ -72,6 +74,7 @@ function main() {
72
74
  runs: { type: "string" },
73
75
  dial: { type: "string" },
74
76
  bail: { type: "boolean" },
77
+ regr: { type: "boolean" },
75
78
  help: { type: "boolean", short: "h" },
76
79
  },
77
80
  });
@@ -89,6 +92,8 @@ function main() {
89
92
  runs: options.runs ? parseInt(options.runs, 10) : undefined,
90
93
  /** Whether to bail on the first failure. */
91
94
  bail: options.bail || false,
95
+ /** Whether to run regression tests only. */
96
+ regr: options.regr || false,
92
97
  /** The path to the dialer file. */
93
98
  dial: options.dial || undefined,
94
99
  /** Whether to show the help message. */
@@ -113,6 +118,8 @@ function main() {
113
118
  radio.emit("logMessage", helpMessage);
114
119
  return;
115
120
  }
121
+ // Divider before the run configuration.
122
+ radio.emit("logMessage", shared_1.LOG_DIVIDER);
116
123
  /**
117
124
  * The relative path to the manifest file, either `Clarinet.toml` or
118
125
  * `Clarinet-<contract-name>.toml`. If the latter exists, it is used.
@@ -129,39 +136,39 @@ function main() {
129
136
  if (runConfig.bail) {
130
137
  radio.emit("logMessage", `Bailing on first failure.`);
131
138
  }
139
+ if (runConfig.regr) {
140
+ radio.emit("logMessage", `Running regression tests.`);
141
+ }
132
142
  if (runConfig.dial !== undefined) {
133
143
  radio.emit("logMessage", `Using dial path: ${runConfig.dial}`);
134
144
  }
135
- /**
136
- * The dialer registry, which is used to keep track of all the custom dialers
137
- * registered by the user using the `--dial` flag.
138
- */
139
- const dialerRegistry = runConfig.dial !== undefined
140
- ? new dialer_1.DialerRegistry(runConfig.dial)
141
- : undefined;
142
- if (dialerRegistry !== undefined) {
143
- dialerRegistry.registerDialers();
144
- }
145
- const simnet = yield (0, citizen_1.issueFirstClassCitizenship)(runConfig.manifestDir, manifestPath, runConfig.sutContractName, radio);
146
- /**
147
- * The list of contract IDs for the SUT contract names, as per the simnet.
148
- */
149
- const rendezvousList = Array.from((0, shared_1.getSimnetDeployerContractsInterfaces)(simnet).keys()).filter((deployedContract) => (0, shared_1.getContractNameFromContractId)(deployedContract) ===
150
- runConfig.sutContractName);
151
- const rendezvousAllFunctions = (0, shared_1.getFunctionsFromContractInterfaces)(new Map(Array.from((0, shared_1.getSimnetDeployerContractsInterfaces)(simnet)).filter(([contractId]) => rendezvousList.includes(contractId))));
152
- // Select the testing routine based on `type`.
153
- // If "invariant", call `checkInvariants` to verify contract invariants.
154
- // If "test", call `checkProperties` for property-based testing.
155
- switch (runConfig.type) {
156
- case "invariant": {
157
- yield (0, invariant_1.checkInvariants)(simnet, runConfig.sutContractName, rendezvousList, rendezvousAllFunctions, runConfig.seed, runConfig.runs, runConfig.bail, dialerRegistry, radio);
158
- break;
159
- }
160
- case "test": {
161
- (0, property_1.checkProperties)(simnet, runConfig.sutContractName, rendezvousList, rendezvousAllFunctions, runConfig.seed, runConfig.runs, runConfig.bail, radio);
162
- break;
145
+ // Divider between the run configuration and the execution.
146
+ radio.emit("logMessage", shared_1.LOG_DIVIDER + "\n");
147
+ const { simnet, resetSession, cleanupSession } = yield (0, citizen_1.issueFirstClassCitizenship)(runConfig.manifestDir, manifestPath, runConfig.sutContractName, radio);
148
+ try {
149
+ /**
150
+ * The list of contract IDs for the SUT contract names, as per the simnet.
151
+ */
152
+ const rendezvousList = Array.from((0, shared_1.getSimnetDeployerContractsInterfaces)(simnet).keys()).filter((deployedContract) => (0, shared_1.getContractNameFromContractId)(deployedContract) ===
153
+ runConfig.sutContractName);
154
+ const rendezvousAllFunctions = (0, shared_1.getFunctionsFromContractInterfaces)(new Map(Array.from((0, shared_1.getSimnetDeployerContractsInterfaces)(simnet)).filter(([contractId]) => rendezvousList.includes(contractId))));
155
+ // Select the testing routine based on `type`.
156
+ // If "invariant", call `checkInvariants` to verify contract invariants.
157
+ // If "test", call `checkProperties` for property-based testing.
158
+ switch (runConfig.type) {
159
+ case "invariant": {
160
+ yield (0, invariant_1.checkInvariants)(simnet, resetSession, runConfig.sutContractName, rendezvousList, rendezvousAllFunctions, runConfig.seed, runConfig.runs, runConfig.dial, runConfig.bail, runConfig.regr, radio);
161
+ break;
162
+ }
163
+ case "test": {
164
+ yield (0, property_1.checkProperties)(simnet, resetSession, runConfig.sutContractName, rendezvousList, rendezvousAllFunctions, runConfig.seed, runConfig.runs, runConfig.bail, runConfig.regr, radio);
165
+ break;
166
+ }
163
167
  }
164
168
  }
169
+ finally {
170
+ cleanupSession();
171
+ }
165
172
  });
166
173
  }
167
174
  if (require.main === module) {
package/dist/citizen.js CHANGED
@@ -34,14 +34,15 @@ const ansicolor_1 = require("ansicolor");
34
34
  * @param manifestPath The path to the manifest file.
35
35
  * @param sutContractName The target contract name.
36
36
  * @param radio The event emitter to send log messages to.
37
- * @returns The initialized simnet.
37
+ * @returns The initialized simnet session, including the simnet, a function to
38
+ * reset the session, and a function to clean up the session.
38
39
  */
39
40
  const issueFirstClassCitizenship = (manifestDir, manifestPath, sutContractName, radio) => __awaiter(void 0, void 0, void 0, function* () {
40
41
  var _a, _b, _c, _d;
41
42
  // First simnet initialization: This will generate the deployment plan and
42
43
  // will type check the project without any Rendezvous tests.
43
44
  try {
44
- radio.emit("logMessage", `\nType-checking your Clarinet project...`);
45
+ radio.emit("logMessage", `Type-checking your Clarinet project...\n`);
45
46
  yield (0, clarinet_sdk_1.generateDeployement)(manifestPath);
46
47
  }
47
48
  catch (error) {
@@ -62,7 +63,7 @@ const issueFirstClassCitizenship = (manifestDir, manifestPath, sutContractName,
62
63
  const rendezvousContractsDir = (0, path_1.join)(tempProjectDir, "contracts");
63
64
  const rendezvousPath = (0, path_1.join)(rendezvousContractsDir, `${contractName}-rendezvous.clar`);
64
65
  (0, fs_1.writeFileSync)(rendezvousPath, rendezvousData.rendezvousSourceCode);
65
- radio.emit("logMessage", `\nType-checking your Rendezvous project...`);
66
+ radio.emit("logMessage", `Type-checking your Rendezvous project...\n`);
66
67
  // Update the manifest in the temp directory to point to the Rendezvous
67
68
  // concatenation.
68
69
  const manifestFileName = (0, path_1.basename)(manifestPath);
@@ -111,7 +112,28 @@ const issueFirstClassCitizenship = (manifestDir, manifestPath, sutContractName,
111
112
  }
112
113
  try {
113
114
  const simnet = yield (0, clarinet_sdk_1.initSimnet)(manifestFileName);
114
- return simnet;
115
+ const resetSession = () => __awaiter(void 0, void 0, void 0, function* () {
116
+ const cwd = process.cwd();
117
+ const origWrite = process.stdout.write;
118
+ process.stdout.write = () => true;
119
+ try {
120
+ process.chdir(tempProjectDir);
121
+ yield (0, clarinet_sdk_1.initSimnet)(manifestFileName);
122
+ }
123
+ finally {
124
+ process.stdout.write = origWrite;
125
+ process.chdir(cwd);
126
+ }
127
+ });
128
+ const cleanupSession = () => {
129
+ try {
130
+ (0, fs_1.rmSync)(tempProjectDir, { recursive: true, force: true });
131
+ }
132
+ catch (error) {
133
+ radio.emit("logMessage", (0, ansicolor_1.yellow)(`Error cleaning up temporary project directory ${tempProjectDir}: ${error.message}. Remove it manually to avoid unnecessary disk space usage.`));
134
+ }
135
+ };
136
+ return { simnet, resetSession, cleanupSession };
115
137
  }
116
138
  finally {
117
139
  // Restore stdout.
@@ -121,13 +143,6 @@ const issueFirstClassCitizenship = (manifestDir, manifestPath, sutContractName,
121
143
  finally {
122
144
  // Restore the original current working directory.
123
145
  process.chdir(originalCwd);
124
- // Cleanup the temp project directory.
125
- try {
126
- (0, fs_1.rmSync)(tempProjectDir, { recursive: true, force: true });
127
- }
128
- catch (error) {
129
- radio.emit("logMessage", (0, ansicolor_1.yellow)(`Error cleaning up temporary project directory ${tempProjectDir}: ${error.message}. Remove it manually to avoid unnecessary disk space usage.`));
130
- }
131
146
  }
132
147
  });
133
148
  exports.issueFirstClassCitizenship = issueFirstClassCitizenship;
@@ -184,14 +199,13 @@ const getDeploymentPlanContractSource = (deploymentPlan, sutContractName, manife
184
199
  * in the deployment plan.
185
200
  */
186
201
  const getSutContractDeploymentPlanEmulatedPublish = (deploymentPlan, sutContractName) => {
187
- var _a, _b;
202
+ var _a;
188
203
  // Filter all emulated contract publish transactions matching the target
189
204
  // contract name from the deployment plan.
190
205
  const contractPublishMatchesByName = deploymentPlan.plan.batches
191
206
  .flatMap((batch) => batch.transactions)
192
- .filter((transaction) => transaction["emulated-contract-publish"] &&
193
- transaction["emulated-contract-publish"]["contract-name"] ===
194
- sutContractName);
207
+ .filter((transaction) => transaction["transaction-type"] === "emulated-contract-publish" &&
208
+ transaction["contract-name"] === sutContractName);
195
209
  // If no matches are found, something went wrong.
196
210
  if (contractPublishMatchesByName.length === 0) {
197
211
  throw new Error(`"${sutContractName}" contract not found in Clarinet.toml.`);
@@ -206,8 +220,7 @@ const getSutContractDeploymentPlanEmulatedPublish = (deploymentPlan, sutContract
206
220
  }
207
221
  // From the list of filtered emulated contract publish transactions with
208
222
  // having the same name, select the one deployed by the deployer.
209
- const targetContractDeploymentData = (_b = contractPublishMatchesByName.find((transaction) => transaction["emulated-contract-publish"]["emulated-sender"] ===
210
- deployer)) === null || _b === void 0 ? void 0 : _b["emulated-contract-publish"];
223
+ const targetContractDeploymentData = contractPublishMatchesByName.find((transaction) => transaction["emulated-sender"] === deployer);
211
224
  // TODO: Consider handling requirements and project contracts separately.
212
225
  // Eventually let the user specify if the contract is a requirement or a
213
226
  // project contract.
@@ -220,8 +233,8 @@ const getSutContractDeploymentPlanEmulatedPublish = (deploymentPlan, sutContract
220
233
  }
221
234
  return targetContractDeploymentData;
222
235
  }
223
- // Only one match was found, return the path to the contract.
224
- const contractNameMatch = contractPublishMatchesByName[0]["emulated-contract-publish"];
236
+ // Only one match was found, return the contract publish data.
237
+ const contractNameMatch = contractPublishMatchesByName[0];
225
238
  if (!contractNameMatch) {
226
239
  throw new Error(`Could not locate "${sutContractName}" contract.`);
227
240
  }
@@ -21,53 +21,67 @@ const shared_1 = require("./shared");
21
21
  * @returns void
22
22
  */
23
23
  function reporter(runDetails, radio, type, statistics) {
24
- var _a, _b;
25
- if (runDetails.failed) {
24
+ const { counterexample, failed, numRuns, path, seed } = runDetails;
25
+ if (failed) {
26
+ const error = runDetails.errorInstance || runDetails.error;
27
+ // Extract the actual Clarity error once for both error types.
28
+ const clarityError = (error === null || error === void 0 ? void 0 : error.clarityError) ||
29
+ (error === null || error === void 0 ? void 0 : error.message) ||
30
+ (error === null || error === void 0 ? void 0 : error.toString()) ||
31
+ "Unknown error";
26
32
  // Report general run data.
27
- radio.emit("logFailure", `\nError: Property failed after ${runDetails.numRuns} tests.`);
28
- radio.emit("logFailure", `Seed : ${runDetails.seed}`);
29
- if (runDetails.path) {
30
- radio.emit("logFailure", `Path : ${runDetails.path}`);
33
+ radio.emit("logFailure", `\nError: Property failed after ${numRuns} tests.`);
34
+ radio.emit("logFailure", `Seed : ${seed}`);
35
+ if (path) {
36
+ radio.emit("logFailure", `Path : ${path}`);
31
37
  }
32
38
  switch (type) {
33
39
  case "invariant": {
34
- const r = runDetails.counterexample[0];
40
+ const ce = counterexample[0];
35
41
  // Report specific run data for the invariant testing type.
36
42
  radio.emit("logFailure", `\nCounterexample:`);
37
- radio.emit("logFailure", `- Contract : ${(0, shared_1.getContractNameFromContractId)(r.rendezvousContractId)}`);
38
- radio.emit("logFailure", `- Functions: ${r.selectedFunctions
43
+ radio.emit("logFailure", `- Contract : ${(0, shared_1.getContractNameFromContractId)(ce.rendezvousContractId)}`);
44
+ radio.emit("logFailure", `- Functions: ${ce.selectedFunctions
39
45
  .map((selectedFunction) => selectedFunction.name)
40
- .join(", ")} (${r.selectedFunctions
46
+ .join(", ")} (${ce.selectedFunctions
41
47
  .map((selectedFunction) => selectedFunction.access)
42
48
  .join(", ")})`);
43
- radio.emit("logFailure", `- Arguments: ${r.selectedFunctionsArgsList
49
+ radio.emit("logFailure", `- Arguments: ${ce.selectedFunctionsArgsList
44
50
  .map((selectedFunctionArgs) => JSON.stringify(selectedFunctionArgs))
45
51
  .join(", ")}`);
46
- radio.emit("logFailure", `- Callers : ${r.sutCallers
52
+ radio.emit("logFailure", `- Callers : ${ce.sutCallers
47
53
  .map((sutCaller) => sutCaller[0])
48
54
  .join(", ")}`);
49
- radio.emit("logFailure", `- Outputs : ${r.selectedFunctions
55
+ radio.emit("logFailure", `- Outputs : ${ce.selectedFunctions
50
56
  .map((selectedFunction) => JSON.stringify(selectedFunction.outputs))
51
57
  .join(", ")}`);
52
- radio.emit("logFailure", `- Invariant: ${r.selectedInvariant.name} (${r.selectedInvariant.access})`);
53
- radio.emit("logFailure", `- Arguments: ${JSON.stringify(r.invariantArgs)}`);
54
- radio.emit("logFailure", `- Caller : ${r.invariantCaller[0]}`);
58
+ radio.emit("logFailure", `- Invariant: ${ce.selectedInvariant.name} (${ce.selectedInvariant.access})`);
59
+ radio.emit("logFailure", `- Arguments: ${JSON.stringify(ce.invariantArgs)}`);
60
+ radio.emit("logFailure", `- Caller : ${ce.invariantCaller[0]}`);
55
61
  radio.emit("logFailure", `\nWhat happened? Rendezvous went on a rampage and found a weak spot:\n`);
56
- const formattedError = `The invariant "${r.selectedInvariant.name}" returned:\n\n${(_a = runDetails.error) === null || _a === void 0 ? void 0 : _a.toString().split("\n").map((line) => " " + line).join("\n")}\n`;
62
+ const formattedError = `The invariant "${ce.selectedInvariant.name}" returned:\n\n${clarityError
63
+ .toString()
64
+ .split("\n")
65
+ .map((line) => " " + line)
66
+ .join("\n")}\n`;
57
67
  radio.emit("logFailure", formattedError);
58
68
  break;
59
69
  }
60
70
  case "test": {
61
- const r = runDetails.counterexample[0];
71
+ const ce = counterexample[0];
62
72
  // Report specific run data for the property testing type.
63
73
  radio.emit("logFailure", `\nCounterexample:`);
64
- radio.emit("logFailure", `- Test Contract : ${(0, shared_1.getContractNameFromContractId)(r.testContractId)}`);
65
- radio.emit("logFailure", `- Test Function : ${r.selectedTestFunction.name} (${r.selectedTestFunction.access})`);
66
- radio.emit("logFailure", `- Arguments : ${JSON.stringify(r.functionArgs)}`);
67
- radio.emit("logFailure", `- Caller : ${r.testCaller[0]}`);
68
- radio.emit("logFailure", `- Outputs : ${JSON.stringify(r.selectedTestFunction.outputs)}`);
74
+ radio.emit("logFailure", `- Contract : ${(0, shared_1.getContractNameFromContractId)(ce.rendezvousContractId)}`);
75
+ radio.emit("logFailure", `- Test Function : ${ce.selectedTestFunction.name} (${ce.selectedTestFunction.access})`);
76
+ radio.emit("logFailure", `- Arguments : ${JSON.stringify(ce.functionArgs)}`);
77
+ radio.emit("logFailure", `- Caller : ${ce.testCaller[0]}`);
78
+ radio.emit("logFailure", `- Outputs : ${JSON.stringify(ce.selectedTestFunction.outputs)}`);
69
79
  radio.emit("logFailure", `\nWhat happened? Rendezvous went on a rampage and found a weak spot:\n`);
70
- const formattedError = `The test function "${r.selectedTestFunction.name}" returned:\n\n${(_b = runDetails.error) === null || _b === void 0 ? void 0 : _b.toString().split("\n").map((line) => " " + line).join("\n")}\n`;
80
+ const formattedError = `The test function "${ce.selectedTestFunction.name}" returned:\n\n${clarityError
81
+ .toString()
82
+ .split("\n")
83
+ .map((line) => " " + line)
84
+ .join("\n")}\n`;
71
85
  radio.emit("logFailure", formattedError);
72
86
  break;
73
87
  }
@@ -77,7 +91,7 @@ function reporter(runDetails, radio, type, statistics) {
77
91
  process.exitCode = 1;
78
92
  }
79
93
  else {
80
- radio.emit("logMessage", (0, ansicolor_1.green)(`\nOK, ${type === "invariant" ? "invariants" : "properties"} passed after ${runDetails.numRuns} runs.\n`));
94
+ radio.emit("logMessage", (0, ansicolor_1.green)(`\nOK, ${type === "invariant" ? "invariants" : "properties"} passed after ${numRuns} runs.\n`));
81
95
  }
82
96
  reportStatistics(statistics, type, radio);
83
97
  radio.emit("logMessage", "\n");
package/dist/invariant.js CHANGED
@@ -12,7 +12,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
12
12
  return (mod && mod.__esModule) ? mod : { "default": mod };
13
13
  };
14
14
  Object.defineProperty(exports, "__esModule", { value: true });
15
- exports.initializeClarityContext = exports.initializeLocalContext = exports.checkInvariants = void 0;
15
+ exports.FalsifiedInvariantError = exports.initializeClarityContext = exports.initializeLocalContext = exports.checkInvariants = void 0;
16
16
  const shared_1 = require("./shared");
17
17
  const transactions_1 = require("@stacks/transactions");
18
18
  const heatstroke_1 = require("./heatstroke");
@@ -20,33 +20,27 @@ const fast_check_1 = __importDefault(require("fast-check"));
20
20
  const ansicolor_1 = require("ansicolor");
21
21
  const traits_1 = require("./traits");
22
22
  const dialer_1 = require("./dialer");
23
+ const persistence_1 = require("./persistence");
24
+ const path_1 = require("path");
23
25
  /**
24
26
  * Runs invariant testing on the target contract and logs the progress. Reports
25
27
  * the test results through a custom reporter.
26
28
  * @param simnet The Simnet instance.
29
+ * @param resetSession Resets the simnet session to a clean state.
27
30
  * @param targetContractName The name of the target contract.
28
31
  * @param rendezvousList The list of contract IDs for each target contract.
29
32
  * @param rendezvousAllFunctions The map of all function interfaces for each
30
33
  * target contract.
31
34
  * @param seed The seed for reproducible invariant testing.
32
35
  * @param runs The number of test runs.
36
+ * @param dial The path to the dialer file.
33
37
  * @param bail Stop execution after the first failure and prevent further
34
38
  * shrinking.
35
- * @param dialerRegistry The custom dialer registry.
39
+ * @param regr Whether to run regression tests only.
36
40
  * @param radio The custom logging event emitter.
37
41
  * @returns void
38
42
  */
39
- const checkInvariants = (simnet, targetContractName, rendezvousList, rendezvousAllFunctions, seed, runs, bail, dialerRegistry, radio) => __awaiter(void 0, void 0, void 0, function* () {
40
- const statistics = {
41
- sut: {
42
- successful: new Map(),
43
- failed: new Map(),
44
- },
45
- invariant: {
46
- successful: new Map(),
47
- failed: new Map(),
48
- },
49
- };
43
+ const checkInvariants = (simnet, resetSession, targetContractName, rendezvousList, rendezvousAllFunctions, seed, runs, dial, bail, regr, radio) => __awaiter(void 0, void 0, void 0, function* () {
50
44
  // The Rendezvous identifier is the first one in the list. Only one contract
51
45
  // can be fuzzed at a time.
52
46
  const rendezvousContractId = rendezvousList[0];
@@ -54,20 +48,10 @@ const checkInvariants = (simnet, targetContractName, rendezvousList, rendezvousA
54
48
  // arrays of their SUT (System Under Test) functions. This map will be used
55
49
  // to access the SUT functions for each Rendezvous contract afterwards.
56
50
  const rendezvousSutFunctions = filterSutFunctions(rendezvousAllFunctions);
57
- // Initialize the statistics for the SUT functions.
58
- for (const functionInterface of rendezvousSutFunctions.get(rendezvousContractId)) {
59
- statistics.sut.successful.set(functionInterface.name, 0);
60
- statistics.sut.failed.set(functionInterface.name, 0);
61
- }
62
51
  // A map where the keys are the Rendezvous identifiers and the values are
63
52
  // arrays of their invariant functions. This map will be used to access the
64
53
  // invariant functions for each Rendezvous contract afterwards.
65
54
  const rendezvousInvariantFunctions = filterInvariantFunctions(rendezvousAllFunctions);
66
- // Initialize the statistics for the invariant functions.
67
- for (const functionInterface of rendezvousInvariantFunctions.get(rendezvousContractId)) {
68
- statistics.invariant.successful.set(functionInterface.name, 0);
69
- statistics.invariant.failed.set(functionInterface.name, 0);
70
- }
71
55
  const sutFunctions = rendezvousSutFunctions.get(rendezvousContractId);
72
56
  const traitReferenceSutFunctions = sutFunctions.filter(traits_1.isTraitReferenceFunction);
73
57
  const invariantFunctions = rendezvousInvariantFunctions.get(rendezvousContractId);
@@ -90,22 +74,8 @@ const checkInvariants = (simnet, targetContractName, rendezvousList, rendezvousA
90
74
  // functions will be skipped during invariant testing. Otherwise, the
91
75
  // invariant testing routine can fail during argument generation.
92
76
  const invariantFunctionsWithMissingTraits = (0, traits_1.getNonTestableTraitFunctions)(enrichedInvariantFunctionsInterfaces, invariantTraitReferenceMap, projectTraitImplementations, rendezvousContractId);
93
- if (sutFunctionsWithMissingTraits.length > 0 ||
94
- invariantFunctionsWithMissingTraits.length > 0) {
95
- if (sutFunctionsWithMissingTraits.length > 0) {
96
- const functionList = sutFunctionsWithMissingTraits
97
- .map((fn) => ` - ${fn}`)
98
- .join("\n");
99
- radio.emit("logMessage", (0, ansicolor_1.yellow)(`\nWarning: The following SUT functions reference traits without eligible implementations and will be skipped:\n\n${functionList}\n`));
100
- }
101
- if (invariantFunctionsWithMissingTraits.length > 0) {
102
- const functionList = invariantFunctionsWithMissingTraits
103
- .map((fn) => ` - ${fn}`)
104
- .join("\n");
105
- radio.emit("logMessage", (0, ansicolor_1.yellow)(`\nWarning: The following invariant functions reference traits without eligible implementations and will be skipped:\n\n${functionList}\n`));
106
- }
107
- radio.emit("logMessage", (0, ansicolor_1.yellow)(`Note: You can add contracts implementing traits either as project contracts or as Clarinet requirements.\n`));
108
- }
77
+ // Emit warnings for functions with missing trait implementations
78
+ emitMissingTraitWarnings(radio, sutFunctionsWithMissingTraits, invariantFunctionsWithMissingTraits);
109
79
  // Filter out functions with missing trait implementations from the enriched
110
80
  // map.
111
81
  const executableSutFunctions = new Map([
@@ -126,14 +96,6 @@ const checkInvariants = (simnet, targetContractName, rendezvousList, rendezvousA
126
96
  .filter((f) => !invariantFunctionsWithMissingTraits.includes(f.name)),
127
97
  ],
128
98
  ]);
129
- // Set up local context to track SUT function call counts.
130
- const localContext = (0, exports.initializeLocalContext)(executableSutFunctions);
131
- // Set up context in simnet by initializing state for SUT.
132
- (0, exports.initializeClarityContext)(simnet, executableSutFunctions);
133
- radio.emit("logMessage", `\nStarting invariant testing type for the ${targetContractName} contract...\n`);
134
- const simnetAccounts = simnet.getAccounts();
135
- const eligibleAccounts = new Map([...simnetAccounts].filter(([key]) => key !== "faucet"));
136
- const simnetAddresses = Array.from(simnetAccounts.values());
137
99
  const functions = (0, shared_1.getFunctionsListForContract)(executableSutFunctions, rendezvousContractId);
138
100
  const invariants = (0, shared_1.getFunctionsListForContract)(executableInvariantFunctions, rendezvousContractId);
139
101
  if ((functions === null || functions === void 0 ? void 0 : functions.length) === 0) {
@@ -144,9 +106,100 @@ const checkInvariants = (simnet, targetContractName, rendezvousList, rendezvousA
144
106
  radio.emit("logMessage", (0, ansicolor_1.red)(`No invariant functions found for the "${targetContractName}" contract. Beware, for your contract may be exposed to unforeseen issues.\n`));
145
107
  return;
146
108
  }
147
- const radioReporter = (runDetails) => {
148
- (0, heatstroke_1.reporter)(runDetails, radio, "invariant", statistics);
109
+ if (regr) {
110
+ // Run regression tests only.
111
+ radio.emit("logMessage", `Regressions loaded from: ${(0, path_1.resolve)((0, persistence_1.getFailureFilePath)(rendezvousContractId))}`);
112
+ radio.emit("logMessage", `Loading ${targetContractName} contract regressions...\n`);
113
+ const regressions = (0, persistence_1.loadFailures)(rendezvousContractId, "invariant");
114
+ radio.emit("logMessage", `Found ${(0, ansicolor_1.underline)(`${regressions.length} regressions`)} for the ${targetContractName} contract.\n`);
115
+ for (const regression of regressions) {
116
+ emitInvariantRegressionTestHeader(radio, targetContractName, regression.seed, regression.numRuns, regression.dial, regression.timestamp);
117
+ yield resetSession();
118
+ yield invariantTest({
119
+ simnet,
120
+ targetContractName,
121
+ rendezvousContractId,
122
+ runs: regression.numRuns < 100 ? 100 : regression.numRuns,
123
+ seed: regression.seed,
124
+ bail,
125
+ dial: regression.dial,
126
+ radio,
127
+ functions,
128
+ invariants,
129
+ projectTraitImplementations,
130
+ });
131
+ }
132
+ }
133
+ else {
134
+ // Run fresh invariant tests using user-provided configuration.
135
+ radio.emit("logMessage", `Starting fresh round of invariant testing for the ${targetContractName} contract using user-provided configuration...\n`);
136
+ yield invariantTest({
137
+ simnet,
138
+ targetContractName,
139
+ rendezvousContractId,
140
+ runs,
141
+ seed,
142
+ bail,
143
+ dial,
144
+ radio,
145
+ functions,
146
+ invariants,
147
+ projectTraitImplementations,
148
+ });
149
+ }
150
+ });
151
+ exports.checkInvariants = checkInvariants;
152
+ /**
153
+ * Runs an invariant test.
154
+ * @param config The union of the configuration and context for the invariant
155
+ * test.
156
+ * @returns A promise that resolves when the invariant test is complete.
157
+ */
158
+ const invariantTest = (config) => __awaiter(void 0, void 0, void 0, function* () {
159
+ const { simnet, targetContractName, rendezvousContractId, runs, seed, bail, dial, radio, functions, invariants, projectTraitImplementations, } = config;
160
+ // Derive accounts and addresses from simnet.
161
+ const simnetAccounts = simnet.getAccounts();
162
+ const eligibleAccounts = new Map([...simnetAccounts].filter(([key]) => key !== "faucet"));
163
+ const simnetAddresses = Array.from(simnetAccounts.values());
164
+ /**
165
+ * The dialer registry, which is used to keep track of all the custom dialers
166
+ * registered by the user using the `--dial` flag.
167
+ */
168
+ const dialerRegistry = dial !== undefined ? new dialer_1.DialerRegistry(dial) : undefined;
169
+ if (dialerRegistry !== undefined) {
170
+ dialerRegistry.registerDialers();
171
+ }
172
+ const statistics = {
173
+ sut: {
174
+ successful: new Map(),
175
+ failed: new Map(),
176
+ },
177
+ invariant: {
178
+ successful: new Map(),
179
+ failed: new Map(),
180
+ },
149
181
  };
182
+ // Initialize the statistics for the SUT functions.
183
+ for (const functionInterface of functions) {
184
+ statistics.sut.successful.set(functionInterface.name, 0);
185
+ statistics.sut.failed.set(functionInterface.name, 0);
186
+ }
187
+ // Initialize the statistics for the invariant functions.
188
+ for (const functionInterface of invariants) {
189
+ statistics.invariant.successful.set(functionInterface.name, 0);
190
+ statistics.invariant.failed.set(functionInterface.name, 0);
191
+ }
192
+ const radioReporter = (runDetails) => __awaiter(void 0, void 0, void 0, function* () {
193
+ (0, heatstroke_1.reporter)(runDetails, radio, "invariant", statistics);
194
+ // Persist failures for regression testing.
195
+ if (runDetails.failed) {
196
+ (0, persistence_1.persistFailure)(runDetails, "invariant", rendezvousContractId, dial);
197
+ }
198
+ });
199
+ // Set up local context to track SUT function call counts.
200
+ const localContext = (0, exports.initializeLocalContext)(rendezvousContractId, functions);
201
+ // Set up context in simnet by initializing state for SUT.
202
+ (0, exports.initializeClarityContext)(simnet, rendezvousContractId, functions);
150
203
  yield fast_check_1.default.assert(fast_check_1.default.asyncProperty(fast_check_1.default
151
204
  .record({
152
205
  // The target contract identifier. It is a constant value equal
@@ -333,7 +386,7 @@ const checkInvariants = (simnet, targetContractName, rendezvousList, rendezvousA
333
386
  // Invariant call went through, but returned something other than
334
387
  // `true`. Create a custom error to distinguish this case from
335
388
  // runtime errors.
336
- throw new FalsifiedInvariantError(`Invariant failed for ${targetContractName} contract: "${r.selectedInvariant.name}" returned ${invariantCallClarityResult}`);
389
+ throw new FalsifiedInvariantError(`Invariant failed for ${targetContractName} contract: "${r.selectedInvariant.name}" returned ${invariantCallClarityResult}`, invariantCallClarityResult);
337
390
  }
338
391
  }
339
392
  catch (error) {
@@ -362,27 +415,51 @@ const checkInvariants = (simnet, targetContractName, rendezvousList, rendezvousA
362
415
  verbose: true,
363
416
  });
364
417
  });
365
- exports.checkInvariants = checkInvariants;
418
+ /**
419
+ * Emits warnings for functions that reference traits without eligible
420
+ * implementations.
421
+ */
422
+ function emitMissingTraitWarnings(radio, sutFunctions, invariantFunctions) {
423
+ if (sutFunctions.length === 0 && invariantFunctions.length === 0) {
424
+ return;
425
+ }
426
+ if (sutFunctions.length > 0) {
427
+ const functionList = sutFunctions.map((fn) => ` - ${fn}`).join("\n");
428
+ radio.emit("logMessage", (0, ansicolor_1.yellow)(`\nWarning: The following SUT functions reference traits without eligible implementations and will be skipped:\n\n${functionList}\n`));
429
+ }
430
+ if (invariantFunctions.length > 0) {
431
+ const functionList = invariantFunctions.map((fn) => ` - ${fn}`).join("\n");
432
+ radio.emit("logMessage", (0, ansicolor_1.yellow)(`\nWarning: The following invariant functions reference traits without eligible implementations and will be skipped:\n\n${functionList}\n`));
433
+ }
434
+ radio.emit("logMessage", (0, ansicolor_1.yellow)(`Note: You can add contracts implementing traits either as project contracts or as Clarinet requirements.\n`));
435
+ }
366
436
  /**
367
437
  * Initializes the local context, setting the number of times each function
368
438
  * has been called to zero.
369
- * @param rendezvousSutFunctions The Rendezvous functions.
439
+ * @param contractId The contract identifier.
440
+ * @param functions The SUT functions for the contract.
370
441
  * @returns The initialized local context.
371
442
  */
372
- const initializeLocalContext = (rendezvousSutFunctions) => Object.fromEntries(Array.from(rendezvousSutFunctions.entries()).map(([contractId, functions]) => [
373
- contractId,
374
- Object.fromEntries(functions.map((f) => [f.name, 0])),
375
- ]));
443
+ const initializeLocalContext = (contractId, functions) => ({
444
+ [contractId]: Object.fromEntries(functions.map((f) => [f.name, 0])),
445
+ });
376
446
  exports.initializeLocalContext = initializeLocalContext;
377
- const initializeClarityContext = (simnet, rendezvousSutFunctions) => rendezvousSutFunctions.forEach((fns, contractId) => {
378
- fns.forEach((fn) => {
447
+ /**
448
+ * Initializes the Clarity context by calling update-context for each SUT
449
+ * function.
450
+ * @param simnet The Simnet instance.
451
+ * @param contractId The contract identifier.
452
+ * @param functions The SUT functions for the contract.
453
+ */
454
+ const initializeClarityContext = (simnet, contractId, functions) => {
455
+ functions.forEach((fn) => {
379
456
  const { result: initialize } = simnet.callPublicFn(contractId, "update-context", [transactions_1.Cl.stringAscii(fn.name), transactions_1.Cl.uint(0)], simnet.deployer);
380
457
  const jsonResult = (0, transactions_1.cvToJSON)(initialize);
381
458
  if (!jsonResult.value || !jsonResult.success) {
382
459
  throw new Error(`Failed to initialize the context for function: ${fn.name}.`);
383
460
  }
384
461
  });
385
- });
462
+ };
386
463
  exports.initializeClarityContext = initializeClarityContext;
387
464
  /**
388
465
  * Filter the System Under Test (`SUT`) functions from the map of all contract
@@ -406,7 +483,19 @@ const filterInvariantFunctions = (allFunctionsMap) => new Map(Array.from(allFunc
406
483
  functions.filter(({ access, name }) => access === "read_only" && name.startsWith("invariant-")),
407
484
  ]));
408
485
  class FalsifiedInvariantError extends Error {
409
- constructor(message) {
486
+ constructor(message, clarityError) {
410
487
  super(message);
488
+ this.clarityError = clarityError;
411
489
  }
412
490
  }
491
+ exports.FalsifiedInvariantError = FalsifiedInvariantError;
492
+ const emitInvariantRegressionTestHeader = (radio, targetContractName, seed, numRuns, dial, timestamp) => {
493
+ radio.emit("logMessage", shared_1.LOG_DIVIDER);
494
+ radio.emit("logMessage", `
495
+ Running ${(0, ansicolor_1.underline)(timestamp)} regression test for the ${targetContractName} contract with:
496
+
497
+ - Seed: ${seed}
498
+ - Runs: ${numRuns}
499
+ - Dial: ${dial !== null && dial !== void 0 ? dial : "none (default)"}
500
+ `);
501
+ };
package/dist/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stacks/rendezvous",
3
- "version": "0.13.1",
3
+ "version": "0.14.0",
4
4
  "description": "Meet your contract's vulnerabilities head-on.",
5
5
  "main": "app.js",
6
6
  "bin": {
@@ -31,17 +31,17 @@
31
31
  },
32
32
  "dependencies": {
33
33
  "@iarna/toml": "^2.2.5",
34
- "@stacks/clarinet-sdk": "^3.12.0",
35
- "@stacks/transactions": "^7.2.0",
34
+ "@stacks/clarinet-sdk": "^3.14.0",
35
+ "@stacks/transactions": "^7.3.1",
36
36
  "ansicolor": "^2.0.3",
37
- "fast-check": "^4.3.0",
38
- "yaml": "^2.8.1"
37
+ "fast-check": "^4.5.3",
38
+ "yaml": "^2.8.2"
39
39
  },
40
40
  "devDependencies": {
41
- "@stacks/clarinet-sdk-wasm": "^3.12.0",
41
+ "@stacks/clarinet-sdk-wasm": "^3.14.0",
42
42
  "@types/jest": "^30.0.0",
43
43
  "jest": "^30.2.0",
44
- "ts-jest": "^29.4.5",
44
+ "ts-jest": "^29.4.6",
45
45
  "typescript": "^5.9.3"
46
46
  }
47
47
  }
@@ -0,0 +1,97 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.loadFailures = exports.persistFailure = exports.getFailureFilePath = void 0;
4
+ const fs_1 = require("fs");
5
+ const path_1 = require("path");
6
+ /** Default configuration for persistence behavior. */
7
+ const DEFAULT_CONFIG = {
8
+ baseDir: ".rendezvous-regressions",
9
+ };
10
+ /**
11
+ * Gets the absolute file path for a contract's failure store.
12
+ * Uses contractId as filename (e.g., "ST1...ADDR.counter.json")
13
+ * @param contractId The contract identifier being tested
14
+ * @param baseDir The base directory for storing regression files
15
+ * @returns The file path for the failure store
16
+ */
17
+ const getFailureFilePath = (contractId, baseDir = DEFAULT_CONFIG.baseDir) => {
18
+ return (0, path_1.resolve)(baseDir, `${contractId}.json`);
19
+ };
20
+ exports.getFailureFilePath = getFailureFilePath;
21
+ /**
22
+ * Loads the failure store for a contract, or creates an empty one.
23
+ * @param contractId The contract identifier being tested
24
+ * @param baseDir The base directory for storing regression files
25
+ * @returns The failure store
26
+ */
27
+ const loadFailureStore = (contractId, baseDir = DEFAULT_CONFIG.baseDir) => {
28
+ const filePath = (0, exports.getFailureFilePath)(contractId, baseDir);
29
+ try {
30
+ const content = (0, fs_1.readFileSync)(filePath, "utf-8");
31
+ return JSON.parse(content);
32
+ }
33
+ catch (error) {
34
+ return { invariant: [], test: [] };
35
+ }
36
+ };
37
+ /**
38
+ * Saves the failure store for a contract.
39
+ * @param contractId The contract identifier being tested
40
+ * @param baseDir The base directory for storing regression files
41
+ * @param store The failure store to save
42
+ */
43
+ const saveFailureStore = (contractId, baseDir, store) => {
44
+ // Ensure the base directory exists.
45
+ (0, fs_1.mkdirSync)(baseDir, { recursive: true });
46
+ const filePath = (0, exports.getFailureFilePath)(contractId, baseDir);
47
+ (0, fs_1.writeFileSync)(filePath, JSON.stringify(store, null, 2), "utf-8");
48
+ };
49
+ /**
50
+ * Persists a test failure for future regression testing.
51
+ *
52
+ * @param runDetails The test run details from fast-check
53
+ * @param type The type of test that failed
54
+ * @param contractId The contract identifier being tested
55
+ * @param dial The path to the dialer file used for this test run
56
+ * @param config Optional configuration for persistence behavior
57
+ */
58
+ const persistFailure = (runDetails, type, contractId, dial, config) => {
59
+ const { baseDir } = Object.assign(Object.assign({}, DEFAULT_CONFIG), config);
60
+ // Load existing store.
61
+ const store = loadFailureStore(contractId, baseDir);
62
+ const record = {
63
+ seed: runDetails.seed,
64
+ dial: dial,
65
+ numRuns: runDetails.numRuns,
66
+ timestamp: Date.now(),
67
+ };
68
+ // Get the array for this test type.
69
+ const failures = store[type];
70
+ // Check if this seed already exists.
71
+ const seedExists = failures.some((f) => f.seed === record.seed);
72
+ if (seedExists) {
73
+ // Already recorded.
74
+ return;
75
+ }
76
+ // Add new failure.
77
+ failures.push(record);
78
+ // Sort the failures in descending order by timestamp.
79
+ failures.sort((a, b) => b.timestamp - a.timestamp);
80
+ // Save back to file.
81
+ saveFailureStore(contractId, baseDir, store);
82
+ };
83
+ exports.persistFailure = persistFailure;
84
+ /**
85
+ * Loads persisted failures for a given contract and test type.
86
+ *
87
+ * @param contractId The contract identifier
88
+ * @param type The type of test ("invariant" or "test")
89
+ * @param config Optional configuration
90
+ * @returns Array of failure records, or empty array if none exist
91
+ */
92
+ const loadFailures = (contractId, type, config) => {
93
+ const { baseDir } = Object.assign(Object.assign({}, DEFAULT_CONFIG), config);
94
+ const store = loadFailureStore(contractId, baseDir);
95
+ return store[type];
96
+ };
97
+ exports.loadFailures = loadFailures;
package/dist/property.js CHANGED
@@ -1,19 +1,31 @@
1
1
  "use strict";
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
2
11
  var __importDefault = (this && this.__importDefault) || function (mod) {
3
12
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
13
  };
5
14
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.isReturnTypeBoolean = exports.isParamsMatch = exports.isTestDiscardedInPlace = exports.checkProperties = void 0;
15
+ exports.PropertyTestError = exports.isReturnTypeBoolean = exports.isParamsMatch = exports.isTestDiscardedInPlace = exports.checkProperties = void 0;
7
16
  const fast_check_1 = __importDefault(require("fast-check"));
8
17
  const transactions_1 = require("@stacks/transactions");
9
18
  const heatstroke_1 = require("./heatstroke");
10
19
  const shared_1 = require("./shared");
11
20
  const ansicolor_1 = require("ansicolor");
12
21
  const traits_1 = require("./traits");
22
+ const persistence_1 = require("./persistence");
23
+ const path_1 = require("path");
13
24
  /**
14
25
  * Runs property-based tests on the target contract and logs the progress.
15
26
  * Reports the test results through a custom reporter.
16
27
  * @param simnet The simnet instance.
28
+ * @param resetSession Resets the simnet session to a clean state.
17
29
  * @param targetContractName The name of the target contract.
18
30
  * @param rendezvousList The list of contract IDs for each target contract.
19
31
  * @param rendezvousAllFunctions A map of all target contract IDs to their
@@ -22,27 +34,16 @@ const traits_1 = require("./traits");
22
34
  * @param runs The number of test runs.
23
35
  * @param bail Stop execution after the first failure and prevent further
24
36
  * shrinking.
37
+ * @param regr Whether to run regression tests only.
25
38
  * @param radio The custom logging event emitter.
26
39
  * @returns void
27
40
  */
28
- const checkProperties = (simnet, targetContractName, rendezvousList, rendezvousAllFunctions, seed, runs, bail, radio) => {
29
- const statistics = {
30
- test: {
31
- successful: new Map(),
32
- discarded: new Map(),
33
- failed: new Map(),
34
- },
35
- };
36
- const testContractId = rendezvousList[0];
41
+ const checkProperties = (simnet, resetSession, targetContractName, rendezvousList, rendezvousAllFunctions, seed, runs, bail, regr, radio) => __awaiter(void 0, void 0, void 0, function* () {
37
42
  // A map where the keys are the test contract identifiers and the values are
38
43
  // arrays of their test functions. This map will be used to access the test
39
44
  // functions for each test contract in the property-based testing routine.
40
45
  const testContractsTestFunctions = filterTestFunctions(rendezvousAllFunctions);
41
- for (const functionInterface of testContractsTestFunctions.get(testContractId)) {
42
- statistics.test.successful.set(functionInterface.name, 0);
43
- statistics.test.discarded.set(functionInterface.name, 0);
44
- statistics.test.failed.set(functionInterface.name, 0);
45
- }
46
+ const testContractId = rendezvousList[0];
46
47
  const allTestFunctions = testContractsTestFunctions.get(testContractId);
47
48
  const traitReferenceFunctionsCount = allTestFunctions.filter(traits_1.isTraitReferenceFunction).length;
48
49
  const traitReferenceMap = (0, traits_1.buildTraitReferenceMap)(allTestFunctions);
@@ -59,13 +60,7 @@ const checkProperties = (simnet, targetContractName, rendezvousList, rendezvousA
59
60
  : [];
60
61
  // If the tests contain trait reference functions without eligible trait
61
62
  // implementations, log a warning and filter out the functions.
62
- if (functionsMissingTraitImplementations.length > 0) {
63
- const functionList = functionsMissingTraitImplementations
64
- .map((fn) => ` - ${fn}`)
65
- .join("\n");
66
- radio.emit("logMessage", (0, ansicolor_1.yellow)(`\nWarning: The following test functions reference traits without eligible implementations and will be skipped:\n\n${functionList}\n`));
67
- radio.emit("logMessage", (0, ansicolor_1.yellow)(`Note: You can add contracts implementing traits either as project contracts or as requirements.\n`));
68
- }
63
+ emitMissingTraitWarning(radio, functionsMissingTraitImplementations);
69
64
  // Filter out test functions with missing trait implementations from the
70
65
  // enriched map.
71
66
  const executableTestContractsTestFunctions = new Map([
@@ -76,7 +71,6 @@ const checkProperties = (simnet, targetContractName, rendezvousList, rendezvousA
76
71
  .filter((functionInterface) => !functionsMissingTraitImplementations.includes(functionInterface.name)),
77
72
  ],
78
73
  ]);
79
- radio.emit("logMessage", `\nStarting property testing type for the ${targetContractName} contract...\n`);
80
74
  // Search for discard functions, for each test function. This map will
81
75
  // be used to pair the test functions with their corresponding discard
82
76
  // functions.
@@ -102,20 +96,91 @@ const checkProperties = (simnet, targetContractName, rendezvousList, rendezvousA
102
96
  if (hasDiscardFunctionErrors) {
103
97
  return;
104
98
  }
105
- const simnetAccounts = simnet.getAccounts();
106
- const eligibleAccounts = new Map([...simnetAccounts].filter(([key]) => key !== "faucet"));
107
- const simnetAddresses = Array.from(simnetAccounts.values());
108
99
  const testFunctions = (0, shared_1.getFunctionsListForContract)(executableTestContractsTestFunctions, testContractId);
109
100
  if ((testFunctions === null || testFunctions === void 0 ? void 0 : testFunctions.length) === 0) {
110
101
  radio.emit("logMessage", (0, ansicolor_1.red)(`No test functions found for the "${targetContractName}" contract.\n`));
111
102
  return;
112
103
  }
113
- const radioReporter = (runDetails) => {
114
- (0, heatstroke_1.reporter)(runDetails, radio, "test", statistics);
104
+ if (regr) {
105
+ // Run regression tests only.
106
+ radio.emit("logMessage", `Regressions loaded from: ${(0, path_1.resolve)((0, persistence_1.getFailureFilePath)(testContractId))}`);
107
+ radio.emit("logMessage", `Loading ${targetContractName} contract regressions...\n`);
108
+ const regressions = (0, persistence_1.loadFailures)(testContractId, "test");
109
+ radio.emit("logMessage", `Found ${(0, ansicolor_1.underline)(`${regressions.length} regressions`)} for the ${targetContractName} contract.\n`);
110
+ for (const regression of regressions) {
111
+ emitPropertyRegressionTestHeader(radio, targetContractName, regression.seed, regression.numRuns, regression.timestamp);
112
+ yield resetSession();
113
+ yield propertyTest({
114
+ simnet,
115
+ targetContractName,
116
+ testContractId,
117
+ // If the number of runs that failed is less than 100, set it to the
118
+ // default value of 100. If more runs were needed to reproduce the
119
+ // failure, use the number of runs that failed.
120
+ runs: regression.numRuns < 100 ? 100 : regression.numRuns,
121
+ seed: regression.seed,
122
+ bail,
123
+ radio,
124
+ testFunctions,
125
+ projectTraitImplementations,
126
+ testContractsPairedFunctions,
127
+ });
128
+ }
129
+ }
130
+ else {
131
+ // Run fresh tests using user-provided configuration.
132
+ radio.emit("logMessage", `Starting fresh round of property testing for the ${targetContractName} contract using user-provided configuration...\n`);
133
+ yield propertyTest({
134
+ simnet,
135
+ targetContractName,
136
+ testContractId,
137
+ runs,
138
+ seed,
139
+ bail,
140
+ radio,
141
+ testFunctions,
142
+ projectTraitImplementations,
143
+ testContractsPairedFunctions,
144
+ });
145
+ }
146
+ });
147
+ exports.checkProperties = checkProperties;
148
+ /**
149
+ * Runs a property test.
150
+ * @param config The union of the configuration and context for the property
151
+ * test.
152
+ * @returns A promise that resolves when the property test is complete.
153
+ */
154
+ const propertyTest = (config) => __awaiter(void 0, void 0, void 0, function* () {
155
+ const { simnet, targetContractName, testContractId, runs, seed, bail, radio, testFunctions, projectTraitImplementations, testContractsPairedFunctions, } = config;
156
+ // Derive accounts and addresses from simnet.
157
+ const simnetAccounts = simnet.getAccounts();
158
+ const eligibleAccounts = new Map([...simnetAccounts].filter(([key]) => key !== "faucet"));
159
+ const simnetAddresses = Array.from(simnetAccounts.values());
160
+ const statistics = {
161
+ test: {
162
+ successful: new Map(),
163
+ discarded: new Map(),
164
+ failed: new Map(),
165
+ },
115
166
  };
116
- fast_check_1.default.assert(fast_check_1.default.property(fast_check_1.default
167
+ for (const functionInterface of testFunctions) {
168
+ statistics.test.successful.set(functionInterface.name, 0);
169
+ statistics.test.discarded.set(functionInterface.name, 0);
170
+ statistics.test.failed.set(functionInterface.name, 0);
171
+ }
172
+ const radioReporter = (runDetails) => __awaiter(void 0, void 0, void 0, function* () {
173
+ (0, heatstroke_1.reporter)(runDetails, radio, "test", statistics);
174
+ // Persist failures for regression testing.
175
+ if (runDetails.failed) {
176
+ (0, persistence_1.persistFailure)(runDetails, "test", testContractId,
177
+ // No dialers in property-based testing.
178
+ undefined);
179
+ }
180
+ });
181
+ yield fast_check_1.default.assert(fast_check_1.default.asyncProperty(fast_check_1.default
117
182
  .record({
118
- testContractId: fast_check_1.default.constant(testContractId),
183
+ rendezvousContractId: fast_check_1.default.constant(testContractId),
119
184
  testCaller: fast_check_1.default.constantFrom(...eligibleAccounts.entries()),
120
185
  canMineBlocks: fast_check_1.default.boolean(),
121
186
  })
@@ -144,7 +209,7 @@ const checkProperties = (simnet, targetContractName, rendezvousList, rendezvousA
144
209
  })
145
210
  : fast_check_1.default.constant(0),
146
211
  })
147
- .map((burnBlocks) => (Object.assign(Object.assign({}, r), burnBlocks)))), (r) => {
212
+ .map((burnBlocks) => (Object.assign(Object.assign({}, r), burnBlocks)))), (r) => __awaiter(void 0, void 0, void 0, function* () {
148
213
  const selectedTestFunctionArgs = (0, shared_1.argsToCV)(r.selectedTestFunction, r.functionArgs);
149
214
  const printedTestFunctionArgs = r.functionArgs
150
215
  .map((arg) => {
@@ -160,9 +225,9 @@ const checkProperties = (simnet, targetContractName, rendezvousList, rendezvousA
160
225
  .join(" ");
161
226
  const [testCallerWallet, testCallerAddress] = r.testCaller;
162
227
  const discardFunctionName = testContractsPairedFunctions
163
- .get(r.testContractId)
228
+ .get(r.rendezvousContractId)
164
229
  .get(r.selectedTestFunction.name);
165
- const discarded = isTestDiscarded(discardFunctionName, selectedTestFunctionArgs, r.testContractId, simnet, testCallerAddress);
230
+ const discarded = isTestDiscarded(discardFunctionName, selectedTestFunctionArgs, r.rendezvousContractId, simnet, testCallerAddress);
166
231
  if (discarded) {
167
232
  statistics.test.discarded.set(r.selectedTestFunction.name, statistics.test.discarded.get(r.selectedTestFunction.name) + 1);
168
233
  radio.emit("logMessage", `₿ ${simnet.burnBlockHeight.toString().padStart(8)} ` +
@@ -177,7 +242,7 @@ const checkProperties = (simnet, targetContractName, rendezvousList, rendezvousA
177
242
  try {
178
243
  // If the function call results in a runtime error, the error will
179
244
  // be caught and logged as a test failure in the catch block.
180
- const { result: testFunctionCallResult } = simnet.callPublicFn(r.testContractId, r.selectedTestFunction.name, selectedTestFunctionArgs, testCallerAddress);
245
+ const { result: testFunctionCallResult } = simnet.callPublicFn(r.rendezvousContractId, r.selectedTestFunction.name, selectedTestFunctionArgs, testCallerAddress);
181
246
  const testFunctionCallResultJson = (0, transactions_1.cvToJSON)(testFunctionCallResult);
182
247
  const discardedInPlace = (0, exports.isTestDiscardedInPlace)(testFunctionCallResultJson);
183
248
  const testFunctionCallClarityResult = (0, transactions_1.cvToString)(testFunctionCallResult);
@@ -238,15 +303,38 @@ const checkProperties = (simnet, targetContractName, rendezvousList, rendezvousA
238
303
  throw error;
239
304
  }
240
305
  }
241
- }), {
306
+ })), {
242
307
  endOnFailure: bail,
243
308
  numRuns: runs,
244
309
  reporter: radioReporter,
245
310
  seed: seed,
246
311
  verbose: true,
247
312
  });
313
+ });
314
+ /**
315
+ * Emits a warning for test functions that reference traits without eligible
316
+ * implementations.
317
+ */
318
+ const emitMissingTraitWarning = (radio, functionNames) => {
319
+ if (functionNames.length === 0) {
320
+ return;
321
+ }
322
+ const functionList = functionNames.map((fn) => ` - ${fn}`).join("\n");
323
+ radio.emit("logMessage", (0, ansicolor_1.yellow)(`\nWarning: The following test functions reference traits without eligible implementations and will be skipped:\n\n${functionList}\n`));
324
+ radio.emit("logMessage", (0, ansicolor_1.yellow)(`Note: You can add contracts implementing traits either as project contracts or as requirements.\n`));
325
+ };
326
+ /**
327
+ * Emits a header for a regression test run with seed and run count information.
328
+ */
329
+ const emitPropertyRegressionTestHeader = (radio, targetContractName, seed, numRuns, timestamp) => {
330
+ radio.emit("logMessage", shared_1.LOG_DIVIDER);
331
+ radio.emit("logMessage", `
332
+ Running ${(0, ansicolor_1.underline)(timestamp)} regression test for the ${targetContractName} contract with:
333
+
334
+ - Seed: ${seed}
335
+ - Runs: ${numRuns}
336
+ `);
248
337
  };
249
- exports.checkProperties = checkProperties;
250
338
  const filterTestFunctions = (allFunctionsMap) => new Map(Array.from(allFunctionsMap, ([contractId, functions]) => [
251
339
  contractId,
252
340
  functions.filter((f) => f.access === "public" && f.name.startsWith("test-")),
@@ -326,3 +414,4 @@ class PropertyTestError extends Error {
326
414
  this.clarityError = clarityError;
327
415
  }
328
416
  }
417
+ exports.PropertyTestError = PropertyTestError;
package/dist/shared.js CHANGED
@@ -3,10 +3,12 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.getContractNameFromContractId = exports.argsToCV = exports.hexaString = exports.functionToArbitrary = exports.getFunctionsListForContract = exports.getFunctionsFromContractInterfaces = exports.getSimnetDeployerContractsInterfaces = void 0;
6
+ exports.getContractNameFromContractId = exports.argsToCV = exports.hexaString = exports.functionToArbitrary = exports.getFunctionsListForContract = exports.getFunctionsFromContractInterfaces = exports.getSimnetDeployerContractsInterfaces = exports.LOG_DIVIDER = void 0;
7
7
  const fast_check_1 = __importDefault(require("fast-check"));
8
8
  const transactions_1 = require("@stacks/transactions");
9
9
  const traits_1 = require("./traits");
10
+ /** 79 characters long divider for logging. */
11
+ exports.LOG_DIVIDER = "-------------------------------------------------------------------------------";
10
12
  /**
11
13
  * Retrieves the contract interfaces of the contracts deployed by a specific
12
14
  * deployer from the simnet instance.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stacks/rendezvous",
3
- "version": "0.13.1",
3
+ "version": "0.14.0",
4
4
  "description": "Meet your contract's vulnerabilities head-on.",
5
5
  "main": "app.js",
6
6
  "bin": {
@@ -31,17 +31,17 @@
31
31
  },
32
32
  "dependencies": {
33
33
  "@iarna/toml": "^2.2.5",
34
- "@stacks/clarinet-sdk": "^3.12.0",
35
- "@stacks/transactions": "^7.2.0",
34
+ "@stacks/clarinet-sdk": "^3.14.0",
35
+ "@stacks/transactions": "^7.3.1",
36
36
  "ansicolor": "^2.0.3",
37
- "fast-check": "^4.3.0",
38
- "yaml": "^2.8.1"
37
+ "fast-check": "^4.5.3",
38
+ "yaml": "^2.8.2"
39
39
  },
40
40
  "devDependencies": {
41
- "@stacks/clarinet-sdk-wasm": "^3.12.0",
41
+ "@stacks/clarinet-sdk-wasm": "^3.14.0",
42
42
  "@types/jest": "^30.0.0",
43
43
  "jest": "^30.2.0",
44
- "ts-jest": "^29.4.5",
44
+ "ts-jest": "^29.4.6",
45
45
  "typescript": "^5.9.3"
46
46
  }
47
47
  }