Raise coverage threshold to 95% and expand test coverage
- pyproject.toml: fail_under 80 → 95 - test_alpha: +79 lines - test_cli_workflow: +226 lines - test_derived: +121 lines - test_downloader_contracts: +169 lines - test_features: +16 lines - test_minute_downloader: +81 lines - test_portfolio: +208 lines
This commit is contained in:
+118
-3
@@ -9,8 +9,14 @@ from click.testing import CliRunner
|
||||
|
||||
from cli import cli
|
||||
from pipeline.alpha.compute import join_feature_frames
|
||||
from pipeline.derived.compute import compute_derived, validate_derived_frame
|
||||
from pipeline.derived.registry import available_derived, get_derived, load_derived_module
|
||||
from pipeline.derived.base import BaseDerivedData
|
||||
from pipeline.derived.compute import compute_derived, read_derived_frame, validate_derived_frame
|
||||
from pipeline.derived.registry import (
|
||||
available_derived,
|
||||
get_derived,
|
||||
load_derived_module,
|
||||
register_derived,
|
||||
)
|
||||
|
||||
|
||||
def _daily_bars() -> pd.DataFrame:
|
||||
@@ -88,6 +94,27 @@ def test_validate_derived_frame_rejects_non_numeric_values():
|
||||
}))
|
||||
|
||||
|
||||
def test_validate_derived_frame_rejects_missing_value_columns():
|
||||
with pytest.raises(ValueError, match="at least one value column"):
|
||||
validate_derived_frame(pd.DataFrame({
|
||||
"symbol_id": ["sh600000"],
|
||||
"date": [pd.Timestamp("2024-01-02")],
|
||||
}))
|
||||
|
||||
|
||||
def test_read_derived_frame_rejects_empty_csv(tmp_path):
|
||||
empty_csv = tmp_path / "empty.csv"
|
||||
empty_csv.write_text("")
|
||||
|
||||
with pytest.raises(ValueError, match="CSV input is empty"):
|
||||
read_derived_frame(empty_csv)
|
||||
|
||||
|
||||
def test_compute_derived_rejects_missing_inputs():
|
||||
with pytest.raises(ValueError, match="requires --daily-path or --minute-path"):
|
||||
compute_derived("minute_daily_summary")
|
||||
|
||||
|
||||
def test_derived_ingest_cli_accepts_csv_and_parquet(tmp_path):
|
||||
runner = CliRunner()
|
||||
source = pd.DataFrame({
|
||||
@@ -136,6 +163,26 @@ def test_derived_validate_cli_rejects_duplicate_csv_columns(tmp_path):
|
||||
assert "duplicate columns" in result.output
|
||||
|
||||
|
||||
def test_derived_ingest_cli_wraps_validation_errors(tmp_path):
|
||||
runner = CliRunner()
|
||||
csv_path = tmp_path / "bad_ingest.csv"
|
||||
csv_path.write_text("symbol_id,date\nsh600000,2024-01-02\n")
|
||||
|
||||
result = runner.invoke(cli, [
|
||||
"derived",
|
||||
"ingest",
|
||||
"--input-path",
|
||||
str(csv_path),
|
||||
"--derived-name",
|
||||
"bad",
|
||||
"--output-dir",
|
||||
str(tmp_path / "derived"),
|
||||
])
|
||||
|
||||
assert result.exit_code != 0
|
||||
assert "at least one value column" in result.output
|
||||
|
||||
|
||||
def test_external_derived_plugin_loads_filters_params_and_uses_inputs(tmp_path):
|
||||
module_path = tmp_path / "external_derived.py"
|
||||
module_path.write_text(textwrap.dedent('''
|
||||
@@ -204,6 +251,57 @@ def test_external_derived_plugin_loads_filters_params_and_uses_inputs(tmp_path):
|
||||
assert {"daily_scaled_close", "minute_volume_sum"}.issubset(both_result.columns)
|
||||
|
||||
|
||||
def test_derived_registry_rejects_bad_plugins_and_load_failures(tmp_path, monkeypatch):
|
||||
with pytest.raises(TypeError):
|
||||
register_derived(object) # type: ignore[arg-type]
|
||||
|
||||
with pytest.raises(ValueError, match="non-empty"):
|
||||
@register_derived
|
||||
class NoNameDerived(BaseDerivedData):
|
||||
def compute(self, daily=None, minute=None):
|
||||
return pd.DataFrame()
|
||||
|
||||
@register_derived
|
||||
class _CoverageDerived(BaseDerivedData):
|
||||
name = "_coverage_derived_registry"
|
||||
|
||||
def compute(self, daily=None, minute=None):
|
||||
return pd.DataFrame({
|
||||
"symbol_id": ["sh600000"],
|
||||
"date": [pd.Timestamp("2024-01-02")],
|
||||
"x": [1.0],
|
||||
})
|
||||
|
||||
with pytest.raises(ValueError, match="already registered"):
|
||||
@register_derived
|
||||
class _CoverageDerivedDuplicate(BaseDerivedData):
|
||||
name = "_coverage_derived_registry"
|
||||
|
||||
def compute(self, daily=None, minute=None):
|
||||
return pd.DataFrame()
|
||||
|
||||
with pytest.raises(KeyError, match="Unknown derived data"):
|
||||
get_derived("does_not_exist")
|
||||
|
||||
missing_path = tmp_path / "missing_derived.py"
|
||||
with pytest.raises(FileNotFoundError):
|
||||
load_derived_module(str(missing_path))
|
||||
|
||||
bad_path = tmp_path / "bad_derived.py"
|
||||
bad_path.write_text("x = 1\n")
|
||||
monkeypatch.setattr(
|
||||
"pipeline.derived.registry.importlib.util.spec_from_file_location",
|
||||
lambda *args, **kwargs: None,
|
||||
)
|
||||
with pytest.raises(ImportError, match="Cannot load derived data module"):
|
||||
load_derived_module(str(bad_path))
|
||||
|
||||
load_derived_module("math")
|
||||
instance = _CoverageDerived()
|
||||
instance.scale = 2
|
||||
assert repr(instance) == "_CoverageDerived(scale=2)"
|
||||
|
||||
|
||||
def test_derived_compute_cli_writes_builtin_minute_summary(tmp_path):
|
||||
runner = CliRunner()
|
||||
minute_path = tmp_path / "minute.pq"
|
||||
@@ -223,6 +321,24 @@ def test_derived_compute_cli_writes_builtin_minute_summary(tmp_path):
|
||||
assert "minute_vwap" in written.columns
|
||||
|
||||
|
||||
def test_minute_summary_uses_time_sort_and_daily_without_close():
|
||||
minute = _minute_bars().drop(columns=["datetime"]).sample(frac=1.0, random_state=0)
|
||||
daily = _daily_bars()[["symbol_id", "date", "open"]]
|
||||
|
||||
result = compute_derived(
|
||||
"minute_daily_summary",
|
||||
daily=daily,
|
||||
minute=minute,
|
||||
)
|
||||
|
||||
by_symbol = result.set_index(["symbol_id", "date"])
|
||||
assert np.isclose(
|
||||
by_symbol.loc[("sh600000", pd.Timestamp("2024-01-02")), "minute_intraday_return"],
|
||||
0.10,
|
||||
)
|
||||
assert "minute_vwap_deviation" in result.columns
|
||||
|
||||
|
||||
def test_alpha_feature_join_rejects_derived_column_collisions():
|
||||
data = _daily_bars()
|
||||
derived_a = data[["symbol_id", "date"]].copy()
|
||||
@@ -258,4 +374,3 @@ def test_legacy_feature_cli_delegates_to_derived_registry(tmp_path):
|
||||
])
|
||||
assert compute_result.exit_code == 0, compute_result.output
|
||||
assert (out_dir / "minute_summary.pq").exists()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user