blob: aea65c6a33433b63122771e09bc8914c2392cfc5 [file]
# 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
from datetime import datetime, timezone
from unittest.mock import Mock
import pyarrow as pa
import pytest
from otava.config import load_config_from_file
from otava.data_selector import DataSelector
from otava.importer import DataImportError, InfluxDBImporter
from otava.influxdb import InfluxDB, InfluxDBConfig
from otava.main import create_otava_cli_parser
from otava.test_config import (
InfluxDBMetric,
InfluxDBTestConfig,
TestConfigError,
create_test_config,
)
def selector():
result = DataSelector()
result.since_time = datetime(2024, 1, 1, tzinfo=timezone.utc)
result.until_time = datetime(2024, 1, 5, tzinfo=timezone.utc)
return result
def test_influxdb_connection_config_precedence(tmp_path, monkeypatch):
config_file = tmp_path / "otava.yaml"
config_file.write_text(
"influxdb:\n host: yaml-host\n database: yaml-db\n token: yaml-token\n"
)
monkeypatch.setenv("INFLUXDB_HOST", "env-host")
monkeypatch.setenv("INFLUXDB_DATABASE", "env-db")
monkeypatch.setenv("INFLUXDB_TOKEN", "env-token")
config = load_config_from_file(
str(config_file),
arg_overrides=["--influxdb-host", "cli-host", "--influxdb-token", "cli-token"],
)
assert config.influxdb.host == "cli-host"
assert config.influxdb.database == "env-db"
assert config.influxdb.token == "cli-token"
assert os.environ["INFLUXDB_HOST"] == "env-host"
def test_cli_help_includes_influxdb_options():
help_text = create_otava_cli_parser().format_help()
assert "InfluxDB Options:" in help_text
assert "--influxdb-host" in help_text
assert "--influxdb-database" in help_text
assert "--influxdb-token" in help_text
def test_influxdb_test_config_defaults_to_sql_and_parses_metrics():
test = create_test_config(
"latency",
{
"type": "influxdb",
"query": "SELECT * FROM latency",
"attributes": ["branch"],
"metrics": {"p95": {"column": "p95_ms", "direction": -1, "scale": 0.001}},
},
)
assert isinstance(test, InfluxDBTestConfig)
assert test.query_language == "sql"
assert test.metrics["p95"] == InfluxDBMetric("p95", -1, 0.001, "p95_ms")
def test_influxdb_test_config_supports_influxql_and_rejects_unknown_language():
test = create_test_config(
"latency",
{"type": "influxdb", "query": "SELECT * FROM latency", "metrics": ["p95_ms"], "query_language": "influxql"},
)
assert test.query_language == "influxql"
with pytest.raises(TestConfigError):
create_test_config(
"latency",
{"type": "influxdb", "query": "SELECT * FROM latency", "metrics": ["p95_ms"], "query_language": "flux"},
)
def test_influxdb_importer_reads_arrow_table_and_applies_selection():
client = Mock()
client.query.return_value = pa.table(
{
"time": pa.array(
[
datetime(2023, 12, 31),
datetime(2024, 1, 2),
datetime(2024, 1, 3),
datetime(2024, 1, 5),
],
type=pa.timestamp("ns"),
),
"branch": ["main", "main", "main", "main"],
"commit": ["before", "b", "c", "after"],
"p95_ms": [10, 20, 30, 40],
}
)
backend = InfluxDB(InfluxDBConfig("host", "database", "token"))
backend._client = client
test = InfluxDBTestConfig(
"latency",
"SELECT * FROM latency",
metrics=[InfluxDBMetric("p95", -1, 0.001, "p95_ms")],
attributes=["branch", "commit"],
)
chosen = selector()
chosen.metrics = ["p95"]
chosen.last_n_points = 2
series = InfluxDBImporter(backend).fetch_data(test, chosen)
assert series.branch is None
assert series.time == [1704153600.0, 1704240000.0]
assert series.data == {"p95": [20.0, 30.0]}
assert series.attributes == {"branch": ["main", "main"], "commit": ["b", "c"]}
assert client.query.call_args.kwargs == {"query": "SELECT * FROM latency", "language": "sql"}
def test_influxdb_importer_escapes_branch_for_sql():
backend = Mock()
backend.fetch_data.return_value = (
["time", "p95_ms"],
[(datetime(2024, 1, 2, tzinfo=timezone.utc), 4)],
)
test = InfluxDBTestConfig(
"latency",
"SELECT * FROM latency WHERE branch = %{BRANCH}",
metrics=[InfluxDBMetric("p95", 1, 1.0, "p95_ms")],
)
chosen = selector()
chosen.branch = "release'candidate"
series = InfluxDBImporter(backend).fetch_data(test, chosen)
assert series.data["p95"] == [4.0]
assert backend.fetch_data.call_args.args == (
"SELECT * FROM latency WHERE branch = 'release''candidate'",
"sql",
)
def test_influxdb_importer_escapes_branch_for_influxql():
backend = Mock()
backend.fetch_data.return_value = (
["time", "p95_ms"],
[(datetime(2024, 1, 2, tzinfo=timezone.utc), 4)],
)
test = InfluxDBTestConfig(
"latency",
"SELECT * FROM latency WHERE branch = %{BRANCH}",
query_language="influxql",
metrics=[InfluxDBMetric("p95", 1, 1.0, "p95_ms")],
)
chosen = selector()
chosen.branch = "release'candidate\\path\r\nnext"
series = InfluxDBImporter(backend).fetch_data(test, chosen)
assert series.data["p95"] == [4.0]
assert backend.fetch_data.call_args.args == (
"SELECT * FROM latency WHERE branch = 'release\\'candidate\\\\path\\r\\nnext'",
"influxql",
)
def test_influxdb_importer_reports_missing_columns_and_client_errors():
test = InfluxDBTestConfig(
"latency",
"SELECT * FROM latency",
metrics=[InfluxDBMetric("p95", 1, 1.0, "missing")],
)
backend = Mock()
backend.fetch_data.return_value = (["time"], [])
with pytest.raises(DataImportError) as missing_error:
InfluxDBImporter(backend).fetch_data(test, selector())
assert missing_error.value.message == "Column not found 'missing' is not in list"
backend.fetch_data.side_effect = RuntimeError("server unavailable")
with pytest.raises(DataImportError) as client_error:
InfluxDBImporter(backend).fetch_data(
InfluxDBTestConfig(
"latency", "SELECT * FROM latency", metrics=[InfluxDBMetric("p95", 1, 1.0, "p95_ms")]
),
selector(),
)
assert "latency" in client_error.value.message
assert "server unavailable" in client_error.value.message