blob: e14de8d447740f9a2e9cb63d57327974b6ce142e [file] [log] [blame]
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
import os
import shutil
import socket
import subprocess
import textwrap
import time
from contextlib import contextmanager
from pathlib import Path
import pytest
def test_analyze():
"""
End-to-end test for the PostgreSQL example.
Starts the docker-compose stack from examples/postgresql/docker-compose.yaml,
waits for Postgres to be ready, runs the otava analysis in a one-off
container, and compares stdout to the expected output (seeded data uses
deterministic 2025 timestamps).
"""
with postgres_container() as (postgres_container_id, host_port):
# Run the Otava analysis
proc = subprocess.run(
["uv", "run", "otava", "analyze", "aggregate_mem"],
capture_output=True,
text=True,
timeout=600,
env=dict(
os.environ,
OTAVA_CONFIG=Path("examples/postgresql/config/otava.yaml"),
POSTGRES_HOSTNAME="localhost",
POSTGRES_PORT=host_port,
POSTGRES_USERNAME="exampleuser",
POSTGRES_PASSWORD="examplepassword",
POSTGRES_DATABASE="benchmark_results",
BRANCH="trunk",
),
)
if proc.returncode != 0:
pytest.fail(
"Command returned non-zero exit code.\n\n"
f"Command: {proc.args!r}\n"
f"Exit code: {proc.returncode}\n\n"
f"Stdout:\n{proc.stdout}\n\n"
f"Stderr:\n{proc.stderr}\n"
)
expected_output = textwrap.dedent(
"""\
time experiment_id commit config_id process_cumulative_rate_mean process_cumulative_rate_stderr process_cumulative_rate_diff
------------------------- ------------------ -------- ----------- ------------------------------ -------------------------------- ------------------------------
2025-03-13 10:03:02 +0000 aggregate-36e5ccd2 36e5ccd2 1 61160 2052 13558
2025-03-25 10:03:02 +0000 aggregate-d5460f38 d5460f38 1 60160 2142 13454
2025-04-02 10:03:02 +0000 aggregate-bc9425cb bc9425cb 1 60960 2052 13053
······························
-5.6%
······························
2025-04-06 10:03:02 +0000 aggregate-14df1b11 14df1b11 1 57123 2052 14052
2025-04-13 10:03:02 +0000 aggregate-ac40c0d8 ac40c0d8 1 57980 2052 13521
2025-04-27 10:03:02 +0000 aggregate-0af4ccbc 0af4ccbc 1 56950 2052 13532
"""
)
assert _remove_trailing_whitespaces(proc.stdout) == expected_output.rstrip("\n")
# Verify the DB was updated with the detected change.
# Query the updated change metric at the detected change point.
query_proc = subprocess.run(
[
"docker",
"exec",
postgres_container_id,
"psql",
"-U",
"exampleuser",
"-d",
"benchmark_results",
"-Atc",
"""
SELECT
process_cumulative_rate_mean_rel_forward_change,
process_cumulative_rate_mean_rel_backward_change,
process_cumulative_rate_mean_p_value
FROM results
WHERE experiment_id='aggregate-14df1b11' AND config_id=1;
""",
],
capture_output=True,
text=True,
timeout=60,
)
if query_proc.returncode != 0:
pytest.fail(
"Command returned non-zero exit code.\n\n"
f"Command: {query_proc.args!r}\n"
f"Exit code: {query_proc.returncode}\n\n"
f"Stdout:\n{query_proc.stdout}\n\n"
f"Stderr:\n{query_proc.stderr}\n"
)
# psql -Atc returns rows like: value|pvalue
forward_change, backward_change, p_value = query_proc.stdout.strip().split("|")
# --update-postgres was not specified, so no change point should be recorded
assert forward_change == backward_change == p_value == ""
def test_analyze_and_update_postgres():
"""
End-to-end test for the PostgreSQL example.
Starts the docker-compose stack from examples/postgresql/docker-compose.yaml,
waits for Postgres to be ready, runs the otava analysis in a one-off
container, and compares stdout to the expected output (seeded data uses
deterministic 2025 timestamps).
"""
with postgres_container() as (postgres_container_id, host_port):
# Run the Otava analysis
proc = subprocess.run(
["uv", "run", "otava", "analyze", "aggregate_mem", "--update-postgres"],
capture_output=True,
text=True,
timeout=600,
env=dict(
os.environ,
OTAVA_CONFIG=Path("examples/postgresql/config/otava.yaml"),
POSTGRES_HOSTNAME="localhost",
POSTGRES_PORT=host_port,
POSTGRES_USERNAME="exampleuser",
POSTGRES_PASSWORD="examplepassword",
POSTGRES_DATABASE="benchmark_results",
BRANCH="trunk",
),
)
if proc.returncode != 0:
pytest.fail(
"Command returned non-zero exit code.\n\n"
f"Command: {proc.args!r}\n"
f"Exit code: {proc.returncode}\n\n"
f"Stdout:\n{proc.stdout}\n\n"
f"Stderr:\n{proc.stderr}\n"
)
expected_output = textwrap.dedent(
"""\
time experiment_id commit config_id process_cumulative_rate_mean process_cumulative_rate_stderr process_cumulative_rate_diff
------------------------- ------------------ -------- ----------- ------------------------------ -------------------------------- ------------------------------
2025-03-13 10:03:02 +0000 aggregate-36e5ccd2 36e5ccd2 1 61160 2052 13558
2025-03-25 10:03:02 +0000 aggregate-d5460f38 d5460f38 1 60160 2142 13454
2025-04-02 10:03:02 +0000 aggregate-bc9425cb bc9425cb 1 60960 2052 13053
······························
-5.6%
······························
2025-04-06 10:03:02 +0000 aggregate-14df1b11 14df1b11 1 57123 2052 14052
2025-04-13 10:03:02 +0000 aggregate-ac40c0d8 ac40c0d8 1 57980 2052 13521
2025-04-27 10:03:02 +0000 aggregate-0af4ccbc 0af4ccbc 1 56950 2052 13532
"""
)
assert _remove_trailing_whitespaces(proc.stdout) == expected_output.rstrip("\n")
# Verify the DB was updated with the detected change.
# Query the updated change metric at the detected change point.
query_proc = subprocess.run(
[
"docker",
"exec",
postgres_container_id,
"psql",
"-U",
"exampleuser",
"-d",
"benchmark_results",
"-Atc",
"""
SELECT
process_cumulative_rate_mean_rel_forward_change,
process_cumulative_rate_mean_rel_backward_change,
process_cumulative_rate_mean_p_value
FROM results
WHERE experiment_id='aggregate-14df1b11' AND config_id=1;
""",
],
capture_output=True,
text=True,
timeout=60,
)
if query_proc.returncode != 0:
pytest.fail(
"Command returned non-zero exit code.\n\n"
f"Command: {query_proc.args!r}\n"
f"Exit code: {query_proc.returncode}\n\n"
f"Stdout:\n{query_proc.stdout}\n\n"
f"Stderr:\n{query_proc.stderr}\n"
)
# psql -Atc returns rows like: value|pvalue
forward_change, backward_change, p_value = query_proc.stdout.strip().split("|")
forward_change = float(forward_change)
backward_change = float(backward_change)
p_value = float(p_value)
if abs(forward_change - (-5.6)) > 0.2:
pytest.fail(f"DB change value {forward_change!r} not within tolerance of -5.6")
if abs(backward_change - 5.94) > 0.2:
pytest.fail(f"DB backward change {backward_change!r} not within tolerance of 5.94")
if p_value >= 0.001:
pytest.fail(f"DB p-value {p_value!r} not less than 0.01")
def test_regressions():
"""
End-to-end test for the PostgreSQL regressions command.
Starts the docker-compose stack from examples/postgresql/docker-compose.yaml,
waits for Postgres to be ready, runs the otava regressions command,
and compares stdout to the expected output.
"""
with postgres_container() as (postgres_container_id, host_port):
# Run the Otava regressions command
proc = subprocess.run(
["uv", "run", "otava", "regressions", "aggregate_mem"],
capture_output=True,
text=True,
timeout=600,
env=dict(
os.environ,
OTAVA_CONFIG=Path("examples/postgresql/config/otava.yaml"),
POSTGRES_HOSTNAME="localhost",
POSTGRES_PORT=host_port,
POSTGRES_USERNAME="exampleuser",
POSTGRES_PASSWORD="examplepassword",
POSTGRES_DATABASE="benchmark_results",
BRANCH="trunk",
),
)
if proc.returncode != 0:
pytest.fail(
"Command returned non-zero exit code.\n\n"
f"Command: {proc.args!r}\n"
f"Exit code: {proc.returncode}\n\n"
f"Stdout:\n{proc.stdout}\n\n"
f"Stderr:\n{proc.stderr}\n"
)
expected_output = textwrap.dedent(
"""\
aggregate_mem:
process_cumulative_rate_mean: 6.08e+04 --> 5.74e+04 ( -5.6%)
Regressions in 1 test found
"""
)
assert proc.stdout == expected_output
# Verify the DB was NOT updated since --update-postgres was not specified
query_proc = subprocess.run(
[
"docker",
"exec",
postgres_container_id,
"psql",
"-U",
"exampleuser",
"-d",
"benchmark_results",
"-Atc",
"""
SELECT
process_cumulative_rate_mean_rel_forward_change,
process_cumulative_rate_mean_rel_backward_change,
process_cumulative_rate_mean_p_value
FROM results
WHERE experiment_id='aggregate-14df1b11' AND config_id=1;
""",
],
capture_output=True,
text=True,
timeout=60,
)
if query_proc.returncode != 0:
pytest.fail(
"Command returned non-zero exit code.\n\n"
f"Command: {query_proc.args!r}\n"
f"Exit code: {query_proc.returncode}\n\n"
f"Stdout:\n{query_proc.stdout}\n\n"
f"Stderr:\n{query_proc.stderr}\n"
)
# psql -Atc returns rows like: value|pvalue
forward_change, backward_change, p_value = query_proc.stdout.strip().split("|")
# --update-postgres was not specified, so no change point should be recorded
assert forward_change == backward_change == p_value == ""
@contextmanager
def postgres_container():
"""
Context manager for running a PostgreSQL container.
Yields the container ID and ensures cleanup on exit.
"""
if not shutil.which("docker"):
pytest.fail("docker is not available on PATH")
container_id = None
try:
# Start postgres container
cmd = [
"docker",
"run",
"-d",
"--env",
"POSTGRES_USER=exampleuser",
"--env",
"POSTGRES_PASSWORD=examplepassword",
"--env",
"POSTGRES_DB=benchmark_results",
"--volume",
f"{Path('examples/postgresql/init-db').resolve()}:/docker-entrypoint-initdb.d",
"--publish",
"5432",
"postgres:latest",
]
proc = subprocess.run(cmd, capture_output=True, text=True, timeout=60)
if proc.returncode != 0:
pytest.fail(
"Docker command returned non-zero exit code.\n\n"
f"Command: {cmd!r}\n"
f"Exit code: {proc.returncode}\n\n"
f"Stdout:\n{proc.stdout}\n\n"
f"Stderr:\n{proc.stderr}\n"
)
container_id = proc.stdout.strip()
# Determine the randomly assigned host port for 5432/tcp
inspect_cmd = [
"docker",
"inspect",
"-f",
'{{ (index (index .NetworkSettings.Ports "5432/tcp") 0).HostPort }}',
container_id,
]
inspect_proc = subprocess.run(inspect_cmd, capture_output=True, text=True, timeout=60)
if inspect_proc.returncode != 0:
pytest.fail(
"Docker inspect returned non-zero exit code.\n\n"
f"Command: {inspect_cmd!r}\n"
f"Exit code: {inspect_proc.returncode}\n\n"
f"Stdout:\n{inspect_proc.stdout}\n\n"
f"Stderr:\n{inspect_proc.stderr}\n"
)
host_port = inspect_proc.stdout.strip()
# Wait until Postgres responds
deadline = time.time() + 60
ready = False
while time.time() < deadline:
# First ensure the assigned host port accepts TCP connections
try:
with socket.create_connection(("localhost", int(host_port)), timeout=1):
port_ready = True
except OSError:
port_ready = False
continue
# Then check pg_isready inside the container
cmd = [
"docker",
"exec",
container_id,
"pg_isready",
"-U",
"exampleuser",
"-d",
"benchmark_results",
]
proc = subprocess.run(cmd, capture_output=True, text=True)
if port_ready and proc.returncode == 0:
ready = True
break
time.sleep(1)
if not ready:
pytest.fail("Postgres did not become ready within timeout.")
yield container_id, host_port
finally:
if container_id:
res = subprocess.run(
["docker", "stop", container_id], capture_output=True, text=True, timeout=60
)
if res.returncode != 0:
pytest.fail(
f"Docker command returned non-zero exit code: {res.returncode}\nStdout: {res.stdout}\nStderr: {res.stderr}"
)
def _remove_trailing_whitespaces(s: str) -> str:
return "\n".join(line.rstrip() for line in s.splitlines())