Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[DRAFT] Stateful testing setup #67

Draft
wants to merge 23 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 20 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
823cefc
Add fast-check dependency to package.json for stateful testing
moodmosaic Jul 14, 2024
e807d2b
Add types.ts for defining the Model type, and an ice-breaker invariant
moodmosaic Jul 14, 2024
a290542
Add initial stateful test for BNS-V2 state interactions
moodmosaic Jul 14, 2024
54c1071
Add GetOwnerNone invariant
BowTiedRadone Jul 16, 2024
919de04
Add GetBnsFromIdNone invariant
BowTiedRadone Jul 16, 2024
58cb131
Add GetPrimaryNameNone invariant
BowTiedRadone Jul 16, 2024
1690083
Add GetNamespacePropertiesErr invariant
BowTiedRadone Jul 16, 2024
1643b1a
Add GetNamespacePrice invariant
BowTiedRadone Jul 16, 2024
452f704
Add CanNamespaceBeRegisteredTrue invariant
BowTiedRadone Jul 17, 2024
2ae47bb
Add NamespacePreorder invariant
BowTiedRadone Jul 17, 2024
3f63649
Add NamespaceReveal invariant
BowTiedRadone Jul 19, 2024
bf96e57
Add GetTokenUri invariant
BowTiedRadone Jul 24, 2024
c4ddb64
Add GetIdFromBnsNone
BowTiedRadone Jul 24, 2024
c0f3ee9
Add GetBnsInfoNone invariant
BowTiedRadone Jul 24, 2024
3c44adf
Add checks for burned uSTX and TTLs
BowTiedRadone Jul 24, 2024
ea102b0
Add MngNamePreorder invariant
BowTiedRadone Jul 25, 2024
487be2d
Add name to namePreorders key creation
BowTiedRadone Jul 26, 2024
a8106d8
Use ST27PT00YS01KBAEEETAH45C1H46C3FMJR31SN2S3.TESTNET-BNS-V2 in tests…
moodmosaic Jul 26, 2024
13fe63d
Implement weighted invariants logic
BowTiedRadone Jul 26, 2024
c091c86
Update `numRuns` and `size` to fit vitest timeout
BowTiedRadone Jul 26, 2024
cb28312
Update logging function to log live
BowTiedRadone Jul 31, 2024
a79383f
Fix NamespaceReveal check condition
BowTiedRadone Jul 31, 2024
61935de
Add NamespaceLaunch invariant
BowTiedRadone Jul 31, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion Clarinet.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ description = ''
authors = []
telemetry = true
cache_dir = './.cache'
requirements = []

[[project.requirements]]
contract_id = 'ST27PT00YS01KBAEEETAH45C1H46C3FMJR31SN2S3.TESTNET-BNS-V2'
[contracts.BNS-V2]
path = 'contracts/BNS-V2.clar'
clarity_version = 2
Expand Down
18 changes: 18 additions & 0 deletions deployments/default.simnet-plan.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,24 @@ genesis:
plan:
batches:
- id: 0
transactions:
- emulated-contract-publish:
contract-name: commission-trait
emulated-sender: ST27PT00YS01KBAEEETAH45C1H46C3FMJR31SN2S3
path: "./.cache/requirements/ST27PT00YS01KBAEEETAH45C1H46C3FMJR31SN2S3.commission-trait.clar"
clarity-version: 1
- emulated-contract-publish:
contract-name: sip-09
emulated-sender: ST27PT00YS01KBAEEETAH45C1H46C3FMJR31SN2S3
path: "./.cache/requirements/ST27PT00YS01KBAEEETAH45C1H46C3FMJR31SN2S3.sip-09.clar"
clarity-version: 1
- emulated-contract-publish:
contract-name: TESTNET-BNS-V2
emulated-sender: ST27PT00YS01KBAEEETAH45C1H46C3FMJR31SN2S3
path: "./.cache/requirements/ST27PT00YS01KBAEEETAH45C1H46C3FMJR31SN2S3.TESTNET-BNS-V2.clar"
clarity-version: 1
epoch: "2.1"
- id: 1
transactions:
- emulated-contract-publish:
contract-name: commission-trait
Expand Down
37 changes: 37 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
"@hirosystems/clarinet-sdk": "^2.5.0",
"@stacks/transactions": "^6.12.0",
"chokidar-cli": "^3.0.0",
"fast-check": "^3.20.0",
"typescript": "^5.3.3",
"vite": "^5.1.4",
"vitest": "^1.3.1",
Expand Down
17 changes: 17 additions & 0 deletions tests/BNS-V2.helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -129,3 +129,20 @@ export const invalidNameBuffSalt = createHash160Name(
namespaceBuff,
saltBuff
);

export const prettyConsoleLog = (...items: (string | undefined)[]) => {
// Ensure we only render up to the first 10 items for brevity.
const renderItems = items.slice(0, 10);
const columnWidth = 30; // Standard width for each column after the first two.
const halfColumns = Math.floor(columnWidth / 2);

// Pad columns to their widths: half for the first two, full for the rest.
const prettyPrint = renderItems.map((content, index) =>
// Check if the index is less than 2 (i.e., first two items).
content
? (index < 2 ? content.padEnd(halfColumns + 2) : content.padEnd(columnWidth))
: (index < 2 ? "".padEnd(halfColumns) : "".padEnd(columnWidth))
);

console.log(prettyPrint.join(""));
};
63 changes: 63 additions & 0 deletions tests/BNS-V2.stateful.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import fc from "fast-check";
import { it } from "vitest";

import { GetLastTokenId } from "./state/GetLastTokenId.ts";
import { GetOwnerNone } from "./state/GetOwnerNone.ts";
import { GetBnsFromIdNone } from "./state/GetBnsFromIdNone.ts";
import { GetPrimaryNameNone } from "./state/GetPrimaryNameNone.ts";
import { GetNamespacePropertiesErr } from "./state/GetNamespacePropertiesErr.ts";
import { GetNamespacePrice } from "./state/GetNamespacePrice.ts";
import { CanNamespaceBeRegisteredTrue } from "./state/CanNamespaceBeRegistered.ts";
import { NamespacePreorder } from "./state/NamespacePreorder.ts";
import { NamespaceReveal } from "./state/NamespaceReveal.ts";
import { GetBnsInfoNone } from "./state/GetBnsInfoNone.ts";
import { MngNamePreorder } from "./state/MngNamePreorder.ts";

it("executes BNS-V2 state interactions", async () => {
const excludedAccounts = ["faucet", "deployer"];
const filteredAccounts = new Map(
[...simnet.getAccounts()].filter(([key]) =>
!excludedAccounts.includes(key)
),
);

const model = {
burnBlockHeight: 0,
lastTokenId: 0,
owners: new Map(),
indexToName: new Map(),
nameToIndex: new Map(),
nameProperties: new Map(),
namespaces: new Map(),
namespaceProperties: new Map(),
namespaceSinglePreorder: new Map(),
nameSinglePreorder: new Map(),
namespacePreorders: new Map(),
namePreorders: new Map(),
};

const invariants = [
GetLastTokenId(filteredAccounts),
GetOwnerNone(filteredAccounts),
GetBnsFromIdNone(filteredAccounts),
GetPrimaryNameNone(filteredAccounts),
GetNamespacePropertiesErr(filteredAccounts),
GetNamespacePrice(filteredAccounts),
GetBnsInfoNone(filteredAccounts),
CanNamespaceBeRegisteredTrue(filteredAccounts),
NamespacePreorder(filteredAccounts),
{ arbitrary: MngNamePreorder(filteredAccounts), weight: 3 },
NamespaceReveal(filteredAccounts, model),
];

fc.assert(
fc.property(
fc.array(fc.oneof(...invariants), { size: "medium" }),
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This size and number of runs below are an approximation, right?

(cmds) => {
const state = () => ({ model, real: simnet });
fc.modelRun(state, cmds);
},
),
{ numRuns: 100, verbose: fc.VerbosityLevel.VeryVerbose },
);
});
57 changes: 57 additions & 0 deletions tests/state/CanNamespaceBeRegistered.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import fc from "fast-check";
import { Model } from "./types";
import { Simnet } from "@hirosystems/clarinet-sdk";
import { expect } from "vitest";
import { Cl } from "@stacks/transactions";
import { encoder, prettyConsoleLog } from "../BNS-V2.helper";

const NAMESPACE_LAUNCHABILITY_TTL = 52595;

export const CanNamespaceBeRegisteredTrue = (accounts: Map<string, string>) =>
fc
.record({
sender: fc.constantFrom(...accounts),
namespace: fc.string({ maxLength: 20 }),
})
.map((r) => ({
check: (model: Readonly<Model>) => {
const namespace = model.namespaces.get(r.namespace);

if (
!namespace ||
(!namespace.launchedAt &&
model.burnBlockHeight + 1 >
(namespace?.revealedAt || 0) + NAMESPACE_LAUNCHABILITY_TTL) ||
!namespace.revealedAt
) {
return true;
}

return false;
},
run: (_model: Model, real: Simnet) => {
const [wallet, address] = r.sender;
const namespaceBuff = encoder.encode(r.namespace);
// Act
const { result: canRegisterNamespaceResponse } = real.callReadOnlyFn(
"ST27PT00YS01KBAEEETAH45C1H46C3FMJR31SN2S3.TESTNET-BNS-V2",
"can-namespace-be-registered",
[Cl.buffer(namespaceBuff)],
address,
);

// Assert
expect(canRegisterNamespaceResponse).toBeOk(Cl.bool(true));

prettyConsoleLog(
"Ӿ tx-sender",
wallet,
"✓",
"can-namespace-be-registered",
`namespace: "${r.namespace}"`,
"response: (ok true)",
);
},
toString: () =>
`can-namespace-be-registered namespace "${r.namespace}" response (ok true)`,
}));
44 changes: 44 additions & 0 deletions tests/state/GetBnsFromIdNone.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import fc from "fast-check";
import { Model } from "./types";
import { Simnet } from "@hirosystems/clarinet-sdk";
import { expect } from "vitest";
import { Cl } from "@stacks/transactions";
import { prettyConsoleLog } from "../BNS-V2.helper";

export const GetBnsFromIdNone = (accounts: Map<string, string>) =>
fc
.record({
sender: fc.constantFrom(...accounts),
tokenId: fc.integer({ min: 1 }),
})
.map((r) => ({
check: (model: Readonly<Model>) => {
return (
!(model.lastTokenId >= r.tokenId) ||
!(model.indexToName.has(r.tokenId))
);
},
run: (_model: Model, real: Simnet) => {
const [wallet, address] = r.sender;
// Act
const { result: bnsOptional } = real.callReadOnlyFn(
"ST27PT00YS01KBAEEETAH45C1H46C3FMJR31SN2S3.TESTNET-BNS-V2",
"get-bns-from-id",
[Cl.uint(r.tokenId)],
address,
);

// Assert
expect(bnsOptional).toBeNone();

prettyConsoleLog(
"Ӿ tx-sender",
wallet,
"✓",
"get-bns-from-id",
`token-id: ${r.tokenId}`,
"bns: none",
);
},
toString: () => `get-bns-from-id token-id ${r.tokenId} bns none`,
}));
55 changes: 55 additions & 0 deletions tests/state/GetBnsInfoNone.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import fc from "fast-check";
import { Model } from "./types";
import { Simnet } from "@hirosystems/clarinet-sdk";
import { expect } from "vitest";
import { Cl } from "@stacks/transactions";
import { encoder, prettyConsoleLog } from "../BNS-V2.helper";

export const GetBnsInfoNone = (accounts: Map<string, string>) =>
fc
.record({
sender: fc.constantFrom(...accounts),
namespace: fc.string({ maxLength: 20 }),
name: fc.string({ maxLength: 48 }),
})
.map((r) => ({
check: (model: Readonly<Model>) => {
return !model.nameProperties.has(
JSON.stringify({
name: r.name,
namespace: r.namespace,
})
);
},
run: (_model: Readonly<Model>, real: Simnet) => {
const [wallet, address] = r.sender;
const nameBuff = encoder.encode(r.name);
const namespaceBuff = encoder.encode(r.namespace);

// Act
const { result: getBnsInfoResponse } = real.callReadOnlyFn(
"ST27PT00YS01KBAEEETAH45C1H46C3FMJR31SN2S3.TESTNET-BNS-V2",
"get-bns-info",
[
// (name (buff 48))
Cl.buffer(nameBuff),
// (namespace (buff 20))
Cl.buffer(namespaceBuff),
],
address
);

expect(getBnsInfoResponse).toBeNone();

prettyConsoleLog(
"Ӿ tx-sender",
wallet,
"✓",
"get-bns-info",
`name: "${r.name}"`,
`namespace: "${r.namespace}"`,
`bns info: none`
);
},
toString: () => `get-bns-info name ${r.name} namespace ${r.namespace}`,
}));
52 changes: 52 additions & 0 deletions tests/state/GetIdFromBnsNone.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import fc from "fast-check";
import { Model } from "./types";
import { Simnet } from "@hirosystems/clarinet-sdk";
import { expect } from "vitest";
import { Cl } from "@stacks/transactions";
import { encoder, prettyConsoleLog } from "../BNS-V2.helper";

export const getIdFromBnsNone = (accounts: Map<string, string>) =>
fc
.record({
sender: fc.constantFrom(...accounts),
namespace: fc.string({ maxLength: 20 }),
name: fc.string({ maxLength: 48 }),
})
.map((r) => ({
check: (model: Readonly<Model>) => {
return !model.nameToIndex.has(
JSON.stringify({ name: r.name, namespace: r.namespace })
);
},
run: (_model: Readonly<Model>, real: Simnet) => {
const [wallet, address] = r.sender;
const nameBuff = encoder.encode(r.name);
const namespaceBuff = encoder.encode(r.namespace);

// Act
const { result: getIdFromBnsResponse } = real.callReadOnlyFn(
"ST27PT00YS01KBAEEETAH45C1H46C3FMJR31SN2S3.TESTNET-BNS-V2",
"get-bns-info",
[
// (name (buff 48))
Cl.buffer(nameBuff),
// (namespace (buff 20))
Cl.buffer(namespaceBuff),
],
address
);

expect(getIdFromBnsResponse).toBeNone();

prettyConsoleLog(
"Ӿ tx-sender",
wallet,
"✓",
"get-id-from-bns",
`name: "${r.name}"`,
`namespace: "${r.namespace}"`,
`id: none`
);
},
toString: () => `get-id-from-bns name ${r.name} namespace ${r.namespace}`,
}));
Loading