Skip to content

Commit

Permalink
more docs about check (#872)
Browse files Browse the repository at this point in the history
  • Loading branch information
xlc authored Jan 7, 2025
1 parent 9e83573 commit 8f25c37
Show file tree
Hide file tree
Showing 3 changed files with 302 additions and 6 deletions.
114 changes: 114 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,120 @@ return {
}
```

## Testing with @acala-network/chopsticks-testing

The `@acala-network/chopsticks-testing` package provides powerful utilities for testing blockchain data, making it easier to write and maintain tests for your Substrate-based chain. It offers features like data redaction, event filtering, snapshot testing, and XCM message checking.

### Installation

```bash
npm install --save-dev @acala-network/chopsticks-testing
```

### Basic Usage

```typescript
import { withExpect, setupContext } from '@acala-network/chopsticks-testing';
import { expect } from 'vitest'; // or jest, or other test runners

// Create testing utilities with your test runner's expect function
const { check, checkEvents, checkSystemEvents, checkUmp, checkHrmp } = withExpect(expect);

describe('My Chain Tests', () => {
it('should process events correctly', async () => {
const network = setupContext({ endpoint: 'wss://polkadot-rpc.dwellir.com' });
// Check and redact system events
await checkSystemEvents(network)
.redact({ number: 2, hash: true })
.toMatchSnapshot('system events');

// Filter specific events
await checkSystemEvents(network, 'balances', { section: 'system', method: 'ExtrinsicSuccess' })
.toMatchSnapshot('filtered events');
});
});
```

### Data Redaction

The testing package provides powerful redaction capabilities to make your tests more stable and focused on what matters:

```typescript
await check(someData)
.redact({
number: 2, // Redact numbers with 2 decimal precision
hash: true, // Redact 32-byte hex values
hex: true, // Redact any hex values
address: true, // Redact base58 addresses
redactKeys: /hash/, // Redact values of keys matching regex
removeKeys: /time/ // Remove keys matching regex entirely
})
.toMatchSnapshot('redacted data');
```

### Event Filtering

Filter and check specific blockchain events:

```typescript
// Check all balances events
await checkSystemEvents(api, 'balances')
.toMatchSnapshot('balances events');

// Check specific event type
await checkSystemEvents(api, { section: 'system', method: 'ExtrinsicSuccess' })
.toMatchSnapshot('successful extrinsics');

// Multiple filters
await checkSystemEvents(api,
'balances',
{ section: 'system', method: 'ExtrinsicSuccess' }
)
.toMatchSnapshot('filtered events');
```

### XCM Testing

Test XCM (Cross-Chain Message) functionality:

```typescript
// Check UMP (Upward Message Passing) messages
await checkUmp(api)
.redact()
.toMatchSnapshot('upward messages');

// Check HRMP (Horizontal Relay-routed Message Passing) messages
await checkHrmp(api)
.redact()
.toMatchSnapshot('horizontal messages');
```

### Data Format Conversion

Convert data to different formats for testing:

```typescript
// Convert to human-readable format
await check(data).toHuman().toMatchSnapshot('human readable');

// Convert to hex format
await check(data).toHex().toMatchSnapshot('hex format');

// Convert to JSON format (default)
await check(data).toJson().toMatchSnapshot('json format');
```

### Custom Transformations

Apply custom transformations to your data:

```typescript
await check(data)
.map(value => value.filter(item => item.amount > 1000))
.redact()
.toMatchSnapshot('filtered and redacted');
```

## Testing big migrations

When testing migrations with lots of keys, you may want to fetch and cache some storages.
Expand Down
132 changes: 126 additions & 6 deletions packages/testing/src/check.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,30 +3,70 @@ import { Codec } from '@polkadot/types/types'

type CodecOrArray = Codec | Codec[]

/**
* Processes a Codec or array of Codecs with a given transformation function
* @param codec - Single Codec or array of Codecs to process
* @param fn - Transformation function to apply to each Codec
* @returns Processed value(s)
*/
const processCodecOrArray = (codec: CodecOrArray, fn: (c: Codec) => any) =>
Array.isArray(codec) ? codec.map(fn) : fn(codec)

/**
* Converts Codec data to human-readable format
* @param codec - Codec data to convert
*/
const toHuman = (codec: CodecOrArray) => processCodecOrArray(codec, (c) => c?.toHuman?.() ?? c)

/**
* Converts Codec data to hexadecimal format
* @param codec - Codec data to convert
*/
const toHex = (codec: CodecOrArray) => processCodecOrArray(codec, (c) => c?.toHex?.() ?? c)

/**
* Converts Codec data to JSON format
* @param codec - Codec data to convert
*/
const toJson = (codec: CodecOrArray) => processCodecOrArray(codec, (c) => c?.toJSON?.() ?? c)

/**
* Defines a filter for blockchain events
* Can be either a string (section name) or an object with method and section
*/
export type EventFilter = string | { method: string; section: string }

/**
* Configuration options for data redaction
*/
export type RedactOptions = {
number?: boolean | number // precision
hash?: boolean // 32 byte hex
hex?: boolean // any hex with 0x prefix
address?: boolean // base58 address
redactKeys?: RegExp // redact value for keys matching regex
removeKeys?: RegExp // filter out keys matching regex
/** Redact numbers with optional precision */
number?: boolean | number
/** Redact 32-byte hex values */
hash?: boolean
/** Redact any hex values with 0x prefix */
hex?: boolean
/** Redact base58 addresses */
address?: boolean
/** Regex pattern for keys whose values should be redacted */
redactKeys?: RegExp
/** Regex pattern for keys that should be removed */
removeKeys?: RegExp
}

/**
* Function type for test assertions
*/
export type ExpectFn = (value: any) => {
toMatchSnapshot: (msg?: string) => void
toMatch(value: any, msg?: string): void
toMatchObject(value: any, msg?: string): void
}

/**
* Main class for checking and validating blockchain data
* Provides a fluent interface for data transformation, filtering, and assertion
*/
export class Checker {
readonly #expectFn: ExpectFn
readonly #value: any
Expand All @@ -36,32 +76,49 @@ export class Checker {
#message: string | undefined
#redactOptions: RedactOptions | undefined

/**
* Creates a new Checker instance
* @param expectFn - Function for making test assertions
* @param value - Value to check
* @param message - Optional message for assertions
*/
constructor(expectFn: ExpectFn, value: any, message?: string) {
this.#expectFn = expectFn
this.#value = value
this.#message = message
}

/** Convert the checked value to human-readable format */
toHuman() {
this.#format = 'human'
return this
}

/** Convert the checked value to hexadecimal format */
toHex() {
this.#format = 'hex'
return this
}

/** Convert the checked value to JSON format */
toJson() {
this.#format = 'json'
return this
}

/**
* Set a message for test assertions
* @param message - Message to use in assertions
*/
message(message: string) {
this.#message = message
return this
}

/**
* Filter blockchain events based on provided filters
* @param filters - Event filters to apply
*/
filterEvents(...filters: EventFilter[]) {
this.toHuman()
this.#pipeline.push((value) => {
Expand All @@ -83,6 +140,10 @@ export class Checker {
return this
}

/**
* Apply redaction rules to the checked value
* @param options - Redaction options
*/
redact(options: RedactOptions = { number: 2, hash: true }) {
this.#redactOptions = {
...this.#redactOptions,
Expand Down Expand Up @@ -171,15 +232,27 @@ export class Checker {
return process(value)
}

/**
* Add a transformation function to the processing pipeline
* @param fn - Transformation function
*/
map(fn: (value: any) => any) {
this.#pipeline.push(fn)
return this
}

/**
* Apply a function to the current Checker instance
* @param fn - Function to apply
*/
pipe(fn?: (value: Checker) => Checker) {
return fn ? fn(this) : this
}

/**
* Get the final processed value
* @returns Processed value after applying all transformations
*/
async value() {
let value = await this.#value

Expand All @@ -204,20 +277,44 @@ export class Checker {
return value
}

/**
* Assert that the value matches a snapshot
* @param msg - Optional message for the assertion
*/
async toMatchSnapshot(msg?: string) {
return this.#expectFn(await this.value()).toMatchSnapshot(msg ?? this.#message)
}

/**
* Assert that the value matches an expected value
* @param value - Expected value
* @param msg - Optional message for the assertion
*/
async toMatch(value: any, msg?: string) {
return this.#expectFn(await this.value()).toMatch(value, msg ?? this.#message)
}

/**
* Assert that the value matches an expected object structure
* @param value - Expected object structure
* @param msg - Optional message for the assertion
*/
async toMatchObject(value: any, msg?: string) {
return this.#expectFn(await this.value()).toMatchObject(value, msg ?? this.#message)
}
}

/**
* Creates a set of checking utilities with a provided assertion function
* @param expectFn - Function for making test assertions
* @returns Object containing various checking utilities
*/
export const withExpect = (expectFn: ExpectFn) => {
/**
* Create a new Checker instance
* @param value - Value to check
* @param msg - Optional message for assertions
*/
const check = (value: any, msg?: string) => {
if (value instanceof Checker) {
if (msg) {
Expand All @@ -230,21 +327,39 @@ export const withExpect = (expectFn: ExpectFn) => {

type Api = { api: ApiPromise }

/**
* Check blockchain events with filtering and redaction
* @param events - Events to check
* @param filters - Event filters to apply
*/
const checkEvents = ({ events }: { events: Promise<Codec[] | Codec> }, ...filters: EventFilter[]) =>
check(events, 'events')
.filterEvents(...filters)
.redact()

/**
* Check system events with filtering and redaction
* @param api - Polkadot API instance
* @param filters - Event filters to apply
*/
const checkSystemEvents = ({ api }: Api, ...filters: EventFilter[]) =>
check(api.query.system.events(), 'system events')
.filterEvents(...filters)
.redact()

/**
* Check Upward Message Passing (UMP) messages
* @param api - Polkadot API instance
*/
const checkUmp = ({ api }: Api) =>
check(api.query.parachainSystem.upwardMessages(), 'ump').map((value) =>
api.createType('Vec<XcmVersionedXcm>', value).toJSON(),
)

/**
* Check HRMP (Horizontal Relay-routed Message Passing) messages
* @param api - Polkadot API instance
*/
const checkHrmp = ({ api }: Api) =>
check(api.query.parachainSystem.hrmpOutboundMessages(), 'hrmp').map((value) =>
(value as any[]).map(({ recipient, data }) => ({
Expand All @@ -253,6 +368,11 @@ export const withExpect = (expectFn: ExpectFn) => {
})),
)

/**
* Check a value in hexadecimal format
* @param value - Value to check
* @param msg - Optional message for assertions
*/
const checkHex = (value: any, msg?: string) => check(value, msg).toHex()

return {
Expand Down
Loading

0 comments on commit 8f25c37

Please sign in to comment.