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
File renamed without changes.
37 changes: 37 additions & 0 deletions .github/workflows/test.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
name: Test

# Run the test suite for every pull request before it is merged.
on:
pull_request:

# The workflow only needs to read source code.
permissions:
contents: read

jobs:
# Build an isolated, lockfile-pinned environment and execute the test suite.
test:
name: Test with Python 3.12
runs-on: ubuntu-latest
steps:
# Make the pull request's exact source revision available to subsequent steps.
- name: Check out repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1

# Use the minimum Python version supported by the published package.
- name: Set up Python
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: "3.12"

# Install Poetry independently of the project environment to manage dependencies.
- name: Install Poetry
run: pipx install poetry

# Recreate the environment from poetry.lock and remove undeclared packages.
- name: Install locked dependencies
run: poetry install --sync

# Run all tests; tests requiring an unavailable external Soufflé binary skip themselves.
- name: Run tests
run: poetry run pytest
26 changes: 26 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# Python bytecode and tooling caches
__pycache__/
*.py[cod]
*.so
.pytest_cache/
.mypy_cache/
.ruff_cache/

# Virtual environments and local Python-version selectors
.venv/
venv/
env/
.python-version

# Packaging and coverage output
build/
dist/
*.egg-info/
.coverage
coverage.xml
htmlcov/

# Generated analysis and experiment output
.souffle/
.souffle-test/
sample-data-results/
16 changes: 15 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,19 @@
A command line tool to establish the equivalence of two alternatively built
Python wheels.

> **External dependency:** Installing `daleq4py` from PyPI installs only the
> Python package. The separately installed [Soufflé](https://souffle-lang.github.io/)
> executable is required for inference-based commands; it is not bundled in
> the `py3-none-any` wheel.

## Requirements

- Python >= 3.12
- [Poetry](https://python-poetry.org/) for dependency management and builds
- [Soufflé](https://souffle-lang.github.io/) available on `PATH` for
inference-based commands: `inference`, `compare-wheels`, and
`run-experiments`. Soufflé is a separately installed system executable, not
a Python package dependency.

## Building

Expand All @@ -25,6 +34,10 @@ poetry build
poetry run pytest
```

## Continuous integration

Every pull request runs the [test workflow](.github/workflows/test.yaml) on GitHub Actions. It creates a clean Python environment from `poetry.lock`, installs the package and its development dependencies, then runs the complete pytest suite. Tests that require the separately installed Soufflé executable skip when it is not available on the runner.

## Running

### daleq4py
Expand Down Expand Up @@ -80,7 +93,8 @@ poetry run inference EDB_DIR
| `EDB_DIR` | path to the `edb/` directory produced by `extract-edb` |
| `-v`, `--verbose` | enable verbose (debug) logging |

The `souffle` binary must be on `PATH`.
Soufflé must be installed separately and its `souffle` binary must be on
`PATH`.

### project-idb

Expand Down
11 changes: 10 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,16 +1,25 @@
[project]
name = "daleq4py"
version = "0.1.0"
description = "CLI tool to establish the equivalence of alternatively built python packages."
description = "CLI for comparing alternatively built Python packages; Soufflé is required for inference-based commands."
authors = [{ name = "Jens Dietrich", email = "jens.dietrich@gmail.com" }]
readme = "README.md"
license = "UPL-1.0"
license-files = ["LICENSE.md"]
requires-python = ">=3.12"
dependencies = ["packaging>=24", "lark>=1.1"]
keywords = ["python", "wheel", "reproducible-builds", "supply-chain-security"]
classifiers = [
"Environment :: Console",
"Programming Language :: Python :: 3 :: Only",
"Programming Language :: Python :: 3.12",
"Topic :: Software Development :: Quality Assurance",
]

[project.urls]
Homepage = "https://github.com/binaryeq/daleq4py"
Repository = "https://github.com/binaryeq/daleq4py"
Issues = "https://github.com/binaryeq/daleq4py/issues"

[project.scripts]
daleq4py = "daleq4py.cli:main"
Expand Down
5 changes: 4 additions & 1 deletion src/daleq4py/inference.py
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,10 @@ def _rel(p: Path) -> str:

if shutil.which(SOUFFLE_EXECUTABLE) is None:
raise InferenceError(
f"{SOUFFLE_EXECUTABLE!r} executable not found on PATH"
"Soufflé is required for inference but its 'souffle' executable "
"was not found on PATH. Install Soufflé "
"(https://souffle-lang.github.io/) and ensure 'souffle' is "
"available on PATH."
)

logger.info("running souffle on %s (cwd=%s)", MERGED_FILENAME, session_dir)
Expand Down
29 changes: 26 additions & 3 deletions tests/test_inference.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
# Author: Jens Dietrich (https://github.com/jensdietrich/), co-authored by claude code

"""Tests for souffle inference on extracted EDBs."""
"""Tests for Soufflé inference on extracted EDBs.

Input-validation tests and the missing-executable diagnostic are independent
of Soufflé and must pass in every environment. End-to-end tests exercise the
generated Datalog rules with the real ``souffle`` executable; they are skipped
when it is not installed, so contributors can run the Python-only test suite.
"""

import shutil

Expand All @@ -14,6 +20,8 @@
run_inference,
)

# End-to-end tests need the separately installed system executable. Keep the
# marker at module scope so their external prerequisite is explicit and uniform.
requires_souffle = pytest.mark.skipif(
shutil.which("souffle") is None,
reason="souffle executable not on PATH",
Expand All @@ -25,7 +33,7 @@ def _read_rows(tsv_path):
return [tuple(line.split("\t")) for line in text.splitlines()]


# --- input validation (no souffle invocation needed) ---
# --- Python-only input validation (no souffle invocation needed) ---


def test_run_inference_rejects_missing_dir(tmp_path):
Expand All @@ -47,7 +55,22 @@ def test_run_inference_rejects_edb_dir_without_facts(tmp_path):
run_inference(edb)


# --- end-to-end (require souffle on PATH) ---
def test_run_inference_explains_how_to_install_missing_souffle(
edb_base_dir, monkeypatch
):
"""Missing Soufflé must produce an actionable installation/PATH message."""
content_path = extract_edb(COLORLOG_PYPI, base_dir=edb_base_dir)
# Simulate a machine without Soufflé regardless of the developer's PATH.
monkeypatch.setattr("daleq4py.inference.shutil.which", lambda _: None)

with pytest.raises(
InferenceError,
match="Install Soufflé .* ensure 'souffle' is available on PATH",
):
run_inference(content_path.parent)


# --- End-to-end execution (requires Soufflé on PATH) ---


@requires_souffle
Expand Down