mirror of
https://github.com/NicolasBohn/NexQuant.git
synced 2026-07-28 16:07:46 +00:00
416 lines
15 KiB
Python
416 lines
15 KiB
Python
|
|
"""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
|