robotframework-okw-env-docker 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,97 @@
1
+ Metadata-Version: 2.4
2
+ Name: robotframework-okw-env-docker
3
+ Version: 0.1.0
4
+ Summary: Robot Framework library providing OKW environment provisioning with Docker.
5
+ Project-URL: Repository, http://192.168.1.130:3000/Hrabovszki1023/robotframework-okw-env-docker
6
+ Requires-Python: >=3.10
7
+ Description-Content-Type: text/markdown
8
+ Requires-Dist: robotframework>=6.0
9
+ Requires-Dist: robotframework-okw-env>=0.1.0
10
+ Requires-Dist: docker>=7.0.0
11
+ Requires-Dist: pyyaml>=6.0
12
+
13
+ # robotframework-okw-env-docker
14
+
15
+ Docker Compose provider for [okw-env](http://192.168.1.130:3000/Hrabovszki1023/robotframework-okw-env).
16
+
17
+ > Deutsche Version: [README_de.md](README_de.md)
18
+
19
+ ## What
20
+
21
+ Implements the OKW Environment Provider Contract using Docker Compose.
22
+ Generates `docker-compose.yml` from YAML component definitions and
23
+ manages the full container lifecycle (create, start, health check,
24
+ logs, snapshot, stop, destroy).
25
+
26
+ ## Why — Signal vs. NOISE
27
+
28
+ Docker Compose syntax is NOISE. The tester only writes:
29
+
30
+ ```robot
31
+ ENV_Start PostgresDB
32
+ ENV_BuildAndRun
33
+ ENV_WaitForReady PostgresDB
34
+ ```
35
+
36
+ The framework generates the Compose file, starts the container,
37
+ and polls the health check. No Docker knowledge required in test code.
38
+
39
+ ## Install
40
+
41
+ ```bash
42
+ pip install robotframework-okw-env-docker
43
+ ```
44
+
45
+ This automatically installs `robotframework-okw-env` (core) and
46
+ `robotframework-docker` (Docker Compose engine).
47
+
48
+ ## Component YAML
49
+
50
+ ```yaml
51
+ PostgresDB:
52
+ provider: docker
53
+ image: postgres
54
+ version: "17"
55
+ port: 5432
56
+ env:
57
+ POSTGRES_DB: testdb
58
+ POSTGRES_PASSWORD: testpass
59
+ healthcheck: "pg_isready -U postgres"
60
+ timeout: 30s
61
+ ```
62
+
63
+ ## Minimal Example
64
+
65
+ ```robot
66
+ *** Settings ***
67
+ Library okw_env.library.OkwEnvLibrary components_dir=components WITH NAME ENV
68
+
69
+ *** Test Cases ***
70
+ Database Is Reachable
71
+ ENV_Start PostgresDB
72
+ ENV_BuildAndRun
73
+ ENV_WaitForReady PostgresDB
74
+ Log PostgresDB is ready for testing.
75
+ [Teardown] ENV_Stop
76
+ ```
77
+
78
+ ## Architecture
79
+
80
+ ```
81
+ okw-env (keywords, YAML loader, registry)
82
+
83
+
84
+ okw-env-docker (this library)
85
+
86
+
87
+ DockerComposeLibrary (robotframework-docker)
88
+
89
+
90
+ Docker Compose CLI → Docker Engine
91
+ ```
92
+
93
+ ## Documentation
94
+
95
+ - [CONTRACT.md](docs/CONTRACT.md) — How this provider implements the contract
96
+ - [SPECIFICATION.md](docs/SPECIFICATION.md) — Lifecycle, isolation, snapshot model
97
+ - [KEYWORDS.md](docs/KEYWORDS.md) — Keyword reference with examples
@@ -0,0 +1,85 @@
1
+ # robotframework-okw-env-docker
2
+
3
+ Docker Compose provider for [okw-env](http://192.168.1.130:3000/Hrabovszki1023/robotframework-okw-env).
4
+
5
+ > Deutsche Version: [README_de.md](README_de.md)
6
+
7
+ ## What
8
+
9
+ Implements the OKW Environment Provider Contract using Docker Compose.
10
+ Generates `docker-compose.yml` from YAML component definitions and
11
+ manages the full container lifecycle (create, start, health check,
12
+ logs, snapshot, stop, destroy).
13
+
14
+ ## Why — Signal vs. NOISE
15
+
16
+ Docker Compose syntax is NOISE. The tester only writes:
17
+
18
+ ```robot
19
+ ENV_Start PostgresDB
20
+ ENV_BuildAndRun
21
+ ENV_WaitForReady PostgresDB
22
+ ```
23
+
24
+ The framework generates the Compose file, starts the container,
25
+ and polls the health check. No Docker knowledge required in test code.
26
+
27
+ ## Install
28
+
29
+ ```bash
30
+ pip install robotframework-okw-env-docker
31
+ ```
32
+
33
+ This automatically installs `robotframework-okw-env` (core) and
34
+ `robotframework-docker` (Docker Compose engine).
35
+
36
+ ## Component YAML
37
+
38
+ ```yaml
39
+ PostgresDB:
40
+ provider: docker
41
+ image: postgres
42
+ version: "17"
43
+ port: 5432
44
+ env:
45
+ POSTGRES_DB: testdb
46
+ POSTGRES_PASSWORD: testpass
47
+ healthcheck: "pg_isready -U postgres"
48
+ timeout: 30s
49
+ ```
50
+
51
+ ## Minimal Example
52
+
53
+ ```robot
54
+ *** Settings ***
55
+ Library okw_env.library.OkwEnvLibrary components_dir=components WITH NAME ENV
56
+
57
+ *** Test Cases ***
58
+ Database Is Reachable
59
+ ENV_Start PostgresDB
60
+ ENV_BuildAndRun
61
+ ENV_WaitForReady PostgresDB
62
+ Log PostgresDB is ready for testing.
63
+ [Teardown] ENV_Stop
64
+ ```
65
+
66
+ ## Architecture
67
+
68
+ ```
69
+ okw-env (keywords, YAML loader, registry)
70
+
71
+
72
+ okw-env-docker (this library)
73
+
74
+
75
+ DockerComposeLibrary (robotframework-docker)
76
+
77
+
78
+ Docker Compose CLI → Docker Engine
79
+ ```
80
+
81
+ ## Documentation
82
+
83
+ - [CONTRACT.md](docs/CONTRACT.md) — How this provider implements the contract
84
+ - [SPECIFICATION.md](docs/SPECIFICATION.md) — Lifecycle, isolation, snapshot model
85
+ - [KEYWORDS.md](docs/KEYWORDS.md) — Keyword reference with examples
@@ -0,0 +1,28 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "robotframework-okw-env-docker"
7
+ version = "0.1.0"
8
+ description = "Robot Framework library providing OKW environment provisioning with Docker."
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ dependencies = [
12
+ "robotframework>=6.0",
13
+ "robotframework-okw-env>=0.1.0",
14
+ "docker>=7.0.0",
15
+ "pyyaml>=6.0"
16
+ ]
17
+
18
+ [project.urls]
19
+ Repository = "http://192.168.1.130:3000/Hrabovszki1023/robotframework-okw-env-docker"
20
+
21
+ [project.entry-points."okw_env.providers"]
22
+ docker = "okw_env_docker:DockerProvider"
23
+
24
+ [tool.setuptools]
25
+ package-dir = {"" = "src"}
26
+
27
+ [tool.setuptools.packages.find]
28
+ where = ["src"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,4 @@
1
+ from .docker_provider import DockerProvider
2
+ from .library import OkwEnvDockerLibrary
3
+
4
+ __all__ = ["DockerProvider", "OkwEnvDockerLibrary"]
@@ -0,0 +1,117 @@
1
+ """Docker provider for OKW environments.
2
+
3
+ Uses the Docker Python SDK (docker-py) to communicate directly with the
4
+ Docker Engine API. No local Docker CLI installation required.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import os
10
+ import uuid
11
+
12
+ import docker
13
+
14
+ from okw_env.provider_base import OkwEnvProviderBase
15
+
16
+
17
+ class DockerProvider(OkwEnvProviderBase):
18
+ """Provisions environment components as Docker containers via the Engine API."""
19
+
20
+ def __init__(self, base_url: str | None = None, component: dict | None = None):
21
+ component = component or {}
22
+ self._base_url = (
23
+ base_url
24
+ or component.get("docker_host")
25
+ or os.environ.get("DOCKER_HOST", "unix:///var/run/docker.sock")
26
+ )
27
+ self._client: docker.DockerClient | None = None
28
+ self._containers: dict[str, docker.models.containers.Container] = {}
29
+
30
+ def _get_client(self) -> docker.DockerClient:
31
+ if self._client is None:
32
+ self._client = docker.DockerClient(base_url=self._base_url)
33
+ return self._client
34
+
35
+ def create(self, component: dict) -> str:
36
+ """Pull image and create a container (without starting)."""
37
+ image = component.get("image", "")
38
+ version = component.get("version", "latest")
39
+ full_image = f"{image}:{version}"
40
+ port = component.get("port")
41
+ env = component.get("env", {})
42
+ healthcheck_cmd = component.get("healthcheck")
43
+ name = f"okw-{uuid.uuid4().hex[:8]}"
44
+
45
+ self._get_client().images.pull(image, tag=version)
46
+
47
+ ports = {f"{port}/tcp": None} if port else {}
48
+ env_list = [f"{k}={v}" for k, v in env.items()]
49
+
50
+ healthcheck = None
51
+ if healthcheck_cmd:
52
+ healthcheck = docker.types.Healthcheck(
53
+ test=["CMD-SHELL", healthcheck_cmd],
54
+ interval=5_000_000_000,
55
+ timeout=3_000_000_000,
56
+ retries=10,
57
+ )
58
+
59
+ container = self._get_client().containers.create(
60
+ full_image,
61
+ name=name,
62
+ ports=ports,
63
+ environment=env_list,
64
+ healthcheck=healthcheck,
65
+ detach=True,
66
+ )
67
+ self._containers[name] = container
68
+ return name
69
+
70
+ def start(self, component_id: str) -> None:
71
+ container = self._require_container(component_id)
72
+ container.start()
73
+
74
+ def get_runtime_info(self, component_id: str) -> dict:
75
+ """Read the actual host port assigned by Docker after start."""
76
+ container = self._require_container(component_id)
77
+ container.reload()
78
+ ports = container.attrs.get("NetworkSettings", {}).get("Ports", {})
79
+ for container_port, bindings in ports.items():
80
+ if bindings:
81
+ return {"port": int(bindings[0]["HostPort"])}
82
+ return {}
83
+
84
+ def is_ready(self, component_id: str) -> bool:
85
+ container = self._require_container(component_id)
86
+ container.reload()
87
+ state = container.attrs.get("State", {})
88
+
89
+ health = state.get("Health", {})
90
+ if health:
91
+ return health.get("Status") == "healthy"
92
+
93
+ return state.get("Status") == "running"
94
+
95
+ def get_logs(self, component_id: str) -> str:
96
+ container = self._require_container(component_id)
97
+ return container.logs().decode("utf-8", errors="replace")
98
+
99
+ def snapshot(self, component_id: str) -> str:
100
+ container = self._require_container(component_id)
101
+ snapshot_tag = f"{component_id}-snapshot"
102
+ container.commit(repository=snapshot_tag)
103
+ return snapshot_tag
104
+
105
+ def stop(self, component_id: str) -> None:
106
+ container = self._require_container(component_id)
107
+ container.stop(timeout=10)
108
+
109
+ def destroy(self, component_id: str) -> None:
110
+ container = self._require_container(component_id)
111
+ container.remove(v=True, force=True)
112
+ del self._containers[component_id]
113
+
114
+ def _require_container(self, component_id: str):
115
+ if component_id not in self._containers:
116
+ raise KeyError(f"Unknown container '{component_id}'. Active: {list(self._containers.keys())}")
117
+ return self._containers[component_id]
@@ -0,0 +1,139 @@
1
+ """OKW Environment Docker Library – Robot Framework keywords for Docker-based environments."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from robot.api.deco import library
6
+
7
+ from okw_env.library import OkwEnvLibrary
8
+
9
+ from .docker_provider import DockerProvider
10
+
11
+
12
+ @library(scope="GLOBAL")
13
+ class OkwEnvDockerLibrary(OkwEnvLibrary):
14
+ """OKW Environment provisioning for Docker.
15
+
16
+ = Zero-Install Architecture =
17
+
18
+ This library communicates with the Docker Engine API directly via
19
+ the [https://docker-py.readthedocs.io/|Docker SDK for Python].
20
+ **No Docker CLI, no Docker Desktop, no docker-compose installation
21
+ required** — only ``pip install robotframework-okw-env-docker``
22
+ and a reachable Docker host.
23
+
24
+ This dramatically reduces installation NOISE: every machine that can
25
+ run Python and reach a Docker host over the network can provision
26
+ test environments — no admin rights, no platform-specific installers.
27
+
28
+ = Connection =
29
+
30
+ The Docker host is configured **per component in the YAML file** via
31
+ the ``docker_host`` key. This is intentional: the connection target is
32
+ a property of the test environment, not of the test machine. A test
33
+ suite can mix components on different Docker hosts — or even different
34
+ providers (Docker + Proxmox) — without any local configuration.
35
+
36
+ Resolution order for ``docker_host``:
37
+
38
+ | 1. | ``docker_host`` key in component YAML (recommended) |
39
+ | 2. | ``docker_host`` parameter on the Library constructor (fallback) |
40
+ | 3. | Environment variable ``DOCKER_HOST`` |
41
+ | 4. | Default: ``unix:///var/run/docker.sock`` (Linux only) |
42
+
43
+ *Why YAML and not a Library parameter?*
44
+
45
+ The test machine (Windows laptop, Linux CI runner) should not determine
46
+ where Docker runs. A developer on Windows and a CI runner on Linux use
47
+ the same YAML — the connection target stays the same. No ``DOCKER_HOST``
48
+ environment variable, no platform-specific config, no surprises.
49
+
50
+ = Installation =
51
+
52
+ | ``pip install robotframework-okw-env-docker`` |
53
+
54
+ That's it. One ``pip install``, one IP address in the YAML, test environments ready.
55
+
56
+ = Usage =
57
+
58
+ | **Settings** |
59
+ | Library | OkwEnvDockerLibrary | components_dir=components |
60
+
61
+ | **Test Cases** |
62
+ | Webshop mit Datenbank |
63
+ | | ENV_Start | PostgresDB |
64
+ | | ENV_BuildAndRun | |
65
+ | | ENV_WaitForReady | PostgresDB |
66
+ | | # ... test steps ... | |
67
+ | | [Teardown] | Run Keywords | ENV_SnapshotOnFail | ENV_Stop |
68
+
69
+ = Component YAML =
70
+
71
+ Each environment component is defined in a YAML file.
72
+ The ``docker_host`` tells the provider where to connect:
73
+
74
+ | ``PostgresDB.yaml`` |
75
+ | provider: docker |
76
+ | docker_host: tcp://192.168.1.123:2375 |
77
+ | image: postgres |
78
+ | version: "17" |
79
+ | port: 5432 |
80
+ | env: |
81
+ | POSTGRES_DB: testdb |
82
+ | healthcheck: "pg_isready -U postgres" |
83
+ | timeout: 30s |
84
+
85
+ = Environment Variables via $MEM{} =
86
+
87
+ After ``ENV_BuildAndRun``, **all YAML keys** are published as Robot
88
+ Framework test variables using dot-notation. This bridges environment
89
+ provisioning with test keywords — the test knows where to connect
90
+ without hardcoding anything.
91
+
92
+ | *Variable* | *Source* | *Example value* |
93
+ | ``${PostgresDB.image}`` | YAML key | ``postgres`` |
94
+ | ``${PostgresDB.version}`` | YAML key | ``17`` |
95
+ | ``${PostgresDB.port}`` | YAML key | ``5432`` |
96
+ | ``${PostgresDB.env.POSTGRES_DB}`` | YAML nested key | ``testdb`` |
97
+ | ``${PostgresDB.env.POSTGRES_PASSWORD}`` | YAML nested key | ``secret`` |
98
+ | ``${PostgresDB.docker_host}`` | YAML key | ``tcp://192.168.1.123:2375`` |
99
+ | ``${PostgresDB.ID}`` | Runtime (container ID) | ``okw-3df2dcc8`` |
100
+
101
+ Usage in test:
102
+
103
+ | ENV_Start | PostgresDB |
104
+ | ENV_BuildAndRun | |
105
+ | ENV_WaitForReady | PostgresDB |
106
+ | Log | DB name: ${PostgresDB.env.POSTGRES_DB} |
107
+ | Log | Port: ${PostgresDB.port} |
108
+
109
+ In OKW GUI/API keywords, use ``$MEM{}`` syntax:
110
+
111
+ | SetValue | DB_Host | $MEM{PostgresDB.docker_host} |
112
+ | SetValue | DB_Name | $MEM{PostgresDB.env.POSTGRES_DB} |
113
+
114
+ *Design rationale:* The connection data lives in the YAML — not in
115
+ environment variables, not in CI config, not hardcoded in the test.
116
+ The same test runs against any Docker host by changing one YAML file.
117
+
118
+ = Signal vs. NOISE =
119
+
120
+ | *Signal* | *NOISE (hidden)* |
121
+ | ``ENV_Start PostgresDB`` | Docker API calls, image pull, container config |
122
+ | ``ENV_WaitForReady PostgresDB`` | Health check polling, timeout logic |
123
+ | ``${PostgresDB.env.POSTGRES_DB}`` | YAML parsing, variable publishing |
124
+ | ``ENV_Stop`` | Container stop, remove, volume cleanup |
125
+
126
+ The test case reads like a specification. The infrastructure is invisible.
127
+
128
+ = Runnable Examples =
129
+
130
+ See [https://github.com/Hrabovszki1023/okw-examples|okw-examples]
131
+ for complete working test suites.
132
+ """
133
+
134
+ ROBOT_LIBRARY_SCOPE = "GLOBAL"
135
+
136
+ def __init__(self, components_dir: str | None = None, docker_host: str | None = None):
137
+ super().__init__(components_dir=components_dir)
138
+ provider = DockerProvider(base_url=docker_host)
139
+ self._manager.set_provider(provider)
@@ -0,0 +1,97 @@
1
+ Metadata-Version: 2.4
2
+ Name: robotframework-okw-env-docker
3
+ Version: 0.1.0
4
+ Summary: Robot Framework library providing OKW environment provisioning with Docker.
5
+ Project-URL: Repository, http://192.168.1.130:3000/Hrabovszki1023/robotframework-okw-env-docker
6
+ Requires-Python: >=3.10
7
+ Description-Content-Type: text/markdown
8
+ Requires-Dist: robotframework>=6.0
9
+ Requires-Dist: robotframework-okw-env>=0.1.0
10
+ Requires-Dist: docker>=7.0.0
11
+ Requires-Dist: pyyaml>=6.0
12
+
13
+ # robotframework-okw-env-docker
14
+
15
+ Docker Compose provider for [okw-env](http://192.168.1.130:3000/Hrabovszki1023/robotframework-okw-env).
16
+
17
+ > Deutsche Version: [README_de.md](README_de.md)
18
+
19
+ ## What
20
+
21
+ Implements the OKW Environment Provider Contract using Docker Compose.
22
+ Generates `docker-compose.yml` from YAML component definitions and
23
+ manages the full container lifecycle (create, start, health check,
24
+ logs, snapshot, stop, destroy).
25
+
26
+ ## Why — Signal vs. NOISE
27
+
28
+ Docker Compose syntax is NOISE. The tester only writes:
29
+
30
+ ```robot
31
+ ENV_Start PostgresDB
32
+ ENV_BuildAndRun
33
+ ENV_WaitForReady PostgresDB
34
+ ```
35
+
36
+ The framework generates the Compose file, starts the container,
37
+ and polls the health check. No Docker knowledge required in test code.
38
+
39
+ ## Install
40
+
41
+ ```bash
42
+ pip install robotframework-okw-env-docker
43
+ ```
44
+
45
+ This automatically installs `robotframework-okw-env` (core) and
46
+ `robotframework-docker` (Docker Compose engine).
47
+
48
+ ## Component YAML
49
+
50
+ ```yaml
51
+ PostgresDB:
52
+ provider: docker
53
+ image: postgres
54
+ version: "17"
55
+ port: 5432
56
+ env:
57
+ POSTGRES_DB: testdb
58
+ POSTGRES_PASSWORD: testpass
59
+ healthcheck: "pg_isready -U postgres"
60
+ timeout: 30s
61
+ ```
62
+
63
+ ## Minimal Example
64
+
65
+ ```robot
66
+ *** Settings ***
67
+ Library okw_env.library.OkwEnvLibrary components_dir=components WITH NAME ENV
68
+
69
+ *** Test Cases ***
70
+ Database Is Reachable
71
+ ENV_Start PostgresDB
72
+ ENV_BuildAndRun
73
+ ENV_WaitForReady PostgresDB
74
+ Log PostgresDB is ready for testing.
75
+ [Teardown] ENV_Stop
76
+ ```
77
+
78
+ ## Architecture
79
+
80
+ ```
81
+ okw-env (keywords, YAML loader, registry)
82
+
83
+
84
+ okw-env-docker (this library)
85
+
86
+
87
+ DockerComposeLibrary (robotframework-docker)
88
+
89
+
90
+ Docker Compose CLI → Docker Engine
91
+ ```
92
+
93
+ ## Documentation
94
+
95
+ - [CONTRACT.md](docs/CONTRACT.md) — How this provider implements the contract
96
+ - [SPECIFICATION.md](docs/SPECIFICATION.md) — Lifecycle, isolation, snapshot model
97
+ - [KEYWORDS.md](docs/KEYWORDS.md) — Keyword reference with examples
@@ -0,0 +1,13 @@
1
+ README.md
2
+ pyproject.toml
3
+ src/okw_env_docker/__init__.py
4
+ src/okw_env_docker/docker_provider.py
5
+ src/okw_env_docker/library.py
6
+ src/robotframework_okw_env_docker.egg-info/PKG-INFO
7
+ src/robotframework_okw_env_docker.egg-info/SOURCES.txt
8
+ src/robotframework_okw_env_docker.egg-info/dependency_links.txt
9
+ src/robotframework_okw_env_docker.egg-info/entry_points.txt
10
+ src/robotframework_okw_env_docker.egg-info/requires.txt
11
+ src/robotframework_okw_env_docker.egg-info/top_level.txt
12
+ tests/test_docker_provider.py
13
+ tests/test_integration_docker.py
@@ -0,0 +1,2 @@
1
+ [okw_env.providers]
2
+ docker = okw_env_docker:DockerProvider
@@ -0,0 +1,4 @@
1
+ robotframework>=6.0
2
+ robotframework-okw-env>=0.1.0
3
+ docker>=7.0.0
4
+ pyyaml>=6.0
@@ -0,0 +1,204 @@
1
+ """Unit tests for DockerProvider using mocked Docker SDK."""
2
+
3
+ from unittest.mock import MagicMock, patch
4
+
5
+ import pytest
6
+
7
+ from okw_env_docker.docker_provider import DockerProvider
8
+
9
+
10
+ @pytest.fixture
11
+ def mock_client():
12
+ with patch("okw_env_docker.docker_provider.docker.DockerClient") as mock_cls:
13
+ client = MagicMock()
14
+ mock_cls.return_value = client
15
+ yield client
16
+
17
+
18
+ @pytest.fixture
19
+ def provider(mock_client):
20
+ return DockerProvider(base_url="tcp://fake:2375")
21
+
22
+
23
+ @pytest.fixture
24
+ def component():
25
+ return {
26
+ "image": "postgres",
27
+ "version": "16",
28
+ "port": 5432,
29
+ "env": {"POSTGRES_PASSWORD": "test"},
30
+ "healthcheck": "pg_isready -U postgres",
31
+ }
32
+
33
+
34
+ def test_create_pulls_image_and_creates_container(provider, mock_client, component):
35
+ mock_container = MagicMock()
36
+ mock_client.containers.create.return_value = mock_container
37
+
38
+ cid = provider.create(component)
39
+
40
+ mock_client.images.pull.assert_called_once_with("postgres", tag="16")
41
+ mock_client.containers.create.assert_called_once()
42
+ assert cid.startswith("okw-")
43
+
44
+
45
+ def test_create_without_healthcheck(provider, mock_client):
46
+ mock_client.containers.create.return_value = MagicMock()
47
+
48
+ provider.create({"image": "redis", "version": "7"})
49
+
50
+ call_kwargs = mock_client.containers.create.call_args[1]
51
+ assert call_kwargs["healthcheck"] is None
52
+
53
+
54
+ def test_create_default_version(provider, mock_client):
55
+ mock_client.containers.create.return_value = MagicMock()
56
+
57
+ provider.create({"image": "nginx"})
58
+
59
+ mock_client.images.pull.assert_called_once_with("nginx", tag="latest")
60
+
61
+
62
+ def test_start(provider, mock_client, component):
63
+ mock_container = MagicMock()
64
+ mock_client.containers.create.return_value = mock_container
65
+ cid = provider.create(component)
66
+
67
+ provider.start(cid)
68
+
69
+ mock_container.start.assert_called_once()
70
+
71
+
72
+ def test_is_ready_healthy(provider, mock_client, component):
73
+ mock_container = MagicMock()
74
+ mock_container.attrs = {"State": {"Health": {"Status": "healthy"}}}
75
+ mock_client.containers.create.return_value = mock_container
76
+ cid = provider.create(component)
77
+
78
+ assert provider.is_ready(cid) is True
79
+ mock_container.reload.assert_called_once()
80
+
81
+
82
+ def test_is_ready_unhealthy(provider, mock_client, component):
83
+ mock_container = MagicMock()
84
+ mock_container.attrs = {"State": {"Health": {"Status": "starting"}}}
85
+ mock_client.containers.create.return_value = mock_container
86
+ cid = provider.create(component)
87
+
88
+ assert provider.is_ready(cid) is False
89
+
90
+
91
+ def test_is_ready_no_healthcheck_running(provider, mock_client):
92
+ mock_container = MagicMock()
93
+ mock_container.attrs = {"State": {"Status": "running"}}
94
+ mock_client.containers.create.return_value = mock_container
95
+ cid = provider.create({"image": "redis"})
96
+
97
+ assert provider.is_ready(cid) is True
98
+
99
+
100
+ def test_is_ready_no_healthcheck_not_running(provider, mock_client):
101
+ mock_container = MagicMock()
102
+ mock_container.attrs = {"State": {"Status": "created"}}
103
+ mock_client.containers.create.return_value = mock_container
104
+ cid = provider.create({"image": "redis"})
105
+
106
+ assert provider.is_ready(cid) is False
107
+
108
+
109
+ def test_get_logs(provider, mock_client, component):
110
+ mock_container = MagicMock()
111
+ mock_container.logs.return_value = b"2024-01-01 LOG: ready"
112
+ mock_client.containers.create.return_value = mock_container
113
+ cid = provider.create(component)
114
+
115
+ logs = provider.get_logs(cid)
116
+
117
+ assert "ready" in logs
118
+
119
+
120
+ def test_snapshot(provider, mock_client, component):
121
+ mock_container = MagicMock()
122
+ mock_client.containers.create.return_value = mock_container
123
+ cid = provider.create(component)
124
+
125
+ tag = provider.snapshot(cid)
126
+
127
+ mock_container.commit.assert_called_once_with(repository=f"{cid}-snapshot")
128
+ assert tag == f"{cid}-snapshot"
129
+
130
+
131
+ def test_stop(provider, mock_client, component):
132
+ mock_container = MagicMock()
133
+ mock_client.containers.create.return_value = mock_container
134
+ cid = provider.create(component)
135
+
136
+ provider.stop(cid)
137
+
138
+ mock_container.stop.assert_called_once_with(timeout=10)
139
+
140
+
141
+ def test_destroy(provider, mock_client, component):
142
+ mock_container = MagicMock()
143
+ mock_client.containers.create.return_value = mock_container
144
+ cid = provider.create(component)
145
+
146
+ provider.destroy(cid)
147
+
148
+ mock_container.remove.assert_called_once_with(v=True, force=True)
149
+
150
+
151
+ def test_destroy_removes_from_tracking(provider, mock_client, component):
152
+ mock_container = MagicMock()
153
+ mock_client.containers.create.return_value = mock_container
154
+ cid = provider.create(component)
155
+ provider.destroy(cid)
156
+
157
+ with pytest.raises(KeyError, match="Unknown container"):
158
+ provider.stop(cid)
159
+
160
+
161
+ def test_unknown_container_raises(provider):
162
+ with pytest.raises(KeyError, match="Unknown container"):
163
+ provider.start("nonexistent")
164
+
165
+
166
+ def test_get_runtime_info_returns_host_port(provider, mock_client, component):
167
+ mock_container = MagicMock()
168
+ mock_container.attrs = {
169
+ "NetworkSettings": {"Ports": {"5432/tcp": [{"HostIp": "0.0.0.0", "HostPort": "49321"}]}}
170
+ }
171
+ mock_client.containers.create.return_value = mock_container
172
+ cid = provider.create(component)
173
+
174
+ info = provider.get_runtime_info(cid)
175
+
176
+ assert info == {"port": 49321}
177
+
178
+
179
+ def test_get_runtime_info_no_ports(provider, mock_client):
180
+ mock_container = MagicMock()
181
+ mock_container.attrs = {"NetworkSettings": {"Ports": {}}}
182
+ mock_client.containers.create.return_value = mock_container
183
+ cid = provider.create({"image": "redis"})
184
+
185
+ info = provider.get_runtime_info(cid)
186
+
187
+ assert info == {}
188
+
189
+
190
+ def test_create_uses_dynamic_host_port(provider, mock_client, component):
191
+ mock_client.containers.create.return_value = MagicMock()
192
+ provider.create(component)
193
+
194
+ call_kwargs = mock_client.containers.create.call_args[1]
195
+ assert call_kwargs["ports"] == {"5432/tcp": None}
196
+
197
+
198
+ def test_env_var_fallback():
199
+ with patch("okw_env_docker.docker_provider.docker.DockerClient") as mock_cls, \
200
+ patch.dict("os.environ", {"DOCKER_HOST": "tcp://envhost:2375"}):
201
+ mock_cls.return_value = MagicMock()
202
+ p = DockerProvider()
203
+ p._get_client()
204
+ mock_cls.assert_called_once_with(base_url="tcp://envhost:2375")
@@ -0,0 +1,73 @@
1
+ """Integration test against a real Docker host.
2
+
3
+ Requires: Docker Engine at tcp://192.168.1.123:2375
4
+ Run with: pytest tests/test_integration_docker.py -v -s
5
+ """
6
+
7
+ import pytest
8
+
9
+ from okw_env_docker.docker_provider import DockerProvider
10
+
11
+ DOCKER_HOST = "tcp://192.168.1.123:2375"
12
+
13
+
14
+ @pytest.fixture
15
+ def provider():
16
+ return DockerProvider(base_url=DOCKER_HOST)
17
+
18
+
19
+ @pytest.fixture
20
+ def redis_component():
21
+ return {
22
+ "image": "redis",
23
+ "version": "7",
24
+ "port": 6379,
25
+ "healthcheck": "redis-cli ping",
26
+ }
27
+
28
+
29
+ @pytest.fixture
30
+ def redis_container(provider, redis_component):
31
+ cid = provider.create(redis_component)
32
+ yield cid
33
+ try:
34
+ provider.stop(cid)
35
+ except Exception:
36
+ pass
37
+ try:
38
+ provider.destroy(cid)
39
+ except Exception:
40
+ pass
41
+
42
+
43
+ def test_full_lifecycle(provider, redis_component, redis_container):
44
+ cid = redis_container
45
+ print(f"\n Container: {cid}")
46
+
47
+ # Start
48
+ provider.start(cid)
49
+ print(" Started.")
50
+
51
+ # Wait for ready (poll)
52
+ import time
53
+ for i in range(30):
54
+ if provider.is_ready(cid):
55
+ print(f" Ready after {i + 1} poll(s).")
56
+ break
57
+ time.sleep(1)
58
+ else:
59
+ pytest.fail("Container not ready after 30s")
60
+
61
+ # Logs
62
+ logs = provider.get_logs(cid)
63
+ print(f" Logs ({len(logs)} chars): {logs[:200]}")
64
+ assert len(logs) > 0
65
+
66
+ # Snapshot
67
+ tag = provider.snapshot(cid)
68
+ print(f" Snapshot: {tag}")
69
+ assert "snapshot" in tag
70
+
71
+ # Stop
72
+ provider.stop(cid)
73
+ print(" Stopped.")