From 181dd27297086b6d829dd38dfc0eabd01b7a080e Mon Sep 17 00:00:00 2001 From: Bhagyashri Date: Thu, 2 Jul 2026 22:27:33 +0530 Subject: [PATCH 1/3] Language Runtime Container tests --- .../container-base/test_nginx/test_nginx.py | 24 +----- .../container-base/test_nodejs/Dockerfile | 6 ++ .../container-base/test_nodejs/response.txt | 1 + .../container-base/test_nodejs/server.js | 17 ++++ .../container-base/test_nodejs/test_nodejs.py | 33 ++++++++ .../container-base/test_php/Dockerfile | 6 ++ .../container-base/test_php/response.txt | 1 + .../container-base/test_php/router.php | 29 +++++++ .../container-base/test_php/test_php.py | 41 ++++++++++ .../container-base/test_python/Dockerfile | 6 ++ .../runtime/container-base/test_python/app.py | 18 +++++ .../container-base/test_python/response.txt | 1 + .../container-base/test_python/test_python.py | 33 ++++++++ .../container-base/test_ruby/Dockerfile | 6 ++ .../runtime/container-base/test_ruby/app.rb | 19 +++++ .../container-base/test_ruby/response.txt | 1 + .../container-base/test_ruby/test_ruby.py | 33 ++++++++ base/images/tests/conftest.py | 81 +++++++++++++++++++ 18 files changed, 334 insertions(+), 22 deletions(-) create mode 100644 base/images/tests/cases/runtime/container-base/test_nodejs/Dockerfile create mode 100644 base/images/tests/cases/runtime/container-base/test_nodejs/response.txt create mode 100644 base/images/tests/cases/runtime/container-base/test_nodejs/server.js create mode 100644 base/images/tests/cases/runtime/container-base/test_nodejs/test_nodejs.py create mode 100644 base/images/tests/cases/runtime/container-base/test_php/Dockerfile create mode 100644 base/images/tests/cases/runtime/container-base/test_php/response.txt create mode 100644 base/images/tests/cases/runtime/container-base/test_php/router.php create mode 100644 base/images/tests/cases/runtime/container-base/test_php/test_php.py create mode 100644 base/images/tests/cases/runtime/container-base/test_python/Dockerfile create mode 100644 base/images/tests/cases/runtime/container-base/test_python/app.py create mode 100644 base/images/tests/cases/runtime/container-base/test_python/response.txt create mode 100644 base/images/tests/cases/runtime/container-base/test_python/test_python.py create mode 100644 base/images/tests/cases/runtime/container-base/test_ruby/Dockerfile create mode 100644 base/images/tests/cases/runtime/container-base/test_ruby/app.rb create mode 100644 base/images/tests/cases/runtime/container-base/test_ruby/response.txt create mode 100644 base/images/tests/cases/runtime/container-base/test_ruby/test_ruby.py diff --git a/base/images/tests/cases/runtime/container-base/test_nginx/test_nginx.py b/base/images/tests/cases/runtime/container-base/test_nginx/test_nginx.py index 21efe837037..79fb2244a0b 100644 --- a/base/images/tests/cases/runtime/container-base/test_nginx/test_nginx.py +++ b/base/images/tests/cases/runtime/container-base/test_nginx/test_nginx.py @@ -7,24 +7,9 @@ from __future__ import annotations -import time - import pytest -def wait_for_http(container_exec_shell, url: str): - """Poll until an HTTP endpoint responds successfully.""" - result = None - for _ in range(5): - result = container_exec_shell(f"curl -sf {url}") - if result.exit_code == 0: - return result - time.sleep(1) - - assert result is not None - return result - - @pytest.mark.dockerfile() def test_nginx_config_valid(container_exec_shell) -> None: """nginx configuration must pass validation.""" @@ -35,11 +20,6 @@ def test_nginx_config_valid(container_exec_shell) -> None: @pytest.mark.dockerfile() -def test_nginx_health_endpoint(container_exec_shell) -> None: +def test_nginx_health_endpoint(assert_http_server) -> None: """nginx /health endpoint must return 200.""" - start = container_exec_shell("nginx") - assert start.exit_code == 0, f"nginx failed to start: {start.output}" - - result = wait_for_http(container_exec_shell, "http://localhost:80/health") - assert result.exit_code == 0, f"health check failed: {result.output}" - assert "healthy" in result.output + assert_http_server("nginx", "http://localhost:80/health", "healthy") diff --git a/base/images/tests/cases/runtime/container-base/test_nodejs/Dockerfile b/base/images/tests/cases/runtime/container-base/test_nodejs/Dockerfile new file mode 100644 index 00000000000..e77d6929f7f --- /dev/null +++ b/base/images/tests/cases/runtime/container-base/test_nodejs/Dockerfile @@ -0,0 +1,6 @@ +ARG BASE_IMAGE +FROM ${BASE_IMAGE} +RUN dnf install -y nodejs curl && dnf clean all +COPY server.js /app/server.js +COPY response.txt /app/response.txt +EXPOSE 8080 diff --git a/base/images/tests/cases/runtime/container-base/test_nodejs/response.txt b/base/images/tests/cases/runtime/container-base/test_nodejs/response.txt new file mode 100644 index 00000000000..b63647486c5 --- /dev/null +++ b/base/images/tests/cases/runtime/container-base/test_nodejs/response.txt @@ -0,0 +1 @@ +Hello from the node server running from Azure Linux NodeJS container. diff --git a/base/images/tests/cases/runtime/container-base/test_nodejs/server.js b/base/images/tests/cases/runtime/container-base/test_nodejs/server.js new file mode 100644 index 00000000000..f721110356d --- /dev/null +++ b/base/images/tests/cases/runtime/container-base/test_nodejs/server.js @@ -0,0 +1,17 @@ +'use strict'; + +// Minimal stdlib HTTP server used to validate the Node.js runtime. +const http = require('http'); +const fs = require('fs'); +const path = require('path'); + +const PORT = 8080; +const HOST = '0.0.0.0'; +const RESPONSE = fs.readFileSync(path.join(__dirname, 'response.txt')); + +const server = http.createServer((req, res) => { + res.writeHead(200, { 'Content-Type': 'text/plain' }); + res.end(RESPONSE); +}); + +server.listen(PORT, HOST); diff --git a/base/images/tests/cases/runtime/container-base/test_nodejs/test_nodejs.py b/base/images/tests/cases/runtime/container-base/test_nodejs/test_nodejs.py new file mode 100644 index 00000000000..f5bf4778bfb --- /dev/null +++ b/base/images/tests/cases/runtime/container-base/test_nodejs/test_nodejs.py @@ -0,0 +1,33 @@ +# SPDX-License-Identifier: MIT +"""Validate the Node.js runtime works on the container-base image. + +Uses ``@pytest.mark.dockerfile()`` to build a custom image with Node.js +installed on top of the image-under-test, then runs a stdlib ``http`` +server and checks its response. +""" + +from __future__ import annotations + +from pathlib import Path + +import pytest + +EXPECTED_RESPONSE = (Path(__file__).with_name("response.txt")).read_text().strip() + + +@pytest.mark.dockerfile() +def test_nodejs_version(container_exec_shell) -> None: + """Node.js interpreter must be present and report a version.""" + result = container_exec_shell("node --version") + assert result.exit_code == 0, f"node --version failed: {result.output}" + assert result.output.strip().startswith("v") + + +@pytest.mark.dockerfile() +def test_nodejs_http_server(assert_http_server) -> None: + """A stdlib http server must serve the expected response.""" + assert_http_server( + "nohup node /app/server.js > /tmp/server.log 2>&1 &", + "http://localhost:8080/", + EXPECTED_RESPONSE, + ) diff --git a/base/images/tests/cases/runtime/container-base/test_php/Dockerfile b/base/images/tests/cases/runtime/container-base/test_php/Dockerfile new file mode 100644 index 00000000000..2976eb4b41c --- /dev/null +++ b/base/images/tests/cases/runtime/container-base/test_php/Dockerfile @@ -0,0 +1,6 @@ +ARG BASE_IMAGE +FROM ${BASE_IMAGE} +RUN dnf install -y php-cli php-pecl-zip unzip curl && dnf clean all +COPY router.php /app/router.php +COPY response.txt /app/response.txt +EXPOSE 8080 diff --git a/base/images/tests/cases/runtime/container-base/test_php/response.txt b/base/images/tests/cases/runtime/container-base/test_php/response.txt new file mode 100644 index 00000000000..f441717f929 --- /dev/null +++ b/base/images/tests/cases/runtime/container-base/test_php/response.txt @@ -0,0 +1 @@ +Hello from the PHP server running from Azure Linux PHP container. Saves zip file correctly. diff --git a/base/images/tests/cases/runtime/container-base/test_php/router.php b/base/images/tests/cases/runtime/container-base/test_php/router.php new file mode 100644 index 00000000000..342c7ddb129 --- /dev/null +++ b/base/images/tests/cases/runtime/container-base/test_php/router.php @@ -0,0 +1,29 @@ +open($filename, ZipArchive::CREATE) !== TRUE) { + exit("cannot open <$filename>\n"); + } + $zip->addFromString("testfilephp.txt", "#1 This is a test string added as testfilephp.txt.\n"); + $zip->addFromString("testfilephp2.txt", "#2 This is a test string added as testfilephp2.txt.\n"); + $zip->addFile("/app/router.php", "router.php"); + $zip->close(); + if (file_exists($filename)) { + shell_exec("unzip $filename -d $testFolder/test_folder"); + foreach (array("testfilephp.txt", "testfilephp2.txt", "router.php") as $testFile) { + if (!file_exists("$testFolder/test_folder/$testFile")) return false; + } + shell_exec("rm -rf $testFolder"); + echo file_get_contents(__DIR__ . "/response.txt"); + } else { + exit("Zip archive not created, server reached though."); + } +} +?> diff --git a/base/images/tests/cases/runtime/container-base/test_php/test_php.py b/base/images/tests/cases/runtime/container-base/test_php/test_php.py new file mode 100644 index 00000000000..43a3168aef5 --- /dev/null +++ b/base/images/tests/cases/runtime/container-base/test_php/test_php.py @@ -0,0 +1,41 @@ +# SPDX-License-Identifier: MIT +"""Validate the PHP runtime works on the container-base image. + +Uses ``@pytest.mark.dockerfile()`` to build a custom image with PHP and +the zip extension installed on top of the image-under-test, then runs +the built-in PHP web server and verifies a zip round-trip via router.php. +""" + +from __future__ import annotations + +from pathlib import Path + +import pytest + +EXPECTED_RESPONSE = (Path(__file__).with_name("response.txt")).read_text().strip() + + +@pytest.mark.dockerfile() +def test_php_version(container_exec_shell) -> None: + """PHP interpreter must be present and report a version.""" + result = container_exec_shell("php --version") + assert result.exit_code == 0, f"php --version failed: {result.output}" + assert "PHP" in result.output + + +@pytest.mark.dockerfile() +def test_php_zip_extension_loaded(container_exec_shell) -> None: + """The zip extension must be loaded in the PHP runtime.""" + result = container_exec_shell("php -m") + assert result.exit_code == 0, f"php -m failed: {result.output}" + assert "zip" in result.output + + +@pytest.mark.dockerfile() +def test_php_http_server(assert_http_server) -> None: + """The built-in PHP server must serve a successful zip round-trip.""" + assert_http_server( + "nohup php -S 0.0.0.0:8080 /app/router.php > /tmp/server.log 2>&1 &", + "http://localhost:8080/", + EXPECTED_RESPONSE, + ) diff --git a/base/images/tests/cases/runtime/container-base/test_python/Dockerfile b/base/images/tests/cases/runtime/container-base/test_python/Dockerfile new file mode 100644 index 00000000000..665b4065055 --- /dev/null +++ b/base/images/tests/cases/runtime/container-base/test_python/Dockerfile @@ -0,0 +1,6 @@ +ARG BASE_IMAGE +FROM ${BASE_IMAGE} +RUN dnf install -y python3 curl && dnf clean all +COPY app.py /app/app.py +COPY response.txt /app/response.txt +EXPOSE 8080 diff --git a/base/images/tests/cases/runtime/container-base/test_python/app.py b/base/images/tests/cases/runtime/container-base/test_python/app.py new file mode 100644 index 00000000000..b41959ca046 --- /dev/null +++ b/base/images/tests/cases/runtime/container-base/test_python/app.py @@ -0,0 +1,18 @@ +"""Minimal stdlib HTTP server used to validate the Python runtime.""" + +from http.server import BaseHTTPRequestHandler, HTTPServer +from pathlib import Path + +RESPONSE = Path(__file__).with_name("response.txt").read_bytes() + + +class Handler(BaseHTTPRequestHandler): + def do_GET(self): # noqa: N802 - http.server API + self.send_response(200) + self.send_header("Content-Type", "text/plain") + self.end_headers() + self.wfile.write(RESPONSE) + + +if __name__ == "__main__": + HTTPServer(("0.0.0.0", 8080), Handler).serve_forever() diff --git a/base/images/tests/cases/runtime/container-base/test_python/response.txt b/base/images/tests/cases/runtime/container-base/test_python/response.txt new file mode 100644 index 00000000000..7f06994db14 --- /dev/null +++ b/base/images/tests/cases/runtime/container-base/test_python/response.txt @@ -0,0 +1 @@ +Hello from the python server running from Azure Linux Python container. diff --git a/base/images/tests/cases/runtime/container-base/test_python/test_python.py b/base/images/tests/cases/runtime/container-base/test_python/test_python.py new file mode 100644 index 00000000000..494a936b03c --- /dev/null +++ b/base/images/tests/cases/runtime/container-base/test_python/test_python.py @@ -0,0 +1,33 @@ +# SPDX-License-Identifier: MIT +"""Validate the Python runtime works on the container-base image. + +Uses ``@pytest.mark.dockerfile()`` to build a custom image with the +Python interpreter installed on top of the image-under-test, then runs +a stdlib ``http.server`` app and checks its response. +""" + +from __future__ import annotations + +from pathlib import Path + +import pytest + +EXPECTED_RESPONSE = (Path(__file__).with_name("response.txt")).read_text().strip() + + +@pytest.mark.dockerfile() +def test_python_version(container_exec_shell) -> None: + """Python interpreter must be present and report version 3.""" + result = container_exec_shell("python3 --version") + assert result.exit_code == 0, f"python3 --version failed: {result.output}" + assert "Python 3" in result.output + + +@pytest.mark.dockerfile() +def test_python_http_server(assert_http_server) -> None: + """A stdlib http.server app must serve the expected response.""" + assert_http_server( + "nohup python3 /app/app.py > /tmp/server.log 2>&1 &", + "http://localhost:8080/", + EXPECTED_RESPONSE, + ) diff --git a/base/images/tests/cases/runtime/container-base/test_ruby/Dockerfile b/base/images/tests/cases/runtime/container-base/test_ruby/Dockerfile new file mode 100644 index 00000000000..80c7d6824a7 --- /dev/null +++ b/base/images/tests/cases/runtime/container-base/test_ruby/Dockerfile @@ -0,0 +1,6 @@ +ARG BASE_IMAGE +FROM ${BASE_IMAGE} +RUN dnf install -y ruby curl && dnf clean all +COPY app.rb /app/app.rb +COPY response.txt /app/response.txt +EXPOSE 8080 diff --git a/base/images/tests/cases/runtime/container-base/test_ruby/app.rb b/base/images/tests/cases/runtime/container-base/test_ruby/app.rb new file mode 100644 index 00000000000..9a793791019 --- /dev/null +++ b/base/images/tests/cases/runtime/container-base/test_ruby/app.rb @@ -0,0 +1,19 @@ +# Minimal stdlib TCP HTTP server used to validate the Ruby runtime. +require 'socket' + +server = TCPServer.new('0.0.0.0', 8080) +body = File.read(File.join(__dir__, 'response.txt')) + +trap("INT") { server.close; exit } + +loop do + client = server.accept + client.gets + client.print "HTTP/1.1 200 OK\r\n" + client.print "Content-Type: text/plain\r\n" + client.print "Content-Length: #{body.bytesize}\r\n" + client.print "Connection: close\r\n" + client.print "\r\n" + client.print body + client.close +end diff --git a/base/images/tests/cases/runtime/container-base/test_ruby/response.txt b/base/images/tests/cases/runtime/container-base/test_ruby/response.txt new file mode 100644 index 00000000000..1385f3d3343 --- /dev/null +++ b/base/images/tests/cases/runtime/container-base/test_ruby/response.txt @@ -0,0 +1 @@ +Hello from Ruby diff --git a/base/images/tests/cases/runtime/container-base/test_ruby/test_ruby.py b/base/images/tests/cases/runtime/container-base/test_ruby/test_ruby.py new file mode 100644 index 00000000000..75850626d0a --- /dev/null +++ b/base/images/tests/cases/runtime/container-base/test_ruby/test_ruby.py @@ -0,0 +1,33 @@ +# SPDX-License-Identifier: MIT +"""Validate the Ruby runtime works on the container-base image. + +Uses ``@pytest.mark.dockerfile()`` to build a custom image with Ruby +installed on top of the image-under-test, then runs a stdlib socket +HTTP server and checks its response. +""" + +from __future__ import annotations + +from pathlib import Path + +import pytest + +EXPECTED_RESPONSE = (Path(__file__).with_name("response.txt")).read_text().strip() + + +@pytest.mark.dockerfile() +def test_ruby_version(container_exec_shell) -> None: + """Ruby interpreter must be present and report a version.""" + result = container_exec_shell("ruby --version") + assert result.exit_code == 0, f"ruby --version failed: {result.output}" + assert "ruby" in result.output + + +@pytest.mark.dockerfile() +def test_ruby_http_server(assert_http_server) -> None: + """A stdlib socket HTTP server must serve the expected response.""" + assert_http_server( + "nohup ruby /app/app.rb > /tmp/server.log 2>&1 &", + "http://localhost:8080/", + EXPECTED_RESPONSE, + ) diff --git a/base/images/tests/conftest.py b/base/images/tests/conftest.py index e168770eed6..a0d2268d9ef 100644 --- a/base/images/tests/conftest.py +++ b/base/images/tests/conftest.py @@ -9,9 +9,11 @@ from __future__ import annotations import logging +import shlex import shutil import subprocess import tempfile +import time from pathlib import Path import pytest @@ -390,3 +392,82 @@ def _exec_shell(command: str, *, shell: str = "bash"): [shell, "-c", command], ) return _exec_shell + + +@pytest.fixture +def wait_for_http(container_exec_shell): + """Callable that polls an in-container HTTP endpoint until it responds. + + Runs ``curl -sSf `` inside the running test container, retrying + until the request succeeds. Bounded connect/read timeouts keep a + hung server from stalling the suite, and the call fails explicitly + (raising ``AssertionError``) once the retries are exhausted rather + than returning a failed result a caller might forget to check. + + Usage:: + + def test_example(container_exec_shell, wait_for_http): + container_exec_shell("nginx") + result = wait_for_http("http://localhost:80/health") + assert "healthy" in result.output + """ + def _wait( + url: str, + *, + retries: int = 5, + delay: float = 1.0, + connect_timeout: float = 2.0, + max_time: float = 5.0, + ): + result = None + for _ in range(retries): + result = container_exec_shell( + f"curl -sSf --connect-timeout {connect_timeout} " + f"--max-time {max_time} {shlex.quote(url)}" + ) + if result.exit_code == 0: + return result + time.sleep(delay) + + output = result.output if result is not None else "" + raise AssertionError( + f"HTTP endpoint {url} did not respond after {retries} attempt(s): " + f"{output}" + ) + return _wait + + +@pytest.fixture +def assert_http_server(container_exec_shell, wait_for_http): + """Start an HTTP server in the container and assert its response. + + Runs ``start_command`` inside the running test container, waits for + ``url`` to respond, and asserts ``expected`` appears in the response + body. Returns the successful result for further assertions. + + Usage:: + + def test_example(assert_http_server): + assert_http_server( + "nohup python3 /app/app.py > /tmp/server.log 2>&1 &", + "http://localhost:8080/", + "Hello from the server", + ) + """ + def _assert( + start_command: str, + url: str, + expected: str, + *, + retries: int = 5, + delay: float = 1.0, + ): + start = container_exec_shell(start_command) + assert start.exit_code == 0, f"failed to start server: {start.output}" + + result = wait_for_http(url, retries=retries, delay=delay) + assert expected in result.output, ( + f"unexpected response body: {result.output!r}" + ) + return result + return _assert From be1660daeb1f86e252fae7ce31fea78002608067 Mon Sep 17 00:00:00 2001 From: Bhagyashri Date: Thu, 2 Jul 2026 22:51:35 +0530 Subject: [PATCH 2/3] Document new fixtures in README.md --- base/images/tests/README.md | 2 ++ .../tests/cases/runtime/container-base/test_python/app.py | 2 ++ 2 files changed, 4 insertions(+) diff --git a/base/images/tests/README.md b/base/images/tests/README.md index f0f66909bb2..be33980781b 100644 --- a/base/images/tests/README.md +++ b/base/images/tests/README.md @@ -155,6 +155,8 @@ base/images/ | `running_container` | function | `ContainerInstance` | Fresh container per test — auto-skips on VMs | | `container_exec_shell` | function | callable | `(cmd, shell="bash") → ContainerExecResult` | | `container_exec` | function | callable | `(args) → ContainerExecResult` | +| `wait_for_http` | function | callable | `(url, *, retries=5, delay=1.0, connect_timeout=2.0, max_time=5.0) → ContainerExecResult` — polls an in-container HTTP endpoint with `curl`; raises after retries | +| `assert_http_server` | function | callable | `(start_command, url, expected, *, retries=5, delay=1.0) → ContainerExecResult` — starts a server, waits for `url`, asserts `expected` in body | ## Adding tests diff --git a/base/images/tests/cases/runtime/container-base/test_python/app.py b/base/images/tests/cases/runtime/container-base/test_python/app.py index b41959ca046..2e1209097bb 100644 --- a/base/images/tests/cases/runtime/container-base/test_python/app.py +++ b/base/images/tests/cases/runtime/container-base/test_python/app.py @@ -1,5 +1,7 @@ """Minimal stdlib HTTP server used to validate the Python runtime.""" +from __future__ import annotations + from http.server import BaseHTTPRequestHandler, HTTPServer from pathlib import Path From bb500c755789495e7635c844c2fd14714d763892 Mon Sep 17 00:00:00 2001 From: bhagyapathak Date: Thu, 2 Jul 2026 23:06:40 +0530 Subject: [PATCH 3/3] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- .../tests/cases/runtime/container-base/test_python/app.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/base/images/tests/cases/runtime/container-base/test_python/app.py b/base/images/tests/cases/runtime/container-base/test_python/app.py index 2e1209097bb..8e2581e3157 100644 --- a/base/images/tests/cases/runtime/container-base/test_python/app.py +++ b/base/images/tests/cases/runtime/container-base/test_python/app.py @@ -9,7 +9,10 @@ class Handler(BaseHTTPRequestHandler): + """Serve the fixed response body for every request.""" + def do_GET(self): # noqa: N802 - http.server API + """Write the response body with a 200 OK status.""" self.send_response(200) self.send_header("Content-Type", "text/plain") self.end_headers()