Skip to content

Commit

Permalink
wtf: Improve test cases
Browse files Browse the repository at this point in the history
  • Loading branch information
amotl committed Jun 4, 2024
1 parent 49d1a3f commit fead705
Show file tree
Hide file tree
Showing 12 changed files with 195 additions and 17 deletions.
21 changes: 14 additions & 7 deletions cratedb_toolkit/sqlalchemy/patch.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,15 @@
from decimal import Decimal
from uuid import UUID

import numpy as np
import sqlalchemy as sa

try:
import numpy as np

has_numpy = True
except ImportError:
has_numpy = False


def patch_inspector():
"""
Expand Down Expand Up @@ -65,10 +71,11 @@ def default(self, o):

# NumPy ndarray and friends.
# https://stackoverflow.com/a/49677241
if isinstance(o, np.integer):
return int(o)
elif isinstance(o, np.floating):
return float(o)
elif isinstance(o, np.ndarray):
return o.tolist()
if has_numpy:
if isinstance(o, np.integer):
return int(o)
elif isinstance(o, np.floating):
return float(o)
elif isinstance(o, np.ndarray):
return o.tolist()
return json.JSONEncoder.default(self, o)
17 changes: 13 additions & 4 deletions cratedb_toolkit/wtf/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -144,16 +144,21 @@ def job_statistics(ctx: click.Context):


@make_command(job_statistics, "collect", "Collect queries from sys.jobs_log.")
@click.option("--once", is_flag=True, default=False, required=False, help="Whether to record only one sample")
@click.pass_context
def job_statistics_collect(ctx: click.Context):
def job_statistics_collect(ctx: click.Context, once: bool):
"""
Run jobs_log collector.
# TODO: Forward `cratedb_sqlalchemy_url` properly.
"""
import cratedb_toolkit.wtf.query_collector

cratedb_toolkit.wtf.query_collector.main()
cratedb_toolkit.wtf.query_collector.init()
if once:
cratedb_toolkit.wtf.query_collector.record_once()
else:
cratedb_toolkit.wtf.query_collector.record_forever()


@make_command(job_statistics, "view", "View job statistics about collected queries.")
Expand All @@ -180,13 +185,17 @@ def job_statistics_view(ctx: click.Context):


@make_command(cli, "record", "Record `info` and `job-info` outcomes.")
@click.option("--once", is_flag=True, default=False, required=False, help="Whether to record only one sample")
@click.pass_context
def record(ctx: click.Context):
def record(ctx: click.Context, once: bool):
cratedb_sqlalchemy_url = ctx.meta["cratedb_sqlalchemy_url"]
scrub = ctx.meta.get("scrub", False)
adapter = DatabaseAdapter(dburi=cratedb_sqlalchemy_url, echo=False)
recorder = InfoRecorder(adapter=adapter, scrub=scrub)
recorder.record_forever()
if once:
recorder.record_once()
else:
recorder.record_forever()


@make_command(cli, "serve", help_serve)
Expand Down
2 changes: 1 addition & 1 deletion cratedb_toolkit/wtf/http.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@
# Distributed under the terms of the AGPLv3 license, see LICENSE.
import logging
import os
import typing as t
from functools import lru_cache

import typing_extensions as t
from fastapi import Depends, FastAPI, HTTPException

from cratedb_toolkit.util import DatabaseAdapter
Expand Down
15 changes: 10 additions & 5 deletions cratedb_toolkit/wtf/query_collector.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ def init_stmts(stmts):


def write_stats_to_db():
logger.info("Writing statistics to database")
logger.info(f"Writing statistics to database table: {stmt_log_table}")
write_query_stmt = (
f"INSERT INTO {stmt_log_table} "
f"(id, stmt, calls, bucket, username, query_type, avg_duration, nodes, last_used) "
Expand Down Expand Up @@ -230,18 +230,23 @@ def scrape_db():
last_scrape = next_scrape


def run():
def record_once():
logger.info("Recording information snapshot")
scrape_db()
write_stats_to_db()


def main():
init()
def record_forever():
while True:
run()
record_once()
logger.info(f"Sleeping for {interval} seconds")
time.sleep(interval)


def main():
init()
record_forever()


if __name__ == "__main__":
main()
1 change: 1 addition & 0 deletions cratedb_toolkit/wtf/recorder.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,4 +55,5 @@ def do_record_forever(self):
self.record_once()
except Exception:
logger.exception("Failed to record information snapshot")
logger.info(f"Sleeping for {self.interval_seconds} seconds")
time.sleep(self.interval_seconds)
2 changes: 2 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,7 @@ service = [
]
test = [
"cratedb-toolkit[testing]",
"httpx<0.28",
"pueblo[dataframe]",
"pytest<9",
"pytest-cov<6",
Expand Down Expand Up @@ -288,6 +289,7 @@ extend-exclude = [
[tool.ruff.lint.per-file-ignores]
"doc/conf.py" = ["A001", "ERA001"]
"tests/*" = ["S101"] # Allow use of `assert`, and `print`.
"tests/wtf/test_http.py" = ["E402"]
"examples/*" = ["T201"] # Allow `print`
"cratedb_toolkit/retention/cli.py" = ["T201"] # Allow `print`
"cratedb_toolkit/sqlalchemy/__init__.py" = ["F401"] # Allow `module´ imported but unused
Expand Down
7 changes: 7 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,13 @@
TESTDRIVE_DATA_SCHEMA = "testdrive-data"
TESTDRIVE_EXT_SCHEMA = "testdrive-ext"
RESET_TABLES = [
# FIXME: Let all subsystems use configured schema instead of hard-coded ones.
'"doc"."clusterinfo"',
'"doc"."jobinfo"',
'"ext"."clusterinfo"',
'"ext"."jobinfo"',
'"stats"."statement_log"',
'"stats"."last_execution"',
f'"{TESTDRIVE_EXT_SCHEMA}"."retention_policy"',
f'"{TESTDRIVE_DATA_SCHEMA}"."raw_metrics"',
f'"{TESTDRIVE_DATA_SCHEMA}"."sensor_readings"',
Expand Down
25 changes: 25 additions & 0 deletions tests/sqlalchemy/test_patch.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import datetime
import json

import pytest
import sqlalchemy as sa

from cratedb_toolkit.sqlalchemy import patch_inspector
from cratedb_toolkit.sqlalchemy.patch import CrateJsonEncoderWithNumPy
from tests.conftest import TESTDRIVE_DATA_SCHEMA


Expand Down Expand Up @@ -40,3 +45,23 @@ def test_inspector_patched(database):

table_names = inspector.get_table_names()
assert "foobar" in table_names


def test_json_encoder_date():
"""
Verify the extended JSON encoder also accepts Python's `date` types.
"""
data = {"date": datetime.date(2024, 6, 4)}
encoded = json.dumps(data, cls=CrateJsonEncoderWithNumPy)
assert encoded == '{"date": 1717459200000}'


def test_json_encoder_numpy():
"""
Verify the extended JSON encoder also accepts NumPy types.
"""
np = pytest.importorskip("numpy")

data = {"scalar-int": np.float32(42.42).astype(int), "scalar-float": np.float32(42.42), "ndarray": np.ndarray([1])}
encoded = json.dumps(data, cls=CrateJsonEncoderWithNumPy)
assert encoded == """{"scalar-int": 42, "scalar-float": 42.41999816894531, "ndarray": [2.08e-322]}"""
Empty file added tests/util/__init__.py
Empty file.
15 changes: 15 additions & 0 deletions tests/util/test_platform.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
from cratedb_toolkit.util.platform import PlatformInfo


def test_platforminfo_application():
pi = PlatformInfo()
outcome = pi.application()
assert "name" in outcome
assert "version" in outcome
assert "platform" in outcome


def test_platforminfo_libraries():
pi = PlatformInfo()
outcome = pi.libraries()
assert isinstance(outcome, dict)
64 changes: 64 additions & 0 deletions tests/wtf/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

from boltons.iterutils import get_path
from click.testing import CliRunner
from yarl import URL

from cratedb_toolkit.wtf.cli import cli

Expand Down Expand Up @@ -88,6 +89,40 @@ def test_wtf_cli_job_info(cratedb):
assert "performance15min" in data_keys


def test_wtf_cli_statistics_collect(cratedb, caplog):
"""
Verify `cratedb-wtf job-statistics collect`.
"""

uri = URL(cratedb.database.dburi)

# Invoke command.
runner = CliRunner(env={"CRATEDB_SQLALCHEMY_URL": cratedb.database.dburi})
result = runner.invoke(
cli,
args="job-statistics collect --once",
env={"HOSTNAME": f"{uri.host}:{uri.port}"},
catch_exceptions=False,
)
assert result.exit_code == 0

# Verify outcome: Log output.
assert "Recording information snapshot" in caplog.messages

# Verify outcome: Database content.
# stats.statement_log, stats.last_execution
results = cratedb.database.run_sql("SHOW TABLES", records=True)
assert {"table_name": "last_execution"} in results
assert {"table_name": "statement_log"} in results

# FIXME: Table is empty. Why?
cratedb.database.run_sql('REFRESH TABLE "stats"."statement_log"')
assert cratedb.database.count_records("stats.statement_log") == 0

cratedb.database.run_sql('REFRESH TABLE "stats"."last_execution"')
assert cratedb.database.count_records("stats.last_execution") == 1


def test_wtf_cli_statistics_view(cratedb):
"""
Verify `cratedb-wtf job-statistics view`.
Expand All @@ -109,3 +144,32 @@ def test_wtf_cli_statistics_view(cratedb):

data_keys = list(info["data"].keys())
assert "stats" in data_keys


def test_wtf_cli_record(cratedb, caplog):
"""
Verify `cratedb-wtf record`.
"""

# Invoke command.
runner = CliRunner(env={"CRATEDB_SQLALCHEMY_URL": cratedb.database.dburi})
result = runner.invoke(
cli,
args="--debug record --once",
catch_exceptions=False,
)
assert result.exit_code == 0

# Verify outcome: Log output.
assert "Recording information snapshot" in caplog.messages

# Verify outcome: Database content.
results = cratedb.database.run_sql("SHOW TABLES", records=True)
assert {"table_name": "clusterinfo"} in results
assert {"table_name": "jobinfo"} in results

cratedb.database.run_sql('REFRESH TABLE "ext"."clusterinfo"')
assert cratedb.database.count_records("ext.clusterinfo") == 1

cratedb.database.run_sql('REFRESH TABLE "ext"."jobinfo"')
assert cratedb.database.count_records("ext.jobinfo") == 1
43 changes: 43 additions & 0 deletions tests/wtf/test_http.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import datetime as dt
import os

import pytest

pytest.importorskip("fastapi")


from fastapi.testclient import TestClient

from cratedb_toolkit import __appname__, __version__
from cratedb_toolkit.wtf.http import app

client = TestClient(app)


def test_http_root():
response = client.get("/")
data = response.json()
assert response.status_code == 200
assert data["application_name"] == __appname__
assert data["application_version"].startswith(__version__)
assert dt.datetime.fromisoformat(data["system_time"]).year == dt.datetime.now().year


def test_http_info(cratedb, mocker):
mocker.patch.dict(os.environ, {"CRATEDB_SQLALCHEMY_URL": cratedb.database.dburi})

response = client.get("/info/all")
info = response.json()
assert response.status_code == 200

assert info["meta"]["application_name"] == __appname__
assert info["meta"]["application_version"].startswith(__version__)
assert dt.datetime.fromisoformat(info["meta"]["system_time"]).year == dt.datetime.now().year
assert "elements" in info["meta"]
assert len(info["meta"]["elements"]) > 15

assert "data" in info
assert info["data"]["database"]["cluster_nodes_count"] == 1
assert info["data"]["system"]["application"]["name"] == __appname__

assert "eco" in info["data"]["system"]

0 comments on commit fead705

Please sign in to comment.