ecu-tests/docker/README.md

16 KiB

Docker — quick reference

Full reference: 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

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.

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):

    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):

    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:

  4. Enable WSL integration (one-time):

    • Docker Desktop → SettingsResourcesWSL Integration.
    • Toggle on integration for every WSL distro you'll run docker from (Ubuntu, Debian, …).
    • Click Apply & Restart.
  5. Verify from inside WSL:

    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:

    [boot]
    systemd=true
    

    Then from Windows PowerShell:

    wsl --shutdown
    

    Reopen the WSL terminal; check systemctl --version runs.

  2. Install Docker Engine (Ubuntu / Debian example — Docker's official apt repo):

    # 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:

    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:

    sudo systemctl enable --now docker
    
  5. Verify:

    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.

  1. Install usbipd-win on Windows (PowerShell, admin):

    winget install --interactive --exact dorssel.usbipd-win
    

    Reboot.

  2. List USB devices to find the BUSID of the serial adapter:

    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):

    usbipd bind --busid 2-3
    
  4. Attach the device to WSL (every time you plug it in, normal user):

    usbipd attach --wsl --busid 2-3
    
  5. Confirm it appeared inside WSL:

    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

# 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

docker build -f docker/Dockerfile -t ecu-tests:mock .

Run the mock suite

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:

xdg-open reports/report.html      # Linux
open reports/report.html          # macOS
start reports\report.html         # Windows

Interactive shell

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:

# 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

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:

docker run --rm ecu-tests:hw \
    python -c "import pylin, pymumclient, pylinframe; print('OK')"

Run the hardware suite — direct docker run

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

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

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:

# 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:

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:

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.

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:

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:

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?"

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 §8 for the full table.