# Yorvana — Testing Setup & Infrastructure

This document summarizes the testing architecture and the steps required to re-establish the environment, especially when working within a Distrobox or containerized setup.

## 1. Test Organization (Naming Convention)

To maintain a clear distinction between fast JVM tests and Android-dependent tests, we follow these naming rules:

- **`*Test.kt`**: Unit Tests or Local UI Tests. Run on the local JVM (extremely fast). UI tests use **Robolectric** to simulate the Android environment.
- **`*IT.kt`**: Local Integration Tests. Use **Robolectric** to simulate Android and test interactions between multiple components (e.g., Repositories + Storage). These are found in `src/test/java`.
- **`app/src/androidTest/...`**: Instrumented Integration Tests. Run on a **real Android OS** (Emulator or Device). Used for high-fidelity verification of features that depend heavily on actual OS behavior.
- **`*ScreenshotTest.kt`**: Visual regression tests using Roborazzi running on the JVM (Robolectric).

Note: Historically, some `*IT.kt` files were located in `src/androidTest`, but we are migrating them to `src/test` to leverage Robolectric for faster feedback loops. New integration tests should be added to `src/test`.

## 2. Instrumented Tests via Gradle Managed Devices (GMD)

We use GMD to automate emulator management. This avoids the need to manually create AVDs.

### Running Tests

```bash
# Clean existing snapshots/devices (recommended after env changes or GPU mode changes)
./gradlew cleanManagedDevices

# Run the instrumented tests on the managed Pixel 2 (API 33)
./gradlew pixel2api33Check
```

### Configuration Details
- **Smoke device**: Pixel 2, API Level 33.
- **Store screenshot devices**: Pixel 6, Nexus 7, and Pixel Tablet, API Level 34.
- **Image**: `aosp`.
- **Location**: Defined in `app/build.gradle.kts` under `testOptions.managedDevices`.
- **GPU mode**: Set via `android.testoptions.manageddevices.emulator.gpu` in `gradle.properties` (see below).

---

## 3. GPU Rendering Modes

The emulator GPU mode controls how the Android guest renders graphics. Choosing the right mode depends on your environment.

### `host` — Hardware acceleration (host GPU passthrough)

The emulator uses the host machine's physical GPU via Vulkan/gfxstream.

| | |
|---|---|
| **Pros** | Fastest; required for screenshot tests to produce accurate pixel output |
| **Cons** | Requires KVM + GPU passthrough; can hang the host if the GPU driver conflicts |

**Works on:**
- Bare metal Linux with KVM and a compatible GPU driver
- Distrobox with KVM + GPU passthrough configured

**Does NOT work reliably on:**
- Distrobox without GPU passthrough
- GitHub Actions (Ubuntu runners have no physical GPU)

### `swiftshader_indirect` — Software rendering (Google SwiftShader)

The emulator uses Google's SwiftShader Vulkan ICD bundled with the Android SDK. No host GPU involved.

| | |
|---|---|
| **Pros** | No GPU required; used by CI |
| **Cons** | Crashes (SIGSEGV in QEMU) on Linux kernel ≥ 6.17 due to a conflict in the gfxstream `GLAsyncSwap` code path |

**Works on:**
- GitHub Actions (`ubuntu-latest` with KVM, kernel < 6.17)
- Bare metal Linux with kernel < 6.17

**Does NOT work on:**
- Linux kernel ≥ 6.17 (e.g. Fedora 43 / kernel 6.17): QEMU segfaults during cold boot

### `angle_indirect` — Software rendering (ANGLE + lavapipe/llvmpipe)

The emulator treats this as an invalid option and falls back to `auto`, which selects Mesa's lavapipe (llvmpipe, LLVM-JIT software rasterizer) via ANGLE. Crucially, lavapipe disables `GLAsyncSwap`, which avoids the SIGSEGV present in the SwiftShader path.

| | |
|---|---|
| **Pros** | Works on kernel ≥ 6.17; no GPU required; does not hang the host |
| **Cons** | Slower than `host`; the fallback to lavapipe is an implementation detail of the emulator, not an officially documented mode |

**Works on:**
- Distrobox on Linux kernel ≥ 6.17 (current local setup)
- Any Linux without a GPU

---

## 4. Current Configuration

| Environment | `gradle.properties` setting | Effective renderer | Notes |
|---|---|---|---|
| **Local (Distrobox, kernel 6.17+)** | `angle_indirect` | lavapipe (llvmpipe via ANGLE) | Set in `gradle.properties` |
| **CI (GitHub Actions, ubuntu-latest)** | `swiftshader_indirect` | SwiftShader | Overridden at runtime via `-Pandroid.testoptions.manageddevices.emulator.gpu=swiftshader_indirect` |
| **Bare metal with GPU** | `host` | Host GPU (NVIDIA etc.) | Override `gradle.properties` locally |

To run locally with hardware acceleration (e.g. on bare metal):
```bash
./gradlew cleanManagedDevices
./gradlew pixel2api33Check -Pandroid.testoptions.manageddevices.emulator.gpu=host
```

---

## 5. Continuous Integration (CI)

CI is configured in `.github/workflows/ci.yml`. Instrumented tests run on `ubuntu-latest` with KVM enabled:

```yaml
- name: Enable KVM permissions
  run: sudo chmod 666 /dev/kvm

- name: Run All Tests & Generate Combined Coverage
  run: ./gradlew verifyWithCoverage -Pandroid.testoptions.manageddevices.emulator.gpu=swiftshader_indirect -Proborazzi.test.verify=true -Pcoverage --info
```

CI uses `swiftshader_indirect` for software rendering because GitHub-hosted runners lack a physical GPU. Local executions will default to the `host` GPU (specified in `gradle.properties`) for maximum performance, but can be overridden using the same `-P` flag if hardware acceleration is unavailable.

---

## 6. Troubleshooting

- **Segfault (Exit Code 139) with `host` GPU on Distrobox (NVIDIA)**: The Android Emulator may crash on startup due to incompatible Mesa Vulkan wrappers (e.g., `dzn_icd.json`). To fix this, explicitly point the emulator to the NVIDIA ICD. Add `export VK_ICD_FILENAMES=/usr/share/vulkan/icd.d/nvidia_icd.x86_64.json` to your `~/.bashrc` inside the Distrobox container.
- **Segfault (Exit Code 139) with `swiftshader_indirect`**: Known crash on Linux kernel ≥ 6.17. Switch to `angle_indirect` in `gradle.properties`.
- **Host machine hangs during tests with `host` GPU**: GPU driver conflict (gfxstream + NVIDIA via KVM). Switch to `angle_indirect` for software rendering.
- **KVM Access**: Verify with `ls -l /dev/kvm`. It must be accessible to your user.
- **Clean Slate**: If GMD gets into a weird state, always try `./gradlew cleanManagedDevices` before re-running.
- **Changing GPU mode**: Always run `cleanManagedDevices` first — snapshots created under one GPU mode are not reusable under another.

---

## 7. Screenshot Testing (Roborazzi)

Most screenshot tests use **Roborazzi** and are executed on the **JVM using Robolectric** rather than via Gradle Managed Devices (GMD).

### Why JVM instead of GMD?
While GMD provides higher fidelity through hardware-accelerated rendering, it introduces two critical workflow blockers:
1. **Silent Failures:** Roborazzi running in GMD instrumentation fails to properly verify or assert missing snapshots, silently passing tests.
2. **Artifact Extraction:** The GMD emulator is ephemeral. It spins up, runs tests, writes snapshots to its internal storage, and tears down immediately. Pulling the newly recorded `*.png` files back to the host machine for commit becomes extremely difficult and manual.

Running screenshot tests in `src/test` ensures snapshots are generated directly onto the host filesystem, providing a reliable and frictionless developer experience.

The exception is the Play Store marketing suite in
`app/src/androidTest/java/com/yorvana/screenshots/`. It is intentionally
instrumented because store assets should use real platform rendering and
native device dimensions. The suite captures PNGs from the emulator display
into the managed device additional-output directory.

### High-Fidelity Screenshots (Release Build)
By default, `generateStoreScreenshots` runs against the **debug** variant for
speed and stability. To generate production-representative assets from the
minified **release** build (hiding developer settings and matching production
UI exactly), use the `-PscreenshotBuild` flag:

```bash
./gradlew generateStoreScreenshots -PscreenshotBuild
```

This task runs only the `com.yorvana.screenshots` package on the three API 34
store devices and copies the generated PNGs into
`distribution/store-assets/screenshots/{phone,tablet-7,tablet-10}/`. The
existing Pixel 2 smoke device is kept separate and excludes the screenshot
package during smoke invocations.

---

## 8. Robolectric Configuration

We use Robolectric to run Android-dependent tests (including UI and integration tests) on the JVM for faster execution and better developer experience.

### Graphics Mode
By default, all Robolectric tests in this project inherit the **NATIVE** graphics mode. This is configured globally in `app/build.gradle.kts` via `systemProperty("robolectric.graphicsMode", "NATIVE")`.
- **Native Graphics**: Required for Roborazzi screenshot tests and provides high-fidelity rendering for complex UI interactions.
- **Overriding**: You only need to add `@GraphicsMode(GraphicsMode.Mode.NATIVE)` or `@GraphicsMode(GraphicsMode.Mode.LEGACY)` if a test specifically requires an override or for explicit documentation.

### Migration Checklist (androidTest to test)
We prioritize running tests on the JVM via Robolectric. However, a test must remain in (or be added to) `src/androidTest` if it:
- Uses `ActivityScenario`, `createAndroidComposeRule`, or otherwise requires a real instrumentation-backed `Activity`/scenario. (`createComposeRule()` is supported in `src/test` Robolectric/JVM tests.)
- Depends on `UiDevice` or UiAutomator APIs for system-level interactions.
- Interacts with the real filesystem via the Storage Access Framework (SAF) using a non-mocked `ContentResolver`, `DocumentsProvider`, or `DocumentFile`.
- Requires a running `Instrumentation` instance (e.g., `InstrumentationRegistry.getInstrumentation()`).
- Tests hardware-dependent behavior (camera, sensors, Bluetooth, etc.).

If a test only *mocks* these dependencies (e.g., using `mockk<DocumentFile>()`), it is eligible for migration to the JVM.

---

## 9. Smoke Test Suite (GMD)

The Smoke Test suite (`app/src/androidTest/java/com/yorvana/SmokeTest.kt`) performs end-to-end verification of critical happy-path journeys on a real Android OS using Gradle Managed Devices (GMD).

### Purpose
Smoke tests catch issues that only surface on a real Android environment:
- Actual Activity lifecycle behavior, plus Compose navigation, state restoration, and back-stack handling.
- Real file-based vault I/O on device/emulator storage, plus attachment access through Android `ContentResolver` / `MediaStore` integration.
- System-level intent flows (camera capture and file-picker interactions).
- Cross-screen navigation and back-stack integrity.

### Scenarios Covered
The suite covers 17 scenarios (S01–S17), including:
- **Core Happy Path**: First launch, vault configuration, adding/viewing vehicles and records.
- **Attachments**: Adding via file picker, viewing in the gallery, removing, and taking photos.
- **Settings & Navigation**: Navigating to Settings/Categories and verifying back-stack behavior.
- **Mutations**: Editing and deleting vehicles and records with confirmation guards.

### Running Smoke Tests

```bash
# Run just the debug androidTest task on the device (fastest for local verification)
./gradlew pixel2api33DebugAndroidTest

# Run the broader device check task
./gradlew pixel2api33Check

# Run tests and refresh the coverage baseline (gmd_smoke.ec)
./gradlew generateGmdCoverage
```

### Refreshing the coverage baseline

When material changes land in `app/src/main` (UI flows, storage logic, ViewModels exercised by smoke tests), regenerate and commit the baseline:

```bash
./gradlew generateGmdCoverage
git add app/coverage-baselines/gmd_smoke.ec
git commit -m "chore: refresh smoke coverage baseline"
```

The `Baseline Reminder` workflow posts a sticky PR comment on any PR that touches `app/src/main/**`, prompting you to refresh the baseline if the changes affect smoke coverage. Note that the `Smoke` workflow itself is path-filtered to UI changes (`app/src/main/**/ui/**`) — non-UI production changes won't trigger smoke, but the reminder will still fire so coverage stays current.

### Coverage Integration
Smoke test results are persisted as a static coverage baseline in `app/coverage-baselines/gmd_smoke.ec`. This baseline is combined with JVM unit test data during report generation:

```bash
# Generate combined report (JVM tests + Smoke test baseline)
./gradlew verifyWithCoverage
```

Since `app/coverage-baselines/*.ec` is tracked via Git LFS, a fresh clone can contain only an LFS pointer; `verifyWithCoverage` will fail until `git lfs pull` is run.

### Troubleshooting

**`verifyWithCoverage` fails with "Coverage baseline is a Git LFS pointer"**
The baseline file in your working tree is the small LFS pointer instead of the real `.ec` content. Fix:
```bash
git lfs pull
```
If you do not have Git LFS installed, install it (`sudo dnf install git-lfs` on Fedora, `brew install git-lfs` on macOS) and run `git lfs install` once before re-pulling. As a fallback you can regenerate the baseline locally with `./gradlew generateGmdCoverage` (requires the GMD emulator to run).

**`verifyWithCoverage` fails with "No dynamic JaCoCo execution data files were found"**
The JVM unit-test execution data is missing. Re-run with coverage enabled:
```bash
./gradlew testDebugUnitTest -Pcoverage
./gradlew verifyWithCoverage
```

**Smoke tests fail with "Activity opened at unexpected destination"**
Stale DataStore state from a previous run. The `ClearAppStateRule` resets this before each test, but if you see it during local debugging, run `./gradlew cleanManagedDevices` before retrying.
