Last active
March 16, 2022 10:43
-
-
Save moodmosaic/ec138d99346e11a3416b5ee216b20033 to your computer and use it in GitHub Desktop.
Clarity contracts can be property-based tested
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)); | |
}) | |
); | |
} | |
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// @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. | |
} | |
}); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
2022-03-05 / Clarity property-based testing primer