Skip to content

Commit

Permalink
Structure improvement functional tests (#309)
Browse files Browse the repository at this point in the history
* added pyproject.toml/poetry.lock and change on README to apply proposition made on issue#252

defined python 3.12 as minimum version for functional tests

* linted tests/{example_test, restart, run_tests}.py

changed example_test I: added assertions at run_test

changed example_test II: added set_params method

changed restart: applied the main call at the end of file

changed run_tests: flexibilized it for individual tests

modified pyproject.toml I: added individual test tasks (example-test, restart-test)

modified pyproject.toml II: defined tests task as a sequence of individual tests

linted tests/tests_framework/{utreexod, floresta_rpc, test_framework, secp256k1, electrum_client, mock_rpc, key, bitcoin}.py

changed floresta_rpc I: diminished the number of arguments of FlorestaRPC

changed floresta_rpc II: created a default value REGTEST_RPC_SERVER

changed test_framework I: create class FlorestaTestMetaClass

changed test_framework II: renamed the class as FlorestaTestFramework

changed test_framework III: modified run_node method due to changes on tests/tests_framework/floresta.py

changed test_framework IV: added methods (set_test_params, add_node_settings, get_node_settings)

changed secp256k1 I: disabled linter on some lines

changed secp256k1 II: moved to tests/test_framework/crypto to comply with definition of modules in key.py

changed bitcoin I: added methods (dsha256, read_compact_size method, get_merkle_root method, Transaction.deserialize)

changed bitcoin II: modified Block.get_merkle_root

* changed .github/workflows/functional.yml to comply with the functional tests changes

---------

Co-authored-by: qlrd <qlrddev [email protected]>
  • Loading branch information
qlrd and qlrd authored Dec 27, 2024
1 parent 59978d7 commit 508fb8c
Show file tree
Hide file tree
Showing 15 changed files with 1,958 additions and 461 deletions.
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

0 comments on commit 508fb8c

Please sign in to comment.