Skip to content
Closed
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
75 changes: 75 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
name: CI

on:
push:
branches: [main]
pull_request:
branches: [main]

jobs:
unit-tests:
name: Unit Tests (Python ${{ matrix.python-version }}, ${{ matrix.os }})
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
python-version: ["3.11", "3.12", "3.13"]
steps:
- uses: actions/checkout@v6
with:
submodules: true

- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v6
with:
python-version: ${{ matrix.python-version }}
cache: 'pip'

- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -e "./amitools[vamos]"
pip install "pytest>=7.0" "pytest-cov>=4.0" "pytest-timeout>=2.0"
pip install -e "." --no-deps

- name: Verify machine68k
run: |
python -c "import machine68k; print(f'machine68k {machine68k.__version__}')"
python -c "import machine68k; machine68k.CPU(1); print('CPU OK')"
continue-on-error: true

- name: Run unit tests
run: pytest tests/unit/ -v --timeout=30

integration-tests:
name: Integration Tests (${{ matrix.os }})
runs-on: ${{ matrix.os }}
needs: unit-tests
strategy:
fail-fast: false
matrix:
# Windows excluded: machine68k segfaults due to opcode table
# over-read (cnvogelg/machine68k#8) and JMP/CAS collision (#9).
# Re-enable once upstream merges those fixes.
os: [ubuntu-latest, macos-latest]
steps:
- uses: actions/checkout@v6
with:
submodules: true

- name: Set up Python 3.13
uses: actions/setup-python@v6
with:
python-version: "3.13"
cache: 'pip'

- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -e "./amitools[vamos]"
pip install "pytest>=7.0" "pytest-cov>=4.0" "pytest-timeout>=2.0"
pip install -e "." --no-deps

- name: Run integration tests
run: pytest tests/integration/ -v -m "integration and not smoke" --timeout=60
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,9 @@ BFFSFilesystem
FastFileSystem
SmartFileSystem
pfs3aio

# Test fixtures (override global ignores for committed fixtures)
!tests/fixtures/handlers/pfs3aio
!tests/fixtures/images/pfs3_test.hdf
!tests/fixtures/images/blank.adf
!tests/fixtures/images/pfs3_8mb.hdf
83 changes: 80 additions & 3 deletions TESTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -271,7 +271,7 @@ python3 tools/amifuse_matrix.py \

Then compare the results with:

[PERFORMANCE.md](/Users/stepan/git/AmiFuse-codex/PERFORMANCE.md)
[PERFORMANCE.md](PERFORMANCE.md)

Important interpretation rules:

Expand All @@ -293,7 +293,7 @@ the matrix is the main current performance harness.

The `amitools` submodule has its own test tree and README:

[amitools/test/README.md](/Users/stepan/git/AmiFuse-codex/amitools/test/README.md)
[amitools/test/README.md](amitools/test/README.md)

The important buckets there are:

Expand Down Expand Up @@ -341,6 +341,81 @@ In practice:
- `readme smoke` catches CLI and docs drift
- `amitools` tests catch lower-level runtime semantics

## Pytest Test Suite

The repo has a structured pytest suite under `tests/` that runs without
external fixtures or a live FUSE mount.

### Quick Start

```sh
# all pytest tests (unit + integration, excludes smoke)
pytest tests/ -v --timeout=60

# unit tests only
pytest tests/unit/ -v --timeout=30

# integration tests only (no smoke)
pytest tests/integration/ -v -m "integration and not smoke" --timeout=60
```

### Test Architecture

Tests are organized into four layers:

| Layer | Directory | What It Covers |
|-------|-----------|----------------|
| **Unit** | `tests/unit/` | Pure logic, mocked dependencies, no I/O |
| **Integration** | `tests/integration/` | Cross-module with committed test fixtures |
| **Smoke** | `tests/integration/` (marker) | Wrappers for `tools/` scripts, external fixtures |
| **Legacy** | `tools/*.py` | Original matrix, readme, and format smoke scripts |

Unit and integration tests use committed fixtures under `tests/fixtures/`
and can run anywhere (local, CI, fresh clone). Smoke tests require the
external fixture tree at `~/AmigaOS/AmiFuse/` and are skipped when those
paths are absent.

### Committed Test Fixtures

The `tests/fixtures/` directory contains small images and handler
binaries checked into the repo:

| Path | Description |
|------|-------------|
| `fixtures/images/blank.adf` | Empty ADF floppy image |
| `fixtures/images/test_ofs.adf` | OFS floppy with known directory tree |
| `fixtures/images/test_ffs.adf` | FFS floppy with known directory tree |
| `fixtures/images/pfs3_test.hdf` | PFS3 hard-drive image with test data |
| `fixtures/images/pfs3_8mb.hdf` | 8 MB PFS3 image for write tests |
| `fixtures/handlers/pfs3aio` | PFS3 handler binary |
| `fixtures/icons/` | Reserved for `.info` icon fixtures (currently empty; icon-parser tests use synthetic data) |
| `fixtures/generate_adf.py` | Script to regenerate ADF fixtures |

These fixtures are generated once and committed so CI does not need
Amiga toolchains or large external downloads.

### Markers

| Marker | Description |
|--------|-------------|
| `integration` | Integration tests requiring real fixtures and machine68k |
| `smoke` | Smoke test wrappers for `tools/` scripts (requires external fixtures) |
| `slow` | Long-running tests |
| `fuse` | Requires FUSE/WinFSP kernel driver |
| `windows` | Windows-specific tests |
| `macos` | macOS-specific tests |
| `linux` | Linux-specific tests |

### CI

GitHub Actions runs on every push to `main` and on pull requests:

- **Unit tests:** 3 OS (Ubuntu, macOS, Windows) x 3 Python (3.11, 3.12, 3.13) = 9 jobs
- **Integration tests:** 3 OS x Python 3.13 = 3 jobs, depends on unit-tests
- **Smoke tests:** not in CI (require external fixtures)

Workflow file: `.github/workflows/ci.yml`

## Current Gaps

The following are still planned, not fully documented as standalone test
Expand All @@ -349,4 +424,6 @@ entry points yet:
- far-end file I/O coverage inside a filesystem that spans a partition
larger than `4GiB`
- fuller long-run generated benchmark recipes
- fixture-layout cleanup for `~/AmigaOS/AmiFuse/`
- ~~fixture-layout cleanup for `~/AmigaOS/AmiFuse/`~~ (committed test
fixtures now live in `tests/fixtures/`; external fixtures still used
by smoke tests)
4 changes: 2 additions & 2 deletions amifuse/startup_runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -1004,8 +1004,8 @@ def _pc_valid(pc: int) -> bool:
return run_state

def send_disk_info(self, state: HandlerLaunchState, info_buf_addr: int):
# Arg1 = InfoData* (APTR)
return self.send_packet(state, ACTION_DISK_INFO, [info_buf_addr])
# Arg1 = InfoData BPTR (AmigaDOS packets use BPTRs)
return self.send_packet(state, ACTION_DISK_INFO, [info_buf_addr >> 2])

def send_read(self, state: HandlerLaunchState, buf_addr: int, offset_bytes: int, length_bytes: int):
# Arg1 = window (not used), Arg2 = offset (block), Arg3 = buf, Arg4 = length
Expand Down
2 changes: 2 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ testpaths = ["tests"]
markers = [
"slow: marks tests as slow (deselect with '-m \"not slow\"')",
"fuse: requires FUSE/WinFSP kernel driver",
"integration: integration tests requiring real fixtures and machine68k",
"smoke: smoke test wrappers for tools/ scripts (requires external fixtures)",
"windows: Windows-specific tests",
"macos: macOS-specific tests",
"linux: Linux-specific tests",
Expand Down
Empty file added tests/fixtures/__init__.py
Empty file.
57 changes: 57 additions & 0 deletions tests/fixtures/generate_adf.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
#!/usr/bin/env python3
"""Generate deterministic ADF test fixtures using amitools API.

Run standalone to regenerate: python tests/fixtures/generate_adf.py
Or import generate_ofs_adf() / generate_ffs_adf() from integration tests.
"""
from pathlib import Path

from amitools.fs.ADFSVolume import ADFSVolume
from amitools.fs.blkdev.ADFBlockDevice import ADFBlockDevice
from amitools.fs.FSString import FSString
from amitools.fs.TimeStamp import TimeStamp
from amitools.fs.RootMetaInfo import RootMetaInfo
import amitools.fs.DosType as DosType

FIXTURES_DIR = Path(__file__).parent / "images"
# Deterministic timestamp for reproducible fixtures
# TimeStamp.parse() uses "%d.%m.%Y %H:%M:%S" format
_FIXED_TS = TimeStamp()
_FIXED_TS.parse("01.01.2024 00:00:00")
_FIXED_META = RootMetaInfo(create_ts=_FIXED_TS, disk_ts=_FIXED_TS, mod_ts=_FIXED_TS)


def generate_ofs_adf(path: Path | None = None) -> Path:
"""Create an 880KB OFS ADF with test files."""
path = path or (FIXTURES_DIR / "test_ofs.adf")
blkdev = ADFBlockDevice(str(path))
blkdev.create() # DD 880KB
vol = ADFSVolume(blkdev)
vol.create(FSString("TestOFS"), meta_info=_FIXED_META, dos_type=DosType.DOS0)
vol.write_file(b"Hello, Amiga!\n", FSString("hello.txt"))
vol.create_dir(FSString("subdir"))
vol.write_file(b"Nested file\n", FSString("subdir/nested.txt"))
vol.close()
blkdev.close()
return path


def generate_ffs_adf(path: Path | None = None) -> Path:
"""Create an 880KB FFS ADF with test files."""
path = path or (FIXTURES_DIR / "test_ffs.adf")
blkdev = ADFBlockDevice(str(path))
blkdev.create() # DD 880KB
vol = ADFSVolume(blkdev)
vol.create(FSString("TestFFS"), meta_info=_FIXED_META, dos_type=DosType.DOS_FFS)
vol.write_file(b"FFS test data\n", FSString("data.txt"))
vol.create_dir(FSString("Dir1"))
vol.write_file(b"A" * 1024, FSString("Dir1/kilobyte.bin"))
vol.close()
blkdev.close()
return path


if __name__ == "__main__":
FIXTURES_DIR.mkdir(parents=True, exist_ok=True)
print(f"Generating OFS ADF: {generate_ofs_adf()}")
print(f"Generating FFS ADF: {generate_ffs_adf()}")
Binary file added tests/fixtures/handlers/pfs3aio
Binary file not shown.
Binary file added tests/fixtures/images/blank.adf
Binary file not shown.
Binary file added tests/fixtures/images/pfs3_8mb.hdf
Binary file not shown.
Binary file added tests/fixtures/images/pfs3_test.hdf
Binary file not shown.
15 changes: 15 additions & 0 deletions tests/fixtures/paths.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
"""Shared fixture path constants for all test layers.

This is a simple constants module (not a pytest conftest). Import path
constants from here in both unit and integration tests.
"""
from pathlib import Path

FIXTURES_ROOT = Path(__file__).parent
HANDLERS_DIR = FIXTURES_ROOT / "handlers"
IMAGES_DIR = FIXTURES_ROOT / "images"

PFS3AIO = HANDLERS_DIR / "pfs3aio"
PFS3_TEST_HDF = IMAGES_DIR / "pfs3_test.hdf"
PFS3_8MB_HDF = IMAGES_DIR / "pfs3_8mb.hdf"
BLANK_ADF = IMAGES_DIR / "blank.adf"
Empty file added tests/integration/__init__.py
Empty file.
Loading
Loading