Includes core prod + GREEN/BLUE subsystems: - prod/ (BLUE harness, configs, scripts, docs) - nautilus_dolphin/ (GREEN Nautilus-native impl + dvae/ preserved) - adaptive_exit/ (AEM engine + models/bucket_assignments.pkl) - Observability/ (EsoF advisor, TUI, dashboards) - external_factors/ (EsoF producer) - mc_forewarning_qlabs_fork/ (MC regime/envelope) Excludes runtime caches, logs, backups, and reproducible artifacts per .gitignore.
289 lines
9.8 KiB
Python
Executable File
289 lines
9.8 KiB
Python
Executable File
"""
|
|
test_dolphin_tui_log_tail.py
|
|
|
|
Verifies the tail_log() method in DolphinDataFetcher:
|
|
- Uses seek(-N, 2) to read only the last N bytes (not the full file)
|
|
- Returns the correct last N lines from a large file
|
|
- Does not load the entire file into memory
|
|
|
|
Tests:
|
|
- test_tail_log_returns_last_n_lines
|
|
- test_tail_log_large_file_seek_not_full_read
|
|
- test_tail_log_large_file_correctness
|
|
- test_tail_log_file_not_found
|
|
- test_tail_log_small_file
|
|
- test_tail_log_empty_file
|
|
- test_tail_log_n_param
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
import sys
|
|
import tempfile
|
|
import unittest
|
|
import types
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Make sure the TUI module is importable from this directory
|
|
# ---------------------------------------------------------------------------
|
|
sys.path.insert(0, os.path.dirname(__file__))
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Stub hazelcast so the import succeeds without the package
|
|
# ---------------------------------------------------------------------------
|
|
_hz_stub = types.ModuleType("hazelcast")
|
|
_hz_stub.HazelcastClient = MagicMock()
|
|
sys.modules.setdefault("hazelcast", _hz_stub)
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Stub textual so dolphin_tui imports cleanly without a terminal
|
|
# ---------------------------------------------------------------------------
|
|
for _mod in ["textual", "textual.app", "textual.containers", "textual.widgets"]:
|
|
if _mod not in sys.modules:
|
|
sys.modules[_mod] = types.ModuleType(_mod)
|
|
|
|
_textual_app = sys.modules["textual.app"]
|
|
_textual_app.App = object
|
|
_textual_app.ComposeResult = object
|
|
|
|
_textual_containers = sys.modules["textual.containers"]
|
|
_textual_containers.Horizontal = object
|
|
|
|
_textual_widgets = sys.modules["textual.widgets"]
|
|
_textual_widgets.Static = object
|
|
_textual_widgets.VerticalScroll = object
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Stub httpx
|
|
# ---------------------------------------------------------------------------
|
|
if "httpx" not in sys.modules:
|
|
_httpx_stub = types.ModuleType("httpx")
|
|
_httpx_stub.AsyncClient = MagicMock()
|
|
sys.modules["httpx"] = _httpx_stub
|
|
|
|
from dolphin_tui import DolphinDataFetcher, LOG_TAIL_CHUNK_BYTES # noqa: E402
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Helper
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def _make_fetcher() -> DolphinDataFetcher:
|
|
return DolphinDataFetcher(hz_host="localhost", hz_port=5701)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Tests
|
|
# ---------------------------------------------------------------------------
|
|
|
|
class TestTailLogReturnsLastNLines(unittest.TestCase):
|
|
"""test_tail_log_returns_last_n_lines
|
|
|
|
Create a temp file with 1000 known lines, call tail_log(path, 50),
|
|
verify exactly the last 50 lines are returned.
|
|
"""
|
|
|
|
def setUp(self):
|
|
self.tmp = tempfile.NamedTemporaryFile(mode="w", suffix=".log", delete=False)
|
|
for i in range(1000):
|
|
self.tmp.write(f"Line {i}\n")
|
|
self.tmp.close()
|
|
|
|
def tearDown(self):
|
|
os.unlink(self.tmp.name)
|
|
|
|
def test_tail_log_returns_last_n_lines(self):
|
|
fetcher = _make_fetcher()
|
|
result = fetcher.tail_log(self.tmp.name, n=50)
|
|
|
|
self.assertEqual(len(result), 50, f"Expected 50 lines, got {len(result)}")
|
|
# The last 50 lines should be Line 950 .. Line 999
|
|
for i, line in enumerate(result):
|
|
expected = f"Line {950 + i}"
|
|
self.assertEqual(line, expected, f"Line {i}: expected {expected!r}, got {line!r}")
|
|
|
|
|
|
class TestTailLogLargeFileSeekNotFullRead(unittest.TestCase):
|
|
"""test_tail_log_large_file_seek_not_full_read
|
|
|
|
Verify that tail_log uses seek(-chunk, 2) and does NOT call read()
|
|
with the full file size.
|
|
"""
|
|
|
|
def setUp(self):
|
|
# Write a file that is clearly larger than the chunk size
|
|
self.tmp = tempfile.NamedTemporaryFile(mode="wb", suffix=".log", delete=False)
|
|
line = b"2026-01-01 00:00:00 [INFO] padding " + b"x" * 100 + b"\n"
|
|
# Write enough to be > LOG_TAIL_CHUNK_BYTES
|
|
total = 0
|
|
while total < LOG_TAIL_CHUNK_BYTES * 3:
|
|
self.tmp.write(line)
|
|
total += len(line)
|
|
self.tmp.close()
|
|
self.file_size = os.path.getsize(self.tmp.name)
|
|
|
|
def tearDown(self):
|
|
os.unlink(self.tmp.name)
|
|
|
|
def test_tail_log_large_file_seek_not_full_read(self):
|
|
fetcher = _make_fetcher()
|
|
|
|
read_sizes = []
|
|
original_open = open
|
|
|
|
def spy_open(path, mode="r", **kwargs):
|
|
fh = original_open(path, mode, **kwargs)
|
|
original_read = fh.read
|
|
|
|
def tracking_read(size=-1):
|
|
read_sizes.append(size)
|
|
return original_read(size)
|
|
|
|
fh.read = tracking_read
|
|
return fh
|
|
|
|
with patch("builtins.open", side_effect=spy_open):
|
|
fetcher.tail_log(self.tmp.name, n=50)
|
|
|
|
# read() must never be called with the full file size
|
|
self.assertNotIn(
|
|
self.file_size,
|
|
read_sizes,
|
|
f"read() was called with full file size {self.file_size} — full file was loaded",
|
|
)
|
|
# At least one read() call must have happened
|
|
self.assertTrue(len(read_sizes) > 0, "read() was never called")
|
|
|
|
|
|
class TestTailLogLargeFileCorrectness(unittest.TestCase):
|
|
"""test_tail_log_large_file_correctness
|
|
|
|
Create a temp file >10MB of repeated log lines, call tail_log,
|
|
verify the returned lines match the actual last N lines of the file.
|
|
"""
|
|
|
|
def setUp(self):
|
|
self.tmp = tempfile.NamedTemporaryFile(mode="w", suffix=".log", delete=False, encoding="utf-8")
|
|
line_template = "2026-01-01 00:00:00 [INFO] Line {i} padding " + "x" * 100
|
|
self.lines = []
|
|
total_bytes = 0
|
|
i = 0
|
|
while total_bytes < 10 * 1024 * 1024: # 10 MB
|
|
line = line_template.format(i=i)
|
|
self.tmp.write(line + "\n")
|
|
self.lines.append(line)
|
|
total_bytes += len(line) + 1
|
|
i += 1
|
|
self.tmp.close()
|
|
|
|
def tearDown(self):
|
|
os.unlink(self.tmp.name)
|
|
|
|
def test_tail_log_large_file_correctness(self):
|
|
fetcher = _make_fetcher()
|
|
result = fetcher.tail_log(self.tmp.name, n=50)
|
|
|
|
expected = self.lines[-50:]
|
|
self.assertEqual(len(result), 50, f"Expected 50 lines, got {len(result)}")
|
|
self.assertEqual(result, expected, "Returned lines do not match the actual last 50 lines")
|
|
|
|
|
|
class TestTailLogFileNotFound(unittest.TestCase):
|
|
"""test_tail_log_file_not_found
|
|
|
|
When path doesn't exist, returns ["Log not found: <path>"].
|
|
"""
|
|
|
|
def test_tail_log_file_not_found(self):
|
|
fetcher = _make_fetcher()
|
|
missing = "/tmp/this_file_does_not_exist_dolphin_test_xyz.log"
|
|
result = fetcher.tail_log(missing, n=50)
|
|
|
|
self.assertEqual(len(result), 1)
|
|
self.assertEqual(result[0], f"Log not found: {missing}")
|
|
|
|
|
|
class TestTailLogSmallFile(unittest.TestCase):
|
|
"""test_tail_log_small_file
|
|
|
|
File smaller than the seek chunk still returns correct lines
|
|
(seek hits start of file via OSError fallback).
|
|
"""
|
|
|
|
def setUp(self):
|
|
self.tmp = tempfile.NamedTemporaryFile(mode="w", suffix=".log", delete=False)
|
|
for i in range(20):
|
|
self.tmp.write(f"SmallLine {i}\n")
|
|
self.tmp.close()
|
|
|
|
def tearDown(self):
|
|
os.unlink(self.tmp.name)
|
|
|
|
def test_tail_log_small_file(self):
|
|
fetcher = _make_fetcher()
|
|
result = fetcher.tail_log(self.tmp.name, n=50)
|
|
|
|
# File only has 20 lines — should return all 20
|
|
self.assertEqual(len(result), 20, f"Expected 20 lines, got {len(result)}")
|
|
for i, line in enumerate(result):
|
|
self.assertEqual(line, f"SmallLine {i}")
|
|
|
|
|
|
class TestTailLogEmptyFile(unittest.TestCase):
|
|
"""test_tail_log_empty_file
|
|
|
|
Empty file returns empty list.
|
|
"""
|
|
|
|
def setUp(self):
|
|
self.tmp = tempfile.NamedTemporaryFile(mode="w", suffix=".log", delete=False)
|
|
self.tmp.close()
|
|
|
|
def tearDown(self):
|
|
os.unlink(self.tmp.name)
|
|
|
|
def test_tail_log_empty_file(self):
|
|
fetcher = _make_fetcher()
|
|
result = fetcher.tail_log(self.tmp.name, n=50)
|
|
|
|
self.assertEqual(result, [], f"Expected empty list for empty file, got {result!r}")
|
|
|
|
|
|
class TestTailLogNParam(unittest.TestCase):
|
|
"""test_tail_log_n_param
|
|
|
|
Calling with n=10 returns exactly 10 lines, n=100 returns 100 lines
|
|
(when file has enough).
|
|
"""
|
|
|
|
def setUp(self):
|
|
self.tmp = tempfile.NamedTemporaryFile(mode="w", suffix=".log", delete=False)
|
|
for i in range(500):
|
|
self.tmp.write(f"NParamLine {i}\n")
|
|
self.tmp.close()
|
|
|
|
def tearDown(self):
|
|
os.unlink(self.tmp.name)
|
|
|
|
def test_tail_log_n_10(self):
|
|
fetcher = _make_fetcher()
|
|
result = fetcher.tail_log(self.tmp.name, n=10)
|
|
|
|
self.assertEqual(len(result), 10, f"Expected 10 lines, got {len(result)}")
|
|
for i, line in enumerate(result):
|
|
self.assertEqual(line, f"NParamLine {490 + i}")
|
|
|
|
def test_tail_log_n_100(self):
|
|
fetcher = _make_fetcher()
|
|
result = fetcher.tail_log(self.tmp.name, n=100)
|
|
|
|
self.assertEqual(len(result), 100, f"Expected 100 lines, got {len(result)}")
|
|
for i, line in enumerate(result):
|
|
self.assertEqual(line, f"NParamLine {400 + i}")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|