diff --git a/base/images/tests/cases/runtime/container-base/test_math/Dockerfile b/base/images/tests/cases/runtime/container-base/test_math/Dockerfile new file mode 100644 index 00000000000..b5cdacfef9e --- /dev/null +++ b/base/images/tests/cases/runtime/container-base/test_math/Dockerfile @@ -0,0 +1,4 @@ +ARG BASE_IMAGE +FROM ${BASE_IMAGE} +RUN dnf install -y python3 && dnf clean all +COPY pi.py /opt/azl-tests/pi.py diff --git a/base/images/tests/cases/runtime/container-base/test_math/pi.py b/base/images/tests/cases/runtime/container-base/test_math/pi.py new file mode 100644 index 00000000000..977de27db41 --- /dev/null +++ b/base/images/tests/cases/runtime/container-base/test_math/pi.py @@ -0,0 +1,72 @@ +# SPDX-License-Identifier: MIT +# Pi calculation adapted from https://github.com/MrBlaise/learnpython/blob/master/Numbers/pi.py +"""Spigot-algorithm Pi calculation, run inside the container for compute validation.""" + +from __future__ import annotations + +import time +from collections.abc import Iterator + +PI_1000 = ( + "3.1415926535897932384626433832795028841971693993751058209749445923078164062862089986280348253421170679821480865132823066470938446095505822317253594081284811174502841027019385211055596446229489549303819644288109756659334461284756482337867831652712019091456485669234603486104543266482133936072602491412" + "737245870066063155881748815209209628292540917153643678925903600113305305488204665213841469519415116094330572703657595919530921861173819326117931051185480744623799627495673518857527248912279381830119491298336733624406566430860213949463952247371907021798609437027705392171762931767523846748184676694051" + "320005681271452635608277857713427577896091736371787214684409012249534301465495853710507922796892589235420199561121290219608640344181598136297747713099605187072113499999983729780499510597317328160963185950244594553469083026425223082533446850352619311881710100031378387528865875332083814206171776691473" + "035982534904287554687311595628638823537875937519577818577805321712268066130019278766111959092164201989" +) + +MAX_SECONDS_PER_COMPUTE = 20.0 + + +def calc_pi(limit: int) -> Iterator[object]: + """Calculate Pi digits one at a time via the spigot algorithm.""" + q, r, t, k, n, step = 1, 0, 1, 1, 3, 3 + counter = 0 + while counter != limit + 1: + if 4 * q + r - t < n * t: + yield n + if counter == 0: + yield "." + if limit == counter: + break + counter += 1 + nr = 10 * (r - n * t) + n = ((10 * (3 * q + r)) // t) - 10 * n + q *= 10 + r = nr + else: + nr = (2 * q + r) * step + nn = (q * (7 * k) + 2 + (r * step)) // (t * step) + q *= k + t *= step + step += 2 + k += 1 + n = nn + r = nr + + +def pi_to_places(places: int) -> str: + """Return Pi, accurate to the given number of decimal places.""" + return "".join(str(d) for d in calc_pi(places)) + + +def verify_pi_1000() -> bool: + """Check that Pi to 1000 places matches the known reference value.""" + return pi_to_places(1000) == PI_1000 + + +def verify_pi_n_times_1000(nrange: int = 10, mult: int = 1000) -> bool: + """Repeatedly compute Pi at growing precision and assert performance (max 20s per computation).""" + for count in range(nrange + 1): + places = max(count * mult, 3) + start = time.time() + answer = pi_to_places(places) + if len(answer) != places + 2 or time.time() - start > MAX_SECONDS_PER_COMPUTE: + return False + return True + + +if __name__ == "__main__": + import sys + + checks = {"1000": verify_pi_1000, "n1000": verify_pi_n_times_1000} + sys.exit(0 if checks[sys.argv[1]]() else 1) diff --git a/base/images/tests/cases/runtime/container-base/test_math/test_math.py b/base/images/tests/cases/runtime/container-base/test_math/test_math.py new file mode 100644 index 00000000000..44ff29c1574 --- /dev/null +++ b/base/images/tests/cases/runtime/container-base/test_math/test_math.py @@ -0,0 +1,22 @@ +# SPDX-License-Identifier: MIT +"""Verify the container's python3 computes Pi correctly (Dockerfile adds python3).""" + +from __future__ import annotations + +import pytest + +_PI = "/opt/azl-tests/pi.py" + + +@pytest.mark.dockerfile() +def test_pi_1000_places(container_exec_shell) -> None: + """Pi computed to 1000 places must match the known value.""" + result = container_exec_shell(f"python3 {_PI} 1000") + assert result.exit_code == 0, f"Pi(1000) verification failed: {result.output}" + + +@pytest.mark.dockerfile() +def test_pi_n_times_1000_places(container_exec_shell) -> None: + """Sustained Pi compute (10 x 1000 places) must stay correct and fast.""" + result = container_exec_shell(f"python3 {_PI} n1000") + assert result.exit_code == 0, f"Pi series verification failed: {result.output}" diff --git a/base/images/tests/cases/runtime/container-base/test_network/Dockerfile b/base/images/tests/cases/runtime/container-base/test_network/Dockerfile new file mode 100644 index 00000000000..9b723bf2477 --- /dev/null +++ b/base/images/tests/cases/runtime/container-base/test_network/Dockerfile @@ -0,0 +1,4 @@ +ARG BASE_IMAGE +FROM ${BASE_IMAGE} +RUN dnf install -y python3 && dnf clean all +COPY netcheck.py /opt/azl-tests/netcheck.py diff --git a/base/images/tests/cases/runtime/container-base/test_network/netcheck.py b/base/images/tests/cases/runtime/container-base/test_network/netcheck.py new file mode 100644 index 00000000000..851188a9041 --- /dev/null +++ b/base/images/tests/cases/runtime/container-base/test_network/netcheck.py @@ -0,0 +1,92 @@ +# SPDX-License-Identifier: MIT +"""Outbound HTTPS fetch + parse helpers, run inside the container to validate networking.""" + +from __future__ import annotations + +import re +import time +import urllib.request +from urllib.error import HTTPError, URLError + +WEATHER_URLS = ( + "https://iotadbselfhostreportstor.blob.core.windows.net/marinerextendedtests/mockweathergov.html", + "https://forecast.weather.gov/MapClick.php?lat=47.6786&lon=-122.1316", +) +REPO_CONFIG_URLS = ( + "https://packages.microsoft.com/azurelinux/3.0/prod/base/x86_64/config.repo", + "https://packages.microsoft.com/cbl-mariner/2.0/prod/base/x86_64/config.repo", +) + + +def fetch(url: str, retries: int = 4) -> str: + """Fetch a URL, retrying transient HTTP errors with exponential backoff, and a 30s timeout to avoid hangs.""" + for attempt in range(1, retries + 1): + try: + with urllib.request.urlopen(urllib.request.Request(url), timeout=30) as resp: # noqa: S310 — https literals only + return resp.read().decode() + except HTTPError: + time.sleep(attempt * 2) + except URLError: + break + return "" + + +def fetch_first(urls: tuple[str, ...]) -> str: + """Return the first non-empty page across the given URLs.""" + for url in urls: + page = fetch(url) + if page: + return page + return "" + + +def substring_between(source: str, before: str, after: str) -> str: + """Return the markup-stripped text between two markers, or empty string.""" + beg = source.find(before) + if beg < 0: + return "" + sub = source[beg + len(before) :] + end = sub.find(after) + if end >= 0: + sub = sub[:end] + return re.sub(r"<.*?>", "", sub).strip() + + +def verify_weather(*, strict: bool = True) -> bool: + """Fetch Redmond weather; strict requires all forecast fields present.""" + page = fetch_first(WEATHER_URLS) + visibility = substring_between(page, "Visibility", "") + dewpoint = substring_between(page, "Dewpoint", "") + humidity = substring_between(page, "Humidity", "") + wind = substring_between(page, "Wind Speed", "") + forecast = substring_between(page, 'alt="Today:', '"') + print( + f"weather: bytes={len(page)} humidity={humidity!r} wind={wind!r} " + f"visibility={visibility!r} dewpoint={dewpoint!r} forecast={forecast!r}" + ) + if not (wind and humidity and visibility and dewpoint) and strict: + return False + return len(page) > 0 + + +def verify_sustained_https(iterations: int = 50, *, strict: bool = True) -> bool: + """Repeat repo-config fetch; strict requires name+enabled fields each time.""" + for i in range(iterations): + page = fetch_first(REPO_CONFIG_URLS) + name = substring_between(page, "name", "\n") + enabled = substring_between(page, "enabled", "\n") + print(f"fetch {i + 1}/{iterations}: bytes={len(page)} name={name!r} enabled={enabled!r}") + if not (name and enabled) and strict: + return False + if len(page) == 0: + return False + time.sleep(1) + return True + + +if __name__ == "__main__": + import sys + + checks = {"weather": verify_weather, "sustained": verify_sustained_https} + strict = "--strict" in sys.argv + sys.exit(0 if checks[sys.argv[1]](strict=strict) else 1) diff --git a/base/images/tests/cases/runtime/container-base/test_network/test_network.py b/base/images/tests/cases/runtime/container-base/test_network/test_network.py new file mode 100644 index 00000000000..0fe1aa7fdf2 --- /dev/null +++ b/base/images/tests/cases/runtime/container-base/test_network/test_network.py @@ -0,0 +1,22 @@ +# SPDX-License-Identifier: MIT +"""Verify outbound networking from the container (Dockerfile adds python3).""" + +from __future__ import annotations + +import pytest + +_NET = "/opt/azl-tests/netcheck.py" + + +@pytest.mark.dockerfile() +def test_online_service_weather(container_exec_shell) -> None: + """Test Online Services: a single outbound HTTPS fetch must return a non-empty page.""" + result = container_exec_shell(f"python3 {_NET} weather") + assert result.exit_code == 0, f"Outbound HTTPS fetch failed: {result.output}" + + +@pytest.mark.dockerfile() +def test_sustained_https_fetch(container_exec_shell) -> None: + """Test Core Networking: 50 sequential outbound HTTPS fetches must all return valid repo config.""" + result = container_exec_shell(f"python3 {_NET} sustained --strict") + assert result.exit_code == 0, f"Sustained HTTPS fetch failed: {result.output}"