From eb2572e52695e3fe2d71a645528cd46fbb87481f Mon Sep 17 00:00:00 2001 From: stringintech Date: Tue, 19 May 2026 12:12:41 +0330 Subject: [PATCH 1/5] Report skipped tests in summaries This adds skipped counts to suite and total summaries, prints clear timeout messages when remaining suites or test cases are skipped, and makes skipped tests cause a non-zero exit because not all tests completed successfully. --- cmd/runner/main.go | 24 ++++++++++++++++++------ runner/runner.go | 24 ++++++++++-------------- 2 files changed, 28 insertions(+), 20 deletions(-) diff --git a/cmd/runner/main.go b/cmd/runner/main.go index bef31e2..1709f49 100644 --- a/cmd/runner/main.go +++ b/cmd/runner/main.go @@ -66,16 +66,26 @@ func main() { totalPassed := 0 totalFailed := 0 totalTests := 0 + loggedTimeout := false - 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) @@ -83,7 +93,6 @@ func main() { 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. @@ -98,9 +107,10 @@ func main() { 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) } } @@ -110,7 +120,9 @@ func printResults(suite *runner.TestSuite, result runner.TestResult) { if suite.Description != "" { fmt.Printf("Description: %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\n", result.TotalTests, result.PassedTests, + result.FailedTests, totalSkipped) for i, tr := range result.TestResults { status := "✓" diff --git a/runner/runner.go b/runner/runner.go index 9d896d0..530de6b 100644 --- a/runner/runner.go +++ b/runner/runner.go @@ -132,6 +132,13 @@ func (tr *TestRunner) RunTestSuite(ctx context.Context, suite TestSuite, verbosi 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 @@ -145,7 +152,7 @@ func (tr *TestRunner) RunTestSuite(ctx context.Context, suite TestSuite, verbosi } } else { // Execute the test against the handler - testResult = tr.runTest(ctx, test) + testResult = tr.runTest(test) // Track dependencies and add verbose output if requested or on failure if verbosity != VerbosityQuiet { @@ -168,7 +175,7 @@ func (tr *TestRunner) RunTestSuite(ctx context.Context, suite TestSuite, verbosi } else { result.FailedTests++ if suite.Stateful { - skipTests = true + break } } } @@ -178,18 +185,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{ From 1e6c1d88f954ed37bbdc15b1ef955d27f08f7944 Mon Sep 17 00:00:00 2001 From: stringintech Date: Tue, 19 May 2026 12:13:39 +0330 Subject: [PATCH 2/5] Print less noise at lower verbosity This changes output behavior by verbosity level: - quiet: print nothing for suites with no failures - on-failure (-v): print the suite summary, but hide passed test cases - always (-vv): print full suite and test output like before --- Makefile | 4 ++-- cmd/runner/main.go | 23 ++++++++++++++++------- runner/runner.go | 37 ++++++++++++------------------------- 3 files changed, 30 insertions(+), 34 deletions(-) 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 1709f49..8a384c1 100644 --- a/cmd/runner/main.go +++ b/cmd/runner/main.go @@ -89,7 +89,7 @@ func main() { // Run suite result := testRunner.RunTestSuite(ctx, *suite, verbosity) - printResults(suite, result) + printResults(suite, result, verbosity) totalPassed += result.PassedTests totalFailed += result.FailedTests @@ -115,18 +115,27 @@ func main() { } } -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) } totalSkipped := result.TotalTests - (result.PassedTests + result.FailedTests) - fmt.Printf("Total: %d, Passed: %d, Failed: %d, Skipped: %d\n\n", result.TotalTests, result.PassedTests, + 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 = "✗" } diff --git a/runner/runner.go b/runner/runner.go index 530de6b..07e6f45 100644 --- a/runner/runner.go +++ b/runner/runner.go @@ -129,8 +129,6 @@ 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 { @@ -141,29 +139,18 @@ func (tr *TestRunner) RunTestSuite(ctx context.Context, suite TestSuite, verbosi 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(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 } } } From 06fc6bf5bcf16c516f2aa4ad29b937fc3ec41141 Mon Sep 17 00:00:00 2001 From: stringintech Date: Tue, 19 May 2026 15:53:02 +0330 Subject: [PATCH 3/5] Add failed-test recap for runner output Print each failed test case as ' ()' above the total summary section in all verbosity modes. --- cmd/runner/main.go | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/cmd/runner/main.go b/cmd/runner/main.go index 8a384c1..0a796b6 100644 --- a/cmd/runner/main.go +++ b/cmd/runner/main.go @@ -67,6 +67,7 @@ func main() { totalFailed := 0 totalTests := 0 loggedTimeout := false + failedTestLines := make([]string, 0) for suiteIdx, testFile := range testFiles { // Load test suite from embedded FS @@ -90,6 +91,7 @@ func main() { // Run suite result := testRunner.RunTestSuite(ctx, *suite, verbosity) printResults(suite, result, verbosity) + failedTestLines = append(failedTestLines, collectFailedTestLines(suite, result)...) totalPassed += result.PassedTests totalFailed += result.FailedTests @@ -101,6 +103,15 @@ 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") @@ -152,3 +163,14 @@ func printResults(suite *runner.TestSuite, result runner.TestResult, verbosity r 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 +} From c62440a2fc449703aaf42b5ddac083e2f243e657 Mon Sep 17 00:00:00 2001 From: stringintech Date: Sat, 6 Jun 2026 22:01:30 +0330 Subject: [PATCH 4/5] Extract refs from result and track them as ref creators MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit extractRefsFromResult walks any Result value recursively to collect every {"ref": "..."} string it contains — covering both single-ref create responses and multi-ref array responses. The dependency tracker now uses this instead of the request's Ref field so refs nested inside results are tracked and their stateful dependencies are propagated correctly. --- runner/dependency_tracker.go | 47 +++++++++++++++++++++++++++---- runner/dependency_tracker_test.go | 40 ++++++++++++++++++++++++++ 2 files changed, 82 insertions(+), 5 deletions(-) diff --git a/runner/dependency_tracker.go b/runner/dependency_tracker.go index 2f55c49..086672f 100644 --- a/runner/dependency_tracker.go +++ b/runner/dependency_tracker.go @@ -71,11 +71,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 +134,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..a3526d7 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 From 7884d4072bcf191dad0092aaad16aefa30d291c5 Mon Sep 17 00:00:00 2001 From: stringintech Date: Tue, 2 Jun 2026 12:02:10 +0330 Subject: [PATCH 5/5] Extend protocol to support testing callback interfaces Handlers are expected to expose callback interface objects that record synchronous kernel callback firings into an internal queue and flush them on drain. The runner can then assert which callbacks fired, in what order, and with what values. Adds schemas, handler-spec documentation, a demonstration suite (testdata/callbacks.jon), and runner dependency tracking for the new protocol methods. --- docs/handler-spec.md | 65 +++- docs/methods-spec.md | 115 ++++++ .../btck_block_tree_entry_get_height.json | 49 +++ ..._block_tree_entry_get_height.response.json | 5 + .../btck_block_validation_state_destroy.json | 49 +++ ...ock_validation_state_destroy.response.json | 4 + ..._validation_state_get_validation_mode.json | 50 +++ ...on_state_get_validation_mode.response.json | 17 + docs/schemas/btck_context_create.json | 9 + .../notification_callbacks_create.json | 63 ++++ ...otification_callbacks_create.response.json | 5 + .../schemas/notification_callbacks_drain.json | 50 +++ ...notification_callbacks_drain.response.json | 88 +++++ docs/schemas/shared.json | 7 + docs/schemas/suite-schema.json | 21 ++ docs/schemas/validation_callbacks_drain.json | 50 +++ .../validation_callbacks_drain.response.json | 57 +++ ...validation_interface_callbacks_create.json | 60 ++++ ...n_interface_callbacks_create.response.json | 5 + runner/dependency_tracker.go | 8 +- runner/dependency_tracker_test.go | 88 +++++ testdata/callbacks.json | 336 ++++++++++++++++++ 22 files changed, 1197 insertions(+), 4 deletions(-) create mode 100644 docs/schemas/btck_block_tree_entry_get_height.json create mode 100644 docs/schemas/btck_block_tree_entry_get_height.response.json create mode 100644 docs/schemas/btck_block_validation_state_destroy.json create mode 100644 docs/schemas/btck_block_validation_state_destroy.response.json create mode 100644 docs/schemas/btck_block_validation_state_get_validation_mode.json create mode 100644 docs/schemas/btck_block_validation_state_get_validation_mode.response.json create mode 100644 docs/schemas/notification_callbacks_create.json create mode 100644 docs/schemas/notification_callbacks_create.response.json create mode 100644 docs/schemas/notification_callbacks_drain.json create mode 100644 docs/schemas/notification_callbacks_drain.response.json create mode 100644 docs/schemas/validation_callbacks_drain.json create mode 100644 docs/schemas/validation_callbacks_drain.response.json create mode 100644 docs/schemas/validation_interface_callbacks_create.json create mode 100644 docs/schemas/validation_interface_callbacks_create.response.json create mode 100644 testdata/callbacks.json 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 086672f..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. diff --git a/runner/dependency_tracker_test.go b/runner/dependency_tracker_test.go index a3526d7..382bcef 100644 --- a/runner/dependency_tracker_test.go +++ b/runner/dependency_tracker_test.go @@ -285,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/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": {} + } + ] +}