Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions base/images/tests/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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."""
Expand All @@ -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")
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Hello from the node server running from Azure Linux NodeJS container.
Original file line number Diff line number Diff line change
@@ -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);
Original file line number Diff line number Diff line change
@@ -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,
)
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Hello from the PHP server running from Azure Linux PHP container. Saves zip file correctly.
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<?php
// router.php
// Exercises the php-pecl-zip extension by creating, writing and
// extracting a zip archive, then verifying the round-trip.
if (preg_match('/\.(?:png|jpg|jpeg|gif)$/', $_SERVER["REQUEST_URI"])) {
return false; // serve the requested resource as-is.
} else {
$zip = new ZipArchive();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question(non-blocking): I'm wondering what motivated the more extensive testing (zip/unzip) for php and why you didn't do something similar for the other languages.

@bhagyapathak bhagyapathak Jul 2, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you @tobias for the review.
test_php_zip_extension_loaded is intentionally PHP-specific. This is a direct port of the existing golden-container smoke tests, and my current goal is to bring them to the Azure Linux 4.0 base container with minimal changes. Once the golden containers are available, I plan to run the same test suite against them as well. For that reason, I have kept the tests as close to the original implementation as possible. But I am open to the option of adding this test later when golden container is availble.

The ZIP validation originates from the upstream PHP golden-container test. The php-pecl-zip package is an optional extension, so the presence of PHP alone does not guarantee that the ZIP extension is available. That's why the test explicitly verifies it through both a php -m check and a functional round-trip validation.

The other runtimes do not have an equivalent optional, headless-relevant extension.

$testFolder = shell_exec("mktemp -d | tr -d '\n'");
$filename = "$testFolder/test_archive.zip";
if ($zip->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.");
}
}
?>
Original file line number Diff line number Diff line change
@@ -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,
)
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
"""Minimal stdlib HTTP server used to validate the Python runtime."""

from __future__ import annotations

from http.server import BaseHTTPRequestHandler, HTTPServer
Comment thread
Copilot marked this conversation as resolved.
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
Comment thread
Copilot marked this conversation as resolved.
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()
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Hello from the python server running from Azure Linux Python container.
Original file line number Diff line number Diff line change
@@ -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,
)
Original file line number Diff line number Diff line change
@@ -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
19 changes: 19 additions & 0 deletions base/images/tests/cases/runtime/container-base/test_ruby/app.rb
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Hello from Ruby
Original file line number Diff line number Diff line change
@@ -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,
)
Loading
Loading