Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
21 changes: 18 additions & 3 deletions .github/workflows/functional.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,29 @@ jobs:
steps:
- uses: actions/checkout@v4

- name: Install poetry
run: pipx install poetry

- uses: actions/setup-python@v5
with:
python-version: '3.12'
cache: 'poetry'
cache-dependency-path: './poetry.lock'

- name: Prepare environment
run: sudo apt-get install -y python3-pip && pip3 install -r tests/requirements.txt
run: poetry install --no-root

- name: Run black formatting
run: poetry run poe format --check --verbose

- name: Run pylint linter
run: poetry run poe lint --verbose

- name: Cache Rust
uses: Swatinem/rust-cache@v2

- name: Build Floresta
run: cargo build

- name: Run functional tests
run: python tests/run_tests.py
- name: Run functional tests tasks
run: poetry run poe tests
48 changes: 47 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -252,7 +252,53 @@ For the full test suite, including long-running tests, use:
cargo test --release
```

Additional functional tests are available. Install dependencies and run the test script with:
#### Functional tests

Additional functional tests are available (minimum python version: 3.12).

* Install [poetry dependencies manager](https://python-poetry.org/docs/#installation). There are many ways to do this:

```bash
# recomended way
pipx install poetry
```

```bash
# official installer (linux / mac)
curl -sSL https://install.python-poetry.org | python3 -
```

```pwsh
# official isntaller (windows - powershell)
(Invoke-WebRequest -Uri https://install.python-poetry.org -UseBasicParsing).Content | py -
```

```bash
# mannually
python3 -m venv $VENV_PATH
$VENV_PATH/bin/pip install -U pip setuptools
$VENV_PATH/bin/pip install poetry
```

* Configure an isolated environment and install module dependencies:

```bash
poetry install --no-root
```

* Run tests:

```bash
poetry run poe tests
```

* Before run tests, check the pre-commit:

```bash
poetry run poe pre-commit
```

* Manual way without poetry: install dependencies and run the test script. This is discouraged since that can lead to inconsistences between different python versions:

```bash
pip3 install -r tests/requirements.txt
Expand Down
489 changes: 489 additions & 0 deletions poetry.lock

Large diffs are not rendered by default.

43 changes: 43 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# With this the user can use a custom
# python environment, like those created
# with venv or pyenv. The default will be
# the system python.
[virtualenvs]
prefer-active-python = true

# This isn't really needed on pratical aspect
# since this is only for organize the functional tests
# but is necessary since its a requirement for poetry.
[tool.poetry]
name = "floresta-functional-tests"
version = "0.0.1"
description = "collection of tools to help with functional testing of Floresta"
authors = ["The Floresta Project Developers"]
license = "MIT"

# To add a new dependency, use `poetry add <package>`.
# To update, first find outdated packages with `poetry show --outdated`,
# then re-install with `poetry add <package>@<version>`.
[tool.poetry.dependencies]
python = ">=3.12"
jsonrpclib = "^0.2.1"
requests = "^2.32.3"
black = "^24.10.0"
pylint = "^3.3.2"
poethepoet = "^0.31.1"

# Here we can define some custom tasks
# (even bash or rust ones can be defined).
# All of them will run on virtualenv environment.
# For more information, see guidelines in https://poethepoet.natn.io
[tool.poe.tasks]
format = "black ./tests"
lint = "pylint ./tests"
example-test = "python tests/run_tests.py --test-name example_test"
restart-test = "python tests/run_tests.py --test-name restart"
tests = ["example-test", "restart-test"]
pre-commit = ["format", "lint", "tests"]

[build-system]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"
107 changes: 96 additions & 11 deletions tests/example_test.py
Original file line number Diff line number Diff line change
@@ -1,31 +1,116 @@
"""
This is an example of how tests should look like, see the class bellow for more info
example_test.py

This is an example of how tests should look like, see the class bellow for more info.

Every test should define, at least, two special methods:

- `set_test_params`: change default values for number of node, topology, etc.
- `run_test`: the test itself

Any attempt to define a test without these methods will raise a TypeError.

Other methods are available to do more things. Some of them make sense only in `set_test_params`
and others make sense only in `run_tests`.

- `add_node_settings`: register a node settings to create a node after. A node will be a spawned
`cargo run --features json-rpc --bin florestad -- --network <chain>` process.
In summary, its a `FlorestaRPC` instance.

The chain can be one of ["regtest", "signet", "testnet"].
You can pass some extra arguments with `extra_args` and it will be appended
to the process command. The `rpcserver` is a dictionary defining a "host",
"port", "username" and "password". The "data_dir" is optional and can be
used to create a temporary directory to store files. It will return an
integer pointing an index of a list of nodes.

- `get_node_settings`: get a registered node settings

- `run_node`: run a node for a registered node settings at some index,
configured with `add_node_settings`.

- `get_node`: get a resgistered running node.

- `wait_for_rpc_connection`: given a node index, wait for it to be available.

- `run_rpc`: our RPC is a MockUtreexod running on a thread.

- `stop_node`: given a running node at index, stop it.
If any directory was created, it will be removed.

- `stop`: stop all registered nodes.

After the definition of test within a class, you should call `MyTest().main()` at the end of file.
"""
import time
import os

from test_framework.test_framework import TestFramework
import json
from test_framework.test_framework import FlorestaTestFramework
from test_framework.electrum_client import ElectrumClient
from test_framework.mock_rpc import MockUtreexod
from test_framework.floresta_rpc import REGTEST_RPC_SERVER


class ExampleTest(FlorestaTestFramework):
"""
Tests should be a child class from FlorestaTestFramework

class ExampleTest(TestFramework):
""" Tests should be a child class from TestFramework """
In each test class definition, `set_test_params` and `run_test`, say what
the test do and the expected result in the docstrings
"""

index = [-1]
expected_version = ["Floresta 0.3.0", "1.4"]
expected_height = 0
expected_block = "0f9188f13cb7b2c71f2a335e3a4fc328bf5beb436012afca590b1a11466e2206"
expected_difficulty = 1
expected_leaf_count = 0

def set_test_params(self):
"""
Here we define setup for test adding a node definition
"""
ExampleTest.index[0] = self.add_node_settings(
chain="regtest", extra_args=[], rpcserver=REGTEST_RPC_SERVER
)

# All tests should override the run_test method
def run_test(self):
"""
Here we define the test itself:

- creates a dummy rpc listening on port 8080
- wait until the rpc is ready and start a new node (this crate's binary)
- wait the node to start
- perform some requests to FlorestaRPC node
- Create an instance of the Electrum Client, a small implementation of the electrum
protocol, to test our own electrum implementation
"""
# This creates a dummy rpc listening on port 8080
self.run_rpc()

# Wait until the rpc is ready
# Start a new node (this crate's binary)
node1 = self.run_node("./data/test1", "regtest")
self.run_node(ExampleTest.index[0])

# Wait the node to start
self.wait_for_rpc_connection()
self.wait_for_rpc_connection(ExampleTest.index[0])

# Perform for some defined requests to FlorestaRPC
node = self.get_node(ExampleTest.index[0])
inf_response = node.get_blockchain_info()

# Create an instance of the Electrum Client, a small implementation of the electrum
# protocol, to test our own electrum implementation
electrum = ElectrumClient("localhost", 50001)
print(electrum.get_version())
rpc_response = json.loads(electrum.get_version())

# Make assertions
assert rpc_response["result"][0] == ExampleTest.expected_version[0]
assert rpc_response["result"][1] == ExampleTest.expected_version[1]
assert inf_response["height"] == ExampleTest.expected_height
assert inf_response["best_block"] == ExampleTest.expected_block
assert inf_response["difficulty"] == ExampleTest.expected_difficulty
assert inf_response["leaf_count"] == ExampleTest.expected_leaf_count


if __name__ == '__main__':
if __name__ == "__main__":
ExampleTest().main()
70 changes: 57 additions & 13 deletions tests/restart.py
Original file line number Diff line number Diff line change
@@ -1,21 +1,65 @@
import subprocess
"""
restart.py

A simple test that restart a Floresta node and a related data directory.

The directories used between each power-on/power-off must not be corrupted.
"""

import time
import os
import filecmp
from test_framework.test_framework import TestFramework
from test_framework.test_framework import FlorestaTestFramework
from test_framework.floresta_rpc import REGTEST_RPC_SERVER


class TestRestart(FlorestaTestFramework):
"""
Test the restart of a node, calling a first node (0) the recall it as (1);
We need to check if given data_dirs arent corrupted between restarts
"""

indexes = [-1, -1]

def set_test_params(self):
"""
Here we define setup for test
"""
dirname = os.path.dirname(__file__)
TestRestart.indexes[0] = self.add_node_settings(
chain="regtest",
extra_args=[],
rpcserver=REGTEST_RPC_SERVER,
data_dir=os.path.normpath(os.path.join(dirname, "data", "0")),
)
TestRestart.indexes[1] = self.add_node_settings(
chain="regtest",
extra_args=[],
rpcserver=REGTEST_RPC_SERVER,
data_dir=os.path.normpath(os.path.join(dirname, "data", "1")),
)

class TestRestart(TestFramework):
def run_test(self):
"""
Tests if we don't corrupt our data dir between restarts. This would have caught,
the error fixed in #9
Tests if we don't corrupt our data dir between restarts. This would have caught,
the error fixed in #9
"""
base_testdir = "data/TestRestart/"
self.run_node(base_testdir + "1/")
time.sleep(5)
self.stop_node(0)
self.run_node(base_testdir + "2/")
time.sleep(5)
self.stop_node(0)
assert (filecmp.dircmp(base_testdir + "2/", base_testdir + "1/"))
# start first node, wait and then kill
self.run_node(TestRestart.indexes[0])
time.sleep(5.0)
self.stop_node(TestRestart.indexes[0])

# start second node, wait and then kill
self.run_node(TestRestart.indexes[1])
time.sleep(5.0)
self.stop_node(TestRestart.indexes[1])

# check for any corruption
assert filecmp.dircmp(
self.get_node_settings(TestRestart.indexes[0])["data_dir"],
self.get_node_settings(TestRestart.indexes[1])["data_dir"],
)


if __name__ == "__main__":
TestRestart().main()
Loading
Loading