Skip to content

Instantly share code, notes, and snippets.

@moodmosaic
Last active March 16, 2022 10:43
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save moodmosaic/ec138d99346e11a3416b5ee216b20033 to your computer and use it in GitHub Desktop.
Save moodmosaic/ec138d99346e11a3416b5ee216b20033 to your computer and use it in GitHub Desktop.
Clarity contracts can be property-based tested
import { Clarinet, Tx, Chain, Account, types }
from 'https://deno.land/x/clarinet@v0.14.0/index.ts';
import { assert, assertEquals }
from 'https://deno.land/std@0.90.0/testing/asserts.ts';
import fc
from 'https://cdn.skypack.dev/fast-check';
Clarinet.test({
name: 'get-message returns none when write-sup is not called',
async fn(chain: Chain, accounts: Map<string, Account>) {
// Arrange
// Act
let results = [...accounts.values()].map(account => {
const who = types.principal(account.address);
const msg = chain.callReadOnlyFn(
'sup', 'get-message', [who], account.address);
return msg.result;
});
// Assert
assert(results.length > 0);
results.forEach(msg => msg.expectNone());
}
});
Clarinet.test({
name: 'write-sup returns expected string',
async fn(chain: Chain, accounts: Map<string, Account>) {
// Property-based test, runs 100 times by default.
fc.assert(fc.property(
// Generate pseudo-random 'lorem ipsum' string and a number.
fc.lorem(), fc.integer(1, 100), (lorem: string, integer: number) => {
// Arrange
const deployer = accounts.get('deployer')!;
const msg = types.utf8(lorem);
const stx = types.uint(integer);
// Act
const block = chain.mineBlock([
Tx.contractCall(
'sup', 'write-sup', [msg, stx], deployer.address)
]);
const result = block.receipts[0].result;
// Assert
result
.expectOk()
.expectAscii('Sup written successfully');
})
);
}
});
Clarinet.test({
name: 'write-sup increases total count by 1',
async fn(chain: Chain, accounts: Map<string, Account>) {
// Property-based test, runs 100 times by default.
fc.assert(fc.property(
// Generate pseudo-random 'lorem ipsum' string and a number.
fc.lorem(), fc.integer(1, 100), (lorem: string, integer: number) => {
// Arrange
const deployer = accounts.get('deployer')!;
let startCount = chain.callReadOnlyFn(
'sup', 'get-sups', [], deployer.address).result;
const msg = types.utf8(lorem);
const stx = types.uint(integer);
// Act
chain.mineBlock([
Tx.contractCall(
'sup', 'write-sup', [msg, stx], deployer.address)
]);
// Assert
const endCount = chain.callReadOnlyFn(
'sup', 'get-sups', [], deployer.address).result;
startCount = startCount.replace('u', ''); // u123 -> 123
endCount.expectUint(Number(startCount) + 1);
})
);
}
});
Clarinet.test({
name: 'sups are not specific to the tx-sender',
async fn(chain: Chain, accounts: Map<string, Account>) {
// Property-based test, runs 100 times by default.
fc.assert(fc.property(
// Generate pseudo-random 'lorem ipsum' string and a number.
fc.lorem(), fc.integer(1, 100), (lorem: string, integer: number) => {
// Arrange
const deployer = accounts.get('deployer')!;
let startCount = chain.callReadOnlyFn(
'sup', 'get-sups', [], deployer.address).result;
const msg = types.utf8(lorem);
const stx = types.uint(integer);
const addresses = [...accounts.values()]
.slice(0, -1)
.map(x => x.address);
// Act
const txs = addresses.map((_, i) =>
Tx.contractCall(
'sup', 'write-sup', [msg,stx], addresses[i]));
chain.mineBlock(txs);
let results = [...accounts.values()].map(account =>
chain.callReadOnlyFn(
'sup', 'get-sups', [], account.address).result
);
// Assert
assert(results.length > 0);
startCount = startCount.replace('u', ''); // u123 -> 123
const expectedCount = Number(startCount) + txs.length;
results.forEach(actualCount =>
actualCount.expectUint(expectedCount));
})
);
}
});
// @ts-nocheck
// https://github.com/dubzzz/fast-check/issues/2781
import { Clarinet, Tx, Chain, Account, types }
from 'https://deno.land/x/clarinet@v0.14.0/index.ts';
import fc
from 'https://cdn.skypack.dev/fast-check';
class Principal {
readonly value: string;
constructor(value: string) {
this.value = value;
}
toString(): string {
return types.principal(this.value);
}
asString(): string {
return this.value;
}
}
class Utf8 {
readonly value: string;
constructor(value: string) {
this.value = value;
}
toString(): string {
return types.utf8(this.value);
}
asString(): string {
return this.value;
}
}
class Uint {
readonly value: number;
constructor(value: number) {
this.value = value;
}
toString(): string {
return types.uint(this.value);
}
asString(): string {
return this.value.toString();
}
}
type Model = {
messages: Map<Principal, Utf8>
, supCount: number
};
type Real = {
chain: Chain
};
class WriteSupsCommand implements fc.Command<Model, Real> {
readonly who: Principal;
readonly msg: Utf8;
readonly fee: Uint;
constructor(who: Principal, msg: Utf8, fee: Uint) {
this.who = who;
this.msg = msg;
this.fee = fee;
}
check(_: Readonly<Model>): bool {
// Can always write a message
// in exchange for a STX fee.
return true;
}
run(m: Model, real: Real): void {
const block = real.chain.mineBlock([
Tx.contractCall(
'sup', 'write-sup', [this.msg.toString(), this.fee.toString()], this.who.asString())
]);
const actual = block.receipts[0].result;
actual
.expectOk()
.expectAscii('Sup written successfully');
m.messages[this.who] = this.msg;
m.supCount = m.supCount + 1;
console.log(
`for Ӿ${this.fee.asString()} ✎ ${this.msg.asString()}, by ${this.who.asString()}`);
}
}
class GetSupsCommand implements fc.Command<Model, Real> {
readonly who: Principal;
constructor(who: Principal) {
this.who = who;
}
check(_: Readonly<Model>): bool {
// Can always check total-sups.
return true;
}
run(m: Model, real: Real): void {
const msg = real.chain.callReadOnlyFn(
'sup', 'get-sups', [], this.who.asString());
const actual = msg.result;
actual.expectUint(m.supCount);
}
}
class GetMessageCommand implements fc.Command<Model, Real> {
readonly who: Principal;
constructor(who: Principal) {
this.who = who;
}
check(m: Readonly<Model>): bool {
// Can get message if there is one.
return m.messages[this.who] !== undefined;
}
run(m: Model, real: Real): void {
const msg = real.chain.callReadOnlyFn(
'sup', 'get-message', [this.who.toString()], this.who.asString());
const actual = msg.result;
actual
.expectSome()
.expectUtf8(m.messages[this.who].asString());
}
}
Clarinet.test({
name: 'sup.clar stateful property-based testing',
async fn(chain: Chain, accounts: Map<string, Account>) {
const commands = [
// Create a GetSupsCommand.
fc.constantFrom(...accounts.values())
.map(account =>
new GetSupsCommand(
new Principal(account.address))),
// Create a GetMessageCommand.
fc.constantFrom(...accounts.values())
.map(account =>
new GetMessageCommand(
new Principal(account.address))),
// Create a WriteSupsCommand.
fc.record({
who: fc.constantFrom(...accounts.values()).map(account => account.address)
, msg: fc.lorem()
, fee: fc.integer(10, 99)
}).map(r =>
new WriteSupsCommand(
new Principal(
r.who)
, new Utf8(
r.msg)
, new Uint(
r.fee)
)
)
];
const model = {
messages: new Map <Principal, Utf8>()
, supCount: 0
};
fc.assert(fc.property(
// Generate a random command sequence.
fc.commands(commands, { size: '+1' }), (commands) => {
const initialState = () => ({ model: model, real : { chain: chain } });
fc.modelRun(initialState, commands);
}), { numRuns: 10 }); // Run `numRuns` times.
}
});
@moodmosaic
Copy link
Author

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment