update
This commit is contained in:
@@ -1,4 +1,7 @@
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
@@ -9,15 +12,129 @@ import pandas as pd
|
||||
from cbpred.latest_predict import (
|
||||
OUTPUT_COLUMNS,
|
||||
REPO_ROOT,
|
||||
_acquire_update_lock,
|
||||
_database_endpoint,
|
||||
build_parser,
|
||||
build_predictions,
|
||||
default_artifact,
|
||||
default_banbot_config,
|
||||
default_banbot_dir,
|
||||
default_data_dir,
|
||||
default_output_dir,
|
||||
ensure_database,
|
||||
output_columns_for_timeframe,
|
||||
)
|
||||
|
||||
|
||||
class LatestPredictTests(unittest.TestCase):
|
||||
def test_database_url_without_port_uses_postgresql_default(self):
|
||||
self.assertEqual(_database_endpoint("postgresql://localhost/qdb"), ("localhost", 5432))
|
||||
|
||||
def test_database_endpoint_supports_libpq_connection_formats(self):
|
||||
for dsn in (
|
||||
"host=timescale.example port=5433 dbname=qdb",
|
||||
"postgresql:///qdb?host=timescale.example&port=5433",
|
||||
"host=timescale.example,backup.example port=5433,5434 dbname=qdb",
|
||||
):
|
||||
self.assertEqual(_database_endpoint(dsn), ("timescale.example", 5433))
|
||||
|
||||
def test_banbot_defaults_support_checkout_at_drive_root(self):
|
||||
with patch.dict(os.environ, {}, clear=True), patch(
|
||||
"cbpred.latest_predict.REPO_ROOT", Path(Path.cwd().anchor) / "cbpred"
|
||||
):
|
||||
self.assertIsInstance(default_banbot_dir(), Path)
|
||||
|
||||
def test_default_config_prefers_timescale_and_respects_explicit_environment(self):
|
||||
with tempfile.TemporaryDirectory() as directory, patch.dict(os.environ, {}, clear=True):
|
||||
root = Path(directory)
|
||||
banbot_dir = root / "banbot"
|
||||
(banbot_dir / "biz").mkdir(parents=True)
|
||||
(root / "data").mkdir()
|
||||
legacy = banbot_dir / "biz/config.yml"
|
||||
legacy.write_text("database: {}\n")
|
||||
timescale = root / "data/config.timescale.yml"
|
||||
timescale.write_text("database: {db_type: timescale}\n")
|
||||
with patch("cbpred.latest_predict.REPO_ROOT", root / "cbpred"):
|
||||
self.assertEqual(default_banbot_config(banbot_dir), timescale)
|
||||
with patch.dict(os.environ, {"CBPRED_BANBOT_CONFIG": str(legacy)}):
|
||||
self.assertEqual(default_banbot_config(banbot_dir), legacy)
|
||||
|
||||
def test_parser_reads_database_environment(self):
|
||||
with patch.dict(os.environ, {"CBPRED_DSN": "postgresql://localhost/qdb", "CBPRED_DB_TYPE": "timescale"}):
|
||||
args = build_parser().parse_args([])
|
||||
self.assertEqual(args.dsn, "postgresql://localhost/qdb")
|
||||
self.assertEqual(args.db_type, "timescale")
|
||||
|
||||
def test_unavailable_timescale_never_bootstraps_questdb(self):
|
||||
with patch("cbpred.latest_predict.socket.create_connection", side_effect=OSError), patch(
|
||||
"cbpred.latest_predict.subprocess.run"
|
||||
) as bootstrap:
|
||||
ensure_database(Path("unused.yml"), "postgresql://localhost:8812/qdb", "timescale")
|
||||
bootstrap.assert_not_called()
|
||||
|
||||
def test_configured_timescale_never_bootstraps_questdb_in_auto_mode(self):
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
config = Path(directory) / "config.yml"
|
||||
config.write_text("database:\n db_type: timescale\n url: postgresql://localhost:8812/qdb\n")
|
||||
with patch("cbpred.latest_predict.socket.create_connection", side_effect=OSError), patch(
|
||||
"cbpred.latest_predict.subprocess.run"
|
||||
) as bootstrap:
|
||||
ensure_database(config, None)
|
||||
bootstrap.assert_not_called()
|
||||
|
||||
def test_postgresql_backend_aliases_never_bootstrap_questdb(self):
|
||||
with patch("cbpred.latest_predict.socket.create_connection", side_effect=OSError), patch(
|
||||
"cbpred.latest_predict.subprocess.run"
|
||||
) as bootstrap:
|
||||
for backend in ("timescaledb", "postgres", "postgresql", "postgresql+timescale"):
|
||||
ensure_database(Path("unused.yml"), "postgresql://localhost:8812/qdb", backend)
|
||||
bootstrap.assert_not_called()
|
||||
|
||||
def test_multihost_dsn_is_validated_by_libpq_without_bootstrap(self):
|
||||
with patch("cbpred.latest_predict.socket.create_connection") as probe, patch(
|
||||
"cbpred.latest_predict.subprocess.run"
|
||||
) as bootstrap:
|
||||
for backend in ("auto", "timescale"):
|
||||
ensure_database(
|
||||
Path("unused.yml"),
|
||||
"host=primary,secondary port=8812,5433 dbname=qdb",
|
||||
backend,
|
||||
)
|
||||
probe.assert_not_called()
|
||||
bootstrap.assert_not_called()
|
||||
|
||||
def test_update_lock_serializes_processes_and_releases_on_close(self):
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
lock_path = Path(directory) / "update.lock"
|
||||
script = (
|
||||
"import sys\n"
|
||||
"from cbpred.latest_predict import _acquire_update_lock\n"
|
||||
"with open(sys.argv[1], 'a+') as lock:\n"
|
||||
" print('ready', flush=True)\n"
|
||||
" _acquire_update_lock(lock)\n"
|
||||
" print('acquired', flush=True)\n"
|
||||
)
|
||||
with lock_path.open("a+") as lock:
|
||||
_acquire_update_lock(lock)
|
||||
process = subprocess.Popen(
|
||||
[sys.executable, "-c", script, str(lock_path)],
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
text=True,
|
||||
)
|
||||
try:
|
||||
self.assertEqual(process.stdout.readline().strip(), "ready")
|
||||
with self.assertRaises(subprocess.TimeoutExpired):
|
||||
process.communicate(timeout=0.3)
|
||||
lock.close()
|
||||
stdout, stderr = process.communicate(timeout=10)
|
||||
self.assertEqual(process.returncode, 0, stderr)
|
||||
self.assertEqual(stdout.strip(), "acquired")
|
||||
finally:
|
||||
if process.poll() is None:
|
||||
process.kill()
|
||||
process.communicate()
|
||||
|
||||
def test_output_columns_are_stable(self):
|
||||
self.assertEqual(
|
||||
OUTPUT_COLUMNS,
|
||||
|
||||
Reference in New Issue
Block a user