Skip to content

Commit cccd861

Browse files
authored
test: grow the integration scenario set (env_file, runtime interpolation) (#44)
* test: add env_file integration scenario * test: add runtime variable-interpolation integration scenario * fix: escape $ in container-side shell commands so it isn't interpolated by the outer script Both scenarios wrote a bare $COLOR/$TOKEN meant to be read by the CONTAINER's own shell, but compose2pod's own ${VAR} interpolation applies uniformly to every emitted string -- including command: list entries -- so it got resolved by the OUTER script against ITS OWN environment instead, producing an empty value before the command ever reached the container. Confirmed by generating the script locally: the command rendered as "echo \"${COLOR-}\"" rather than the literal "echo \"$COLOR\"". $$ is Compose's own escape for a literal $, verified via the same local-generation check to render correctly.
1 parent e846f2a commit cccd861

3 files changed

Lines changed: 145 additions & 0 deletions

File tree

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
---
2+
summary: Add two more integration scenarios -- `env_file` (a real file resolved against `project_dir`, loaded into the container's actual environment) and runtime `${VAR}` interpolation (a variable set via `monkeypatch.setenv` just before `run_pod`, proving the reference survives generation as a live shell expansion and podman actually sets it) -- reusing the existing `run_pod` fixture unchanged.
3+
---
4+
5+
# Design: grow the integration scenario set (env_file, runtime interpolation)
6+
7+
## Summary
8+
9+
Two more scenarios in `tests/integration/`, continuing the growth started in
10+
`planning/changes/2026-07-13.02-grow-integration-scenarios.md`. Both reuse the
11+
`run_pod` fixture from PR #41 unchanged -- no harness, `conftest.py`,
12+
`pyproject.toml`, or CI changes. Both are behavioral probes (script exit code
13+
+ stdout), matching every existing scenario.
14+
15+
## Motivation
16+
17+
Two mechanisms remain untested against real podman: `env_file` (a
18+
compose-authoring convenience that reads a host file and loads it into the
19+
container's environment via `--env-file`) and `${VAR}` runtime interpolation
20+
(compose2pod's core design choice to leave `${VAR}` references live in the
21+
generated script rather than resolving them at generation time, per
22+
`architecture/supported-subset.md`'s Variable interpolation section). Both
23+
involve a real podman flag (`--env-file`, `-e`) and a real shell expansion
24+
step that only a running script can exercise.
25+
26+
## Design
27+
28+
**Scenario 1 -- `env_file`** (`test_env_file.py`): write `app.env` containing
29+
`COLOR=teal-9\n` to `tmp_path`; the service declares `env_file: "app.env"`;
30+
`run_pod(..., project_dir=tmp_path)`. Command: `echo "$COLOR"`. Mirrors
31+
`test_bind_volume.py`'s file-in-`tmp_path` + `project_dir` override shape
32+
exactly. Proves `--env-file` resolves against `project_dir`
33+
(`compose2pod/emit.py:_add_env_flags`) and podman actually loads the file
34+
into the container's real environment.
35+
36+
**Scenario 2 -- runtime `${VAR}` interpolation** (`test_variable_interpolation.py`):
37+
`monkeypatch.setenv("C2P_IT_TOKEN", <distinctive value>)` before calling
38+
`run_pod`; the service declares `environment: ["TOKEN=${C2P_IT_TOKEN}"]`;
39+
command `echo "$TOKEN"`. `run_pod`'s `subprocess.run` call has no explicit
40+
`env=` override (`tests/integration/conftest.py`), so it inherits the current
41+
process environment -- exactly the one `monkeypatch` just modified. Exit 0 +
42+
the distinctive value in stdout proves the full chain for real: the `${VAR}`
43+
reference survives generation as a live shell expansion (not a baked-in
44+
literal), `to_shell`'s quoting produces a valid `podman run -e TOKEN=...`
45+
invocation once the outer script's own shell expands it, and podman correctly
46+
sets that as the container's actual environment variable.
47+
48+
Both files: no `pytestmark` (the existing `tryfirst` `pytest_collection_modifyitems`
49+
hook in `conftest.py` auto-marks by location); `Callable[..., PodRun]` typed
50+
`run_pod` parameter, matching every existing scenario file.
51+
52+
## Non-goals
53+
54+
- Compose's own `.env`-file-at-project-root convention (implicit variable
55+
defaults) -- out of scope; `env_file:` is the explicit per-service key
56+
already supported and is what this scenario tests.
57+
- Multiple `env_file` entries (list form) -- already unit-tested
58+
(`tests/test_emit.py::test_env_file_list_form`); one file is enough to
59+
prove the real podman load path.
60+
- A second interpolation variable / multiple `${VAR}` references in one
61+
scenario -- one is enough to prove the run-time-expansion mechanism; more
62+
is unit-test territory (`tests/test_shell.py`), not a new podman-facing risk.
63+
64+
## Testing
65+
66+
`uv run --no-sync pytest -m integration --collect-only -q` collects 8 (the 6
67+
existing scenarios plus these 2). `just test-ci` stays at 100% with all 8
68+
deselected. `just lint-ci` and `just check-planning` clean. Real signal: the
69+
`integration` CI job green against real podman, same as every prior round.
70+
71+
## Risk
72+
73+
- **`monkeypatch.setenv` leaking into other tests in the same session** (low
74+
x low): `monkeypatch` is function-scoped and pytest tears it down
75+
automatically after the test, so this is a non-issue -- named for
76+
completeness, not because it's expected to occur.
77+
- **A CI runner already defining `C2P_IT_TOKEN`** (very low x low):
78+
vanishingly unlikely given the deliberately distinctive name; not worth
79+
additional defensive code.

tests/integration/test_env_file.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
"""env_file: a real host file, resolved against project_dir, loaded into the container's env."""
2+
3+
from collections.abc import Callable
4+
from pathlib import Path
5+
6+
from tests.integration.conftest import PodRun
7+
8+
9+
def test_env_file_is_loaded(run_pod: Callable[..., PodRun], tmp_path: Path) -> None:
10+
(tmp_path / "app.env").write_text("COLOR=teal-9\n")
11+
compose = {
12+
"services": {
13+
"app": {
14+
"image": "busybox:1.36",
15+
"env_file": "app.env",
16+
# `$$` escapes to a literal `$` (Compose syntax): a bare `$COLOR` here
17+
# would instead be compose2pod's OWN interpolation, resolved by the
18+
# OUTER script against ITS environment -- not what we want, since COLOR
19+
# only exists inside the container via --env-file.
20+
"command": ["sh", "-c", 'echo "$$COLOR"'],
21+
},
22+
},
23+
}
24+
run = run_pod(compose, target="app", project_dir=tmp_path)
25+
# Exit 0 + the value in stdout proves --env-file resolved against
26+
# project_dir and podman actually loaded the file into the container's
27+
# real environment.
28+
assert run.returncode == 0, run.stderr
29+
assert "teal-9" in run.stdout
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
"""Runtime ${VAR} interpolation: a variable left live in the script, expanded when it runs.
2+
3+
`run_pod`'s subprocess call inherits the current process environment (no explicit
4+
`env=` override), so setting the variable via `monkeypatch.setenv` just before calling
5+
it proves the full chain for real: the `${VAR}` reference survives compose2pod's
6+
generation step as a live shell expansion (never baked into a literal), the outer
7+
script's shell expands it into a valid `podman run -e TOKEN=...` invocation, and podman
8+
sets that as the container's actual environment variable.
9+
"""
10+
11+
from collections.abc import Callable
12+
13+
import pytest
14+
15+
from tests.integration.conftest import PodRun
16+
17+
18+
def test_variable_interpolation_resolves_at_run_time(
19+
run_pod: Callable[..., PodRun], monkeypatch: pytest.MonkeyPatch
20+
) -> None:
21+
monkeypatch.setenv("C2P_IT_TOKEN", "runtime-value-73")
22+
compose = {
23+
"services": {
24+
"app": {
25+
"image": "busybox:1.36",
26+
"environment": ["TOKEN=${C2P_IT_TOKEN}"],
27+
# `$$` escapes to a literal `$` (Compose syntax): a bare `$TOKEN` here
28+
# would instead be compose2pod's OWN interpolation, resolved by the
29+
# OUTER script against ITS environment (which has C2P_IT_TOKEN, not
30+
# TOKEN) -- we want the CONTAINER's shell to read its own $TOKEN.
31+
"command": ["sh", "-c", 'echo "$$TOKEN"'],
32+
},
33+
},
34+
}
35+
run = run_pod(compose, target="app")
36+
assert run.returncode == 0, run.stderr
37+
assert "runtime-value-73" in run.stdout

0 commit comments

Comments
 (0)