diff --git a/Makefile b/Makefile index 6186a1b..48f1509 100644 --- a/Makefile +++ b/Makefile @@ -20,9 +20,9 @@ mock-handler: test: build @echo "Running runner unit tests..." - go test -v ./runner/... + go test ./runner/... @echo "Running conformance tests with mock handler..." - $(RUNNER_BIN) --handler $(MOCK_HANDLER_BIN) -vv + $(RUNNER_BIN) --handler $(MOCK_HANDLER_BIN) suite-validate: @echo "Validating testdata against the suite schema..." diff --git a/cmd/runner/main.go b/cmd/runner/main.go index bef31e2..0a796b6 100644 --- a/cmd/runner/main.go +++ b/cmd/runner/main.go @@ -66,24 +66,35 @@ func main() { totalPassed := 0 totalFailed := 0 totalTests := 0 + loggedTimeout := false + failedTestLines := make([]string, 0) - for _, testFile := range testFiles { - fmt.Printf("\n=== Running test suite ===\n") - + for suiteIdx, testFile := range testFiles { // Load test suite from embedded FS suite, err := runner.LoadTestSuiteFromFS(testdata.FS, testFile) if err != nil { fmt.Fprintf(os.Stderr, "Error loading test suite: %v\n", err) continue } + totalTests += len(suite.Tests) + + // Check if context is already cancelled + if ctx.Err() != nil { + if !loggedTimeout { + fmt.Printf("Skipped remaining %d test suite(s) because total execution timeout (%v) was exceeded!\n", + len(testFiles)-suiteIdx, *timeout) + loggedTimeout = true + } + continue + } // Run suite result := testRunner.RunTestSuite(ctx, *suite, verbosity) - printResults(suite, result) + printResults(suite, result, verbosity) + failedTestLines = append(failedTestLines, collectFailedTestLines(suite, result)...) totalPassed += result.PassedTests totalFailed += result.FailedTests - totalTests += result.TotalTests // Close handler after stateful suites to prevent state leaks. // A new handler process will be spawned on-demand when the next request is sent. @@ -92,29 +103,50 @@ func main() { } } + if len(failedTestLines) > 0 { + fmt.Printf("\n" + strings.Repeat("=", 60) + "\n") + fmt.Printf("FAILED TESTS\n") + fmt.Printf(strings.Repeat("=", 60) + "\n") + for _, line := range failedTestLines { + fmt.Printf("%s\n", line) + } + } + fmt.Printf("\n" + strings.Repeat("=", 60) + "\n") fmt.Printf("TOTAL SUMMARY\n") fmt.Printf(strings.Repeat("=", 60) + "\n") fmt.Printf("Total Tests: %d\n", totalTests) fmt.Printf("Passed: %d\n", totalPassed) fmt.Printf("Failed: %d\n", totalFailed) + fmt.Printf("Skipped: %d\n", totalTests-(totalPassed+totalFailed)) fmt.Printf(strings.Repeat("=", 60) + "\n") - if totalFailed > 0 { + if totalTests > totalPassed { os.Exit(1) } } -func printResults(suite *runner.TestSuite, result runner.TestResult) { - fmt.Printf("\nTest Suite: %s (%s)\n", result.SuiteTitle, result.SuiteFileName) +func printResults(suite *runner.TestSuite, result runner.TestResult, verbosity runner.VerbosityLevel) { + if verbosity < runner.VerbosityOnFailure && result.FailedTests == 0 { + return + } + + fmt.Printf("=== %s (%s) ===\n", result.SuiteTitle, result.SuiteFileName) if suite.Description != "" { - fmt.Printf("Description: %s\n", suite.Description) + fmt.Printf("%s\n", suite.Description) } - fmt.Printf("Total: %d, Passed: %d, Failed: %d\n\n", result.TotalTests, result.PassedTests, result.FailedTests) + totalSkipped := result.TotalTests - (result.PassedTests + result.FailedTests) + fmt.Printf("Total: %d, Passed: %d, Failed: %d, Skipped: %d\n", result.TotalTests, result.PassedTests, + result.FailedTests, totalSkipped) for i, tr := range result.TestResults { - status := "✓" - if !tr.Passed { + var status string + if tr.Passed { + if verbosity < runner.VerbosityAlways { + continue + } + status = "✓" + } else { status = "✗" } @@ -131,3 +163,14 @@ func printResults(suite *runner.TestSuite, result runner.TestResult) { fmt.Printf("\n") } + +func collectFailedTestLines(suite *runner.TestSuite, result runner.TestResult) []string { + lines := make([]string, 0, result.FailedTests) + for i, tr := range result.TestResults { + if tr.Passed { + continue + } + lines = append(lines, fmt.Sprintf("%s %s (%s)", result.SuiteFileName, tr.TestID, suite.Tests[i].Description)) + } + return lines +} diff --git a/docs/handler-spec.md b/docs/handler-spec.md index 23765a5..018f914 100644 --- a/docs/handler-spec.md +++ b/docs/handler-spec.md @@ -81,7 +81,7 @@ Many operations return objects (contexts, blocks, chains, etc.) that must persis // Request {"id": "1", "method": "btck_context_create", "params": {...}, "ref": "$ctx1"} // Response -{"id": "1", "result": {"ref": "$ctx1"}, "error": null} +{"id": "1", "result": {"ref": "$ctx1"}} // Handler action: registry["$ctx1"] = created_context_ptr ``` @@ -91,12 +91,68 @@ Many operations return objects (contexts, blocks, chains, etc.) that must persis // Request {"id": "2", "method": "btck_chainstate_manager_create", "params": {"context": {"ref": "$ctx1"}}, "ref": "$csm1"} // Response -{"id": "2", "result": {"ref": "$csm1"}, "error": null} +{"id": "2", "result": {"ref": "$csm1"}} // Handler action: Extract ref from params.context, look up registry["$ctx1"], create manager, store as registry["$csm1"] ``` **Implementation**: Handlers must maintain a registry (map of reference names to object pointers) throughout their lifetime. Objects remain alive until explicitly destroyed or handler exit. +## Callback Interfaces + +Some kernel operations trigger callbacks — notification events and validation interface events — that fire synchronously during the operation. The protocol requires handlers to implement two callback interfaces as queueing objects: each maintains an internal invocation queue, records every callback firing into it, and exposes that queue via a drain method. This gives the runner a way to assert which callbacks fired, in what order, and what values they carried. + +### Creation and Wiring + +Callback interface objects are binding-level objects — not direct C kernel API handles, but wired into the kernel at context creation time. The protocol introduces dedicated methods for creating them — `notification_callbacks_create` and `validation_interface_callbacks_create` — deliberately omitting the `btck_` prefix to make clear they have no direct C API counterpart. They follow the same ref/registry pattern as kernel objects. Interface refs are passed as optional params to `btck_context_create` to wire them in: + +The `callbacks` param is required and must list at least one callback name. The interface only queues invocations for the listed callbacks; any unlisted callback fires at the C level but is silently discarded. + +```json +// Request +{"id": "1", "method": "notification_callbacks_create", "params": {"callbacks": ["btck_NotifyBlockTip"]}, "ref": "$notif"} +// Response +{"id": "1", "result": {"ref": "$notif"}} + +// Request +{"id": "2", "method": "btck_context_create", "params": {"chain_parameters": {...}, "notifications": {"ref": "$notif"}}, "ref": "$ctx"} +// Response +{"id": "2", "result": {"ref": "$ctx"}} +``` + +Neither interface has a matching destroy method. Both are cleaned up implicitly when the chainstate manager associated with the wired context is destroyed. + +### Invocation Recording + +When a callback fires, the interface implementation must, before the callback returns, append an invocation record to its queue, preserving firing order. The record identifies the callback that fired and carries its arguments, which may be primitives (e.g. an enum like `btck_SynchronizationState`, or a number like `verification_progress`) or objects (e.g. a `btck_BlockTreeEntry`). At this point object arguments sit on the queue and are not yet directly referenceable by the runner — they only become referenceable later, when [drain](#drain) registers them in the registry. + +What the record carries for each object argument depends on how the kernel passes it. For arguments the kernel passes as owned copies, carry the copy directly. For arguments passed as views (pointers or references into kernel-owned memory), the handler must judge whether the underlying memory will remain valid long enough — at least until the last request that may reference this ref: if it will (e.g. a `btck_BlockTreeEntry` view that remains valid for the chainstate manager's lifetime), carry the view directly; if it will not — because the view points to a stack-local or other short-lived storage — copy the object before the callback returns and carry the copy instead. This decision is made at callback time; drain has no visibility into argument lifetimes. + +### Drain + +Drain is what gives the runner access to the queued invocations: the firing order, each callback's primitive argument values, and a way to reference its object arguments in follow-up requests. The drain methods (`notification_callbacks_drain`, `validation_callbacks_drain`) are protocol-level methods with no C API counterpart. Each takes an interface ref, registers every queued object under a deterministic ref name, flushes the queue, and returns the invocation records in firing order with each object argument resolved to `{"ref": ""}`. Until drain is called, callback-produced refs are not in the registry, so the runner must call drain before referencing any callback-produced object in a follow-up request. + +The registered ref name must follow this pattern, so the runner can predict it and use it in follow-up assertions (computing it at callback time is often more straightforward, but the spec only requires that the object end up registered under this name on drain): + +``` +$___ +``` + +- ``: ref name of the interface object, without the leading `$` +- ``: ordinal position of this invocation in the queue since the last drain, counting across all callback types, starting at 1 +- ``: exact C typedef name from `bitcoinkernel.h` +- ``: C parameter name of the object argument + +Examples: `$notif_1_btck_NotifyBlockTip_entry`, `$vi_1_btck_ValidationInterfaceBlockChecked_block`, `$vi_2_btck_ValidationInterfaceBlockConnected_entry`. + +Both callback families are synchronous — all callbacks triggered during a kernel operation complete before the operation returns — so a drain issued after a kernel operation (e.g. `btck_chainstate_manager_process_block`) will always see the complete set of records for that call. An empty array means no callbacks fired since the last drain. + +```json +// Request +{"id": "3", "method": "notification_callbacks_drain", "params": {"interface": {"ref": "$notif"}}} +// Response +{"id": "3", "result": [{"callback": "btck_NotifyBlockTip", "state": "btck_SynchronizationState_POST_INIT", "entry": {"ref": "$notif_1_btck_NotifyBlockTip_entry"}, "verification_progress": 1.0}]} +``` + ## Test Suites Overview The conformance tests are organized into suites, each testing a specific aspect of the Bitcoin Kernel bindings. Test files are located in [`../testdata/`](../testdata/). @@ -209,6 +265,11 @@ Test cases where the verification operation fails to determine validity of the s Sets up blocks, checks chain state, and verifies that the chain tip changes as expected after a reorg scenario. +### Callback Interfaces +**File:** [`callbacks.json`](../testdata/callbacks.json) + +Registers both a notification callbacks interface and a validation interface, wires both into a context, drains init-time invocations after chainstate manager creation, processes a block, drains both interfaces, and asserts on notification entry height, validation mode, and validation entry height. + ## Method Reference Handler method definitions are maintained in the auto-generated [methods-spec.md](./methods-spec.md). diff --git a/docs/methods-spec.md b/docs/methods-spec.md index b515920..8f75776 100644 --- a/docs/methods-spec.md +++ b/docs/methods-spec.md @@ -318,6 +318,45 @@ Gets the block hash from a block tree entry. --- +#### `btck_block_tree_entry_get_height` + +Gets the height of a block tree entry. + +**Parameters:** +- `block_tree_entry` (reference, required): Block tree entry reference + +**Result:** Integer - Height of the block tree entry + +**Error:** `null` (cannot return error) + +--- + +#### `btck_block_validation_state_destroy` + +Destroys a block validation state. + +**Parameters:** +- `state` (reference, required): Block validation state reference to destroy + +**Result:** `null` (void operation) + +**Error:** `null` (cannot return error) + +--- + +#### `btck_block_validation_state_get_validation_mode` + +Gets the validation mode of a block validation state. + +**Parameters:** +- `state` (reference, required): Block validation state reference + +**Result:** Validation mode of the block validation state + +**Error:** `null` (cannot return error) + +--- + #### `btck_chain_contains` Checks whether a block tree entry is part of the active chain. @@ -425,6 +464,8 @@ Creates a context with specified chain parameters. - `btck_ChainType_TESTNET_4` - `btck_ChainType_SIGNET` - `btck_ChainType_REGTEST` +- `notifications` (reference, optional): Notification callbacks interface to wire into this context +- `validation_interface` (reference, optional): Validation interface to wire into this context **Result:** Reference - Contains the created context (e.g., `{"ref": "$context"}`) @@ -878,3 +919,77 @@ Returns the txid as raw bytes encoded as hex. **Result:** String - Hex-encoded raw 32-byte txid **Error:** `null` (cannot return error) + +--- + +#### `notification_callbacks_create` + +Creates a notification callbacks interface. + +**Parameters:** +- `callbacks` (array of strings, required): Allowed values: + - `btck_NotifyBlockTip` + - `btck_NotifyHeaderTip` + - `btck_NotifyProgress` + - `btck_NotifyWarningSet` + - `btck_NotifyWarningUnset` + - `btck_NotifyFlushError` + - `btck_NotifyFatalError` + +**Result:** Reference - Contains the created notification callbacks interface ref + +**Error:** `null` (cannot return error) + +--- + +#### `notification_callbacks_drain` + +Drains all queued callback invocation records from a notification callbacks interface. + +**Parameters:** +- `interface` (reference, required): Notification callbacks interface reference + +**Result:** Array of invocation records, one per callback fired since the last drain. Each record is an object with a `callback` field identifying the type, plus type-specific fields: +- `btck_NotifyBlockTip`: `state` (SynchronizationState string), `entry` (reference to a BlockTreeEntry), `verification_progress` (number) +- `btck_NotifyHeaderTip`: `state` (SynchronizationState string), `height` (integer), `timestamp` (integer), `presync` (boolean) +- `btck_NotifyProgress`: `title` (string), `percent` (integer), `resumable` (boolean) +- `btck_NotifyWarningSet`: `warning` (Warning string), `message` (string) +- `btck_NotifyWarningUnset`: `warning` (Warning string) +- `btck_NotifyFlushError`: `message` (string) +- `btck_NotifyFatalError`: `message` (string) + +**Error:** `null` (cannot return error) + +--- + +#### `validation_callbacks_drain` + +Drains all queued callback invocation records from a validation interface. + +**Parameters:** +- `interface` (reference, required): Validation interface reference + +**Result:** Array of invocation records, one per callback fired since the last drain. Each record is an object with a `callback` field identifying the type, plus type-specific fields: +- `btck_ValidationInterfaceBlockChecked`: `block` (reference to an owned Block copy), `state` (reference to an owned BlockValidationState copy) +- `btck_ValidationInterfacePoWValidBlock`: `block` (reference to an owned Block copy), `entry` (reference to a BlockTreeEntry view) +- `btck_ValidationInterfaceBlockConnected`: `block` (reference to an owned Block copy), `entry` (reference to a BlockTreeEntry view) +- `btck_ValidationInterfaceBlockDisconnected`: `block` (reference to an owned Block copy), `entry` (reference to a BlockTreeEntry view) + +**Error:** `null` (cannot return error) + +--- + +#### `validation_interface_callbacks_create` + +Creates a validation interface. + +**Parameters:** +- `callbacks` (array of strings, required): Allowed values: + - `btck_ValidationInterfaceBlockChecked` + - `btck_ValidationInterfacePoWValidBlock` + - `btck_ValidationInterfaceBlockConnected` + - `btck_ValidationInterfaceBlockDisconnected` + +**Result:** Reference - Contains the created validation interface ref + +**Error:** `null` (cannot return error) diff --git a/docs/schemas/btck_block_tree_entry_get_height.json b/docs/schemas/btck_block_tree_entry_get_height.json new file mode 100644 index 0000000..6b3fe03 --- /dev/null +++ b/docs/schemas/btck_block_tree_entry_get_height.json @@ -0,0 +1,49 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "description": "Gets the height of a block tree entry", + "type": "object", + "additionalProperties": false, + "required": [ + "description", + "request", + "expected_response" + ], + "properties": { + "description": { + "$ref": "./shared.json#/$defs/NonEmptyText" + }, + "request": { + "type": "object", + "additionalProperties": false, + "required": [ + "id", + "method", + "params" + ], + "properties": { + "id": { + "$ref": "./shared.json#/$defs/TestCaseID" + }, + "method": { + "const": "btck_block_tree_entry_get_height" + }, + "params": { + "type": "object", + "additionalProperties": false, + "required": [ + "block_tree_entry" + ], + "properties": { + "block_tree_entry": { + "$ref": "./shared.json#/$defs/RefObject", + "description": "Block tree entry reference" + } + } + } + } + }, + "expected_response": { + "$ref": "./btck_block_tree_entry_get_height.response.json" + } + } +} diff --git a/docs/schemas/btck_block_tree_entry_get_height.response.json b/docs/schemas/btck_block_tree_entry_get_height.response.json new file mode 100644 index 0000000..2cac5ca --- /dev/null +++ b/docs/schemas/btck_block_tree_entry_get_height.response.json @@ -0,0 +1,5 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$ref": "./shared.json#/$defs/IntegerResponse", + "description": "Height of the block tree entry" +} diff --git a/docs/schemas/btck_block_validation_state_destroy.json b/docs/schemas/btck_block_validation_state_destroy.json new file mode 100644 index 0000000..8c115b4 --- /dev/null +++ b/docs/schemas/btck_block_validation_state_destroy.json @@ -0,0 +1,49 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "description": "Destroys a block validation state", + "type": "object", + "additionalProperties": false, + "required": [ + "description", + "request", + "expected_response" + ], + "properties": { + "description": { + "$ref": "./shared.json#/$defs/NonEmptyText" + }, + "request": { + "type": "object", + "additionalProperties": false, + "required": [ + "id", + "method", + "params" + ], + "properties": { + "id": { + "$ref": "./shared.json#/$defs/TestCaseID" + }, + "method": { + "const": "btck_block_validation_state_destroy" + }, + "params": { + "type": "object", + "additionalProperties": false, + "required": [ + "state" + ], + "properties": { + "state": { + "$ref": "./shared.json#/$defs/RefObject", + "description": "Block validation state reference to destroy" + } + } + } + } + }, + "expected_response": { + "$ref": "./btck_block_validation_state_destroy.response.json" + } + } +} diff --git a/docs/schemas/btck_block_validation_state_destroy.response.json b/docs/schemas/btck_block_validation_state_destroy.response.json new file mode 100644 index 0000000..bcff212 --- /dev/null +++ b/docs/schemas/btck_block_validation_state_destroy.response.json @@ -0,0 +1,4 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$ref": "./shared.json#/$defs/NullResponse" +} diff --git a/docs/schemas/btck_block_validation_state_get_validation_mode.json b/docs/schemas/btck_block_validation_state_get_validation_mode.json new file mode 100644 index 0000000..09150c5 --- /dev/null +++ b/docs/schemas/btck_block_validation_state_get_validation_mode.json @@ -0,0 +1,50 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "description": "Gets the validation mode of a block validation state", + "type": "object", + "additionalProperties": false, + "required": [ + "description", + "request", + "expected_response" + ], + "properties": { + "description": { + "$ref": "./shared.json#/$defs/NonEmptyText" + }, + "request": { + "type": "object", + "additionalProperties": false, + "required": [ + "id", + "method", + "params" + ], + "properties": { + "id": { + "$ref": "./shared.json#/$defs/TestCaseID" + }, + "method": { + "const": "btck_block_validation_state_get_validation_mode" + }, + "params": { + "type": "object", + "additionalProperties": false, + "required": [ + "state" + ], + "x-doc-order": ["state"], + "properties": { + "state": { + "$ref": "./shared.json#/$defs/RefObject", + "description": "Block validation state reference" + } + } + } + } + }, + "expected_response": { + "$ref": "./btck_block_validation_state_get_validation_mode.response.json" + } + } +} diff --git a/docs/schemas/btck_block_validation_state_get_validation_mode.response.json b/docs/schemas/btck_block_validation_state_get_validation_mode.response.json new file mode 100644 index 0000000..f0c8080 --- /dev/null +++ b/docs/schemas/btck_block_validation_state_get_validation_mode.response.json @@ -0,0 +1,17 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "description": "Validation mode of the block validation state", + "type": "object", + "additionalProperties": false, + "required": ["result"], + "properties": { + "result": { + "enum": [ + "btck_ValidationMode_VALID", + "btck_ValidationMode_INVALID", + "btck_ValidationMode_INTERNAL_ERROR" + ] + }, + "error": { "type": "null" } + } +} diff --git a/docs/schemas/btck_context_create.json b/docs/schemas/btck_context_create.json index a31541f..28d3042 100644 --- a/docs/schemas/btck_context_create.json +++ b/docs/schemas/btck_context_create.json @@ -34,6 +34,7 @@ "required": [ "chain_parameters" ], + "x-doc-order": ["chain_parameters", "notifications", "validation_interface"], "properties": { "chain_parameters": { "type": "object", @@ -53,6 +54,14 @@ "description": "Chain type" } } + }, + "notifications": { + "$ref": "./shared.json#/$defs/RefObject", + "description": "Notification callbacks interface to wire into this context" + }, + "validation_interface": { + "$ref": "./shared.json#/$defs/RefObject", + "description": "Validation interface to wire into this context" } } }, diff --git a/docs/schemas/notification_callbacks_create.json b/docs/schemas/notification_callbacks_create.json new file mode 100644 index 0000000..954bc81 --- /dev/null +++ b/docs/schemas/notification_callbacks_create.json @@ -0,0 +1,63 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "description": "Creates a notification callbacks interface", + "type": "object", + "additionalProperties": false, + "required": [ + "description", + "request", + "expected_response" + ], + "properties": { + "description": { + "$ref": "./shared.json#/$defs/NonEmptyText" + }, + "request": { + "type": "object", + "additionalProperties": false, + "required": [ + "id", + "method", + "params", + "ref" + ], + "properties": { + "id": { + "$ref": "./shared.json#/$defs/TestCaseID" + }, + "method": { + "const": "notification_callbacks_create" + }, + "params": { + "type": "object", + "additionalProperties": false, + "required": ["callbacks"], + "properties": { + "callbacks": { + "type": "array", + "minItems": 1, + "uniqueItems": true, + "items": { + "enum": [ + "btck_NotifyBlockTip", + "btck_NotifyHeaderTip", + "btck_NotifyProgress", + "btck_NotifyWarningSet", + "btck_NotifyWarningUnset", + "btck_NotifyFlushError", + "btck_NotifyFatalError" + ] + } + } + } + }, + "ref": { + "$ref": "./shared.json#/$defs/RefString" + } + } + }, + "expected_response": { + "$ref": "./notification_callbacks_create.response.json" + } + } +} diff --git a/docs/schemas/notification_callbacks_create.response.json b/docs/schemas/notification_callbacks_create.response.json new file mode 100644 index 0000000..cf1bd35 --- /dev/null +++ b/docs/schemas/notification_callbacks_create.response.json @@ -0,0 +1,5 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$ref": "./shared.json#/$defs/RefResponse", + "description": "Contains the created notification callbacks interface ref" +} diff --git a/docs/schemas/notification_callbacks_drain.json b/docs/schemas/notification_callbacks_drain.json new file mode 100644 index 0000000..63a11f7 --- /dev/null +++ b/docs/schemas/notification_callbacks_drain.json @@ -0,0 +1,50 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "description": "Drains all queued callback invocation records from a notification callbacks interface", + "type": "object", + "additionalProperties": false, + "required": [ + "description", + "request", + "expected_response" + ], + "properties": { + "description": { + "$ref": "./shared.json#/$defs/NonEmptyText" + }, + "request": { + "type": "object", + "additionalProperties": false, + "required": [ + "id", + "method", + "params" + ], + "properties": { + "id": { + "$ref": "./shared.json#/$defs/TestCaseID" + }, + "method": { + "const": "notification_callbacks_drain" + }, + "params": { + "type": "object", + "additionalProperties": false, + "required": [ + "interface" + ], + "x-doc-order": ["interface"], + "properties": { + "interface": { + "$ref": "./shared.json#/$defs/RefObject", + "description": "Notification callbacks interface reference" + } + } + } + } + }, + "expected_response": { + "$ref": "./notification_callbacks_drain.response.json" + } + } +} diff --git a/docs/schemas/notification_callbacks_drain.response.json b/docs/schemas/notification_callbacks_drain.response.json new file mode 100644 index 0000000..24a7dca --- /dev/null +++ b/docs/schemas/notification_callbacks_drain.response.json @@ -0,0 +1,88 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "description": "Array of invocation records, one per callback fired since the last drain. Each record is an object with a `callback` field identifying the type, plus type-specific fields:\n- `btck_NotifyBlockTip`: `state` (SynchronizationState string), `entry` (reference to a BlockTreeEntry), `verification_progress` (number)\n- `btck_NotifyHeaderTip`: `state` (SynchronizationState string), `height` (integer), `timestamp` (integer), `presync` (boolean)\n- `btck_NotifyProgress`: `title` (string), `percent` (integer), `resumable` (boolean)\n- `btck_NotifyWarningSet`: `warning` (Warning string), `message` (string)\n- `btck_NotifyWarningUnset`: `warning` (Warning string)\n- `btck_NotifyFlushError`: `message` (string)\n- `btck_NotifyFatalError`: `message` (string)", + "type": "object", + "additionalProperties": false, + "required": ["result"], + "properties": { + "result": { + "type": "array", + "items": { + "oneOf": [ + { + "type": "object", + "additionalProperties": false, + "required": ["callback", "state", "entry", "verification_progress"], + "properties": { + "callback": { "const": "btck_NotifyBlockTip" }, + "state": { "$ref": "./shared.json#/$defs/SynchronizationState" }, + "entry": { "$ref": "./shared.json#/$defs/RefObject" }, + "verification_progress": { "type": "number" } + } + }, + { + "type": "object", + "additionalProperties": false, + "required": ["callback", "state", "height", "timestamp", "presync"], + "properties": { + "callback": { "const": "btck_NotifyHeaderTip" }, + "state": { "$ref": "./shared.json#/$defs/SynchronizationState" }, + "height": { "type": "integer" }, + "timestamp": { "type": "integer" }, + "presync": { "type": "boolean" } + } + }, + { + "type": "object", + "additionalProperties": false, + "required": ["callback", "title", "percent", "resumable"], + "properties": { + "callback": { "const": "btck_NotifyProgress" }, + "title": { "type": "string" }, + "percent": { "type": "integer" }, + "resumable": { "type": "boolean" } + } + }, + { + "type": "object", + "additionalProperties": false, + "required": ["callback", "warning", "message"], + "properties": { + "callback": { "const": "btck_NotifyWarningSet" }, + "warning": { "enum": ["btck_Warning_UNKNOWN_NEW_RULES_ACTIVATED", "btck_Warning_LARGE_WORK_INVALID_CHAIN"] }, + "message": { "type": "string" } + } + }, + { + "type": "object", + "additionalProperties": false, + "required": ["callback", "warning"], + "properties": { + "callback": { "const": "btck_NotifyWarningUnset" }, + "warning": { "enum": ["btck_Warning_UNKNOWN_NEW_RULES_ACTIVATED", "btck_Warning_LARGE_WORK_INVALID_CHAIN"] } + } + }, + { + "type": "object", + "additionalProperties": false, + "required": ["callback", "message"], + "properties": { + "callback": { "const": "btck_NotifyFlushError" }, + "message": { "type": "string" } + } + }, + { + "type": "object", + "additionalProperties": false, + "required": ["callback", "message"], + "properties": { + "callback": { "const": "btck_NotifyFatalError" }, + "message": { "type": "string" } + } + } + ] + } + }, + "error": { "type": "null" } + } +} diff --git a/docs/schemas/shared.json b/docs/schemas/shared.json index 8fc3b8f..b3290d1 100644 --- a/docs/schemas/shared.json +++ b/docs/schemas/shared.json @@ -82,6 +82,13 @@ "error": { "type": "null" } } }, + "SynchronizationState": { + "enum": [ + "btck_SynchronizationState_INIT_REINDEX", + "btck_SynchronizationState_INIT_DOWNLOAD", + "btck_SynchronizationState_POST_INIT" + ] + }, "GenericErrorResponse": { "type": "object", "additionalProperties": false, diff --git a/docs/schemas/suite-schema.json b/docs/schemas/suite-schema.json index d8357d9..24d2c04 100644 --- a/docs/schemas/suite-schema.json +++ b/docs/schemas/suite-schema.json @@ -94,6 +94,9 @@ { "$ref": "./btck_block_tree_entry_get_block_hash.json" }, + { + "$ref": "./btck_block_tree_entry_get_height.json" + }, { "$ref": "./btck_chain_contains.json" }, @@ -216,6 +219,24 @@ }, { "$ref": "./btck_txid_to_bytes.json" + }, + { + "$ref": "./notification_callbacks_create.json" + }, + { + "$ref": "./notification_callbacks_drain.json" + }, + { + "$ref": "./validation_interface_callbacks_create.json" + }, + { + "$ref": "./validation_callbacks_drain.json" + }, + { + "$ref": "./btck_block_validation_state_destroy.json" + }, + { + "$ref": "./btck_block_validation_state_get_validation_mode.json" } ] } diff --git a/docs/schemas/validation_callbacks_drain.json b/docs/schemas/validation_callbacks_drain.json new file mode 100644 index 0000000..aea71e8 --- /dev/null +++ b/docs/schemas/validation_callbacks_drain.json @@ -0,0 +1,50 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "description": "Drains all queued callback invocation records from a validation interface", + "type": "object", + "additionalProperties": false, + "required": [ + "description", + "request", + "expected_response" + ], + "properties": { + "description": { + "$ref": "./shared.json#/$defs/NonEmptyText" + }, + "request": { + "type": "object", + "additionalProperties": false, + "required": [ + "id", + "method", + "params" + ], + "properties": { + "id": { + "$ref": "./shared.json#/$defs/TestCaseID" + }, + "method": { + "const": "validation_callbacks_drain" + }, + "params": { + "type": "object", + "additionalProperties": false, + "required": [ + "interface" + ], + "x-doc-order": ["interface"], + "properties": { + "interface": { + "$ref": "./shared.json#/$defs/RefObject", + "description": "Validation interface reference" + } + } + } + } + }, + "expected_response": { + "$ref": "./validation_callbacks_drain.response.json" + } + } +} diff --git a/docs/schemas/validation_callbacks_drain.response.json b/docs/schemas/validation_callbacks_drain.response.json new file mode 100644 index 0000000..919da06 --- /dev/null +++ b/docs/schemas/validation_callbacks_drain.response.json @@ -0,0 +1,57 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "description": "Array of invocation records, one per callback fired since the last drain. Each record is an object with a `callback` field identifying the type, plus type-specific fields:\n- `btck_ValidationInterfaceBlockChecked`: `block` (reference to an owned Block copy), `state` (reference to an owned BlockValidationState copy)\n- `btck_ValidationInterfacePoWValidBlock`: `block` (reference to an owned Block copy), `entry` (reference to a BlockTreeEntry view)\n- `btck_ValidationInterfaceBlockConnected`: `block` (reference to an owned Block copy), `entry` (reference to a BlockTreeEntry view)\n- `btck_ValidationInterfaceBlockDisconnected`: `block` (reference to an owned Block copy), `entry` (reference to a BlockTreeEntry view)", + "type": "object", + "additionalProperties": false, + "required": ["result"], + "properties": { + "result": { + "type": "array", + "items": { + "oneOf": [ + { + "type": "object", + "additionalProperties": false, + "required": ["callback", "block", "state"], + "properties": { + "callback": { "const": "btck_ValidationInterfaceBlockChecked" }, + "block": { "$ref": "./shared.json#/$defs/RefObject" }, + "state": { "$ref": "./shared.json#/$defs/RefObject" } + } + }, + { + "type": "object", + "additionalProperties": false, + "required": ["callback", "block", "entry"], + "properties": { + "callback": { "const": "btck_ValidationInterfacePoWValidBlock" }, + "block": { "$ref": "./shared.json#/$defs/RefObject" }, + "entry": { "$ref": "./shared.json#/$defs/RefObject" } + } + }, + { + "type": "object", + "additionalProperties": false, + "required": ["callback", "block", "entry"], + "properties": { + "callback": { "const": "btck_ValidationInterfaceBlockConnected" }, + "block": { "$ref": "./shared.json#/$defs/RefObject" }, + "entry": { "$ref": "./shared.json#/$defs/RefObject" } + } + }, + { + "type": "object", + "additionalProperties": false, + "required": ["callback", "block", "entry"], + "properties": { + "callback": { "const": "btck_ValidationInterfaceBlockDisconnected" }, + "block": { "$ref": "./shared.json#/$defs/RefObject" }, + "entry": { "$ref": "./shared.json#/$defs/RefObject" } + } + } + ] + } + }, + "error": { "type": "null" } + } +} diff --git a/docs/schemas/validation_interface_callbacks_create.json b/docs/schemas/validation_interface_callbacks_create.json new file mode 100644 index 0000000..fee2eaf --- /dev/null +++ b/docs/schemas/validation_interface_callbacks_create.json @@ -0,0 +1,60 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "description": "Creates a validation interface", + "type": "object", + "additionalProperties": false, + "required": [ + "description", + "request", + "expected_response" + ], + "properties": { + "description": { + "$ref": "./shared.json#/$defs/NonEmptyText" + }, + "request": { + "type": "object", + "additionalProperties": false, + "required": [ + "id", + "method", + "params", + "ref" + ], + "properties": { + "id": { + "$ref": "./shared.json#/$defs/TestCaseID" + }, + "method": { + "const": "validation_interface_callbacks_create" + }, + "params": { + "type": "object", + "additionalProperties": false, + "required": ["callbacks"], + "properties": { + "callbacks": { + "type": "array", + "minItems": 1, + "uniqueItems": true, + "items": { + "enum": [ + "btck_ValidationInterfaceBlockChecked", + "btck_ValidationInterfacePoWValidBlock", + "btck_ValidationInterfaceBlockConnected", + "btck_ValidationInterfaceBlockDisconnected" + ] + } + } + } + }, + "ref": { + "$ref": "./shared.json#/$defs/RefString" + } + } + }, + "expected_response": { + "$ref": "./validation_interface_callbacks_create.response.json" + } + } +} diff --git a/docs/schemas/validation_interface_callbacks_create.response.json b/docs/schemas/validation_interface_callbacks_create.response.json new file mode 100644 index 0000000..35a1e7e --- /dev/null +++ b/docs/schemas/validation_interface_callbacks_create.response.json @@ -0,0 +1,5 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$ref": "./shared.json#/$defs/RefResponse", + "description": "Contains the created validation interface ref" +} diff --git a/runner/dependency_tracker.go b/runner/dependency_tracker.go index 2f55c49..6438f52 100644 --- a/runner/dependency_tracker.go +++ b/runner/dependency_tracker.go @@ -9,8 +9,12 @@ import ( // Refs created by these methods are tracked as stateful, meaning tests // using these refs depend on mutable state. var statefulCreatorMethods = map[string]bool{ - "btck_context_create": true, - "btck_chainstate_manager_create": true, + "btck_context_create": true, + "btck_chainstate_manager_create": true, + "notification_callbacks_create": true, + "validation_interface_callbacks_create": true, + "notification_callbacks_drain": true, + "validation_callbacks_drain": true, } // stateMutatingMethods contains methods that mutate internal state. @@ -71,11 +75,13 @@ func (dt *DependencyTracker) OnTestExecuted(test *TestCase) []int { requestChain := dt.buildRequestChain(i, test.Request.ID, refs) - // Track ref creation using the request's ref field. - if test.Request.Ref != "" { - dt.refCreators[test.Request.Ref] = i - if statefulCreatorMethods[test.Request.Method] { - dt.statefulRefs[test.Request.Ref] = true + // Track all refs produced by this test by scanning the result. + for _, ref := range extractRefsFromResult(test.ExpectedResponse.Result) { + if _, alreadyKnown := dt.refCreators[ref]; !alreadyKnown { + dt.refCreators[ref] = i + if statefulCreatorMethods[test.Request.Method] { + dt.statefulRefs[ref] = true + } } } @@ -132,6 +138,41 @@ func (dt *DependencyTracker) computeUsesStatefulRefs(i int, refs []string) bool return result } +// extractRefsFromResult walks a Result value and returns every {"ref": "..."} string found. +func extractRefsFromResult(result Result) []string { + if result.IsNullOrOmitted() { + return nil + } + data := json.RawMessage(result) + + // single ref object — normal create method result + if ref, ok := ParseRefObject(data); ok { + return []string{ref} + } + + // array — recurse into each element + var arr []json.RawMessage + if err := json.Unmarshal(data, &arr); err == nil { + var refs []string + for _, elem := range arr { + refs = append(refs, extractRefsFromResult(Result(elem))...) + } + return refs + } + + // object — recurse into each value + var obj map[string]json.RawMessage + if err := json.Unmarshal(data, &obj); err == nil { + var refs []string + for _, v := range obj { + refs = append(refs, extractRefsFromResult(Result(v))...) + } + return refs + } + + return nil +} + // extractRefsFromParams extracts all reference names from params JSON. // Searches for ref objects with structure {"ref": "..."} at the top level of params, // and also inside array values one level deep. diff --git a/runner/dependency_tracker_test.go b/runner/dependency_tracker_test.go index 98084fb..382bcef 100644 --- a/runner/dependency_tracker_test.go +++ b/runner/dependency_tracker_test.go @@ -51,6 +51,46 @@ func TestExtractRefsFromParams(t *testing.T) { } } +func TestExtractRefsFromResult(t *testing.T) { + tests := []struct { + description string + result string + wantRefs []string + }{ + { + description: "single ref object — normal create method result", + result: `{"ref": "$ctx"}`, + wantRefs: []string{"$ctx"}, + }, + { + description: "array of objects with nested refs — drain result", + result: `[{"callback": "btck_NotifyBlockTip", "entry": {"ref": "$notif_1_btck_NotifyBlockTip_entry"}}, {"callback": "btck_NotifyBlockTip", "entry": {"ref": "$notif_2_btck_NotifyBlockTip_entry"}}]`, + wantRefs: []string{"$notif_1_btck_NotifyBlockTip_entry", "$notif_2_btck_NotifyBlockTip_entry"}, + }, + { + description: "primitive result produces no refs", + result: `42`, + wantRefs: nil, + }, + { + description: "null result produces no refs", + result: `null`, + wantRefs: nil, + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + got := extractRefsFromResult(Result(tt.result)) + slices.Sort(got) + slices.Sort(tt.wantRefs) + if !slices.Equal(got, tt.wantRefs) { + t.Errorf("extractRefsFromResult(%s) = %v, want %v", tt.result, got, tt.wantRefs) + } + }) + } +} + func TestDependencyTracker(t *testing.T) { type entry struct { tc TestCase @@ -245,6 +285,94 @@ func TestDependencyTracker(t *testing.T) { }, }, }, + { + // tests callback interface dependency tracking: refs produced in a drain + // response are stateful, so tests using them inherit all prior state dependencies + name: "callbacks", + cases: []entry{ + { + tc: TestCase{ + Request: Request{ + ID: "callbacks#0", + Method: "notification_callbacks_create", + Params: json.RawMessage(`{"callbacks": ["btck_NotifyBlockTip"]}`), + Ref: "$notif", + }, + ExpectedResponse: Response{Result: Result(`{"ref": "$notif"}`)}, + }, + expectedChain: []int{}, + }, + { + tc: TestCase{ + Request: Request{ + ID: "callbacks#1", + Method: "btck_context_create", + Params: json.RawMessage(`{"notifications": {"ref": "$notif"}}`), + Ref: "$context", + }, + ExpectedResponse: Response{Result: Result(`{"ref": "$context"}`)}, + }, + expectedChain: []int{0}, + }, + { + tc: TestCase{ + Request: Request{ + ID: "callbacks#2", + Method: "btck_chainstate_manager_create", + Params: json.RawMessage(`{"context": {"ref": "$context"}}`), + Ref: "$chainman", + }, + ExpectedResponse: Response{Result: Result(`{"ref": "$chainman"}`)}, + }, + expectedChain: []int{0, 1}, + }, + { + tc: TestCase{ + Request: Request{ + ID: "callbacks#3", + Method: "btck_block_create", + Params: json.RawMessage(`{"raw_block": "deadbeef"}`), + Ref: "$block", + }, + ExpectedResponse: Response{Result: Result(`{"ref": "$block"}`)}, + }, + expectedChain: []int{}, + }, + { + tc: TestCase{ + Request: Request{ + ID: "callbacks#4", + Method: "btck_chainstate_manager_process_block", + Params: json.RawMessage(`{"chainstate_manager": {"ref": "$chainman"}, "block": {"ref": "$block"}}`), + }, + ExpectedResponse: Response{}, + }, + expectedChain: []int{0, 1, 2, 3}, + }, + { + tc: TestCase{ + Request: Request{ + ID: "callbacks#5", + Method: "notification_callbacks_drain", + Params: json.RawMessage(`{"interface": {"ref": "$notif"}}`), + }, + ExpectedResponse: Response{Result: Result(`[{"callback": "btck_NotifyBlockTip", "entry": {"ref": "$entry"}}]`)}, + }, + expectedChain: []int{0, 1, 2, 3, 4}, + }, + { + tc: TestCase{ + Request: Request{ + ID: "callbacks#6", + Method: "btck_block_tree_entry_get_height", + Params: json.RawMessage(`{"entry": {"ref": "$entry"}}`), + }, + ExpectedResponse: Response{Result: Result(`1`)}, + }, + expectedChain: []int{0, 1, 2, 3, 4, 5}, + }, + }, + }, } for _, s := range suites { diff --git a/runner/runner.go b/runner/runner.go index 9d896d0..07e6f45 100644 --- a/runner/runner.go +++ b/runner/runner.go @@ -129,34 +129,28 @@ func (tr *TestRunner) RunTestSuite(ctx context.Context, suite TestSuite, verbosi TotalTests: len(suite.Tests), } - skipTests := false - for i := range suite.Tests { + // Check if context is already cancelled + if ctx.Err() != nil { + fmt.Printf("Skipped remaining %d test case(s) in suite %q because total execution timeout (%v) was exceeded!\n", + len(suite.Tests)-i, suite.Title, tr.timeout) + break + } + test := &suite.Tests[i] - // Run the test case - var testResult SingleTestResult - if skipTests { - // In stateful suites, if any previous test failed, fail all subsequent tests - testResult = SingleTestResult{ - TestID: test.Request.ID, - Passed: false, - Message: "Skipped due to previous test failure in stateful suite", - } - } else { - // Execute the test against the handler - testResult = tr.runTest(ctx, test) - - // Track dependencies and add verbose output if requested or on failure - if verbosity != VerbosityQuiet { - requestChain := depTracker.OnTestExecuted(test) - if (verbosity == VerbosityAlways) || (verbosity == VerbosityOnFailure && !testResult.Passed) { - verboseOutput := formatVerboseOutput(suite.Tests, i, requestChain, &testResult) - if testResult.Message != "" { - testResult.Message = fmt.Sprintf("%s\n%s", testResult.Message, verboseOutput) - } else { - testResult.Message = verboseOutput - } + // Execute the test against the handler + testResult := tr.runTest(test) + + // Track dependencies and add verbose output if requested or on failure + if verbosity != VerbosityQuiet { + requestChain := depTracker.OnTestExecuted(test) + if (verbosity == VerbosityAlways) || (verbosity == VerbosityOnFailure && !testResult.Passed) { + verboseOutput := formatVerboseOutput(suite.Tests, i, requestChain, &testResult) + if testResult.Message != "" { + testResult.Message = fmt.Sprintf("%s\n%s", testResult.Message, verboseOutput) + } else { + testResult.Message = verboseOutput } } } @@ -168,7 +162,7 @@ func (tr *TestRunner) RunTestSuite(ctx context.Context, suite TestSuite, verbosi } else { result.FailedTests++ if suite.Stateful { - skipTests = true + break } } } @@ -178,18 +172,7 @@ func (tr *TestRunner) RunTestSuite(ctx context.Context, suite TestSuite, verbosi // runTest executes a single test case by sending a request, reading the response, // and validating the result matches expected output -func (tr *TestRunner) runTest(ctx context.Context, test *TestCase) SingleTestResult { - // Check if context is already cancelled - select { - case <-ctx.Done(): - return SingleTestResult{ - TestID: test.Request.ID, - Passed: false, - Message: fmt.Sprintf("Total execution timeout exceeded (%v)", tr.timeout), - } - default: - } - +func (tr *TestRunner) runTest(test *TestCase) SingleTestResult { err := tr.SendRequest(test.Request) if err != nil { return SingleTestResult{ diff --git a/testdata/callbacks.json b/testdata/callbacks.json new file mode 100644 index 0000000..762689b --- /dev/null +++ b/testdata/callbacks.json @@ -0,0 +1,336 @@ +{ + "title": "Callback Interfaces", + "description": "Registers notification and validation interfaces, wires both into a context, drains init-time invocations after chainstate manager creation, processes a block, drains both interfaces, and asserts on validation mode and entry height", + "stateful": true, + "tests": [ + { + "description": "Creates a notification callbacks interface enabled for btck_NotifyHeaderTip", + "request": { + "id": "callbacks#1", + "method": "notification_callbacks_create", + "params": { + "callbacks": [ + "btck_NotifyHeaderTip" + ] + }, + "ref": "$notif" + }, + "expected_response": { + "result": { + "ref": "$notif" + } + } + }, + { + "description": "Creates a validation interface enabled for BlockChecked and BlockConnected", + "request": { + "id": "callbacks#2", + "method": "validation_interface_callbacks_create", + "params": { + "callbacks": [ + "btck_ValidationInterfaceBlockChecked", + "btck_ValidationInterfaceBlockConnected" + ] + }, + "ref": "$vi" + }, + "expected_response": { + "result": { + "ref": "$vi" + } + } + }, + { + "description": "Creates a context with regtest chain parameters and both callback interfaces", + "request": { + "id": "callbacks#3", + "method": "btck_context_create", + "params": { + "chain_parameters": { + "chain_type": "btck_ChainType_REGTEST" + }, + "notifications": { + "ref": "$notif" + }, + "validation_interface": { + "ref": "$vi" + } + }, + "ref": "$ctx" + }, + "expected_response": { + "result": { + "ref": "$ctx" + } + } + }, + { + "description": "Creates a chainstate manager from the context", + "request": { + "id": "callbacks#4", + "method": "btck_chainstate_manager_create", + "params": { + "context": { + "ref": "$ctx" + } + }, + "ref": "$csm" + }, + "expected_response": { + "result": { + "ref": "$csm" + } + } + }, + { + "description": "Destroys the context after creating the chainstate manager", + "request": { + "id": "callbacks#5", + "method": "btck_context_destroy", + "params": { + "context": { + "ref": "$ctx" + } + } + }, + "expected_response": {} + }, + { + "description": "Drains init-time notification callbacks, asserting none fired for enabled callbacks", + "request": { + "id": "callbacks#6", + "method": "notification_callbacks_drain", + "params": { + "interface": { + "ref": "$notif" + } + } + }, + "expected_response": { + "result": [] + } + }, + { + "description": "Drains init-time validation callbacks fired for genesis during chainstate manager creation", + "request": { + "id": "callbacks#7", + "method": "validation_callbacks_drain", + "params": { + "interface": { + "ref": "$vi" + } + } + }, + "expected_response": { + "result": [ + { + "callback": "btck_ValidationInterfaceBlockChecked", + "block": { + "ref": "$vi_1_btck_ValidationInterfaceBlockChecked_block" + }, + "state": { + "ref": "$vi_1_btck_ValidationInterfaceBlockChecked_state" + } + }, + { + "callback": "btck_ValidationInterfaceBlockConnected", + "block": { + "ref": "$vi_2_btck_ValidationInterfaceBlockConnected_block" + }, + "entry": { + "ref": "$vi_2_btck_ValidationInterfaceBlockConnected_entry" + } + } + ] + } + }, + { + "description": "Creates block 1 from raw bytes", + "request": { + "id": "callbacks#8", + "method": "btck_block_create", + "params": { + "raw_block": "0000002006226e46111a0b59caaf126043eb5bbf28c34f3a5e332a1fc7b2b73cf188910f35b99ed4e2e165de2ad77f1bba48049358c9bb740445f3c83ebdb3e83aa5bca8dbe5494dffff7f200000000001020000000001010000000000000000000000000000000000000000000000000000000000000000ffffffff025100feffffff0200f2052a010000001976a9142b4569203694fc997e13f2c0a1383b9e16c77a0d88ac0000000000000000266a24aa21a9ede2f61c3f71d1defd3fa999dfa36953755c690689799962b48bebd836974e8cf90120000000000000000000000000000000000000000000000000000000000000000000000000" + }, + "ref": "$block_1" + }, + "expected_response": { + "result": { + "ref": "$block_1" + } + } + }, + { + "description": "Processes block 1", + "request": { + "id": "callbacks#9", + "method": "btck_chainstate_manager_process_block", + "params": { + "chainstate_manager": { + "ref": "$csm" + }, + "block": { + "ref": "$block_1" + } + } + }, + "expected_response": { + "result": { + "new_block": true + } + } + }, + { + "description": "Drains notification callbacks fired by process_block, asserting btck_NotifyHeaderTip for block 1", + "request": { + "id": "callbacks#10", + "method": "notification_callbacks_drain", + "params": { + "interface": { + "ref": "$notif" + } + } + }, + "expected_response": { + "result": [ + { + "callback": "btck_NotifyHeaderTip", + "state": "btck_SynchronizationState_INIT_DOWNLOAD", + "height": 1, + "timestamp": 1296688603, + "presync": false + } + ] + } + }, + { + "description": "Drains validation callbacks fired by process_block, asserting BlockChecked then BlockConnected for block 1", + "request": { + "id": "callbacks#11", + "method": "validation_callbacks_drain", + "params": { + "interface": { + "ref": "$vi" + } + } + }, + "expected_response": { + "result": [ + { + "callback": "btck_ValidationInterfaceBlockChecked", + "block": { + "ref": "$vi_1_btck_ValidationInterfaceBlockChecked_block" + }, + "state": { + "ref": "$vi_1_btck_ValidationInterfaceBlockChecked_state" + } + }, + { + "callback": "btck_ValidationInterfaceBlockConnected", + "block": { + "ref": "$vi_2_btck_ValidationInterfaceBlockConnected_block" + }, + "entry": { + "ref": "$vi_2_btck_ValidationInterfaceBlockConnected_entry" + } + } + ] + } + }, + { + "description": "Asserts validation mode is valid on the block 1 BlockChecked state", + "request": { + "id": "callbacks#12", + "method": "btck_block_validation_state_get_validation_mode", + "params": { + "state": { + "ref": "$vi_1_btck_ValidationInterfaceBlockChecked_state" + } + } + }, + "expected_response": { + "result": "btck_ValidationMode_VALID" + } + }, + { + "description": "Asserts height 1 on the block 1 BlockConnected entry", + "request": { + "id": "callbacks#13", + "method": "btck_block_tree_entry_get_height", + "params": { + "block_tree_entry": { + "ref": "$vi_2_btck_ValidationInterfaceBlockConnected_entry" + } + } + }, + "expected_response": { + "result": 1 + } + }, + { + "description": "Destroys the block 1 BlockChecked state", + "request": { + "id": "callbacks#14", + "method": "btck_block_validation_state_destroy", + "params": { + "state": { + "ref": "$vi_1_btck_ValidationInterfaceBlockChecked_state" + } + } + }, + "expected_response": {} + }, + { + "description": "Destroys the block 1 BlockChecked block", + "request": { + "id": "callbacks#15", + "method": "btck_block_destroy", + "params": { + "block": { + "ref": "$vi_1_btck_ValidationInterfaceBlockChecked_block" + } + } + }, + "expected_response": {} + }, + { + "description": "Destroys the block 1 BlockConnected block", + "request": { + "id": "callbacks#16", + "method": "btck_block_destroy", + "params": { + "block": { + "ref": "$vi_2_btck_ValidationInterfaceBlockConnected_block" + } + } + }, + "expected_response": {} + }, + { + "description": "Destroys block 1", + "request": { + "id": "callbacks#17", + "method": "btck_block_destroy", + "params": { + "block": { + "ref": "$block_1" + } + } + }, + "expected_response": {} + }, + { + "description": "Destroys the chainstate manager", + "request": { + "id": "callbacks#18", + "method": "btck_chainstate_manager_destroy", + "params": { + "chainstate_manager": { + "ref": "$csm" + } + } + }, + "expected_response": {} + } + ] +}