ecu-tests/docker/README.md

497 lines
16 KiB
Markdown

# Docker — quick reference
Full reference: [`docs/20_docker_image.md`](../docs/20_docker_image.md).
This file is just the copy-paste commands.
| File | What it is |
|---|---|
| `Dockerfile` | Multi-stage image. Mock-only by default; hardware variant via `--build-arg INCLUDE_MELEXIS=1` + a BuildKit secret carrying the Melexis Python packages. |
| `compose.hw.yml` | docker-compose service for the hardware variant — host networking, USB device passthrough, reports volume, bench-config bind mount. |
| `../.dockerignore` | Excludes `.venv/`, `reports/*`, the deprecated BabyLIN SDK, generated caches, etc. |
All commands below assume you're running them **from the repo root**
inside a WSL2 distro (Ubuntu / Debian / …) with `docker` already on
the `$PATH`. If `docker --version` doesn't work yet, install it first
— see [§ Prerequisites](#prerequisites--install-docker-on-wsl).
---
## Prerequisites — install Docker on WSL
If `docker --version` already prints a version, skip this section.
Two install paths. **Option A** (Docker Desktop) is the easy one
that most teams use. **Option B** (Docker Engine directly inside
WSL2) is for environments where Docker Desktop's licensing or
policies are blocked.
### Option A — Docker Desktop on Windows (WSL2 backend, recommended)
The Windows host runs Docker Desktop; your WSL2 distro talks to it.
You install the daemon in exactly one place (Windows) and it appears
seamlessly inside every enabled WSL distro.
1. **Make sure WSL2 is current** (Windows PowerShell, admin):
```powershell
wsl --install # no-op if WSL is already there
wsl --set-default-version 2
wsl --update
```
Reboot if Windows prompts.
2. **Verify your distro is on WSL 2** (not WSL 1):
```powershell
wsl -l -v
```
The `VERSION` column should read `2` for your distro. If it shows
`1`, convert: `wsl --set-version <DistroName> 2`.
3. **Install Docker Desktop**:
- Download <https://www.docker.com/products/docker-desktop/>.
- During install, leave **"Use WSL 2 instead of Hyper-V"** ticked.
- Launch Docker Desktop after install completes.
4. **Enable WSL integration** (one-time):
- Docker Desktop → **Settings** → **Resources** → **WSL Integration**.
- Toggle on integration for every WSL distro you'll run `docker`
from (Ubuntu, Debian, …).
- Click **Apply & Restart**.
5. **Verify from inside WSL**:
```bash
docker --version
docker run --rm hello-world
```
`hello-world` should print "Hello from Docker!" and exit 0.
### Option B — Docker Engine inside WSL2 (no Docker Desktop)
Use this when Docker Desktop isn't allowed (corporate / license
policy) or when you want a single isolated Linux install.
1. **Enable `systemd` in WSL2** (Docker's daemon expects it). In
your WSL distro edit `/etc/wsl.conf`:
```ini
[boot]
systemd=true
```
Then from Windows PowerShell:
```powershell
wsl --shutdown
```
Reopen the WSL terminal; check `systemctl --version` runs.
2. **Install Docker Engine** (Ubuntu / Debian example — Docker's
official apt repo):
```bash
# Remove anything old that might shadow the new install
sudo apt-get remove -y docker docker-engine docker.io containerd runc 2>/dev/null || true
# Add Docker's apt key + repo
sudo apt-get update
sudo apt-get install -y ca-certificates curl gnupg lsb-release
sudo install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg \
| sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] \
https://download.docker.com/linux/ubuntu $(. /etc/os-release && echo $VERSION_CODENAME) stable" \
| sudo tee /etc/apt/sources.list.d/docker.list >/dev/null
# Install
sudo apt-get update
sudo apt-get install -y docker-ce docker-ce-cli containerd.io \
docker-buildx-plugin docker-compose-plugin
```
3. **Run without `sudo`**:
```bash
sudo usermod -aG docker $USER
```
Log out and back into WSL (or `exec su -l $USER`) so the new group
membership takes effect.
4. **Start the daemon**:
```bash
sudo systemctl enable --now docker
```
5. **Verify**:
```bash
docker --version
docker run --rm hello-world
```
### Hardware-only — pass the Owon PSU USB device into WSL with `usbipd-win`
The mock image needs nothing beyond Docker itself. The hardware
image needs the Owon PSU's USB-serial adapter exposed inside WSL2.
Windows doesn't share USB devices with WSL2 out of the box; the
de-facto bridge is [`usbipd-win`](https://github.com/dorssel/usbipd-win).
1. **Install `usbipd-win` on Windows** (PowerShell, admin):
```powershell
winget install --interactive --exact dorssel.usbipd-win
```
Reboot.
2. **List USB devices** to find the BUSID of the serial adapter:
```powershell
usbipd list
```
Look for a row that describes your adapter — "USB Serial",
"CH340", "FT232", "Owon" — and note its `BUSID` (e.g. `2-3`).
3. **Bind the device** (one-time per device, admin):
```powershell
usbipd bind --busid 2-3
```
4. **Attach the device to WSL** (every time you plug it in, normal
user):
```powershell
usbipd attach --wsl --busid 2-3
```
5. **Confirm it appeared inside WSL**:
```bash
ls /dev/ttyUSB*
```
You should see `/dev/ttyUSB0` (or similar). That's the path you
pass to `docker run --device /dev/ttyUSB0:/dev/ttyUSB0`.
If you want the device to re-attach automatically every time you
plug it in, use `usbipd attach --auto-attach --wsl --busid 2-3`
(consult `usbipd --help` for the full set of options).
### MUM network access (192.168.7.2)
The MUM presents itself as a **USB-RNDIS Ethernet adapter** on
Windows. With Docker Desktop's WSL2 backend, `--network host` in
the container reaches the MUM automatically — no extra setup beyond
plugging the MUM in and seeing it appear in `ipconfig` (it should
add an interface with a 192.168.7.x address on the Windows side).
If you went with Option B (Engine in WSL2), the MUM still works
because the WSL2 distro shares the Windows network stack for
host-mode containers.
### Sanity check before the hardware run
```bash
# Docker reachable from WSL?
docker version
# USB-serial visible in WSL?
ls -la /dev/ttyUSB*
# MUM reachable?
ping -c 2 192.168.7.2
```
If all three succeed you're ready for the hardware run below.
---
## Mock-only image (CI-ready, no hardware needed)
### Build
```bash
docker build -f docker/Dockerfile -t ecu-tests:mock .
```
### Run the mock suite
```bash
mkdir -p reports
docker run --rm \
-v "$PWD/reports:/reports" \
ecu-tests:mock \
pytest -m "not hardware" -v \
--junitxml=/reports/junit.xml \
--html=/reports/report.html --self-contained-html
```
When the container exits, `reports/report.html` and
`reports/junit.xml` are on the host. Open the HTML report:
```bash
xdg-open reports/report.html # Linux
open reports/report.html # macOS
start reports\report.html # Windows
```
### Interactive shell
```bash
docker run --rm -it -v "$PWD:/workspace" ecu-tests:mock bash
```
Edit files on the host, run `pytest` inside the container — code
changes show up immediately.
---
## Hardware image (real bench)
### One-time setup — Melexis packages
`pylin` / `pymumclient` / `pylinframe` ship inside the Melexis IDE,
not on PyPI. Bundle them into a tarball that you'll pass as a
BuildKit secret:
```bash
# Adjust the path to where Melexis IDE is installed
MELEXIS_SITE="/mnt/c/Program Files/Melexis/Melexis IDE/plugins/com.melexis.mlxide.python_1.2.0.202408130945/python/Lib/site-packages"
tar -czf melexis-pkgs.tar.gz \
-C "$MELEXIS_SITE" \
pylin pymumclient pylinframe
```
The tarball is gitignored (see `.dockerignore`) and never enters
any image layer — BuildKit's `--mount=type=secret` only exposes it
to the single `RUN` step that copies the packages into
`/opt/venv/lib/python3.x/site-packages/`.
> **License**: the resulting image contains proprietary Melexis
> code. Treat it like the Melexis IDE itself — keep it on a private
> registry, not Docker Hub.
### Build
```bash
DOCKER_BUILDKIT=1 docker build \
-f docker/Dockerfile -t ecu-tests:hw \
--build-arg INCLUDE_MELEXIS=1 \
--secret id=melexis_tarball,src=./melexis-pkgs.tar.gz \
.
```
Verify the Melexis packages landed inside the image:
```bash
docker run --rm ecu-tests:hw \
python -c "import pylin, pymumclient, pylinframe; print('OK')"
```
### Run the hardware suite — direct `docker run`
```bash
docker run --rm \
--network host \
--device /dev/ttyUSB0:/dev/ttyUSB0 \
--group-add dialout \
-v "$PWD/reports:/reports" \
-v "$PWD/config/test_config.yaml:/workspace/config/test_config.yaml:ro" \
-e ECU_TESTS_CONFIG=/workspace/config/test_config.yaml \
ecu-tests:hw \
pytest -m "hardware and mum and not slow" -v \
--junitxml=/reports/junit.xml \
--html=/reports/report.html --self-contained-html
```
The flags, in plain English:
| Flag | Reason |
|---|---|
| `--network host` | MUM is at `192.168.7.2` via USB-RNDIS on the host; bridge networking would hide it. |
| `--device /dev/ttyUSB0:/dev/ttyUSB0` | Pass the Owon PSU's USB-serial device into the container. Adjust to whatever `ls /dev/ttyUSB*` shows on the host. |
| `--group-add dialout` | Without it, the `tester` user can't open the serial device. |
| `-v config/test_config.yaml:…:ro` | Tweak bench config without rebuilding the image. |
### Run via docker-compose
```bash
docker compose -f docker/compose.hw.yml build
docker compose -f docker/compose.hw.yml up --abort-on-container-exit
```
Same effect as the `docker run` above, but the parameters are
checked into `compose.hw.yml` so all you remember is the file path.
### Iteration — edit-on-host, run-in-container
```bash
docker run --rm -it \
--network host \
--device /dev/ttyUSB0:/dev/ttyUSB0 \
--group-add dialout \
-v "$PWD:/workspace" \
-v "$PWD/reports:/reports" \
ecu-tests:hw \
bash
```
Inside the container:
```bash
# Run a specific file
pytest tests/hardware/test_overvolt.py -v -s
# Or one parametrized case
pytest "tests/hardware/test_overvolt.py::test_template_voltage_status_parametrized[overvoltage]" -v -s
# Or the settle characterization
pytest -m psu_settling -v -s
```
---
## Where everything lives
After `docker build` and `docker run`, three different stores hold
three different things. Knowing the difference saves time when you
want to find a report, free disk space, or confirm "did my build
actually succeed?"
| Thing | Lives where | How you access it |
|---|---|---|
| The **image** | Docker daemon's content-addressed layer store. Not a single file. | `docker images`, `docker inspect`, `docker history` |
| A **running / stopped container** | Daemon's runtime state. Ephemeral when `--rm` is used. | `docker ps`, `docker ps -a`, `docker logs`, `docker exec` |
| The **test reports** | Host filesystem at `./reports/`, via the `-v` bind-mount in every run command. Survives container deletion. | `ls reports/`, open `reports/report.html` |
### The image
You **don't** navigate to it as files — query it through `docker`:
```bash
docker images # all images on this daemon
docker images ecu-tests # just the ones tagged ecu-tests
docker inspect ecu-tests:mock # full metadata (JSON)
docker history ecu-tests:mock # layer-by-layer breakdown
```
The on-disk location is daemon-internal:
| Host setup | Backing store |
|---|---|
| Native Docker Engine on Linux (Option B in the install section) | `/var/lib/docker/overlay2/…` |
| Docker Desktop + WSL2 (Option A) | Inside a hidden WSL2 distro `docker-desktop-data`. Windows side: `%LOCALAPPDATA%\Docker\wsl\disk\docker_data.vhdx`. **Don't poke directly** — always use the `docker` CLI. |
Images persist across reboots until you delete them:
```bash
docker rmi ecu-tests:mock # one image
docker system prune -a # everything unused (careful)
docker system df # what's eating disk
```
### A running container
`docker run …` creates a container from the image. The container has
its own writable filesystem layer on top of the image's read-only
layers. The image is unchanged when the container exits.
```bash
docker ps # running right now
docker ps -a # all, including exited
docker logs <id> # captured stdout / stderr
docker exec -it <id> bash # shell into a still-running container
```
Every run command in this README uses `--rm`, so the container is
deleted the moment it exits. The **image** stays. The **reports**
(see below) stay too because they're on the host filesystem, not
inside the container.
### Inside the container — what the Dockerfile lays out
```
/ (container root)
├── opt/
│ └── venv/ ← Python venv with all pip-installed deps
├── workspace/ ← the repo, copied in at build time
│ ├── ecu_framework/
│ ├── tests/
│ ├── config/
│ └── …
├── reports/ ← mount point for the host's ./reports/
└── home/tester/ ← unprivileged user home (uid 1000)
```
Peek at the layout from a throwaway container:
```bash
docker run --rm -it ecu-tests:mock bash
# inside:
ls /workspace
ls /opt/venv/bin
which pytest
```
`/workspace` is a **frozen snapshot of the repo from the moment you
ran `docker build`**. Edits to files on the host afterwards do NOT
show up inside the image — unless you bind-mount the repo at run
time:
```bash
docker run --rm -it -v "$PWD:/workspace" ecu-tests:mock bash
```
(That's exactly what the "Iteration" example does.)
### Reports on the host — what you actually look at
Every `docker run` command in this README includes a bind-mount:
```
-v "$PWD/reports:/reports"
```
The container writes its outputs to `/reports/`; the daemon's
bind-mount makes those writes show up on the host at `./reports/`
in your repo. After the container exits, the files are still there:
```
<repo root>/
└── reports/
├── report.html ← open this in a browser
├── junit.xml ← machine-readable for CI
├── summary.md
└── requirements_coverage.json
```
`--rm` deletes the container; it does **not** touch the bind-mounted
host directory.
### Three commands cover 95% of "where is it?"
```bash
docker images ecu-tests # is the image there?
docker run --rm -v "$PWD/reports:/reports" \
ecu-tests:mock pytest -m "not hardware" -q
ls reports/ # outputs landed where?
```
---
## Platform notes
- **Linux**: works as shown above.
- **WSL2 (Windows)**: USB devices need `usbipd-win` to bind them
into the WSL2 distro; from there they appear as `/dev/ttyUSB0`
exactly like on native Linux. Docker Desktop bridges WSL2 to the
host network, so `--network host` reaches the MUM normally.
- **macOS Docker Desktop**: USB passthrough is **not** supported.
Workaround is to run a TCP-to-serial bridge on the host
(`socat`) and have the container connect to that — fiddly,
documented in `docs/20_docker_image.md` §4.3 as a non-default
path.
---
## Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| `ModuleNotFoundError: No module named 'pylin'` | Image built without `INCLUDE_MELEXIS=1` | Rebuild with the build-arg + secret |
| `Permission denied: '/dev/ttyUSB0'` | Missing `--group-add dialout` | Add it (or the group that owns the device on the host) |
| MUM unreachable at 192.168.7.2 | Bridge network instead of host network | Add `--network host` (Linux); on macOS see §4.3 |
| Empty `reports/` after run | `/reports` not bind-mounted | Add `-v "$PWD/reports:/reports"` |
| HTML report missing styling | Forgot `--self-contained-html` | Pytest renders the report without inlined CSS otherwise |
See [`docs/20_docker_image.md`](../docs/20_docker_image.md) §8 for
the full table.