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
7 changes: 5 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,27 +14,30 @@

jobs:
build_install:
name: Build and install the Ledgerblue Python package
name: Build and test the Ledgerblue Python package (Python ${{ matrix.python_version }})
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
python_version: ['3.8', '3.9', '3.10', '3.11', '3.12', '3.13']
steps:
- name: Clone
uses: actions/checkout@v4

- name: Setup Python version
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python_version }}

- name: Build & install
run: |
pip install -U pip
pip install -U .
pip install -U ".[dev]"
- name: pytest
run: pytest

deploy:

Check warning

Code scanning / CodeQL

Workflow does not contain permissions Medium

Actions job or workflow does not limit the permissions of the GITHUB_TOKEN. Consider setting an explicit permissions block, using the following as a minimal starting point: {contents: read}
name: Build and deploy ledgerblue package
needs: build_install
uses: LedgerHQ/ledger-app-workflows/.github/workflows/reusable_pypi_deployment.yml@v1
Expand Down
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,7 @@ dist/
ledgerblue.egg-info/
__pycache__
.python-version
.coverage
tests/.cache/

__version__.py
20 changes: 20 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,26 @@ pip3 install pyscard
Usage:
If the environment variable `PCSC=1` is defined, ledgerblue tools will communicate through the first PCSC interface with a detected NFC tag

## Testing

Run the test suite with:

```bash
Comment on lines +85 to +87
python -m pytest
```

### Golden APDU files

Some tests work by running `loadApp` in offline mode (`--offline`) and comparing the resulting APDU sequence byte-for-byte against a checked-in reference file in `tests/fixtures/`.

If you intentionally change APDU generation (e.g. a new field in the install parameters), regenerate the reference with:

```bash
UPDATE_SNAPSHOTS=1 python -m pytest tests/test_load_app.py -v
```

The updated `tests/fixtures/minimal_reference.apdu` should then be committed alongside the code change so the diff in the reference file documents exactly what changed at the wire level.

Comment on lines +99 to +102
## Ledgerblue documentation

You can generate the Ledgerblue documentation locally.
Expand Down
19 changes: 17 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ Home = "https://github.com/LedgerHQ/blue-loader-python"

[project.optional-dependencies]
smartcard = [
"python-pyscard>=1.6.12"
"pyscard>=1.6.12"
]
doc = [
"sphinx",
Expand All @@ -65,9 +65,24 @@ doc = [
]
dev = [
"pre-commit==3.2.0",
"ruff==0.3.7"
"ruff==0.3.7",
"pytest>=8,<9",
"pytest-cov>=5,<6",
"intelhex>=2.3,<3"
]

[tool.pytest.ini_options]
testpaths = ["tests"]
python_files = ["test_*.py"]
addopts = "--cov=ledgerblue --cov-branch --cov-report=term-missing --cov-report=html"

[tool.coverage.run]
source = ["ledgerblue"]
branch = true

[tool.coverage.report]
show_missing = true

[tool.setuptools_scm]
write_to = "ledgerblue/__version__.py"
local_scheme = "no-local-version"
Expand Down
75 changes: 75 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
from __future__ import annotations

import urllib.error
import urllib.request
from pathlib import Path

import pytest
from elftools.elf.elffile import ELFFile
from intelhex import IntelHex

# ---------------------------------------------------------------------------
# App Boilerplate 2.1.0 ELF release URLs
# ---------------------------------------------------------------------------

_RELEASE_BASE = "https://github.com/LedgerHQ/app-boilerplate/releases/download/2.1.0"
BOILERPLATE_ELF_URLS: dict[str, str] = {
"nanox": f"{_RELEASE_BASE}/app-2.1.0-nanox.elf",
"nanos2": f"{_RELEASE_BASE}/app-2.1.0-nanos2.elf",
"stax": f"{_RELEASE_BASE}/app-2.1.0-stax.elf",
"flex": f"{_RELEASE_BASE}/app-2.1.0-flex.elf",
"apex_p": f"{_RELEASE_BASE}/app-2.1.0-apex_p.elf",
}

_CACHE = Path(__file__).parent / ".cache"


def _fetch_hex(device: str) -> Path:
"""Download the ELF for *device* and convert to Intel HEX, with caching."""
_CACHE.mkdir(exist_ok=True)
url = BOILERPLATE_ELF_URLS[device]
elf = _CACHE / f"app-2.1.0-{device}.elf"
hex_path = _CACHE / f"app-2.1.0-{device}.hex"

if not elf.exists():
try:
urllib.request.urlretrieve(url, elf)
except (urllib.error.URLError, OSError) as exc:
pytest.skip(f"Could not download {url}: {exc}")

if not hex_path.exists():
ih = IntelHex()
with elf.open("rb") as f:
ef = ELFFile(f)
for segment in ef.iter_segments():
if segment["p_type"] == "PT_LOAD":
ih.frombytes(segment.data(), offset=segment["p_paddr"])
ih.start_addr = {"EIP": ef["e_entry"]}
ih.write_hex_file(str(hex_path))

return hex_path


@pytest.fixture(scope="session")
def boilerplate_nanox_hex() -> Path:
return _fetch_hex("nanox")


@pytest.fixture(scope="session")
def boilerplate_nanos2_hex() -> Path:
return _fetch_hex("nanos2")


@pytest.fixture(scope="session")
def boilerplate_stax_hex() -> Path:
return _fetch_hex("stax")


@pytest.fixture(scope="session")
def boilerplate_flex_hex() -> Path:
return _fetch_hex("flex")


@pytest.fixture(scope="session")
def boilerplate_apex_p_hex() -> Path:
return _fetch_hex("apex_p")
139 changes: 139 additions & 0 deletions tests/fixtures/ref_apdu_flag.apdu

Large diffs are not rendered by default.

139 changes: 139 additions & 0 deletions tests/fixtures/ref_boot_addr_above_min.apdu

Large diffs are not rendered by default.

139 changes: 139 additions & 0 deletions tests/fixtures/ref_curve_bls12381g1.apdu

Large diffs are not rendered by default.

139 changes: 139 additions & 0 deletions tests/fixtures/ref_curve_ed25519.apdu

Large diffs are not rendered by default.

139 changes: 139 additions & 0 deletions tests/fixtures/ref_curve_secp256k1.apdu

Large diffs are not rendered by default.

139 changes: 139 additions & 0 deletions tests/fixtures/ref_curve_secp256r1.apdu

Large diffs are not rendered by default.

140 changes: 140 additions & 0 deletions tests/fixtures/ref_delete.apdu

Large diffs are not rendered by default.

139 changes: 139 additions & 0 deletions tests/fixtures/ref_dep_no_version.apdu

Large diffs are not rendered by default.

139 changes: 139 additions & 0 deletions tests/fixtures/ref_dep_with_version.apdu

Large diffs are not rendered by default.

139 changes: 139 additions & 0 deletions tests/fixtures/ref_empty_path.apdu

Large diffs are not rendered by default.

139 changes: 139 additions & 0 deletions tests/fixtures/ref_icon.apdu

Large diffs are not rendered by default.

135 changes: 135 additions & 0 deletions tests/fixtures/ref_installparams_size.apdu

Large diffs are not rendered by default.

137 changes: 137 additions & 0 deletions tests/fixtures/ref_nocrc.apdu

Large diffs are not rendered by default.

139 changes: 139 additions & 0 deletions tests/fixtures/ref_path.apdu

Large diffs are not rendered by default.

139 changes: 139 additions & 0 deletions tests/fixtures/ref_signature.apdu

Large diffs are not rendered by default.

139 changes: 139 additions & 0 deletions tests/fixtures/ref_slip21.apdu

Large diffs are not rendered by default.

139 changes: 139 additions & 0 deletions tests/fixtures/ref_slip21_no_bip32_path.apdu

Large diffs are not rendered by default.

139 changes: 139 additions & 0 deletions tests/fixtures/ref_tlvraw.apdu

Large diffs are not rendered by default.

Loading
Loading