Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 43 additions & 14 deletions doc/running-tests.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ curl -LsSf https://astral.sh/uv/install.sh | sh
```

## Testing Options

There's a set of unit and integration Rust tests that you can run with:

```bash
Expand All @@ -63,11 +64,13 @@ Next sections will cover the Python functional tests.
### Setting Functional Tests Binaries

We provide three way for running functional tests:
* from `just` tool that abstracts what is necessary to run the tests before doing a commit;
* from helper scripts — [prepare.sh](https://github.com/getfloresta/Floresta/blob/master/tests/prepare.sh) and [run.sh](https://github.com/getfloresta/Floresta/blob/master/tests/run.sh) — to automatically build and run the tests;
* from python utility directly: the most laborious, but you can run a specific test suite.

- from `just` tool that abstracts what is necessary to run the tests before doing a commit;
- from helper scripts — [prepare.sh](https://github.com/getfloresta/Floresta/blob/master/tests/prepare.sh) and [run.sh](https://github.com/getfloresta/Floresta/blob/master/tests/run.sh) — to automatically build and run the tests;
- from python utility directly: the most laborious, but you can run a specific test suite.

#### From `just` tool

It abstracts all things that will be explained in the next sections, and for that
reason, we recommend to use it before doing a commit when changes only the functional tests.

Expand Down Expand Up @@ -98,9 +101,9 @@ just test-functional-run "-t floresta-cli -k getblock"

We provide two helper scripts to support our functional tests in this process and guarantee isolation and reproducibility.

* [prepare.sh](https://github.com/getfloresta/Floresta/blob/master/tests/prepare.sh) checks for build dependencies for both `utreexod` and `florestad`, builds them, and sets the `$FLORESTA_TEMP_DIR` environment variable. This variable points to where our functional tests will look for the binaries — specifically at `$FLORESTA_TEMP_DIR/binaries`.
- [prepare.sh](https://github.com/getfloresta/Floresta/blob/master/tests/prepare.sh) checks for build dependencies for both `utreexod` and `florestad`, builds them, and sets the `$FLORESTA_TEMP_DIR` environment variable. This variable points to where our functional tests will look for the binaries — specifically at `$FLORESTA_TEMP_DIR/binaries`.

* [run.sh](https://github.com/getfloresta/Floresta/blob/master/tests/run.sh) adds the binaries found at `$FLORESTA_TEMP_DIR/binaries` to your `$PATH` and runs the tests in that environment.
- [run.sh](https://github.com/getfloresta/Floresta/blob/master/tests/run.sh) adds the binaries found at `$FLORESTA_TEMP_DIR/binaries` to your `$PATH` and runs the tests in that environment.

So a basic usage would be:

Expand All @@ -119,19 +122,23 @@ UTREEXO_REVISION=0.1.0 ./tests/prepare.sh && ./tests/run.sh
```

##### Bitcoin-core

By default, the `prepare.sh` script will obtain a runnable `bitcoind` binary in one of three exclusive ways. The default Bitcoin Core version is `30.2`, but you can override this by setting the `BITCOIN_REVISION` environment variable. The three methods are:

1. **Using a user-provided binary**: If the `BITCOIND_EXE` environment variable is set and points to an executable, that exact binary is used. No download or build is attempted, and any `BITCOIN_REVISION` or build-parallelism settings are ignored.

```bash
BITCOIND_EXE=/path/to/bitcoind ./tests/prepare.sh
```

2. **Downloading a prebuilt binary**: If `BITCOIND_EXE` is not set, the script will try to download a prebuilt Bitcoin Core tarball for the specified `BITCOIN_REVISION`. Prebuilt binaries are available for all platforms and operating systems supported by Bitcoin Core. The supported versions are `30.2`, `29.2`, `28.3` and `27.2`.

```bash
BITCOIN_REVISION=28.3 ./tests/prepare.sh
```

3. **Building from source**: If no prebuilt binary is available for the platform, operating system, or specified version, the script will clone the Bitcoin Core repository and build `bitcoind` from the specified `BITCOIN_REVISION`. This can be a version tag (e.g., `29.1`) or a branch(e.g., `master`) from the remote repository.

```bash
BITCOIN_REVISION=master ./tests/prepare.sh
```
Expand Down Expand Up @@ -168,16 +175,36 @@ Furthermore, you can run a set of specific tests, rather than all at once.
./tests/run.sh -t floresta-cli -k getblock
```

#### Environment Variables

| Variable | Default | Description |
| ---------------------------------- | -------------- | ----------------------------------------------------------------------------------------------------------- |
| `FLORESTA_SYNC_TIMEOUT` | `120` | Stale-state timeout for `wait_for_sync`. The countdown resets on progress; fires only when the node stalls. |
| `FLORESTA_PEER_CONNECTION_TIMEOUT` | `30` | Timeout in seconds for `wait_for_peers_connections` when waiting for nodes to discover each other. |
| `FLORESTA_REQUEST_TIMEOUT` | `15` | Timeout in seconds for individual RPC requests and socket wait operations. |
| `FLORESTA_REQUEST_STALE_TIMEOUT` | `60` | How long to retry transient RPC errors (read timeouts, connection errors) before giving up. |
| `FLORESTA_TEMP_DIR` | — | Directory where functional tests look for binaries (at `$FLORESTA_TEMP_DIR/binaries`). |
| `UTREEXO_REVISION` | latest release | Tag (without `v` prefix) of `utreexod` to build. |
| `BITCOIN_REVISION` | `30.2` | Version tag or branch of Bitcoin Core to use. |
| `BITCOIND_EXE` | — | Path to a user-provided `bitcoind` binary, skipping download/build. |

Example:

```bash
FLORESTA_SYNC_TIMEOUT=300 FLORESTA_PEER_CONNECTION_TIMEOUT=60 just test-functional-run
```

#### From python utility directly

Additional functional tests are available (minimum python version: 3.12).
It's not recommended to run them directly, since you will need to manually
build the binaries yourself and place them at `$FLORESTA_TEMP_DIR/binaries`.
The advantage is that you can run a specific test suite. For this you'll need to:

* Setup `floresta`/`utreexod` environment;
* Setup python utility;
* Run tests from python utility directly;
* Clean up the environment.
- Setup `floresta`/`utreexod` environment;
- Setup python utility;
- Run tests from python utility directly;
- Clean up the environment.

##### Setup `floresta`/`utreexod` environment

Expand All @@ -186,9 +213,10 @@ a `FLORESTA_TEMP_DIR` environment variable. This variable points to where
our functional tests will look for the binaries.

##### Setup python utility
* Recommended: install [uv: a rust-based python package and project manager](https://docs.astral.sh/uv/).

* Configure an isolated environment:
- Recommended: install [uv: a rust-based python package and project manager](https://docs.astral.sh/uv/).

- Configure an isolated environment:

```bash
# create a virtual environment
Expand All @@ -205,7 +233,7 @@ source .venv/bin/activate
which python
```

* Install module dependencies:
- Install module dependencies:

```bash
# installs dependencies listed in pyproject.toml.
Expand All @@ -224,16 +252,17 @@ uv pip install -r tests/requirements.txt
uv sync
```

* Format code
- Format code

```bash
uv run black ./tests

# if you want to just check
uv run black --check --verbose ./tests
```

- Lint code

* Lint code
```bash
uv run pylint ./tests
```
Expand Down
142 changes: 133 additions & 9 deletions tests/test_framework/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,31 +12,58 @@
`add_node_settings`.
"""

import os
import re
import sys
import contextlib
import copy
import inspect
import os
import random
import socket
import re
import shutil
import signal
import contextlib
import socket
import subprocess
import sys
import time
from datetime import datetime, timezone
from enum import Enum
from typing import Any, Dict, List, Pattern, Tuple, Optional
from typing import Any, Dict, List, Optional, Pattern, Tuple

from requests.exceptions import RequestException
from test_framework.crypto.pkcs8 import (
create_pkcs8_private_key,
create_pkcs8_self_signed_certificate,
)
from test_framework.daemon import ConfigP2P
from test_framework.rpc import ConfigRPC
from test_framework.electrum import ConfigElectrum, ConfigTls
from test_framework.node import Node, NodeType
from test_framework.rpc import ConfigRPC
from test_framework.util import Utility

SYNC_TIMEOUT = float(os.environ.get("FLORESTA_SYNC_TIMEOUT", "120"))
Comment thread
jaoleal marked this conversation as resolved.
PEER_CONNECTION_TIMEOUT = float(
os.environ.get("FLORESTA_PEER_CONNECTION_TIMEOUT", "30")
)


def wait_until(predicate, *, timeout=SYNC_TIMEOUT, interval=0.5):
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Move this to util.py

"""Wait until ``predicate()`` returns True, or raise after *timeout* seconds.

This is a general-purpose primitive inspired by Bitcoin Core's
``wait_until_helper_internal``. Domain-specific helpers such as
``FlorestaTestFramework.wait_for_sync`` build on top of this for
cases that need stale-state detection or RPC error tolerance.
"""
deadline = time.time() + timeout
while time.time() < deadline:
if predicate():
return
time.sleep(interval)

source = inspect.getsource(predicate)
raise AssertionError(
f"wait_until() timed out after {timeout}s. Predicate:\n{source}"
)


# pylint: disable=too-many-public-methods
class FlorestaTestFramework:
Expand Down Expand Up @@ -293,8 +320,8 @@ def wait_for_peers_connections(
Wait for two peers to connect/disconnect to each other.
"""
attempts = 0
timeout = time.time() + 30
while time.time() < timeout:
deadline = time.time() + PEER_CONNECTION_TIMEOUT
while time.time() < deadline:
if self.check_connection(peer_one, peer_two, is_connected):
self.log.debug(
f"Peers {peer_one.variant} and {peer_two.variant} are in the expected "
Expand Down Expand Up @@ -329,6 +356,103 @@ def wait_for_peers_connections(
f"connection state within the timeout. Expected connected: {is_connected}."
)

def wait_for_sync(
Comment thread
jaoleal marked this conversation as resolved.
self, node: Node, target_height: int, stale_timeout: float = SYNC_TIMEOUT
):
"""
Wait until a node is synced to the target height and out of IBD with stale detection.

If the node stops making progress for ``stale_timeout`` seconds, the test fails.
"""
last_progress_time = time.time()
prev_height = None
prev_validated = None

while True:
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using while true is not a good idea. The ideal approach is to handle the timeout case here, because inside the while, if that happens, it can exit by returning immediately. If it keeps going after the timeout, it ends up causing an error.

In this PR here: #897 I added a wait_until helper that already does this automatically, so it makes things easier.

try:
info = node.rpc.get_blockchain_info()
except RequestException as exc:
self.log.debug(f"Node '{node.variant}' RPC error (will retry): {exc}")
stale_elapsed = time.time() - last_progress_time
if stale_elapsed >= stale_timeout:
raise AssertionError(
f"Node '{node.variant}' RPC unreachable for {stale_timeout}s "
f"trying to sync to height {target_height}: {exc}"
) from exc
time.sleep(1)
continue

if info["height"] == target_height and not info["ibd"]:
self.log.debug(
f"Node '{node.variant}' synced to height {target_height}"
)
return

cur_height = info["height"]
cur_validated = info.get("validated")

if cur_height != prev_height or cur_validated != prev_validated:
last_progress_time = time.time()
prev_height = cur_height
prev_validated = cur_validated

stale_elapsed = time.time() - last_progress_time
if stale_elapsed >= stale_timeout:
raise AssertionError(
f"Node '{node.variant}' stalled for {stale_timeout}s trying to "
f"sync to height {target_height}: height={info['height']}, "
f"validated={cur_validated}, ibd={info['ibd']}"
)

time.sleep(1)

def wait_for_height(
self, node: Node, target_height: int, stale_timeout: float = SYNC_TIMEOUT
):
"""
Wait until a node reaches the target height, regardless of IBD state.

Use this instead of ``wait_for_sync`` when the node only needs headers
sync(e.g. syncing from bitcoind which doesn't offer utreexo proofs).

Uses the same stale-state detection as ``wait_for_sync``.
"""
last_progress_time = time.time()
prev_height = None

while True:
try:
info = node.rpc.get_blockchain_info()
except RequestException as exc:
self.log.debug(f"Node '{node.variant}' RPC error (will retry): {exc}")
stale_elapsed = time.time() - last_progress_time
if stale_elapsed >= stale_timeout:
raise AssertionError(
f"Node '{node.variant}' RPC unreachable for {stale_timeout}s "
f"trying to reach height {target_height}: {exc}"
) from exc
time.sleep(1)
continue

cur_height = info["height"]
if cur_height == target_height:
self.log.debug(f"Node '{node.variant}' reached height {target_height}")
return

if cur_height != prev_height:
last_progress_time = time.time()
prev_height = cur_height

stale_elapsed = time.time() - last_progress_time
if stale_elapsed >= stale_timeout:
raise AssertionError(
f"Node '{node.variant}' stalled for {stale_timeout}s trying to "
f"reach height {target_height}: height={cur_height}, "
f"ibd={info['ibd']}"
)

time.sleep(1)

def connect_nodes(
self,
peer_one: Node,
Expand Down
Loading