@stacks/rendezvous 0.7.4 → 0.9.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/README.md CHANGED
@@ -56,6 +56,7 @@ npx rv <path-to-clarinet-project> <contract-name> <type>
56
56
  - `--path` – The path to use for the replay functionality.
57
57
  - `--runs` – The number of test iterations to use for exercising the contracts.
58
58
  (default: `100`)
59
+ - `--bail` – Stop after the first failure.
59
60
  - `--dial` – The path to a JavaScript file containing custom pre- and
60
61
  post-execution functions (dialers).
61
62
 
package/dist/app.js CHANGED
@@ -94,10 +94,12 @@ const helpMessage = `
94
94
  --seed - The seed to use for the replay functionality.
95
95
  --path - The path to use for the replay functionality.
96
96
  --runs - The runs to use for iterating over the tests. Default: 100.
97
+ --bail - Stop after the first failure.
97
98
  --dial – The path to a JavaScript file containing custom pre- and post-execution functions (dialers).
98
99
  --help - Show the help message.
99
100
  `;
100
- const parseOptionalArgument = (argName) => {
101
+ const parseBooleanOption = (argName) => process.argv.slice(4).includes(`--${argName}`);
102
+ const parseOption = (argName) => {
101
103
  var _a;
102
104
  return (_a = process.argv
103
105
  .find((arg, idx) => idx >= 4 && arg.toLowerCase().startsWith(`--${argName}`))) === null || _a === void 0 ? void 0 : _a.split("=")[1];
@@ -140,24 +142,28 @@ function main() {
140
142
  const manifestPath = (0, path_1.join)(manifestDir, (0, exports.getManifestFileName)(manifestDir, sutContractName));
141
143
  radio.emit("logMessage", `Using manifest path: ${manifestPath}`);
142
144
  radio.emit("logMessage", `Target contract: ${sutContractName}`);
143
- const seed = parseInt(parseOptionalArgument("seed"), 10) || undefined;
145
+ const seed = parseInt(parseOption("seed"), 10) || undefined;
144
146
  if (seed !== undefined) {
145
147
  radio.emit("logMessage", `Using seed: ${seed}`);
146
148
  }
147
- const path = parseOptionalArgument("path") || undefined;
149
+ const path = parseOption("path") || undefined;
148
150
  if (path !== undefined) {
149
151
  radio.emit("logMessage", `Using path: ${path}`);
150
152
  }
151
- const runs = parseInt(parseOptionalArgument("runs"), 10) || undefined;
153
+ const runs = parseInt(parseOption("runs"), 10) || undefined;
152
154
  if (runs !== undefined) {
153
155
  radio.emit("logMessage", `Using runs: ${runs}`);
154
156
  }
157
+ const bail = parseBooleanOption("bail");
158
+ if (bail) {
159
+ radio.emit("logMessage", `Bailing on first failure.`);
160
+ }
155
161
  /**
156
162
  * The path to the dialer file. The dialer file allows the user to register
157
163
  * custom pre and post-execution JavaScript functions to be executed before
158
164
  * and after the public function calls during invariant testing.
159
165
  */
160
- const dialPath = parseOptionalArgument("dial") || undefined;
166
+ const dialPath = parseOption("dial") || undefined;
161
167
  if (dialPath !== undefined) {
162
168
  radio.emit("logMessage", `Using dial path: ${dialPath}`);
163
169
  }
@@ -181,11 +187,11 @@ function main() {
181
187
  // If "test", call `checkProperties` for property-based testing.
182
188
  switch (type) {
183
189
  case "invariant": {
184
- yield (0, invariant_1.checkInvariants)(simnet, sutContractName, rendezvousList, rendezvousAllFunctions, seed, path, runs, dialerRegistry, radio);
190
+ yield (0, invariant_1.checkInvariants)(simnet, sutContractName, rendezvousList, rendezvousAllFunctions, seed, path, runs, bail, dialerRegistry, radio);
185
191
  break;
186
192
  }
187
193
  case "test": {
188
- (0, property_1.checkProperties)(simnet, sutContractName, rendezvousList, rendezvousAllFunctions, seed, path, runs, radio);
194
+ (0, property_1.checkProperties)(simnet, sutContractName, rendezvousList, rendezvousAllFunctions, seed, path, runs, bail, radio);
189
195
  break;
190
196
  }
191
197
  }
@@ -20,7 +20,7 @@ const shared_1 = require("./shared");
20
20
  * @param type The type of test that failed: invariant or property.
21
21
  * @returns void
22
22
  */
23
- function reporter(runDetails, radio, type) {
23
+ function reporter(runDetails, radio, type, statistics) {
24
24
  var _a, _b;
25
25
  if (runDetails.failed) {
26
26
  // Report general run data.
@@ -79,4 +79,115 @@ function reporter(runDetails, radio, type) {
79
79
  else {
80
80
  radio.emit("logMessage", (0, ansicolor_1.green)(`\nOK, ${type === "invariant" ? "invariants" : "properties"} passed after ${runDetails.numRuns} runs.\n`));
81
81
  }
82
+ reportStatistics(statistics, type, radio);
83
+ radio.emit("logMessage", "\n");
82
84
  }
85
+ const ARROW = "->";
86
+ const SUCCESS_SYMBOL = "+";
87
+ const FAIL_SYMBOL = "-";
88
+ const WARN_SYMBOL = "!";
89
+ /**
90
+ * Reports execution statistics in a tree-like format.
91
+ * @param statistics The statistics object containing test execution data.
92
+ * @param type The type of test being reported.
93
+ * @param radio The event emitter for logging messages.
94
+ */
95
+ function reportStatistics(statistics, type, radio) {
96
+ if ((type === "invariant" && (!statistics.invariant || !statistics.sut)) ||
97
+ (type === "test" && !statistics.test)) {
98
+ radio.emit("logMessage", "No statistics available for this run");
99
+ return;
100
+ }
101
+ radio.emit("logMessage", `\nEXECUTION STATISTICS\n`);
102
+ switch (type) {
103
+ case "invariant": {
104
+ radio.emit("logMessage", "│ PUBLIC FUNCTION CALLS");
105
+ radio.emit("logMessage", "│");
106
+ radio.emit("logMessage", `├─ ${SUCCESS_SYMBOL} SUCCESSFUL`);
107
+ logAsTree(Object.fromEntries(statistics.sut.successful), radio);
108
+ radio.emit("logMessage", "│");
109
+ radio.emit("logMessage", `├─ ${FAIL_SYMBOL} IGNORED`);
110
+ logAsTree(Object.fromEntries(statistics.sut.failed), radio);
111
+ radio.emit("logMessage", "│");
112
+ radio.emit("logMessage", "│ INVARIANT CHECKS");
113
+ radio.emit("logMessage", "│");
114
+ radio.emit("logMessage", `├─ ${SUCCESS_SYMBOL} PASSED`);
115
+ logAsTree(Object.fromEntries(statistics.invariant.successful), radio);
116
+ radio.emit("logMessage", "│");
117
+ radio.emit("logMessage", `└─ ${FAIL_SYMBOL} FAILED`);
118
+ logAsTree(Object.fromEntries(statistics.invariant.failed), radio, {
119
+ isLastSection: true,
120
+ });
121
+ radio.emit("logMessage", "\nLEGEND:\n");
122
+ radio.emit("logMessage", " SUCCESSFUL calls executed and advanced the test");
123
+ radio.emit("logMessage", " IGNORED calls failed but did not affect the test");
124
+ radio.emit("logMessage", " PASSED invariants maintained system integrity");
125
+ radio.emit("logMessage", " FAILED invariants indicate contract vulnerabilities");
126
+ if (computeTotalCount(statistics.invariant.failed) > 0) {
127
+ radio.emit("logFailure", "\n! FAILED invariants require immediate attention as they indicate that your contract can enter an invalid state under certain conditions.");
128
+ }
129
+ break;
130
+ }
131
+ case "test": {
132
+ radio.emit("logMessage", "│ PROPERTY TEST CALLS");
133
+ radio.emit("logMessage", "│");
134
+ radio.emit("logMessage", `├─ ${SUCCESS_SYMBOL} PASSED`);
135
+ logAsTree(Object.fromEntries(statistics.test.successful), radio);
136
+ radio.emit("logMessage", "│");
137
+ radio.emit("logMessage", `├─ ${WARN_SYMBOL} DISCARDED`);
138
+ logAsTree(Object.fromEntries(statistics.test.discarded), radio);
139
+ radio.emit("logMessage", "│");
140
+ radio.emit("logMessage", `└─ ${FAIL_SYMBOL} FAILED`);
141
+ logAsTree(Object.fromEntries(statistics.test.failed), radio, {
142
+ isLastSection: true,
143
+ });
144
+ radio.emit("logMessage", "\nLEGEND:\n");
145
+ radio.emit("logMessage", " PASSED properties verified for given inputs");
146
+ radio.emit("logMessage", " DISCARDED skipped due to invalid preconditions");
147
+ radio.emit("logMessage", " FAILED property violations or unexpected behavior");
148
+ if (computeTotalCount(statistics.test.failed) > 0) {
149
+ radio.emit("logFailure", "\n! FAILED tests indicate that your function properties don't hold for all inputs. Review the counterexamples above for debugging.");
150
+ }
151
+ break;
152
+ }
153
+ }
154
+ }
155
+ /**
156
+ * Displays a tree structure of data.
157
+ * @param tree The object to display as a tree.
158
+ * @param radio The event emitter for logging messages.
159
+ * @param options Configuration options for tree display.
160
+ */
161
+ function logAsTree(tree, radio, options = {}) {
162
+ const { isLastSection = false, baseIndent = " " } = options;
163
+ const printTree = (node, indent = baseIndent, isLastParent = true, radio) => {
164
+ const keys = Object.keys(node);
165
+ keys.forEach((key, index) => {
166
+ const isLast = index === keys.length - 1;
167
+ const connector = isLast ? "└─" : "├─";
168
+ const nextIndent = indent + (isLastParent ? " " : "│ ");
169
+ const leadingChar = isLastSection ? " " : "│";
170
+ if (typeof node[key] === "object" && node[key] !== null) {
171
+ radio.emit("logMessage", `${leadingChar} ${indent}${connector} ${ARROW} ${key}`);
172
+ printTree(node[key], nextIndent, isLast, radio);
173
+ }
174
+ else {
175
+ const count = node[key];
176
+ radio.emit("logMessage", `${leadingChar} ${indent}${connector} ${key}: x${count}`);
177
+ }
178
+ });
179
+ };
180
+ printTree(tree, baseIndent, true, radio);
181
+ }
182
+ /**
183
+ * Computes the total number of failures from a failure map.
184
+ * @param failedMap Map containing failure counts by test name
185
+ * @returns The sum of all failure counts
186
+ */
187
+ const computeTotalCount = (failedMap) => {
188
+ let totalFailures = 0;
189
+ for (const count of failedMap.values()) {
190
+ totalFailures += count;
191
+ }
192
+ return totalFailures;
193
+ };
package/dist/invariant.js CHANGED
@@ -31,22 +31,42 @@ const dialer_1 = require("./dialer");
31
31
  * @param seed The seed for reproducible invariant testing.
32
32
  * @param path The path for reproducible invariant testing.
33
33
  * @param runs The number of test runs.
34
+ * @param bail Stop execution after the first failure and prevent further
35
+ * shrinking.
34
36
  * @param dialerRegistry The custom dialer registry.
35
37
  * @param radio The custom logging event emitter.
36
38
  * @returns void
37
39
  */
38
- const checkInvariants = (simnet, targetContractName, rendezvousList, rendezvousAllFunctions, seed, path, runs, dialerRegistry, radio) => __awaiter(void 0, void 0, void 0, function* () {
40
+ const checkInvariants = (simnet, targetContractName, rendezvousList, rendezvousAllFunctions, seed, path, runs, bail, dialerRegistry, radio) => __awaiter(void 0, void 0, void 0, function* () {
41
+ const statistics = {
42
+ sut: {
43
+ successful: new Map(),
44
+ failed: new Map(),
45
+ },
46
+ invariant: {
47
+ successful: new Map(),
48
+ failed: new Map(),
49
+ },
50
+ };
39
51
  // A map where the keys are the Rendezvous identifiers and the values are
40
52
  // arrays of their SUT (System Under Test) functions. This map will be used
41
53
  // to access the SUT functions for each Rendezvous contract afterwards.
42
54
  const rendezvousSutFunctions = filterSutFunctions(rendezvousAllFunctions);
55
+ // The Rendezvous identifier is the first one in the list. Only one contract
56
+ // can be fuzzed at a time.
57
+ const rendezvousContractId = rendezvousList[0];
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
+ }
43
62
  // A map where the keys are the Rendezvous identifiers and the values are
44
63
  // arrays of their invariant functions. This map will be used to access the
45
64
  // invariant functions for each Rendezvous contract afterwards.
46
65
  const rendezvousInvariantFunctions = filterInvariantFunctions(rendezvousAllFunctions);
47
- // The Rendezvous identifier is the first one in the list. Only one contract
48
- // can be fuzzed at a time.
49
- const rendezvousContractId = rendezvousList[0];
66
+ for (const functionInterface of rendezvousInvariantFunctions.get(rendezvousContractId)) {
67
+ statistics.invariant.successful.set(functionInterface.name, 0);
68
+ statistics.invariant.failed.set(functionInterface.name, 0);
69
+ }
50
70
  const traitReferenceSutFunctions = rendezvousSutFunctions
51
71
  .get(rendezvousContractId)
52
72
  .filter((fn) => (0, traits_1.isTraitReferenceFunction)(fn));
@@ -93,7 +113,7 @@ const checkInvariants = (simnet, targetContractName, rendezvousList, rendezvousA
93
113
  return;
94
114
  }
95
115
  const radioReporter = (runDetails) => {
96
- (0, heatstroke_1.reporter)(runDetails, radio, "invariant");
116
+ (0, heatstroke_1.reporter)(runDetails, radio, "invariant", statistics);
97
117
  };
98
118
  yield fast_check_1.default.assert(fast_check_1.default.asyncProperty(fast_check_1.default
99
119
  .record({
@@ -176,6 +196,7 @@ const checkInvariants = (simnet, targetContractName, rendezvousList, rendezvousA
176
196
  // call during the run.
177
197
  const selectedFunctionClarityResult = (0, transactions_1.cvToString)(functionCall.result);
178
198
  if (functionCallResultJson.success) {
199
+ statistics.sut.successful.set(selectedFunction.name, statistics.sut.successful.get(selectedFunction.name) + 1);
179
200
  localContext[r.rendezvousContractId][selectedFunction.name]++;
180
201
  simnet.callPublicFn(r.rendezvousContractId, "update-context", [
181
202
  transactions_1.Cl.stringAscii(selectedFunction.name),
@@ -204,6 +225,7 @@ const checkInvariants = (simnet, targetContractName, rendezvousList, rendezvousA
204
225
  }
205
226
  else {
206
227
  // Function call failed.
228
+ statistics.sut.failed.set(selectedFunction.name, statistics.sut.failed.get(selectedFunction.name) + 1);
207
229
  radio.emit("logMessage", (0, ansicolor_1.dim)(`₿ ${simnet.burnBlockHeight.toString().padStart(8)} ` +
208
230
  `Ӿ ${simnet.blockHeight.toString().padStart(8)} ` +
209
231
  `${sutCallerWallet} ` +
@@ -255,6 +277,8 @@ const checkInvariants = (simnet, targetContractName, rendezvousList, rendezvousA
255
277
  const invariantCallResultJson = (0, transactions_1.cvToJSON)(invariantCallResult);
256
278
  const invariantCallClarityResult = (0, transactions_1.cvToString)(invariantCallResult);
257
279
  if (invariantCallResultJson.value === true) {
280
+ statistics.invariant.successful.set(r.selectedInvariant.name, statistics.invariant.successful.get(r.selectedInvariant.name) +
281
+ 1);
258
282
  radio.emit("logMessage", `₿ ${simnet.burnBlockHeight.toString().padStart(8)} ` +
259
283
  `Ӿ ${simnet.blockHeight.toString().padStart(8)} ` +
260
284
  `${(0, ansicolor_1.dim)(invariantCallerWallet)} ` +
@@ -265,6 +289,7 @@ const checkInvariants = (simnet, targetContractName, rendezvousList, rendezvousA
265
289
  (0, ansicolor_1.green)(invariantCallClarityResult));
266
290
  }
267
291
  else {
292
+ statistics.invariant.failed.set(r.selectedInvariant.name, statistics.invariant.failed.get(r.selectedInvariant.name) + 1);
268
293
  radio.emit("logMessage", (0, ansicolor_1.red)(`₿ ${simnet.burnBlockHeight.toString().padStart(8)} ` +
269
294
  `Ӿ ${simnet.blockHeight.toString().padStart(8)} ` +
270
295
  `${invariantCallerWallet} ` +
@@ -298,11 +323,12 @@ const checkInvariants = (simnet, targetContractName, rendezvousList, rendezvousA
298
323
  simnet.mineEmptyBurnBlocks(r.burnBlocks);
299
324
  }
300
325
  })), {
301
- verbose: true,
326
+ endOnFailure: bail,
327
+ numRuns: runs,
328
+ path: path,
302
329
  reporter: radioReporter,
303
330
  seed: seed,
304
- path: path,
305
- numRuns: runs,
331
+ verbose: true,
306
332
  });
307
333
  });
308
334
  exports.checkInvariants = checkInvariants;
package/dist/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stacks/rendezvous",
3
- "version": "0.7.4",
3
+ "version": "0.9.0",
4
4
  "description": "Meet your contract's vulnerabilities head-on.",
5
5
  "main": "app.js",
6
6
  "bin": {
package/dist/property.js CHANGED
@@ -21,15 +21,29 @@ const traits_1 = require("./traits");
21
21
  * @param seed The seed for reproducible property-based tests.
22
22
  * @param path The path for reproducible property-based tests.
23
23
  * @param runs The number of test runs.
24
+ * @param bail Stop execution after the first failure and prevent further
25
+ * shrinking.
24
26
  * @param radio The custom logging event emitter.
25
27
  * @returns void
26
28
  */
27
- const checkProperties = (simnet, targetContractName, rendezvousList, rendezvousAllFunctions, seed, path, runs, radio) => {
29
+ const checkProperties = (simnet, targetContractName, rendezvousList, rendezvousAllFunctions, seed, path, runs, bail, radio) => {
30
+ const statistics = {
31
+ test: {
32
+ successful: new Map(),
33
+ discarded: new Map(),
34
+ failed: new Map(),
35
+ },
36
+ };
28
37
  const testContractId = rendezvousList[0];
29
38
  // A map where the keys are the test contract identifiers and the values are
30
39
  // arrays of their test functions. This map will be used to access the test
31
40
  // functions for each test contract in the property-based testing routine.
32
41
  const testContractsTestFunctions = filterTestFunctions(rendezvousAllFunctions);
42
+ for (const functionInterface of testContractsTestFunctions.get(testContractId)) {
43
+ statistics.test.successful.set(functionInterface.name, 0);
44
+ statistics.test.discarded.set(functionInterface.name, 0);
45
+ statistics.test.failed.set(functionInterface.name, 0);
46
+ }
33
47
  const traitReferenceFunctions = testContractsTestFunctions
34
48
  .get(testContractId)
35
49
  .filter((fn) => (0, traits_1.isTraitReferenceFunction)(fn));
@@ -79,7 +93,7 @@ const checkProperties = (simnet, targetContractName, rendezvousList, rendezvousA
79
93
  return;
80
94
  }
81
95
  const radioReporter = (runDetails) => {
82
- (0, heatstroke_1.reporter)(runDetails, radio, "test");
96
+ (0, heatstroke_1.reporter)(runDetails, radio, "test", statistics);
83
97
  };
84
98
  fast_check_1.default.assert(fast_check_1.default.property(fast_check_1.default
85
99
  .record({
@@ -132,6 +146,7 @@ const checkProperties = (simnet, targetContractName, rendezvousList, rendezvousA
132
146
  .get(r.selectedTestFunction.name);
133
147
  const discarded = isTestDiscarded(discardFunctionName, selectedTestFunctionArgs, r.testContractId, simnet, testCallerAddress);
134
148
  if (discarded) {
149
+ statistics.test.discarded.set(r.selectedTestFunction.name, statistics.test.discarded.get(r.selectedTestFunction.name) + 1);
135
150
  radio.emit("logMessage", `₿ ${simnet.burnBlockHeight.toString().padStart(8)} ` +
136
151
  `Ӿ ${simnet.blockHeight.toString().padStart(8)} ` +
137
152
  `${(0, ansicolor_1.dim)(testCallerWallet)} ` +
@@ -149,6 +164,7 @@ const checkProperties = (simnet, targetContractName, rendezvousList, rendezvousA
149
164
  const discardedInPlace = (0, exports.isTestDiscardedInPlace)(testFunctionCallResultJson);
150
165
  const testFunctionCallClarityResult = (0, transactions_1.cvToString)(testFunctionCallResult);
151
166
  if (discardedInPlace) {
167
+ statistics.test.discarded.set(r.selectedTestFunction.name, statistics.test.discarded.get(r.selectedTestFunction.name) + 1);
152
168
  radio.emit("logMessage", `₿ ${simnet.burnBlockHeight.toString().padStart(8)} ` +
153
169
  `Ӿ ${simnet.blockHeight.toString().padStart(8)} ` +
154
170
  `${(0, ansicolor_1.dim)(testCallerWallet)} ` +
@@ -161,6 +177,8 @@ const checkProperties = (simnet, targetContractName, rendezvousList, rendezvousA
161
177
  else if (!discardedInPlace &&
162
178
  testFunctionCallResultJson.success &&
163
179
  testFunctionCallResultJson.value.value === true) {
180
+ statistics.test.successful.set(r.selectedTestFunction.name, statistics.test.successful.get(r.selectedTestFunction.name) +
181
+ 1);
164
182
  radio.emit("logMessage", `₿ ${simnet.burnBlockHeight.toString().padStart(8)} ` +
165
183
  `Ӿ ${simnet.blockHeight.toString().padStart(8)} ` +
166
184
  `${(0, ansicolor_1.dim)(testCallerWallet)} ` +
@@ -174,6 +192,7 @@ const checkProperties = (simnet, targetContractName, rendezvousList, rendezvousA
174
192
  }
175
193
  }
176
194
  else {
195
+ statistics.test.failed.set(r.selectedTestFunction.name, statistics.test.failed.get(r.selectedTestFunction.name) + 1);
177
196
  // The function call did not result in (ok true) or (ok false).
178
197
  // Either the test failed or the test function returned an
179
198
  // unexpected value i.e. `(ok 1)`.
@@ -202,11 +221,12 @@ const checkProperties = (simnet, targetContractName, rendezvousList, rendezvousA
202
221
  }
203
222
  }
204
223
  }), {
205
- verbose: true,
224
+ endOnFailure: bail,
225
+ numRuns: runs,
226
+ path: path,
206
227
  reporter: radioReporter,
207
228
  seed: seed,
208
- path: path,
209
- numRuns: runs,
229
+ verbose: true,
210
230
  });
211
231
  };
212
232
  exports.checkProperties = checkProperties;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stacks/rendezvous",
3
- "version": "0.7.4",
3
+ "version": "0.9.0",
4
4
  "description": "Meet your contract's vulnerabilities head-on.",
5
5
  "main": "app.js",
6
6
  "bin": {