Metadata-Version: 2.5
Name: qamule-pytest
Version: 0.1.0
Summary: Android device and artifact fixtures for pytest
License-Expression: MIT
License-File: LICENSE
Requires-Python: >=3.11
Requires-Dist: pytest>=8.3
Requires-Dist: uiautomator2>=3.7.0
Description-Content-Type: text/markdown

# qamule-pytest

`qamule-pytest` provides reusable Android device and artifact fixtures for
pytest-based automation.

Devices are connected through `uiautomator2` and reused for the whole test
session. Artifact directories are created only when a test or plugin requests
them.

## Install

```bash
pip install qamule-pytest
```

## Use Devices

Without configuration, request the default `d` fixture:

```python
def test_smoke(d):
    assert d.serial is not None
```

Register named devices by repeating `--device NAME:SERIAL`:

```bash
pytest --device phone:emulator-5554 --device tablet:emulator-5556
```

Set one uiautomator2 server port for all configured devices when needed:

```bash
pytest --device phone:emulator-5554 --device-port 26831
```

```python
def test_on_phone(phone):
    assert phone.serial == "emulator-5554"
```

Device names must be non-keyword Python identifiers and must not use a fixture
name reserved by `qamule-pytest`. When `--device-port` is omitted, the server
port is not passed to `uiautomator2` and its default is used.

`qamule-pytest` currently supports single-process pytest execution. It rejects
active `pytest-xdist` runs with a usage error; run without `-n` or
`--numprocesses`.

Use `device_manager.get(name)` for lazy lookup. The `devices` fixture connects
and returns all configured devices.

## Artifact Directories

Use the fixtures exposed by the plugin:

```python
def test_capture(testcase_device_artifacts_dir):
    screenshot = testcase_device_artifacts_dir("phone") / "screen.jpg"
    screenshot.write_bytes(b"example")
```

Artifacts use the following layout:

```text
pytest-artifacts/<session-id>/<sequence>-<testcase>/<device-name>/
```

When a test call fails, each device already connected during the pytest session
is captured automatically:

```text
<device-name>/failure.jpg
<device-name>/hierarchy.xml
```

Failure capture does not connect unused devices. Screenshot and hierarchy
capture are independent best-effort operations, so capture errors never replace
the original test failure. Expected `xfail` results are not captured.

Available fixtures:

- `session`: metadata and paths for the current pytest run.
- `artifacts_dir`: artifact root for the current run.
- `testcase`: metadata and paths for the current test execution.
- `testcase_artifacts_dir`: artifact directory for the current test.
- `testcase_device_artifacts_dir(name)`: artifact directory for one device.

Directories are created lazily. Requesting `session` alone does not write an
artifact directory.

Plugins that cannot request fixtures from a hook can use `get_session(config)`,
`get_testcase(item)`, or `get_testcase_device_dir(item, name)` from
`qamule_pytest.features.artifacts.plugin`.

## Session Reports

Every pytest run prints its QAMule session ID and writes live report data under:

```text
pytest-sessions/<session-id>/events.jsonl
pytest-sessions/<session-id>/state.json
```

`events.jsonl` is an append-only stream containing session, collection, test
lifecycle, phase result, duration, and failure traceback events. `state.json` is
an atomically updated snapshot containing the pytest process ID, session state,
timestamps, exit status, and result totals.

Inspect a session from the directory where pytest started:

```bash
uv run qamule report status <session-id>
uv run qamule report show <session-id> 'tests/test_login.py::test_login'
```

Serve the realtime report locally:

```bash
uv run qamule report serve <session-id>
uv run qamule report serve ./pytest-sessions/<session-id>
uv run qamule report serve <session-id> --host 127.0.0.1 --port 26826 -q
```

The report receives incremental updates over Server-Sent Events. Only the
selected case's evidence is rendered, report JSON contains image metadata only,
and the browser requests each image only when it is near the visible area.
`report serve` accepts either a session ID or the absolute or relative path to
its `pytest-sessions/<session-id>` directory. Use `--root PATH` with a session
ID when another report command runs outside the directory where pytest started.
This package intentionally does not export static report bundles.

## Interactive Pauses

Use the `checkpoint` fixture to stop a test until an external decision arrives:

```python
def test_login(checkpoint):
    decision = checkpoint(
        "confirm the login screen",
        images=["screens/login.jpg"],
    )
    assert decision.result is True
```

Pause automatically after failed test calls with:

```bash
uv run pytest --pause-on-failure
```

Every checkpoint and failure pause times out after 600 seconds by default. Set
a different positive duration for the whole pytest run with
`--pause-timeout SECONDS`. A timed-out checkpoint returns a decision with
`result=None`, `has_result=False`, and `timed_out=True`. A timed-out failure
pause releases pytest so the original test failure can finish.

To exercise checkpoint-dependent tests without external interaction, mock every
checkpoint result for the pytest run:

```bash
uv run pytest --checkpoint-mock-result true
uv run pytest --checkpoint-mock-result false
```

The checkpoint still publishes its `paused` event and artifacts, then
immediately resumes with the selected boolean result. While this option is
enabled, checkpoint watch instructions are not printed and the resume message
is `mock via --checkpoint-mock-result`.

While pytest is paused, use its printed session ID from another terminal:

```bash
uv run qamule watch <session-id>
uv run qamule resume <session-id> <pause-id> \
    --result true \
    --message "login screen approved"
```

Checkpoint resumes require `--result true`, `--result false`, or `--result
none`. Failure pauses do not require a result. Attach images by repeating
`--image PATH`. Common image formats such as JPG, PNG, GIF, and WebP are
supported when Python can identify their MIME type. JPG is recommended and is
the format used for automatic failure screenshots.

Pause state is stored under
`pytest-sessions/<session-id>/pauses/<pause-id>/`. Image bytes are deduplicated
under `pytest-sessions/<session-id>/blobs/<sha256>`; status and resume files
contain only metadata and hashes. `watch` also returns the final session state
if pytest finishes or reports `process_exited` if the pytest process disappears.
Resume and timeout compete for one atomically published `resolution.json`, so a
resume accepted at the deadline cannot later be overwritten by a timeout.

## License

`qamule-pytest` is released under the MIT License.

## Development

From the workspace root:

```bash
uv sync --package qamule-pytest
uv run pytest packages/qamule-pytest/tests -q
```
