Skip to content

Repository files navigation

bornal

It scaffolds some test structure in a jekyll-like style and lets pytest know how to build bitcoin regtest daemons from source:

The philosophy is not to rely on pre-built binaries for bitcoin reference and community implementations (not because we cannot verify them locally or on CI, but because it is a choice to rely on a runtime-specific bitcoin compilation built for the test's purpose).

  • bitcoind
  • electrs
  • btcd (TODO)
  • utreexod (TODO)
  • florestad (TODO)
  • lianad (TODO)

and hands your tests a ready JSON-RPC client — so you write your integration tests, hacking through your preferred bitcoin lib, not plumbing through it.

Warning

This is intended basically for a "krux-ecosystem" integration tests setup (krux, kern, or any related project) without bloating up those repos.

Getting Started

Install:

# prefer pinning to a tag or commit for reproducibility, e.g.
# uv add --dev git+https://github.com/qlrd/bornal.git@<tag-or-commit>
uv add --dev git+https://github.com/qlrd/bornal.git

Scaffold

Scaffold an example test, then run it through the pytest plugin:

# We use uv, but you could use poetry or venv
uv run bornal create <feature> --template <template>

bornal create <feature> only scaffolds some files into your project.

Run tests

This is the fun part: you skip the main coding setup and just let pytest know it through their plugin system:

INTEGRATION_TEMP_DIR=<.cache> uv run pytest tests/integration/test_<feature>.py

Where <.cache> is your pre-built | compiled bitcoin implementation.

build fresh bitcoin nodes

uv run pytest --build-bitcoin latest tests/integration/test_<feature>.py

run with wallet support

uv run pytest --wallet tests/integration/test_<wallet_feat>.py 
  • Each daemon is a pytest plugin
  • each plugin that can be added through a --build-<name> flag

I.e., pytest --build-bitcoin 30.2 (or latest for the newest release) makes bitcoind available in the cache and have some additional flags and environment variables:

  • reused unless --force-build), wipes its data/ and logs/
  • (unless --preserve-data is used)
  • exports BINARIES_DIR
  • exports INTEGRATION_TEMP_DIR
  • exports BITCOIN_CORE_PATH
  • --wallet to build with wallet support.
  • --nproc N sets the compile parallelism (the build's -j).

and then runs your tests/integration. If you choose the "compilation-path", the next time you will not need to use --build-*.

Writing Tests

This is for writing tests for your project.

First create a <mytestpath>/conftest.py as in any pytest setup:

import pytest
from bornal.daemon import free_port
from bornal.node import IntegrationTest

# Define a proper setup
class MyTest(IntegrationTest):
    """A simple class to prepare tests"""

    def set_test_params(self):
        # Alice
        self.add_backend("bitcoin-core", p2p_port=free_port())

        # Bob
        self.add_backend("bitcoin-core", p2p_port=free_port())

    def run_test(self):
        for backend in self.backends:
            self.log.info(f"Running bitcoin (p2p_port={backend.daemon.p2p_port})")

@pytest.fixture(scope="module")
def test_factory(request):
    return MyTest

# `integration_test` is an instance of `MyTest`, so you could use any property
# like: `backends[i].daemon`, `backends[i].client`.
@pytest.fixture
def alice(integration_test):
    return integration_test.backends[0]

@pytest.fixture
def bob(integration_test):
    return integration_test.backends[1]

test_factory is a pre-defined fixture. The default one will fail if it isn't overridden.

integration_test fixture is a customizable instance of the predefined factory class defined in test_factory fixture.

Nodes stay up until the module's last test. So, it's important to consider the bitcoin context in a way that the order matters. For example in a file like <mytestpath>/test_example.py:

from bornal.plugins.bitcoind import UNSPENDABLE_ADDRESS

from bornal.testing import (
    assert_block_count,
    connect_p2p,
    generate_to_address,
    sync_blocks
)

def test_000(alice, bob):
    assert_block_count(alice, 0)
    assert_block_count(bob, 0)

def test_001(alice,bob):
    connect_p2p(alice,bob) # Connect P2P already syncs blocks
    assert_block_count(bob, 0)

def test_002(alice,bob):
    generate_to_address(alice, UNSPENDABLE_ADDRESS, block_amount=1)
    assert_block_count(bob, 0)

def test_003(alice,bob):
    sync_blocks(alice, bob)
    assert_block_count(bob, 1)

Plugins

Daemons are entry-point plugins (group them by bornal.daemons). One plugin wires three classes:

  • Compiler (build the binary)
  • Daemon (the implementation daemon on regtest)
  • Client (talk JSON-RPC like) — and declares the pytest build flag it contributes (e.g., --<bitcoin-impl>--build-<bitcoin-impl>)

For example:

[project.entry-points."bornal.daemons"]
<bitcoin-impl> = "bornal.plugins.<bitcoin-impl>:CoreCompiler"

also you need to register fixtures on src/bornal/fixtures.py so it can be found by:

[project.entry-points.pytest11]
bornal = "bornal.fixtures"

Development

bornal need pytest as dep and not dev-dep.

Fetch the code

git clone https://github.com/qlrd/bornal
cd bornal

Install dev tools

uv sync --group dev
uv run poe hooks

Format code

uv run poe format-check

Lint code

uv run poe lint

Examples

If you add some plugin, is really important to add some examples on examples/. They're real-bitcoind integration tests demos.

Motivation

A previously contribution to the getfloresta/Floresta test framework taught on how to follow some already coded principles of Bitcoin-Core test framework and thus I helped to improve it to support both bitcoind, utreexod, florestad.

While it almost-work (in the sense with random failures in CI), this was fixed by @joaozinhom using pytest. The idea is that pytest already deals with threading and plumbing could be made by fixtures.

Afterwards, I learned about the test framework made by wizardsardine/liana that was made before and is pretty similar, with some nice features.

This WIP is a glue of their work as pytest registered plugin plus a scaffolding and self-tests for custom daemons.

Contributing

Branch off main and open a PR; reviews scan git diff main...HEAD. Keep the gates green (uv run poe lint, format-check, test-cov) and use Conventional Commits — the commit-msg hook enforces them.

About

kit for bitcoin related integration test tools

Topics

Resources

Stars

2 stars

Watchers

0 watching

Forks

Releases

Contributors

Languages