Skip to content
Open
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
41 changes: 29 additions & 12 deletions .github/regression.sh
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,8 @@ elif is_truthy "${CI_BYRON_CLUSTER:-}"; then
export TESTNET_VARIANT="${CLUSTER_ERA:-conway}_slow"
fi

export CARDANO_NODE_SOCKET_PATH_CI="$WORKDIR/state-cluster0/bft1.socket"
CARDANO_NODE_SOCKET_PATH_CI="${CARDANO_NODE_SOCKET_PATH_CI:-$WORKDIR/state-cluster0/bft1.socket}"
export CARDANO_NODE_SOCKET_PATH_CI

# assume we run tests on testnet when `BOOTSTRAP_DIR` is set
if [ -n "${BOOTSTRAP_DIR:-}" ]; then
Expand Down Expand Up @@ -115,16 +116,25 @@ case "${CARDANO_CLI_REV:-}" in
esac

# setup cardano-node binaries
case "${NODE_REV:-}" in
"" | "none" )
NODE_REV=master
;;
esac
# shellcheck disable=SC1091
. .github/source_cardano_node.sh
cardano_bins_build_all "$NODE_REV" "${CARDANO_CLI_REV:-}"
PATH_PREPEND="$(cardano_bins_print_path_prepend "${CARDANO_CLI_REV:-}")${PATH_PREPEND}"
export PATH_PREPEND
if [ -n "${CARDANO_PREBUILT_DIR:-}" ]; then
# Pre-built binaries were baked into the image (e.g. for Antithesis).
# Skip all nix builds and point PATH_PREPEND at the pre-built directories.
_d="${CARDANO_PREBUILT_DIR}"
PATH_PREPEND="${_d}/cardano-node/bin:${_d}/cardano-submit-api/bin:${_d}/cardano-cli/bin:${_d}/bech32/bin:${PATH_PREPEND}"
export PATH_PREPEND
unset _d
else
case "${NODE_REV:-}" in
"" | "none" )
NODE_REV=master
;;
esac
# shellcheck disable=SC1091
. .github/source_cardano_node.sh
cardano_bins_build_all "$NODE_REV" "${CARDANO_CLI_REV:-}"
PATH_PREPEND="$(cardano_bins_print_path_prepend "${CARDANO_CLI_REV:-}")${PATH_PREPEND}"
export PATH_PREPEND
fi

# optimize nix store if running in GitHub Actions
if [ -n "${GITHUB_ACTIONS:-}" ]; then
Expand Down Expand Up @@ -254,7 +264,14 @@ nix develop --accept-flake-config .#testenv --command bash -c '

echo "::group::Python venv setup"
printf "start: %(%H:%M:%S)T\n" -1
. .github/setup_venv.sh clean
# When _VENV_DIR points to a pre-built venv (e.g. baked into the image for
# Antithesis), skip the `clean` flag so the existing venv is reused as-is
# without re-downloading packages.
if [ -n "${_VENV_DIR:-}" ] && [ -e "${_VENV_DIR}" ]; then
. .github/setup_venv.sh
else
. .github/setup_venv.sh clean
fi
echo "::endgroup::" # end group for "Python venv setup"

echo "::group::🧪 Testrun"
Expand Down
2 changes: 1 addition & 1 deletion .github/run_tests.sh
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ EOF

run_pytest() {
if [ -n "${SESSION_TIMEOUT:-}" ]; then
local -a timeout_arr=( "--foreground" "--signal=INT" "--kill-after=0" "$SESSION_TIMEOUT" )
local -a timeout_arr=( "--foreground" "--signal=INT" "--kill-after=120" "$SESSION_TIMEOUT" )
echo "Running: PYTEST_ADDOPTS='${PYTEST_ADDOPTS:-}' timeout ${timeout_arr[*]} pytest $*"
timeout "${timeout_arr[@]}" pytest "$@"
else
Expand Down
29 changes: 29 additions & 0 deletions cardano_node_tests/tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,35 @@ def pytest_keyboard_interrupt() -> None:
(session_basetemp / INTERRUPTED_NAME).touch()


def pytest_runtest_logreport(report: tp.Any) -> None:
"""Emit an Antithesis SDK assertion for every test failure."""
if report.when != "call" or not report.failed:
return
sdk_file = pl.Path(os.environ.get("ANTITHESIS_OUTPUT_DIR", "/tmp/antithesis")) / "sdk.jsonl"
sdk_file.parent.mkdir(parents=True, exist_ok=True)
longrepr = report.longrepr
reprcrash = getattr(longrepr, "reprcrash", None)
exc_message = reprcrash.message if reprcrash else (str(longrepr)[:2000] if longrepr else "")
assertion = {
"antithesis_assert": {
"type": "always",
"condition": False,
"display_name": report.nodeid,
"message": exc_message,
"details": {"traceback": str(longrepr)[-2000:] if longrepr else ""},
"location": {
"function": report.nodeid,
"file": str(report.fspath),
"begin_line": 0,
"begin_column": 0,
"class": "",
},
}
}
with sdk_file.open("a") as f:
f.write(json.dumps(assertion) + "\n")


@pytest.fixture(scope="session")
def init_pytest_temp_dirs(tmp_path_factory: TempPathFactory) -> None:
"""Init `PytestTempDirs`."""
Expand Down
29 changes: 22 additions & 7 deletions cardano_node_tests/tests/test_tx_basic.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
from cardano_node_tests.tests import common
from cardano_node_tests.tests import issues
from cardano_node_tests.tests import tx_common
from cardano_node_tests.utils import antithesis
from cardano_node_tests.utils import cluster_nodes
from cardano_node_tests.utils import clusterlib_utils
from cardano_node_tests.utils import dbsync_utils
Expand Down Expand Up @@ -181,13 +182,27 @@ def test_transfer_funds(
)

out_utxos = cluster.g_query.get_utxo(tx_raw_output=tx_output)
assert (
clusterlib.filter_utxos(utxos=out_utxos, address=src_addr.address)[0].amount
== clusterlib.calculate_utxos_balance(tx_output.txins) - tx_output.fee - amount
), f"Incorrect balance for source address `{src_addr.address}`"
assert (
clusterlib.filter_utxos(utxos=out_utxos, address=dst_addr.address)[0].amount == amount
), f"Incorrect balance for destination address `{dst_addr.address}`"

src_actual = clusterlib.filter_utxos(utxos=out_utxos, address=src_addr.address)[0].amount
src_expected = clusterlib.calculate_utxos_balance(tx_output.txins) - tx_output.fee - amount
antithesis.always(
src_actual == src_expected,
"Source balance decreased by transfer amount and fee",
{"src_addr": src_addr.address, "expected": src_expected, "actual": src_actual},
)
assert src_actual == src_expected, (
f"Incorrect balance for source address `{src_addr.address}`"
)

dst_actual = clusterlib.filter_utxos(utxos=out_utxos, address=dst_addr.address)[0].amount
antithesis.always(
dst_actual == amount,
"Destination received exact transfer amount",
{"dst_addr": dst_addr.address, "expected": amount, "actual": dst_actual},
)
assert dst_actual == amount, (
f"Incorrect balance for destination address `{dst_addr.address}`"
)

common.check_missing_utxos(cluster_obj=cluster, utxos=out_utxos)

Expand Down
42 changes: 42 additions & 0 deletions cardano_node_tests/utils/antithesis.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
"""Antithesis SDK wrappers.

All functions are no-ops when the ``antithesis`` package is not installed,
so tests that use them run normally outside the Antithesis environment.
Install the package only inside the Antithesis Docker image — do not add it
to pyproject.toml.
"""

import typing as tp

try:
import antithesis.assertions as _ant

def always(condition: bool, message: str, details: tp.Mapping[str, tp.Any]) -> None:
"""Assert *condition* is true on every invocation."""
_ant.always(condition, message, details)

def sometimes(condition: bool, message: str, details: tp.Mapping[str, tp.Any]) -> None:
"""Assert *condition* is true at least once across all calls."""
_ant.sometimes(condition, message, details)

def reachable(message: str, details: tp.Mapping[str, tp.Any]) -> None:
"""Assert this code location is reached at least once."""
_ant.reachable(message, details)

def unreachable(message: str, details: tp.Mapping[str, tp.Any]) -> None:
"""Assert this code location is never reached."""
_ant.unreachable(message, details)

except ImportError:

def always(condition: bool, message: str, details: tp.Mapping[str, tp.Any]) -> None: # type: ignore[misc]
pass

def sometimes(condition: bool, message: str, details: tp.Mapping[str, tp.Any]) -> None: # type: ignore[misc]
pass

def reachable(message: str, details: tp.Mapping[str, tp.Any]) -> None: # type: ignore[misc]
pass

def unreachable(message: str, details: tp.Mapping[str, tp.Any]) -> None: # type: ignore[misc]
pass
77 changes: 77 additions & 0 deletions docker-antithesis/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
# Dockerfile for cardano-node-tests (Antithesis-compatible driver image)
#
# All heavy dependencies are baked in at image build time so the container
# runs without any network access (required by Antithesis environments).
#
# Build args:
# GIT_REVISION — git commit hash stored as $GIT_REVISION in the image
# NODE_REV — cardano-node git ref to pre-build (default: master)
#
# Build and push to GHCR before submitting to Antithesis:
# docker build -f docker-antithesis/Dockerfile \
# --build-arg GIT_REVISION=$(git rev-parse HEAD) \
# --build-arg NODE_REV=master \
# -t ghcr.io/saratomaz/cardano-node-tests-antithesis:latest .
# docker push ghcr.io/saratomaz/cardano-node-tests-antithesis:latest

FROM nixos/nix:2.25.5

ARG GIT_REVISION
ARG NODE_REV=master

ENV GIT_REVISION=${GIT_REVISION}
# Store the baked-in node revision for reference at runtime.
ENV BAKED_NODE_REV=${NODE_REV}

# Configure Nix with IOG binary cache and required experimental features.
RUN mkdir -p /etc/nix && \
printf 'extra-substituters = https://cache.iog.io\n\
extra-trusted-public-keys = hydra.iohk.io:f/Ea+s+dFdN+3Y/G+FDgSq+a5NEWhJGzdjvKNGv0/EQ=\n\
experimental-features = nix-command flakes\n\
accept-flake-config = true\n' >> /etc/nix/nix.conf

WORKDIR /work
COPY . /work/

# Pre-build cardano-node, cardano-submit-api, cardano-cli, and bech32 into /opt/cardano/.
# NODE_REV is fixed at image build time — no network access is needed at runtime.
RUN mkdir -p /opt/cardano && \
nix build \
--accept-flake-config --no-write-lock-file \
"github://github.com/IntersectMBO/cardano-node?ref=${NODE_REV}#cardano-node" \
-o /opt/cardano/cardano-node && \
nix build \
--accept-flake-config --no-write-lock-file \
"github://github.com/IntersectMBO/cardano-node?ref=${NODE_REV}#cardano-submit-api" \
-o /opt/cardano/cardano-submit-api && \
nix build \
--accept-flake-config --no-write-lock-file \
"github://github.com/IntersectMBO/cardano-node?ref=${NODE_REV}#cardano-cli" \
-o /opt/cardano/cardano-cli && \
nix build \
--accept-flake-config --no-write-lock-file \
"github://github.com/IntersectMBO/cardano-node?ref=${NODE_REV}#bech32" \
-o /opt/cardano/bech32

# Pre-warm the testenv dev shell (pulls nixpkgs, postgres, uv, python313 into the
# nix store) and create the Python venv at /opt/tests-venv with all project
# dependencies installed. This is the same step regression.sh does at runtime
# but done here so no pip/uv network calls are needed in the Antithesis env.
RUN nix develop --accept-flake-config .#testenv --command \
bash -c 'python3 -m venv /opt/tests-venv --prompt tests-venv && \
. /opt/tests-venv/bin/activate && \
cd /work && \
uv sync --active --no-dev && \
pip install "antithesis>=0.2.0,<0.3.0"'

# Pre-warm the base dev shell (bash, coreutils, git, jq, …) so its store
# paths are cached and the regression.sh shebang resolves offline.
RUN nix develop --accept-flake-config .#base --command true

# Create the Antithesis test driver directory and install the entry-points.
# singleton_driver_* files are run once per test run by Antithesis.
RUN mkdir -p /opt/antithesis/test/v1/quickstart && \
cp /work/docker-antithesis/antithesis_run.sh \
/opt/antithesis/test/v1/quickstart/singleton_driver_regression.sh && \
chmod +x /opt/antithesis/test/v1/quickstart/singleton_driver_regression.sh && \
chmod +x /work/docker-antithesis/node_run.sh
13 changes: 13 additions & 0 deletions docker-antithesis/Dockerfile.config
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Config image for Antithesis.
#
# Contains only the docker-compose.yaml that tells Antithesis how to run
# the services. Must be pushed to the Antithesis registry alongside the
# driver image.
#
# Build:
# docker build -f docker/Dockerfile.config \
# -t us-central1-docker.pkg.dev/<tenant>/antithesis/config:latest .
# docker push us-central1-docker.pkg.dev/<tenant>/antithesis/config:latest

FROM scratch
COPY docker/docker-compose.yaml /docker-compose.yaml
66 changes: 66 additions & 0 deletions docker-antithesis/Dockerfile.dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
# Ignore unnecessary files during Docker build

# Git
.git/
.gitignore

# Python
__pycache__/
*.py[cod]
*$py.class
*.so
.Python
*.egg-info/
dist/
build/
*.egg

# Virtual environments
.venv/
venv/
ENV/
env/

# Testing artifacts
run_workdir/
.artifacts/
.cli_coverage/
.reports/
allure-results/
allure-results.tar.xz
testrun-report.*
*.log
*.json.log

# IDE
.idea/
.vscode/
*.swp
*.swo
*~

# Nix
result
result-*

# Documentation
docs/_build/
*.md

# Temporary files
*.tmp
*.bak
.DS_Store

# Scripts output
scripts/destination/
scripts/destination_working/

# Coverage
.coverage
htmlcov/
cli_coverage.json
requirements_coverage.json

# CI specific
.bin/
Loading