Files

416 lines
15 KiB
Python
Raw Permalink Normal View History

"""Deep tests for rdagent.utils: fmt.py shrink_text and other utility modules."""
from __future__ import annotations
import pickle
import sys
from pathlib import Path
from typing import Any
from unittest.mock import MagicMock, patch
import pytest
PROJECT_ROOT = Path(__file__).parent.parent.parent
sys.path.insert(0, str(PROJECT_ROOT))
# =============================================================================
# Import safety
# =============================================================================
UTIL_MODULES = [
"rdagent.utils",
"rdagent.utils.fmt",
"rdagent.utils.qlib",
"rdagent.utils.env",
"rdagent.utils.workflow",
"rdagent.utils.agent.tpl",
]
class TestUtilsImports:
@pytest.mark.parametrize("module_name", UTIL_MODULES)
def test_module_importable(self, module_name: str) -> None:
import importlib
mod = importlib.import_module(module_name)
assert mod is not None
# =============================================================================
# shrink_text
# =============================================================================
class TestShrinkText:
def test_short_text_unchanged(self) -> None:
from rdagent.utils.fmt import shrink_text
result = shrink_text("hello world", context_lines=10, line_len=100)
assert result == "hello world"
def test_single_line_shorter_than_limit(self) -> None:
from rdagent.utils.fmt import shrink_text
result = shrink_text("abc", context_lines=2, line_len=5)
assert result == "abc"
def test_multi_line_under_threshold_unchanged(self) -> None:
from rdagent.utils.fmt import shrink_text
text = "line1\nline2\nline3"
result = shrink_text(text, context_lines=5, line_len=50)
assert result == text
def test_exactly_at_threshold(self) -> None:
from rdagent.utils.fmt import shrink_text
text = "\n".join([f"line{i}" for i in range(4)])
result = shrink_text(text, context_lines=4, line_len=50)
assert result == text
def test_more_lines_than_context_shrinks(self) -> None:
from rdagent.utils.fmt import shrink_text
text = "\n".join([f"line{i}" for i in range(100)])
result = shrink_text(text, context_lines=10, line_len=100)
assert "lines are hidden" in result
def test_row_shrink_false_preserves_all_lines(self) -> None:
from rdagent.utils.fmt import shrink_text
text = "\n".join([f"line{i}" for i in range(100)])
result = shrink_text(text, context_lines=5, line_len=100, row_shrink=False)
assert result == text
def test_col_shrink_long_lines(self) -> None:
from rdagent.utils.fmt import shrink_text
long_line = "x" * 100
result = shrink_text(long_line, context_lines=5, line_len=20)
assert "chars are hidden" in result
assert len(result) < 100
def test_col_shrink_false_preserves_long_lines(self) -> None:
from rdagent.utils.fmt import shrink_text
long_line = "x" * 100
result = shrink_text(long_line, context_lines=5, line_len=20, col_shrink=False)
assert result == long_line
def test_both_shrink_disabled(self) -> None:
from rdagent.utils.fmt import shrink_text
text = "x" * 1000 + "\n" + "y" * 1000
result = shrink_text(text, context_lines=1, line_len=5, row_shrink=False, col_shrink=False)
assert result == text
def test_first_and_last_lines_preserved(self) -> None:
from rdagent.utils.fmt import shrink_text
text = "\n".join([f"unique_line_{i}" for i in range(100)])
result = shrink_text(text, context_lines=6, line_len=100)
assert "unique_line_0" in result
assert "unique_line_99" in result
assert "unique_line_50" not in result
def test_hidden_lines_count_correct(self) -> None:
from rdagent.utils.fmt import shrink_text
total = 100
ctx = 10
text = "\n".join([f"L{i}" for i in range(total)])
result = shrink_text(text, context_lines=ctx, line_len=100)
half = ctx // 2
hidden = total - half * 2
assert f"({hidden} lines are hidden)" in result
def test_empty_string(self) -> None:
from rdagent.utils.fmt import shrink_text
result = shrink_text("", context_lines=5, line_len=10)
assert result == ""
def test_single_line_with_newline_at_end(self) -> None:
from rdagent.utils.fmt import shrink_text
result = shrink_text("hello\n", context_lines=10, line_len=50)
assert "hello" in result
def test_all_empty_lines(self) -> None:
from rdagent.utils.fmt import shrink_text
text = "\n".join(["" for _ in range(100)])
result = shrink_text(text, context_lines=10, line_len=50)
assert isinstance(result, str)
def test_very_large_context_lines(self) -> None:
from rdagent.utils.fmt import shrink_text
text = "\n".join(["a" for _ in range(50)])
result = shrink_text(text, context_lines=1000, line_len=10)
assert result == text
def test_context_lines_of_one(self) -> None:
from rdagent.utils.fmt import shrink_text
text = "line1\nline2\nline3\nline4\nline5"
result = shrink_text(text, context_lines=1, line_len=100)
assert "lines are hidden" in result
def test_line_len_of_one(self) -> None:
from rdagent.utils.fmt import shrink_text
result = shrink_text("abcdefgh", context_lines=10, line_len=1)
assert "chars are hidden" in result
def test_line_len_zero(self) -> None:
from rdagent.utils.fmt import shrink_text
result = shrink_text("hello", context_lines=10, line_len=0)
assert "chars are hidden" in result
def test_returns_string_always(self) -> None:
from rdagent.utils.fmt import shrink_text
for text in ["", "a", "a\nb\nc", "x" * 1000]:
result = shrink_text(text)
assert isinstance(result, str)
def test_hidden_prefix_format(self) -> None:
from rdagent.utils.fmt import shrink_text
text = "\n".join(["L" for _ in range(100)])
result = shrink_text(text, context_lines=10, line_len=100)
assert "lines are hidden" in result
assert "..." in result
@pytest.mark.parametrize("total_lines,ctx", [
(10, 5), (10, 6), (10, 10), (50, 4), (50, 20), (100, 2),
])
def test_various_combinations(self, total_lines: int, ctx: int) -> None:
from rdagent.utils.fmt import shrink_text
text = "\n".join([f"L{i}" for i in range(total_lines)])
result = shrink_text(text, context_lines=ctx, line_len=100)
assert isinstance(result, str)
assert len(result) > 0
@pytest.mark.parametrize("line_len,chars_per_line", [
(5, 3), (10, 9), (20, 19), (50, 51),
])
def test_line_len_vs_chars(self, line_len: int, chars_per_line: int) -> None:
from rdagent.utils.fmt import shrink_text
text = "x" * chars_per_line
result = shrink_text(text, context_lines=5, line_len=line_len)
if chars_per_line > line_len:
assert "chars are hidden" in result
else:
assert result == text
# =============================================================================
# shrink_text — properties
# =============================================================================
class TestShrinkTextProperties:
def test_output_contains_original_when_small(self) -> None:
from rdagent.utils.fmt import shrink_text
lines = ["a", "b", "c", "d", "e"]
text = "\n".join(lines)
result = shrink_text(text, context_lines=len(lines) + 1, line_len=10000)
assert result == text
def test_shrinking_to_less_lines(self) -> None:
from rdagent.utils.fmt import shrink_text
original = "\n".join([f"line_{i}" for i in range(1000)])
result = shrink_text(original, context_lines=10, line_len=100)
result_lines = result.split("\n")
assert len(result_lines) < 1000
@pytest.mark.parametrize("n_lines", [1, 2, 3, 5, 10])
def test_various_line_counts(self, n_lines: int) -> None:
from rdagent.utils.fmt import shrink_text
text = "\n".join([f"L{i}" for i in range(n_lines)])
result = shrink_text(text, context_lines=50, line_len=200)
assert isinstance(result, str)
assert result == text # all fit within context_lines=50
class TestShrinkTextCombinatorial:
@pytest.mark.parametrize("ctx", [0, 1, 2, 5, 10, 50, 100])
@pytest.mark.parametrize("llen", [0, 1, 5, 10, 50, 200])
def test_parameter_grid(self, ctx: int, llen: int) -> None:
from rdagent.utils.fmt import shrink_text
text = "x" * 60 + "\n" + "y" * 60
result = shrink_text(text, context_lines=ctx, line_len=llen)
assert isinstance(result, str)
@pytest.mark.parametrize("row_shrink", [True, False])
@pytest.mark.parametrize("col_shrink", [True, False])
def test_all_shrink_flag_combinations(self, row_shrink: bool, col_shrink: bool) -> None:
from rdagent.utils.fmt import shrink_text
text = "\n".join(["line"] * 200)
result = shrink_text(text, context_lines=5, line_len=50,
row_shrink=row_shrink, col_shrink=col_shrink)
assert isinstance(result, str)
# =============================================================================
# T (template) system
# =============================================================================
class TestTemplateSystem:
def test_t_class_is_importable(self) -> None:
from rdagent.utils.agent.tpl import T
assert T is not None
def test_t_loads_prompt_template(self) -> None:
from rdagent.utils.agent.tpl import T
tpl = T("scenarios.qlib.prompts:hypothesis_and_feedback")
assert tpl is not None
def test_t_with_invalid_template_raises(self) -> None:
from rdagent.utils.agent.tpl import T
with pytest.raises(FileNotFoundError):
T("nonexistent.module.path:nonexistent_key")
@patch("rdagent.utils.agent.tpl.logger")
def test_t_r_method_renders_template(self, mock_logger: MagicMock) -> None:
from rdagent.utils.agent.tpl import T
tpl = T("scenarios.qlib.prompts:hypothesis_and_feedback")
mock_trace = MagicMock()
mock_trace.hist = []
result = tpl.r(trace=mock_trace)
assert isinstance(result, str)
assert len(result) > 0
# =============================================================================
# Qlib utilities
# =============================================================================
class TestQlibUtils:
def test_validate_qlib_features_importable(self) -> None:
from rdagent.utils.qlib import validate_qlib_features
assert callable(validate_qlib_features)
def test_validate_valid_features(self) -> None:
from rdagent.utils.qlib import validate_qlib_features
assert validate_qlib_features(["$close", "$high / $low", "$volume"]) is True
def test_validate_empty_list(self) -> None:
from rdagent.utils.qlib import validate_qlib_features
result = validate_qlib_features([])
assert isinstance(result, bool)
def test_validate_any_expression(self) -> None:
from rdagent.utils.qlib import validate_qlib_features
result = validate_qlib_features(["not_a_real_field_xyz"])
assert isinstance(result, bool)
def test_alpha20_importable(self) -> None:
from rdagent.utils.qlib import ALPHA20
assert isinstance(ALPHA20, dict)
assert len(ALPHA20) > 0
@pytest.mark.parametrize("feature_exp", [
"$close", "$open", "$high", "$low", "$volume", "$vwap",
"$close / $open", "($high - $low) / $open",
])
def test_individual_feature_validation(self, feature_exp: str) -> None:
from rdagent.utils.qlib import validate_qlib_features
result = validate_qlib_features([feature_exp])
assert isinstance(result, bool)
# =============================================================================
# Env utilities
# =============================================================================
class TestEnvUtils:
def test_env_module_is_importable(self) -> None:
from rdagent.utils import env
assert env is not None
# =============================================================================
# md5_hash
# =============================================================================
class TestMd5Hash:
def test_md5_hash_is_function(self) -> None:
from rdagent.utils import md5_hash
assert callable(md5_hash)
def test_md5_hash_returns_string(self) -> None:
from rdagent.utils import md5_hash
result = md5_hash("test input")
assert isinstance(result, str)
assert len(result) == 64
def test_md5_hash_deterministic(self) -> None:
from rdagent.utils import md5_hash
a = md5_hash("hello")
b = md5_hash("hello")
assert a == b
def test_md5_hash_different_inputs(self) -> None:
from rdagent.utils import md5_hash
a = md5_hash("hello")
b = md5_hash("world")
assert a != b
@pytest.mark.parametrize("input_val", [
"", "a", "abc", "multi\nline\nstring",
])
def test_md5_hash_various_inputs(self, input_val: str) -> None:
from rdagent.utils import md5_hash
result = md5_hash(input_val)
assert isinstance(result, str)
assert len(result) == 64
def test_md5_hash_hex_format(self) -> None:
from rdagent.utils import md5_hash
import re
result = md5_hash("test")
assert re.match(r'^[0-9a-f]{64}$', result) is not None
# =============================================================================
# Workflow utils
# =============================================================================
class TestWorkflowUtils:
def test_loop_base_is_importable(self) -> None:
from rdagent.utils.workflow import LoopBase
assert LoopBase is not None
def test_loop_meta_is_type(self) -> None:
from rdagent.utils.workflow import LoopMeta
assert isinstance(LoopMeta, type)
# =============================================================================
# Large input stress tests
# =============================================================================
class TestLargeInputs:
def test_ten_thousand_lines(self) -> None:
from rdagent.utils.fmt import shrink_text
text = "\n".join([f"L{i}" for i in range(10000)])
result = shrink_text(text, context_lines=50, line_len=100)
assert isinstance(result, str)
assert "lines are hidden" in result
def test_very_long_single_line(self) -> None:
from rdagent.utils.fmt import shrink_text
text = "a" * 100000
result = shrink_text(text, context_lines=5, line_len=100)
assert "chars are hidden" in result
# =============================================================================
# Pickle safety
# =============================================================================
class TestFmtPickleSafety:
def test_shrunk_text_pickle_safety(self) -> None:
from rdagent.utils.fmt import shrink_text
result = shrink_text("x" * 500, context_lines=5, line_len=10)
data = pickle.dumps(result)
loaded = pickle.loads(data)
assert loaded == result
def test_alpha20_pickle_safety(self) -> None:
from rdagent.utils.qlib import ALPHA20
data = pickle.dumps(ALPHA20)
loaded = pickle.loads(data)
assert loaded == ALPHA20