diff --git a/Makefile b/Makefile index 90433f8..37c99fb 100644 --- a/Makefile +++ b/Makefile @@ -22,7 +22,7 @@ test: @echo "Running runner unit tests..." go test -v ./runner/... @echo "Running conformance tests with mock handler..." - $(RUNNER_BIN) -handler $(MOCK_HANDLER_BIN) + $(RUNNER_BIN) --handler $(MOCK_HANDLER_BIN) -vv clean: @echo "Cleaning build artifacts..." diff --git a/README.md b/README.md index 62f81f3..590137f 100644 --- a/README.md +++ b/README.md @@ -66,6 +66,33 @@ make runner The runner automatically detects and recovers from crashed/unresponsive handlers, allowing remaining tests to continue. +#### Verbose Flags + +- **`-v, --verbose`**: Shows request chains and responses for **failed tests only** +- **`-vv`**: Shows request chains and responses for **all tests** (passed and failed) + +The request chains printed by verbose mode can be directly piped to the handler binary for manual debugging: + +```bash +# Example output from -vv mode: +# ✓ chain#4 (Get active chain reference from chainstate manager) +# +# Request chain +# ──────────────────────────────────────── +# {"id":"chain#1","method":"btck_context_create","params":{"chain_parameters":{"chain_type":"btck_ChainType_REGTEST"}},"ref":"$context_ref"} +# {"id":"chain#2","method":"btck_chainstate_manager_create","params":{"context":"$context_ref"},"ref":"$chainstate_manager_ref"} +# {"id":"chain#4","method":"btck_chainstate_manager_get_active_chain","params":{"chainstate_manager":"$chainstate_manager_ref"},"ref":"$chain_ref"} +# +# Response: +# ──────────────────────────────────────── +# {"result":"$chain_ref"} + +# Copy the request chain and pipe it to your handler for debugging: +echo '{"id":"chain#1","method":"btck_context_create","params":{"chain_parameters":{"chain_type":"btck_ChainType_REGTEST"}},"ref":"$context_ref"} +{"id":"chain#2","method":"btck_chainstate_manager_create","params":{"context":"$context_ref"},"ref":"$chainstate_manager_ref"} +{"id":"chain#4","method":"btck_chainstate_manager_get_active_chain","params":{"chainstate_manager":"$chainstate_manager_ref"},"ref":"$chain_ref"}' | ./path/to/your/handler +``` + ### Testing the Runner Build and test the runner: diff --git a/cmd/runner/main.go b/cmd/runner/main.go index fa756d7..f804c8c 100644 --- a/cmd/runner/main.go +++ b/cmd/runner/main.go @@ -2,26 +2,36 @@ package main import ( "context" - "flag" "fmt" "io/fs" "os" + "sort" "strings" "time" + "github.com/spf13/pflag" "github.com/stringintech/kernel-bindings-tests/runner" "github.com/stringintech/kernel-bindings-tests/testdata" ) func main() { - handlerPath := flag.String("handler", "", "Path to handler binary") - handlerTimeout := flag.Duration("handler-timeout", 10*time.Second, "Max time to wait for handler to respond to each test case (e.g., 10s, 500ms)") - timeout := flag.Duration("timeout", 30*time.Second, "Total timeout for executing all test suites (e.g., 30s, 1m)") - flag.Parse() + handlerPath := pflag.String("handler", "", "Path to handler binary") + handlerTimeout := pflag.Duration("handler-timeout", 10*time.Second, "Max time to wait for handler to respond to each test case (e.g., 10s, 500ms)") + timeout := pflag.Duration("timeout", 30*time.Second, "Total timeout for executing all test suites (e.g., 30s, 1m)") + verboseCount := pflag.CountP("verbose", "v", "Verbose mode: -v shows all requests needed to reproduce failed tests, plus received/expected responses; -vv shows this for all tests (passed and failed)") + pflag.Parse() + + // Convert verbose count to verbosity level + verbosity := runner.VerbosityQuiet + if *verboseCount >= 2 { + verbosity = runner.VerbosityAlways + } else if *verboseCount == 1 { + verbosity = runner.VerbosityOnFailure + } if *handlerPath == "" { - fmt.Fprintf(os.Stderr, "Error: -handler flag is required\n") - flag.Usage() + fmt.Fprintf(os.Stderr, "Error: --handler flag is required\n") + pflag.Usage() os.Exit(1) } @@ -37,6 +47,9 @@ func main() { os.Exit(1) } + // Sort test files alphabetically for deterministic execution order + sort.Strings(testFiles) + // Create test runner testRunner, err := runner.NewTestRunner(*handlerPath, *handlerTimeout, *timeout) if err != nil { @@ -65,12 +78,18 @@ func main() { } // Run suite - result := testRunner.RunTestSuite(ctx, *suite) + result := testRunner.RunTestSuite(ctx, *suite, verbosity) printResults(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. + if suite.Stateful { + testRunner.CloseHandler() + } } fmt.Printf("\n" + strings.Repeat("=", 60) + "\n") diff --git a/docs/handler-spec.md b/docs/handler-spec.md index c7b5499..4b30c82 100644 --- a/docs/handler-spec.md +++ b/docs/handler-spec.md @@ -17,7 +17,8 @@ Handlers communicate with the test runner via **stdin/stdout**: { "id": "unique-request-id", "method": "method_name", - "params": { /* method-specific parameters */ } + "params": { /* method-specific parameters */ }, + "ref": "reference-name" } ``` @@ -25,6 +26,7 @@ Handlers communicate with the test runner via **stdin/stdout**: - `id` (string, required): Unique identifier for this request - `method` (string, required): The operation to perform. Each unique method must be implemented by the handler to exercise the corresponding binding API operation. - `params` (object, optional): Method-specific parameters +- `ref` (string, optional): Reference name for storing the returned object. Required for methods that return object references (see [Object References and Registry](#object-references-and-registry)) ### Response @@ -41,12 +43,25 @@ Handlers communicate with the test runner via **stdin/stdout**: ``` **Fields:** -- `result` (any, optional): The return value, or `null` for void/nullptr operations. Must be `null` on error +- `result` (any, optional): The return value, or `null` for void/nullptr operations. Must be `null` on error. For methods that return object references, the result is a reference type object (see [Reference Type](#reference-type)) - `error` (object, optional): Error details. Must be `null` on success. An empty object `{}` is used to indicate an error is raised without further details, it is NOT equivalent to `null` - `code` (object, optional): Error code details - `type` (string, required): Error type (e.g., "btck_ScriptVerifyStatus") - `member` (string, required): Specific error member (e.g., "ERROR_INVALID_FLAGS_COMBINATION") +### Reference Type + +For methods that return object references, the result is an object containing the reference name: + +```json +{ + "ref": "reference-name" +} +``` + +**Fields:** +- `ref` (string, required): The reference name from the request's `ref` field + **Note:** Throughout this protocol, an omitted field is semantically equivalent to `null`. ## Handler Requirements @@ -56,53 +71,223 @@ Handlers communicate with the test runner via **stdin/stdout**: 3. **Error Handling**: Return error responses for invalid requests or failed operations 4. **Exit Behavior**: Exit cleanly when stdin closes -## Test Suites and Expected Responses - -The conformance tests are organized into suites, each testing a specific aspect of the Bitcoin Kernel bindings. Test files are located in [`../testdata/`](../testdata/). - -### Script Verification Success Cases -**File:** [`script_verify_success.json`](../testdata/script_verify_success.json) +## Object References and Registry -Test cases where the script verification operation executes successfully and returns a boolean result (true for valid scripts, false for invalid scripts). +Many operations return objects (contexts, blocks, chains, etc.) that must persist across requests. The protocol uses named references and a registry pattern: -**Method:** `btck_script_pubkey_verify` +**Creating Objects**: Methods that return objects require a `ref` field in the request. The handler stores the object in a registry under that name and returns a reference type object containing the reference name. -**Expected Response Format:** ```json -{ - "result": true -} +// Request +{"id": "1", "method": "btck_context_create", "params": {...}, "ref": "$ctx1"} +// Response +{"id": "1", "result": {"ref": "$ctx1"}, "error": null} +// Handler action: registry["$ctx1"] = created_context_ptr ``` -or + +**Using Objects**: When a parameter is marked as `(reference, required)`, the runner passes a reference type object and the handler extracts the reference name to look it up: + ```json -{ - "result": false -} +// Request +{"id": "2", "method": "btck_chainstate_manager_create", "params": {"context": {"ref": "$ctx1"}}, "ref": "$csm1"} +// Response +{"id": "2", "result": {"ref": "$csm1"}, "error": null} +// 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. + +## 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/). + +### Script Verification Success Cases +**File:** [`script_verify_success.json`](../testdata/script_verify_success.json) + +Test cases where the script verification operation executes successfully and returns a boolean result (true for valid scripts, false for invalid scripts). + ### Script Verification Error Cases **File:** [`script_verify_errors.json`](../testdata/script_verify_errors.json) Test cases where the verification operation fails to determine validity of the script due to bad user input. -**Method:** `btck_script_pubkey_verify` +### Chain Operations +**File:** [`chain.json`](../testdata/chain.json) -**Expected Response Format:** -```json -{ - "result": null, - "error": { - "code": { - "type": "btck_ScriptVerifyStatus", - "member": "ERROR_MEMBER_NAME" - } - } -} -``` +Sets up blocks, checks chain state, and verifies that the chain tip changes as expected after a reorg scenario. + +## Method Reference + +Methods are grouped by functional area. Each method documents its parameters, return values, and possible errors. + +### Context Management + +#### `btck_context_create` + +Creates a context with specified chain parameters. + +**Parameters:** +- `chain_parameters` (object, required): + - `chain_type` (string, required): Chain type ("btck_ChainType_MAINNET", "btck_ChainType_TESTNET", "btck_ChainType_TESTNET_4", "btck_ChainType_SIGNET", "btck_ChainType_REGTEST") + +**Result:** Reference type - Object containing the reference name from the request `ref` field (e.g., `{"ref": "$context"}`) + +**Error:** `{}` when operation fails (C API returned null) + +--- + +### Chainstate Manager Operations + +#### `btck_chainstate_manager_create` + +Creates a chainstate manager from a context. + +**Parameters:** +- `context` (reference, required): Context reference from `btck_context_create` + +**Result:** Reference type - Object containing the reference name from the request `ref` field (e.g., `{"ref": "$chainstate_manager"}`) + +**Error:** `{}` when operation fails (C API returned null) + +--- + +#### `btck_chainstate_manager_get_active_chain` + +Retrieves the currently active chain from the chainstate manager. + +**Parameters:** +- `chainstate_manager` (reference, required): Chainstate manager reference + +**Result:** Reference type - Object containing the reference name from the request `ref` field (e.g., `{"ref": "$chain"}`) + +**Error:** `null` (cannot return error) + +--- + +#### `btck_chainstate_manager_process_block` + +Processes a block through validation checks, disk storage, and UTXO set validation; successful processing does not indicate block validity. + +**Parameters:** +- `chainstate_manager` (reference, required): Chainstate manager reference +- `block` (reference, required): Block reference from `btck_block_create` + +**Result:** Object containing: +- `new_block` (boolean): `true` if this block was not processed before, `false` otherwise + +**Error:** `{}` when processing fails + +--- + +#### `btck_chainstate_manager_destroy` + +Destroys a chainstate manager and frees associated resources. + +**Parameters:** +- `chainstate_manager` (reference, required): Chainstate manager reference to destroy + +**Result:** `null` (void operation) + +**Error:** `null` (cannot return error) + +--- + +### Chain Operations + +#### `btck_chain_get_height` + +Gets the current height of the active chain. + +**Parameters:** +- `chain` (reference, required): Chain reference from `btck_chainstate_manager_get_active_chain` + +**Result:** Integer - The chain height (0 = genesis) + +**Error:** `null` (cannot return error) + +--- + +#### `btck_chain_get_by_height` + +Retrieves a block tree entry at a specific height in the chain. + +**Parameters:** +- `chain` (reference, required): Chain reference +- `block_height` (integer, required): Height to query + +**Result:** Reference type - Object containing the reference name from the request `ref` field (e.g., `{"ref": "$block_tree_entry"}`) + +**Error:** `{}` when height is out of bounds (C API returned null) + +--- + +#### `btck_chain_contains` + +Checks whether a block tree entry is part of the active chain. + +**Parameters:** +- `chain` (reference, required): Chain reference +- `block_tree_entry` (reference, required): Block tree entry reference to check + +**Result:** Boolean - true if block is in the active chain, false otherwise + +**Error:** `null` (cannot return error) + +--- + +### Block Operations + +#### `btck_block_create` + +Creates a block object from raw block data. + +**Parameters:** +- `raw_block` (string, required): Hex-encoded raw block data + +**Result:** Reference type - Object containing the reference name from the request `ref` field (e.g., `{"ref": "$block"}`) + +**Error:** `{}` when operation fails (C API returned null) + +--- + +#### `btck_block_tree_entry_get_block_hash` + +Gets the block hash from a block tree entry. + +**Parameters:** +- `block_tree_entry` (reference, required): Block tree entry reference from `btck_chain_get_by_height` + +**Result:** String - The block hash (hex-encoded, 64 characters) + +**Error:** `null` (cannot return error) + +--- + +### Script Verification + +#### `btck_script_pubkey_verify` + +Verifies a script pubkey against spending conditions. + +**Parameters:** +- `script_pubkey` (string): Hex-encoded script pubkey to be spent +- `amount` (number): Amount of the script pubkey's associated output. May be zero if the witness flag is not set +- `tx_to` (string): Hex-encoded transaction spending the script_pubkey +- `input_index` (number): Index of the input in tx_to spending the script_pubkey +- `flags` (array of strings): Script verification flags controlling validation constraints. Valid flags include: + - `btck_ScriptVerificationFlags_P2SH` + - `btck_ScriptVerificationFlags_DERSIG` + - `btck_ScriptVerificationFlags_NULLDUMMY` + - `btck_ScriptVerificationFlags_CHECKLOCKTIMEVERIFY` + - `btck_ScriptVerificationFlags_CHECKSEQUENCEVERIFY` + - `btck_ScriptVerificationFlags_WITNESS` + - `btck_ScriptVerificationFlags_TAPROOT` +- `spent_outputs` (array of objects): Array of outputs spent by the transaction. May be empty if the taproot flag is not set. Each object contains: + - `script_pubkey` (string): Hex-encoded script pubkey of the spent output + - `amount` (number): Amount in satoshis of the spent output -**Error Members:** +**Result:** Boolean - true if script is valid, false if invalid -| Member | Description | -|--------|-------------| -| `ERROR_INVALID_FLAGS_COMBINATION` | Invalid or inconsistent verification flags were provided. This occurs when the supplied `script_verify_flags` combination violates internal consistency rules. | -| `ERROR_SPENT_OUTPUTS_REQUIRED` | Spent outputs are required but were not provided (e.g., for Taproot verification). | +**Error:** On error, returns error code with type `btck_ScriptVerifyStatus` and member can be one of: +- `ERROR_INVALID_FLAGS_COMBINATION` - Invalid or inconsistent verification flags were provided. This occurs when the supplied `script_verify_flags` combination violates internal consistency rules. +- `ERROR_SPENT_OUTPUTS_REQUIRED` - Spent outputs are required but were not provided (e.g., for Taproot verification). diff --git a/go.mod b/go.mod index 6cf3000..2ee73a4 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,5 @@ module github.com/stringintech/kernel-bindings-tests go 1.23 + +require github.com/spf13/pflag v1.0.10 diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..8ec1276 --- /dev/null +++ b/go.sum @@ -0,0 +1,2 @@ +github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= +github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= diff --git a/runner/dependency_tracker.go b/runner/dependency_tracker.go new file mode 100644 index 0000000..1281e0a --- /dev/null +++ b/runner/dependency_tracker.go @@ -0,0 +1,143 @@ +package runner + +import ( + "encoding/json" + "fmt" +) + +// statefulCreatorMethods contains methods that create stateful objects. +// 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, +} + +// stateMutatingMethods contains methods that mutate internal state. +// Tests using these methods are assumed to affect all subsequent tests +// and are included in dependency chains printed in verbose mode. +var stateMutatingMethods = map[string]bool{ + "btck_chainstate_manager_process_block": true, +} + +// DependencyTracker manages test dependencies and builds request chains for verbose output. +// It tracks both explicit ref dependencies and implicit state dependencies. +type DependencyTracker struct { + // refCreators maps reference names to the test index that created them + refCreators map[string]int + + // statefulRefs tracks refs created by stateful methods. + // Tests using these refs depend on mutable state. + statefulRefs map[string]bool + + // depChains maps test index to its dependency chain (tests it depends on via ref usage) + depChains map[int][]int + + // stateDependencies is a cumulative list of all tests affecting state (state-mutating + // tests and their complete dependency chains) + stateDependencies []int +} + +// NewDependencyTracker creates a new dependency tracker +func NewDependencyTracker() *DependencyTracker { + return &DependencyTracker{ + refCreators: make(map[string]int), + statefulRefs: make(map[string]bool), + depChains: make(map[int][]int), + stateDependencies: []int{}, + } +} + +// BuildDependenciesForTest analyzes a test's parameters to build its complete transitive +// dependency chain. When a test uses refs created by earlier tests, this records all direct +// dependencies (tests that created those refs) and indirect dependencies (their dependencies). +// Must be called after all previous tests have been processed. +func (dt *DependencyTracker) BuildDependenciesForTest(testIndex int, test *TestCase) { + // Build dependency chain for current test based on refs it uses + var parentChains [][]int + for _, ref := range extractRefsFromParams(test.Request.Params) { + if creatorIdx, exists := dt.refCreators[ref]; exists { + // Add the creator as a direct dependency + parentChains = append(parentChains, []int{creatorIdx}) + // Add transitive dependencies (creator's dependencies) + if chain, hasChain := dt.depChains[creatorIdx]; hasChain { + parentChains = append(parentChains, chain) + } + } else { + panic(fmt.Sprintf("test %d (%s) uses undefined reference %s - no prior test created this ref", + testIndex, test.Request.ID, ref)) + } + } + dt.depChains[testIndex] = mergeSortedUnique(parentChains...) +} + +// OnTestExecuted is called after a test executes successfully. It tracks the ref +// created by the test, marks it as stateful if needed, and updates state dependencies +// for state-mutating methods. +func (dt *DependencyTracker) OnTestExecuted(testIndex int, test *TestCase) { + // Track ref creation using the request's ref field + if test.Request.Ref != "" { + dt.refCreators[test.Request.Ref] = testIndex + + // Mark refs from stateful methods + if statefulCreatorMethods[test.Request.Method] { + dt.statefulRefs[test.Request.Ref] = true + } + } + + // Track state-mutating tests and their dependencies + if stateMutatingMethods[test.Request.Method] { + mutatorChain := append(dt.depChains[testIndex], testIndex) + dt.stateDependencies = mergeSortedUnique(dt.stateDependencies, mutatorChain) + } +} + +// BuildRequestChain builds the complete dependency chain for a test +func (dt *DependencyTracker) BuildRequestChain(testIndex int, allTests []TestCase) []int { + refDepChain := dt.depChains[testIndex] + + // Only include state dependencies if the test's dep chain contains any stateful refs + if dt.testUsesStatefulRefs(testIndex, allTests) { + return mergeSortedUnique(refDepChain, dt.stateDependencies) + } + + return refDepChain +} + +// testUsesStatefulRefs checks if a test's dependency chain includes any stateful refs +func (dt *DependencyTracker) testUsesStatefulRefs(testIndex int, allTests []TestCase) bool { + // Check all tests in the dependency chain + for _, depIdx := range dt.depChains[testIndex] { + for _, ref := range extractRefsFromParams(allTests[depIdx].Request.Params) { + if dt.statefulRefs[ref] { + return true + } + } + } + + // Check the test itself + for _, ref := range extractRefsFromParams(allTests[testIndex].Request.Params) { + if dt.statefulRefs[ref] { + return true + } + } + return false +} + +// extractRefsFromParams extracts all reference names from params JSON. +// Searches for ref objects with structure {"ref": "..."} at the first level of params. +func extractRefsFromParams(params json.RawMessage) []string { + var refs []string + + var paramsMap map[string]json.RawMessage + if err := json.Unmarshal(params, ¶msMap); err != nil { + return refs + } + + for _, value := range paramsMap { + if ref, ok := ParseRefObject(value); ok { + refs = append(refs, ref) + } + } + return refs +} diff --git a/runner/dependency_tracker_test.go b/runner/dependency_tracker_test.go new file mode 100644 index 0000000..994e22f --- /dev/null +++ b/runner/dependency_tracker_test.go @@ -0,0 +1,320 @@ +package runner + +import ( + "encoding/json" + "slices" + "testing" +) + +func TestDependencyTracker_BuildDependencyChains(t *testing.T) { + // Create test cases to verify dependency chain building + testsJSON := `[ + { + "request": { + "id": "test0", + "method": "create_a", + "params": {}, + "ref": "$ref_a" + }, + "expected_response": {"result": {"ref": "$ref_a"}} + }, + { + "request": { + "id": "test1", + "method": "create_b", + "params": {"input": {"ref": "$ref_a"}}, + "ref": "$ref_b" + }, + "expected_response": {"result": {"ref": "$ref_b"}} + }, + { + "request": { + "id": "test2", + "method": "create_c", + "params": {}, + "ref": "$ref_c" + }, + "expected_response": {"result": {"ref": "$ref_c"}} + }, + { + "request": { + "id": "test3", + "method": "use_multiple", + "params": {"first": {"ref": "$ref_b"}, "second": {"ref": "$ref_c"}} + }, + "expected_response": {} + } + ]` + + var testCases []TestCase + if err := json.Unmarshal([]byte(testsJSON), &testCases); err != nil { + t.Fatalf("failed to unmarshal test cases: %v", err) + } + + // Create dependency tracker and simulate test execution + tracker := NewDependencyTracker() + + for i := range testCases { + test := &testCases[i] + tracker.BuildDependenciesForTest(i, test) + tracker.OnTestExecuted(i, test) + } + + // Verify dependency chains + tests := []struct { + testIdx int + wantDepChain []int + description string + }{ + { + testIdx: 0, + wantDepChain: []int{}, + description: "test0 has no dependencies", + }, + { + testIdx: 1, + wantDepChain: []int{0}, + description: "test1 depends on test0", + }, + { + testIdx: 2, + wantDepChain: []int{}, + description: "test2 has no dependencies", + }, + { + testIdx: 3, + wantDepChain: []int{0, 1, 2}, + description: "test3 depends on test1 (which depends on test0) and test2", + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + got := tracker.depChains[tt.testIdx] + if !slices.Equal(got, tt.wantDepChain) { + t.Errorf("depChains[%d] = %v, want %v", tt.testIdx, got, tt.wantDepChain) + } + }) + } +} + +func TestDependencyTracker_StatefulRefs(t *testing.T) { + testsJSON := `[ + { + "request": { + "id": "test0", + "method": "btck_context_create", + "params": {}, + "ref": "$context" + }, + "expected_response": {"result": {"ref": "$context"}} + }, + { + "request": { + "id": "test1", + "method": "btck_chainstate_manager_create", + "params": {"context": {"ref": "$context"}}, + "ref": "$chainman" + }, + "expected_response": {"result": {"ref": "$chainman"}} + }, + { + "request": { + "id": "test2", + "method": "btck_block_create", + "params": {"raw_block": "deadbeef"}, + "ref": "$block" + }, + "expected_response": {"result": {"ref": "$block"}} + } + ]` + + var testCases []TestCase + if err := json.Unmarshal([]byte(testsJSON), &testCases); err != nil { + t.Fatalf("failed to unmarshal test cases: %v", err) + } + + tracker := NewDependencyTracker() + + for i := range testCases { + test := &testCases[i] + tracker.BuildDependenciesForTest(i, test) + tracker.OnTestExecuted(i, test) + } + + // Verify that context and chainstate_manager refs are marked as stateful + if !tracker.statefulRefs["$context"] { + t.Error("$context_ref should be marked as stateful") + } + if !tracker.statefulRefs["$chainman"] { + t.Error("$chainman_ref should be marked as stateful") + } + if tracker.statefulRefs["$block"] { + t.Error("$block_ref should NOT be marked as stateful") + } +} + +func TestDependencyTracker_StateMutations(t *testing.T) { + testsJSON := `[ + { + "request": { + "id": "test0", + "method": "btck_context_create", + "params": {}, + "ref": "$context" + }, + "expected_response": {"result": {"ref": "$context"}} + }, + { + "request": { + "id": "test1", + "method": "btck_chainstate_manager_create", + "params": {"context": {"ref": "$context"}}, + "ref": "$chainman" + }, + "expected_response": {"result": {"ref": "$chainman"}} + }, + { + "request": { + "id": "test2", + "method": "btck_block_create", + "params": {"raw_block": "deadbeef"}, + "ref": "$block" + }, + "expected_response": {"result": {"ref": "$block"}} + }, + { + "request": { + "id": "test3", + "method": "btck_chainstate_manager_process_block", + "params": {"chainstate_manager": {"ref": "$chainman"}, "block": {"ref": "$block"}} + }, + "expected_response": {} + }, + { + "request": { + "id": "test4", + "method": "btck_block_create", + "params": {"raw_block": "cafebabe"}, + "ref": "$block2" + }, + "expected_response": {"result": {"ref": "$block2"}} + } + ]` + + var testCases []TestCase + if err := json.Unmarshal([]byte(testsJSON), &testCases); err != nil { + t.Fatalf("failed to unmarshal test cases: %v", err) + } + + tracker := NewDependencyTracker() + + for i := range testCases { + test := &testCases[i] + tracker.BuildDependenciesForTest(i, test) + tracker.OnTestExecuted(i, test) + } + + // State dependencies should include test3 (process_block) and its dependencies (0, 1, 2) + expectedStateDeps := []int{0, 1, 2, 3} + if !slices.Equal(tracker.stateDependencies, expectedStateDeps) { + t.Errorf("state dependencies = %v, want %v", tracker.stateDependencies, expectedStateDeps) + } +} + +func TestDependencyTracker_BuildRequestChain(t *testing.T) { + testsJSON := `[ + { + "request": { + "id": "test0", + "method": "btck_context_create", + "params": {}, + "ref": "$context" + }, + "expected_response": {"result": {"ref": "$context"}} + }, + { + "request": { + "id": "test1", + "method": "btck_chainstate_manager_create", + "params": {"context": {"ref": "$context"}}, + "ref": "$chainman" + }, + "expected_response": {"result": {"ref": "$chainman"}} + }, + { + "request": { + "id": "test2", + "method": "btck_block_create", + "params": {"raw_block": "deadbeef"}, + "ref": "$block" + }, + "expected_response": {"result": {"ref": "$block"}} + }, + { + "request": { + "id": "test3", + "method": "btck_chainstate_manager_process_block", + "params": {"chainstate_manager": {"ref": "$chainman"}, "block": {"ref": "$block"}} + }, + "expected_response": {} + }, + { + "request": { + "id": "test4", + "method": "btck_block_create", + "params": {"raw_block": "cafebabe"}, + "ref": "$block2" + }, + "expected_response": {"result": {"ref": "$block2"}} + }, + { + "request": { + "id": "test5", + "method": "btck_chainstate_manager_get_active_chain", + "params": {"chainstate_manager": {"ref": "$chainman"}}, + "ref": "$chain" + }, + "expected_response": {"result": {"ref": "$chain"}} + } + ]` + + var testCases []TestCase + if err := json.Unmarshal([]byte(testsJSON), &testCases); err != nil { + t.Fatalf("failed to unmarshal test cases: %v", err) + } + + tracker := NewDependencyTracker() + + for i := range testCases { + test := &testCases[i] + tracker.BuildDependenciesForTest(i, test) + tracker.OnTestExecuted(i, test) + } + + tests := []struct { + testIdx int + wantChain []int + description string + }{ + { + testIdx: 4, + wantChain: []int{}, // block_create doesn't use stateful refs, so no state deps included + description: "test4 (block_create) should NOT include state dependencies", + }, + { + testIdx: 5, + wantChain: []int{0, 1, 2, 3}, // uses chainman_ref (stateful), so includes state deps + description: "test5 (get_active_chain) should include state dependencies", + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + got := tracker.BuildRequestChain(tt.testIdx, testCases) + if !slices.Equal(got, tt.wantChain) { + t.Errorf("BuildRequestChain(%d) = %v, want %v", tt.testIdx, got, tt.wantChain) + } + }) + } +} diff --git a/runner/runner.go b/runner/runner.go index 511389d..41afed3 100644 --- a/runner/runner.go +++ b/runner/runner.go @@ -9,9 +9,23 @@ import ( "log/slog" "os" "path/filepath" + "slices" + "strings" "time" ) +// VerbosityLevel represents different levels of test output verbosity +type VerbosityLevel int + +const ( + // VerbosityQuiet shows minimal output (only test results) + VerbosityQuiet VerbosityLevel = iota + // VerbosityOnFailure shows detailed output for failed tests only + VerbosityOnFailure + // VerbosityAlways shows detailed output for all tests + VerbosityAlways +) + // TestRunner executes test suites against a handler binary type TestRunner struct { handler *Handler @@ -100,19 +114,64 @@ func (tr *TestRunner) CloseHandler() { // RunTestSuite executes a test suite. The context can be used to enforce a total // execution timeout across all test suites. -func (tr *TestRunner) RunTestSuite(ctx context.Context, suite TestSuite) TestResult { +// The verbosity parameter controls output detail. +func (tr *TestRunner) RunTestSuite(ctx context.Context, suite TestSuite, verbosity VerbosityLevel) TestResult { + // Create dependency tracker to manage test dependencies and build request chains + depTracker := NewDependencyTracker() + result := TestResult{ SuiteName: suite.Name, TotalTests: len(suite.Tests), } - for _, test := range suite.Tests { - testResult := tr.runTest(ctx, test) + skipTests := false + + for i := range suite.Tests { + 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 { + // Build dependency chain by analyzing which refs this test uses + if verbosity != VerbosityQuiet { + depTracker.BuildDependenciesForTest(i, test) + } + + // Execute the test against the handler + testResult = tr.runTest(ctx, test) + + // Add verbose output if requested or on failure + if (verbosity == VerbosityAlways) || (verbosity == VerbosityOnFailure && !testResult.Passed) { + requestChain := depTracker.BuildRequestChain(i, suite.Tests) + verboseOutput := formatVerboseOutput(suite.Tests, i, requestChain, &testResult) + if testResult.Message != "" { + testResult.Message = fmt.Sprintf("%s\n%s", testResult.Message, verboseOutput) + } else { + testResult.Message = verboseOutput + } + } + + if verbosity != VerbosityQuiet { + depTracker.OnTestExecuted(i, test) + } + } + + // Collect test case result result.TestResults = append(result.TestResults, testResult) if testResult.Passed { result.PassedTests++ } else { result.FailedTests++ + if suite.Stateful { + skipTests = true + } } } @@ -121,7 +180,7 @@ func (tr *TestRunner) RunTestSuite(ctx context.Context, suite TestSuite) TestRes // 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 { +func (tr *TestRunner) runTest(ctx context.Context, test *TestCase) SingleTestResult { // Check if context is already cancelled select { case <-ctx.Done(): @@ -153,20 +212,23 @@ func (tr *TestRunner) runTest(ctx context.Context, test TestCase) SingleTestResu if err := validateResponse(test, resp); err != nil { return SingleTestResult{ - TestID: test.Request.ID, - Passed: false, - Message: fmt.Sprintf("Invalid response: %s", err.Error()), + TestID: test.Request.ID, + Passed: false, + Message: fmt.Sprintf("Invalid response: %s", err.Error()), + ReceivedResponse: resp, } } + return SingleTestResult{ - TestID: test.Request.ID, - Passed: true, + TestID: test.Request.ID, + Passed: true, + ReceivedResponse: resp, } } // validateResponse validates that a response matches the expected test outcome. // Returns an error if the response does not match the expected outcome (error or success). -func validateResponse(test TestCase, resp *Response) error { +func validateResponse(test *TestCase, resp *Response) error { if test.ExpectedResponse.Error != nil { return validateResponseForError(test, resp) } @@ -177,7 +239,7 @@ func validateResponse(test TestCase, resp *Response) error { // validateResponseForError validates that a response correctly represents an error case. // It ensures the response contains an error, the result is null or omitted, and if an // error code is expected, it matches the expected type and member. -func validateResponseForError(test TestCase, resp *Response) error { +func validateResponseForError(test *TestCase, resp *Response) error { if test.ExpectedResponse.Error == nil { panic("validateResponseForError expects non-nil error") } @@ -214,7 +276,7 @@ func validateResponseForError(test TestCase, resp *Response) error { // validateResponseForSuccess validates that a response correctly represents a success case. // It ensures the response contains no error, and if a result is expected, it matches the // expected value. -func validateResponseForSuccess(test TestCase, resp *Response) error { +func validateResponseForSuccess(test *TestCase, resp *Response) error { if test.ExpectedResponse.Error != nil { panic("validateResponseForSuccess expects nil error") } @@ -233,6 +295,23 @@ func validateResponseForSuccess(test TestCase, resp *Response) error { return nil } + if resp.Result.IsNullOrOmitted() { + return fmt.Errorf("expected result with value, got null or omitted result") + } + + // If the request has a ref field, validate that the response is a reference object + if test.Request.Ref != "" { + refValue, ok := ParseRefObject(resp.Result) + if !ok { + return fmt.Errorf("expected reference object result for request with ref field, got: %s", string(resp.Result)) + } + if refValue != test.Request.Ref { + return fmt.Errorf("reference mismatch: expected ref %q, got %q", test.Request.Ref, refValue) + } + return nil + } + + // For non-ref results, normalize and compare expectedNorm, err := test.ExpectedResponse.Result.Normalize() if err != nil { return fmt.Errorf("failed to normalize expected result: %w", err) @@ -260,9 +339,10 @@ type TestResult struct { // SingleTestResult contains the result of a single test type SingleTestResult struct { - TestID string - Passed bool - Message string + TestID string + Passed bool + Message string + ReceivedResponse *Response // The actual response received from the handler } // LoadTestSuiteFromFS loads a test suite from an embedded filesystem @@ -284,3 +364,73 @@ func LoadTestSuiteFromFS(fsys embed.FS, filePath string) (*TestSuite, error) { return &suite, nil } + +// formatVerboseOutput formats the complete verbose output including requestChain, +// received response, and expected response for a test +func formatVerboseOutput(allTests []TestCase, testIdx int, requestChain []int, testResult *SingleTestResult) string { + var result strings.Builder + + // Add request chain header + result.WriteString("\n") + result.WriteString(" Request chain\n") + result.WriteString(" ────────────────────────────────────────\n") + + // Build requests for all dependencies + for _, depIdx := range requestChain { + reqJSON, err := json.Marshal(allTests[depIdx].Request) + if err == nil { + result.WriteString(string(reqJSON)) + result.WriteString("\n") + } + } + + // Add the current test request + reqJSON, err := json.Marshal(allTests[testIdx].Request) + if err == nil { + result.WriteString(string(reqJSON)) + result.WriteString("\n") + } + + if testResult.Passed { + // When test passes, only show expected response + result.WriteString("\n Response:\n") + result.WriteString(" ────────────────────────────────────────\n") + } else { + // When test fails, show both received and expected responses + // Add received response + result.WriteString("\n Received response\n") + result.WriteString(" ────────────────────────────────────────\n") + if testResult.ReceivedResponse != nil { + respJSON, err := json.Marshal(testResult.ReceivedResponse) + if err == nil { + result.WriteString(" ") + result.WriteString(string(respJSON)) + result.WriteString("\n") + } + } else { + result.WriteString(" (no response received)\n") + } + + // Add expected response header + result.WriteString("\n Expected response\n") + result.WriteString(" ────────────────────────────────────────\n") + } + + expectedJSON, err := json.Marshal(allTests[testIdx].ExpectedResponse) + if err == nil { + result.WriteString(" ") + result.WriteString(string(expectedJSON)) + result.WriteString("\n") + } + + result.WriteString("\n") + return result.String() +} + +// mergeSortedUnique returns a sorted array containing unique values from all input arrays. +// NOTE: Since each input array is already sorted, this can be optimized using k-way merge if needed. +func mergeSortedUnique(arrays ...[]int) []int { + merged := slices.Concat(arrays...) + slices.Sort(merged) + return slices.Compact(merged) +} diff --git a/runner/runner_test.go b/runner/runner_test.go index 25aac56..bbb7c91 100644 --- a/runner/runner_test.go +++ b/runner/runner_test.go @@ -170,7 +170,7 @@ func TestValidateResponse(t *testing.T) { { name: "protocol violation with result not null when error present", testCaseJSON: `{ - "request": {"id": "11"}, + "request": {"id": "10"}, "expected_response": { "error": { "code": { @@ -195,7 +195,7 @@ func TestValidateResponse(t *testing.T) { { name: "error generic without code", testCaseJSON: `{ - "request": {"id": "12"}, + "request": {"id": "11"}, "expected_response": { "error": {} } @@ -206,6 +206,41 @@ func TestValidateResponse(t *testing.T) { }`, wantErr: false, }, + { + name: "ref object success", + testCaseJSON: `{ + "request": {"id": "12", "ref": "$ctx1"}, + "expected_response": {"result": {"ref": "$ctx1"}} + }`, + responseJSON: `{ + "result": {"ref": "$ctx1"} + }`, + wantErr: false, + }, + { + name: "ref object mismatch", + testCaseJSON: `{ + "request": {"id": "13", "ref": "$ctx1"}, + "expected_response": {"result": {"ref": "$ctx1"}} + }`, + responseJSON: `{ + "result": {"ref": "$ctx2"} + }`, + wantErr: true, + wantErrMsg: "reference mismatch", + }, + { + name: "ref in request but response not a ref object", + testCaseJSON: `{ + "request": {"id": "14", "ref": "$ctx1"}, + "expected_response": {"result": {"ref": "$ctx1"}} + }`, + responseJSON: `{ + "result": true + }`, + wantErr: true, + wantErrMsg: "expected reference object result", + }, } for _, tt := range tests { @@ -220,7 +255,7 @@ func TestValidateResponse(t *testing.T) { t.Fatalf("failed to unmarshal response: %v", err) } - err := validateResponse(testCase, &response) + err := validateResponse(&testCase, &response) if tt.wantErr { if err == nil { diff --git a/runner/types.go b/runner/types.go index e1c16eb..b2a0f81 100644 --- a/runner/types.go +++ b/runner/types.go @@ -16,6 +16,13 @@ type TestSuite struct { Name string `json:"name"` Description string `json:"description,omitempty"` Tests []TestCase `json:"tests"` + + // Stateful indicates that tests in this suite depend on each other and must + // execute sequentially. If any test fails in a stateful suite, all subsequent + // tests are automatically skipped and considered as failed. Use this for test + // suites where later tests depend on the success of earlier tests + // (e.g., setup -> operation -> verification). + Stateful bool `json:"stateful,omitempty"` } // Request represents a request sent to the handler @@ -23,6 +30,9 @@ type Request struct { ID string `json:"id"` Method string `json:"method"` Params json.RawMessage `json:"params,omitempty"` + // Ref specifies the name the handler should use to store the returned object reference + // in its registry. Required for methods that return object handles. + Ref string `json:"ref,omitempty"` } // Response represents a response from the handler. @@ -75,3 +85,22 @@ func (r Result) Normalize() (string, error) { } return string(normalized), nil } + +// RefObject represents a reference type result structure. +type RefObject struct { + Ref string `json:"ref"` +} + +// ParseRefObject extracts the ref value from a reference object. +// Returns the ref string and true if the result is a valid ref object, +// or empty string and false otherwise. +func ParseRefObject[T ~[]byte](r T) (string, bool) { + var refObj RefObject + if err := json.Unmarshal(r, &refObj); err != nil { + return "", false + } + if refObj.Ref == "" { + return "", false + } + return refObj.Ref, true +} diff --git a/testdata/chain.json b/testdata/chain.json new file mode 100644 index 0000000..a5c5c86 --- /dev/null +++ b/testdata/chain.json @@ -0,0 +1,438 @@ +{ + "name": "Chain", + "description": "Sets up blocks, checks chain state, and verifies that the chain tip changes as expected after a reorg scenario", + "stateful": true, + "tests": [ + { + "description": "Create context with regtest chain parameters", + "request": { + "id": "chain#1", + "method": "btck_context_create", + "params": { + "chain_parameters": { + "chain_type": "btck_ChainType_REGTEST" + } + }, + "ref": "$context" + }, + "expected_response": { + "result": { + "ref": "$context" + } + } + }, + { + "description": "Create chainstate manager from context", + "request": { + "id": "chain#2", + "method": "btck_chainstate_manager_create", + "params": { + "context": { + "ref": "$context" + } + }, + "ref": "$chainstate_manager" + }, + "expected_response": { + "result": { + "ref": "$chainstate_manager" + } + } + }, + { + "description": "Destroy context as it's no longer needed", + "request": { + "id": "chain#3", + "method": "btck_context_destroy", + "params": { + "context": { + "ref": "$context" + } + } + }, + "expected_response": { + "result": null + } + }, + { + "description": "Get active chain reference from chainstate manager", + "request": { + "id": "chain#4", + "method": "btck_chainstate_manager_get_active_chain", + "params": { + "chainstate_manager": { + "ref": "$chainstate_manager" + } + }, + "ref": "$chain" + }, + "expected_response": { + "result": { + "ref": "$chain" + } + } + }, + { + "description": "Assert chain height is 0 (genesis block only)", + "request": { + "id": "chain#5", + "method": "btck_chain_get_height", + "params": { + "chain": { + "ref": "$chain" + } + } + }, + "expected_response": { + "result": 0 + } + }, + { + "description": "Create block 1 from raw hex data", + "request": { + "id": "chain#6", + "method": "btck_block_create", + "params": { + "raw_block": "0000002006226e46111a0b59caaf126043eb5bbf28c34f3a5e332a1fc7b2b73cf188910f35b99ed4e2e165de2ad77f1bba48049358c9bb740445f3c83ebdb3e83aa5bca8dbe5494dffff7f200000000001020000000001010000000000000000000000000000000000000000000000000000000000000000ffffffff025100feffffff0200f2052a010000001976a9142b4569203694fc997e13f2c0a1383b9e16c77a0d88ac0000000000000000266a24aa21a9ede2f61c3f71d1defd3fa999dfa36953755c690689799962b48bebd836974e8cf90120000000000000000000000000000000000000000000000000000000000000000000000000" + }, + "ref": "$block_1" + }, + "expected_response": { + "result": { + "ref": "$block_1" + } + } + }, + { + "description": "Process block 1, extending chain to height 1", + "request": { + "id": "chain#7", + "method": "btck_chainstate_manager_process_block", + "params": { + "chainstate_manager": { + "ref": "$chainstate_manager" + }, + "block": { + "ref": "$block_1" + } + } + }, + "expected_response": { + "result": { + "new_block": true + } + } + }, + { + "description": "Create block 2 from raw hex data", + "request": { + "id": "chain#8", + "method": "btck_block_create", + "params": { + "raw_block": "000000205e2f859d70e29641f32371f3bf17a282466ad851f9e51b44a70738abeace314a9cf876c62dbbe036af4ea4a7363cd4ca1c14c8572095cba3b76a87daa1303ed8dce5494dffff7f200200000001020000000001010000000000000000000000000000000000000000000000000000000000000000ffffffff025200feffffff0200f2052a010000001976a9142b4569203694fc997e13f2c0a1383b9e16c77a0d88ac0000000000000000266a24aa21a9ede2f61c3f71d1defd3fa999dfa36953755c690689799962b48bebd836974e8cf90120000000000000000000000000000000000000000000000000000000000000000001000000" + }, + "ref": "$block_2" + }, + "expected_response": { + "result": { + "ref": "$block_2" + } + } + }, + { + "description": "Process block 2, extending chain to height 2", + "request": { + "id": "chain#9", + "method": "btck_chainstate_manager_process_block", + "params": { + "chainstate_manager": { + "ref": "$chainstate_manager" + }, + "block": { + "ref": "$block_2" + } + } + }, + "expected_response": { + "result": { + "new_block": true + } + } + }, + { + "description": "Create block 3 from raw hex data", + "request": { + "id": "chain#10", + "method": "btck_block_create", + "params": { + "raw_block": "0000002077622c1ae937c9fec6be84d01521cb31b0e6f88ec48150965323dba6a1e36e19354352df0f2a5d635ca7d3a52064f9c95f070d2c13c0a6c087acba03dfeeae66dde5494dffff7f200000000001020000000001010000000000000000000000000000000000000000000000000000000000000000ffffffff025300feffffff0200f2052a010000001976a9142b4569203694fc997e13f2c0a1383b9e16c77a0d88ac0000000000000000266a24aa21a9ede2f61c3f71d1defd3fa999dfa36953755c690689799962b48bebd836974e8cf90120000000000000000000000000000000000000000000000000000000000000000002000000" + }, + "ref": "$block_3" + }, + "expected_response": { + "result": { + "ref": "$block_3" + } + } + }, + { + "description": "Process block 3, extending chain to height 3", + "request": { + "id": "chain#11", + "method": "btck_chainstate_manager_process_block", + "params": { + "chainstate_manager": { + "ref": "$chainstate_manager" + }, + "block": { + "ref": "$block_3" + } + } + }, + "expected_response": { + "result": { + "new_block": true + } + } + }, + { + "description": "Assert chain height is 3 after processing blocks", + "request": { + "id": "chain#12", + "method": "btck_chain_get_height", + "params": { + "chain": { + "ref": "$chain" + } + } + }, + "expected_response": { + "result": 3 + } + }, + { + "description": "Get block tree entry at height 3 to use after reorg", + "request": { + "id": "chain#13", + "method": "btck_chain_get_by_height", + "params": { + "chain": { + "ref": "$chain" + }, + "block_height": 3 + }, + "ref": "$tip_entry" + }, + "expected_response": { + "result": { + "ref": "$tip_entry" + } + } + }, + { + "description": "Assert tip block hash matches expected value", + "request": { + "id": "chain#14", + "method": "btck_block_tree_entry_get_block_hash", + "params": { + "block_tree_entry": { + "ref": "$tip_entry" + } + } + }, + "expected_response": { + "result": "1a81e97231fb262ccf464f937553a1deff996ac6901b062d0b35afe025ee2886" + } + }, + { + "description": "Create reorg block 1 (competing block at height 2)", + "request": { + "id": "chain#15", + "method": "btck_block_create", + "params": { + "raw_block": "000000205e2f859d70e29641f32371f3bf17a282466ad851f9e51b44a70738abeace314a9cf876c62dbbe036af4ea4a7363cd4ca1c14c8572095cba3b76a87daa1303ed8dee5494dffff7f200000000001020000000001010000000000000000000000000000000000000000000000000000000000000000ffffffff025200feffffff0200f2052a010000001976a9142b4569203694fc997e13f2c0a1383b9e16c77a0d88ac0000000000000000266a24aa21a9ede2f61c3f71d1defd3fa999dfa36953755c690689799962b48bebd836974e8cf90120000000000000000000000000000000000000000000000000000000000000000001000000" + }, + "ref": "$reorg_block_1" + }, + "expected_response": { + "result": { + "ref": "$reorg_block_1" + } + } + }, + { + "description": "Process reorg block 1", + "request": { + "id": "chain#16", + "method": "btck_chainstate_manager_process_block", + "params": { + "chainstate_manager": { + "ref": "$chainstate_manager" + }, + "block": { + "ref": "$reorg_block_1" + } + } + }, + "expected_response": { + "result": { + "new_block": true + } + } + }, + { + "description": "Create reorg block 2 (competing block at height 3)", + "request": { + "id": "chain#17", + "method": "btck_block_create", + "params": { + "raw_block": "000000202c6c418b1f714cbe22c9c2906a5c1a3f5c0df22d32989b71f579b2289a0ccd4c354352df0f2a5d635ca7d3a52064f9c95f070d2c13c0a6c087acba03dfeeae66dfe5494dffff7f200200000001020000000001010000000000000000000000000000000000000000000000000000000000000000ffffffff025300feffffff0200f2052a010000001976a9142b4569203694fc997e13f2c0a1383b9e16c77a0d88ac0000000000000000266a24aa21a9ede2f61c3f71d1defd3fa999dfa36953755c690689799962b48bebd836974e8cf90120000000000000000000000000000000000000000000000000000000000000000002000000" + }, + "ref": "$reorg_block_2" + }, + "expected_response": { + "result": { + "ref": "$reorg_block_2" + } + } + }, + { + "description": "Process reorg block 2", + "request": { + "id": "chain#18", + "method": "btck_chainstate_manager_process_block", + "params": { + "chainstate_manager": { + "ref": "$chainstate_manager" + }, + "block": { + "ref": "$reorg_block_2" + } + } + }, + "expected_response": { + "result": { + "new_block": true + } + } + }, + { + "description": "Create reorg block 3 (extending chain to height 4, triggering reorg)", + "request": { + "id": "chain#19", + "method": "btck_block_create", + "params": { + "raw_block": "00000020732f2f7a1035b802d670218031da2b11d0fe7297ddaeb4a428fc51bf588770417bd00ba57498a2dfcf4e3f0d7ef7f279b254fc422133f300a49aed3c8ed7717fe0e5494dffff7f200100000001020000000001010000000000000000000000000000000000000000000000000000000000000000ffffffff025400feffffff0200f2052a010000001976a9142b4569203694fc997e13f2c0a1383b9e16c77a0d88ac0000000000000000266a24aa21a9ede2f61c3f71d1defd3fa999dfa36953755c690689799962b48bebd836974e8cf90120000000000000000000000000000000000000000000000000000000000000000003000000" + }, + "ref": "$reorg_block_3" + }, + "expected_response": { + "result": { + "ref": "$reorg_block_3" + } + } + }, + { + "description": "Process reorg block 3, triggering chain reorganization", + "request": { + "id": "chain#20", + "method": "btck_chainstate_manager_process_block", + "params": { + "chainstate_manager": { + "ref": "$chainstate_manager" + }, + "block": { + "ref": "$reorg_block_3" + } + } + }, + "expected_response": { + "result": { + "new_block": true + } + } + }, + { + "description": "Assert chain height is 4 after processing reorg block 3", + "request": { + "id": "chain#21", + "method": "btck_chain_get_height", + "params": { + "chain": { + "ref": "$chain" + } + } + }, + "expected_response": { + "result": 4 + } + }, + { + "description": "Verify that the old tip is no longer in the active chain after reorg", + "request": { + "id": "chain#22", + "method": "btck_chain_contains", + "params": { + "chain": { + "ref": "$chain" + }, + "block_tree_entry": { + "ref": "$tip_entry" + } + } + }, + "expected_response": { + "result": false + } + }, + { + "description": "Get block tree entry at height 4 (new tip after reorg)", + "request": { + "id": "chain#23", + "method": "btck_chain_get_by_height", + "params": { + "chain": { + "ref": "$chain" + }, + "block_height": 4 + }, + "ref": "$new_tip_entry" + }, + "expected_response": { + "result": { + "ref": "$new_tip_entry" + } + } + }, + { + "description": "Assert new tip block hash matches expected value after reorg", + "request": { + "id": "chain#24", + "method": "btck_block_tree_entry_get_block_hash", + "params": { + "block_tree_entry": { + "ref": "$new_tip_entry" + } + } + }, + "expected_response": { + "result": "18618dcf64dddb10ea15d7850bc4c7965c9a72b613da8530b83057672f29bbfa" + } + }, + { + "description": "Destroy chainstate manager", + "request": { + "id": "chain#25", + "method": "btck_chainstate_manager_destroy", + "params": { + "chainstate_manager": { + "ref": "$chainstate_manager" + } + } + }, + "expected_response": { + "result": null + } + } + ] +} \ No newline at end of file