mirror of
https://github.com/NicolasBohn/NexQuant.git
synced 2026-07-27 23:47:46 +00:00
test: add 39 tests for fx_config, utils, predix_full_eval, exceptions, and log
This commit is contained in:
@@ -0,0 +1,101 @@
|
||||
"""Tests for core/exception and log infrastructure."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
PROJECT_ROOT = Path(__file__).parent.parent.parent
|
||||
sys.path.insert(0, str(PROJECT_ROOT))
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Exception hierarchy
|
||||
# =============================================================================
|
||||
|
||||
|
||||
class TestExceptionHierarchy:
|
||||
def test_workflow_error_is_exception(self):
|
||||
from rdagent.core.exception import WorkflowError
|
||||
with pytest.raises(WorkflowError):
|
||||
raise WorkflowError("test")
|
||||
|
||||
def test_format_error_is_workflow_error(self):
|
||||
from rdagent.core.exception import FormatError, WorkflowError
|
||||
assert issubclass(FormatError, WorkflowError)
|
||||
|
||||
def test_coder_error_is_workflow_error(self):
|
||||
from rdagent.core.exception import CoderError, WorkflowError
|
||||
assert issubclass(CoderError, WorkflowError)
|
||||
|
||||
def test_code_format_error_is_coder_error(self):
|
||||
from rdagent.core.exception import CodeFormatError, CoderError
|
||||
assert issubclass(CodeFormatError, CoderError)
|
||||
|
||||
def test_custom_runtime_error_is_coder_error(self):
|
||||
from rdagent.core.exception import CustomRuntimeError, CoderError
|
||||
assert issubclass(CustomRuntimeError, CoderError)
|
||||
|
||||
def test_no_output_error_is_coder_error(self):
|
||||
from rdagent.core.exception import NoOutputError, CoderError
|
||||
assert issubclass(NoOutputError, CoderError)
|
||||
|
||||
def test_runner_error_is_exception(self):
|
||||
from rdagent.core.exception import RunnerError
|
||||
with pytest.raises(RunnerError):
|
||||
raise RunnerError("test")
|
||||
|
||||
def test_factor_empty_error_is_coder_error(self):
|
||||
from rdagent.core.exception import FactorEmptyError, CoderError
|
||||
assert FactorEmptyError is CoderError
|
||||
|
||||
def test_model_empty_error_is_coder_error(self):
|
||||
from rdagent.core.exception import ModelEmptyError, CoderError
|
||||
assert ModelEmptyError is CoderError
|
||||
|
||||
def test_llm_unavailable_error_is_runtime_error(self):
|
||||
from rdagent.core.exception import LLMUnavailableError
|
||||
with pytest.raises(LLMUnavailableError):
|
||||
raise LLMUnavailableError("LLM down")
|
||||
|
||||
def test_code_block_parse_error(self):
|
||||
from rdagent.core.exception import CodeBlockParseError
|
||||
e = CodeBlockParseError("msg", "content", "python")
|
||||
assert e.message == "msg"
|
||||
assert e.content == "content"
|
||||
assert e.language == "python"
|
||||
assert isinstance(e, Exception)
|
||||
|
||||
def test_coder_error_caused_by_timeout_default(self):
|
||||
from rdagent.core.exception import CoderError
|
||||
assert CoderError.caused_by_timeout is False
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# RDAgentLog singleton
|
||||
# =============================================================================
|
||||
|
||||
|
||||
class TestRDAgentLog:
|
||||
def test_is_singleton(self):
|
||||
from rdagent.log.logger import RDAgentLog
|
||||
a = RDAgentLog()
|
||||
b = RDAgentLog()
|
||||
assert a is b
|
||||
|
||||
def test_has_tag_context(self):
|
||||
from rdagent.log.logger import RDAgentLog
|
||||
assert hasattr(RDAgentLog, "_tag_ctx")
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Daily log session
|
||||
# =============================================================================
|
||||
|
||||
|
||||
class TestDailyLog:
|
||||
def test_session_importable(self):
|
||||
from rdagent.log.daily_log import session
|
||||
assert callable(session)
|
||||
@@ -0,0 +1,64 @@
|
||||
"""Tests for fx_validator config (no langchain dependency)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
PROJECT_ROOT = Path(__file__).parent.parent.parent
|
||||
sys.path.insert(0, str(PROJECT_ROOT))
|
||||
|
||||
|
||||
class TestFXConfig:
|
||||
"""Test config.py directly (bypasses __init__ which imports langchain)."""
|
||||
|
||||
def test_config_is_dict(self):
|
||||
# Import config module directly, not via package __init__
|
||||
import importlib.util
|
||||
spec = importlib.util.spec_from_file_location(
|
||||
"fx_validator_config",
|
||||
PROJECT_ROOT / "rdagent/scenarios/qlib/fx_validator/config.py",
|
||||
)
|
||||
mod = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(mod)
|
||||
assert isinstance(mod.FX_CONFIG, dict)
|
||||
|
||||
def test_required_keys(self):
|
||||
import importlib.util
|
||||
spec = importlib.util.spec_from_file_location(
|
||||
"fx_validator_config",
|
||||
PROJECT_ROOT / "rdagent/scenarios/qlib/fx_validator/config.py",
|
||||
)
|
||||
mod = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(mod)
|
||||
cfg = mod.FX_CONFIG
|
||||
assert "instrument" in cfg
|
||||
assert "max_debate_rounds" in cfg
|
||||
assert "sessions" in cfg
|
||||
assert "spread_bps" in cfg
|
||||
|
||||
def test_sessions_have_four_zones(self):
|
||||
import importlib.util
|
||||
spec = importlib.util.spec_from_file_location(
|
||||
"fx_validator_config",
|
||||
PROJECT_ROOT / "rdagent/scenarios/qlib/fx_validator/config.py",
|
||||
)
|
||||
mod = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(mod)
|
||||
sessions = mod.FX_CONFIG["sessions"]
|
||||
for zone in ("asian", "london", "ny", "overlap"):
|
||||
start, end = sessions[zone]
|
||||
assert isinstance(start, str)
|
||||
assert isinstance(end, str)
|
||||
|
||||
def test_max_debate_rounds_positive(self):
|
||||
import importlib.util
|
||||
spec = importlib.util.spec_from_file_location(
|
||||
"fx_validator_config",
|
||||
PROJECT_ROOT / "rdagent/scenarios/qlib/fx_validator/config.py",
|
||||
)
|
||||
mod = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(mod)
|
||||
assert mod.FX_CONFIG["max_debate_rounds"] > 0
|
||||
@@ -0,0 +1,79 @@
|
||||
"""Tests for scripts/predix_full_eval.py pure functions and dataclasses."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
PROJECT_ROOT = Path(__file__).parent.parent.parent
|
||||
sys.path.insert(0, str(PROJECT_ROOT))
|
||||
|
||||
|
||||
class TestFactorInfo:
|
||||
def test_construction(self):
|
||||
from scripts.predix_full_eval import FactorInfo
|
||||
fi = FactorInfo(
|
||||
workspace_hash="abc123",
|
||||
factor_name="test_factor",
|
||||
factor_code="x=1",
|
||||
)
|
||||
assert fi.workspace_hash == "abc123"
|
||||
assert fi.factor_name == "test_factor"
|
||||
assert fi.factor_code == "x=1"
|
||||
|
||||
|
||||
class TestEvalResult:
|
||||
def test_defaults(self):
|
||||
from scripts.predix_full_eval import EvalResult
|
||||
er = EvalResult(factor_name="f1", workspace_hash="h1")
|
||||
assert er.status == ""
|
||||
assert er.ic is None
|
||||
assert er.error_message is None
|
||||
assert er.non_null_count == 0
|
||||
|
||||
def test_failed_result(self):
|
||||
from scripts.predix_full_eval import EvalResult
|
||||
er = EvalResult(
|
||||
factor_name="f1", workspace_hash="h1",
|
||||
status="failed", error_message="timeout",
|
||||
)
|
||||
assert er.status == "failed"
|
||||
assert er.error_message == "timeout"
|
||||
|
||||
def test_to_dict(self):
|
||||
from scripts.predix_full_eval import EvalResult
|
||||
er = EvalResult(factor_name="f1", workspace_hash="h1", status="success", ic=0.05)
|
||||
d = er.to_dict()
|
||||
assert d["factor_name"] == "f1"
|
||||
assert d["ic"] == 0.05
|
||||
assert d["status"] == "success"
|
||||
|
||||
|
||||
class TestExtractFactorDescription:
|
||||
def test_docstring_extracted(self):
|
||||
from scripts.predix_full_eval import _extract_factor_description
|
||||
code = '"""This is a test factor.\nComputes momentum."""\nx=1'
|
||||
desc = _extract_factor_description(code)
|
||||
assert "test factor" in desc
|
||||
|
||||
def test_comment_extraction(self):
|
||||
from scripts.predix_full_eval import _extract_factor_description
|
||||
code = "# Momentum factor\n# Uses 20-bar window\nx=1"
|
||||
desc = _extract_factor_description(code)
|
||||
assert "Momentum factor" in desc
|
||||
assert "20-bar window" in desc
|
||||
|
||||
def test_no_docstring_or_comments(self):
|
||||
from scripts.predix_full_eval import _extract_factor_description
|
||||
code = "x = 1\ny = 2\n"
|
||||
desc = _extract_factor_description(code)
|
||||
assert desc == "No description available"
|
||||
|
||||
def test_shebang_skipped(self):
|
||||
from scripts.predix_full_eval import _extract_factor_description
|
||||
code = "#!/usr/bin/env python\n# Real comment\nx=1"
|
||||
desc = _extract_factor_description(code)
|
||||
assert "Real comment" in desc
|
||||
assert "usr/bin" not in desc
|
||||
@@ -0,0 +1,113 @@
|
||||
"""Tests for utils/agent/apply_patch, core/prompts, utils/fmt."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
PROJECT_ROOT = Path(__file__).parent.parent.parent
|
||||
sys.path.insert(0, str(PROJECT_ROOT))
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# apply_patch data structures
|
||||
# =============================================================================
|
||||
|
||||
|
||||
class TestApplyPatchDatastructures:
|
||||
def test_action_type_enum(self):
|
||||
from rdagent.utils.agent.apply_patch import ActionType
|
||||
assert ActionType.ADD.value == "add"
|
||||
assert ActionType.DELETE.value == "delete"
|
||||
assert ActionType.UPDATE.value == "update"
|
||||
|
||||
def test_file_change_dataclass(self):
|
||||
from rdagent.utils.agent.apply_patch import FileChange, ActionType
|
||||
fc = FileChange(type=ActionType.UPDATE, old_content="old", new_content="new")
|
||||
assert fc.type == ActionType.UPDATE
|
||||
assert fc.old_content == "old"
|
||||
assert fc.new_content == "new"
|
||||
assert fc.move_path is None
|
||||
|
||||
def test_file_change_defaults(self):
|
||||
from rdagent.utils.agent.apply_patch import FileChange, ActionType
|
||||
fc = FileChange(type=ActionType.ADD)
|
||||
assert fc.old_content is None
|
||||
assert fc.new_content is None
|
||||
|
||||
def test_commit_defaults(self):
|
||||
from rdagent.utils.agent.apply_patch import Commit
|
||||
c = Commit()
|
||||
assert c.changes == {}
|
||||
|
||||
def test_commit_with_changes(self):
|
||||
from rdagent.utils.agent.apply_patch import Commit, FileChange, ActionType
|
||||
c = Commit(changes={"test.py": FileChange(type=ActionType.UPDATE)})
|
||||
assert "test.py" in c.changes
|
||||
|
||||
def test_diff_error_is_value_error(self):
|
||||
from rdagent.utils.agent.apply_patch import DiffError
|
||||
with pytest.raises(DiffError):
|
||||
raise DiffError("test error")
|
||||
|
||||
def test_chunk_dataclass(self):
|
||||
from rdagent.utils.agent.apply_patch import Chunk
|
||||
c = Chunk(orig_index=5, del_lines=["a"], ins_lines=["b", "c"])
|
||||
assert c.orig_index == 5
|
||||
assert c.del_lines == ["a"]
|
||||
assert c.ins_lines == ["b", "c"]
|
||||
|
||||
def test_patch_action_dataclass(self):
|
||||
from rdagent.utils.agent.apply_patch import PatchAction, ActionType
|
||||
pa = PatchAction(type=ActionType.ADD, new_file="test.py")
|
||||
assert pa.type == ActionType.ADD
|
||||
assert pa.new_file == "test.py"
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Prompts (core/prompts.py)
|
||||
# =============================================================================
|
||||
|
||||
|
||||
class TestPrompts:
|
||||
def test_prompts_loads_yaml(self, tmp_path):
|
||||
from rdagent.core.prompts import Prompts
|
||||
yaml_file = tmp_path / "test.yaml"
|
||||
yaml_file.write_text("key1: value1\nkey2: value2\n")
|
||||
p = Prompts(file_path=yaml_file)
|
||||
assert p["key1"] == "value1"
|
||||
assert p["key2"] == "value2"
|
||||
|
||||
def test_prompts_raises_on_empty(self, tmp_path):
|
||||
from rdagent.core.prompts import Prompts
|
||||
yaml_file = tmp_path / "empty.yaml"
|
||||
yaml_file.write_text("")
|
||||
with pytest.raises(ValueError, match="Failed to load"):
|
||||
Prompts(file_path=yaml_file)
|
||||
|
||||
def test_prompts_is_dict_subclass(self, tmp_path):
|
||||
from rdagent.core.prompts import Prompts
|
||||
yaml_file = tmp_path / "test.yaml"
|
||||
yaml_file.write_text("k: v\n")
|
||||
p = Prompts(file_path=yaml_file)
|
||||
assert isinstance(p, dict)
|
||||
assert len(p) == 1
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# SingletonBaseClass (core/utils.py)
|
||||
# =============================================================================
|
||||
|
||||
|
||||
class TestSingletonBaseClass:
|
||||
def test_singleton_returns_same_instance(self):
|
||||
from rdagent.core.utils import SingletonBaseClass
|
||||
|
||||
class MySingleton(SingletonBaseClass):
|
||||
pass
|
||||
|
||||
a = MySingleton()
|
||||
b = MySingleton()
|
||||
assert a is b
|
||||
Reference in New Issue
Block a user