forked from ChelseaKR/sprout
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathtest_eval_plugin_api.py
More file actions
177 lines (137 loc) · 5.91 KB
/
Copy pathtest_eval_plugin_api.py
File metadata and controls
177 lines (137 loc) · 5.91 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
"""EXP-14 / ADR-0019: entry-point suite discovery and the frozen public API surface."""
from __future__ import annotations
from dataclasses import dataclass
from typing import Any
import pytest
import sprout.eval as eval_pkg
from sprout.eval.dataset import Dataset
from sprout.eval.judge import DeterministicJudge, Judge
from sprout.eval.suite import (
_REGISTRY,
ENTRY_POINT_GROUP,
EvalContext,
MetricDefinition,
Suite,
SuiteResult,
_reset_entry_point_cache,
available,
load_entry_point_suites,
register,
resolve_suites,
)
class _StubSuite:
"""A minimal, valid third-party ``Suite`` for discovery tests."""
def __init__(self, name: str) -> None:
self.name = name
self.metric = MetricDefinition(name=name, definition="stub", threshold=0.5)
def run(self, ctx: EvalContext) -> SuiteResult: # pragma: no cover - not exercised
raise NotImplementedError
@dataclass
class _FakeEntryPoint:
name: str
_target: Any
@property
def value(self) -> str:
return f"<stub {self.name}>"
def load(self) -> Any:
if isinstance(self._target, Exception):
raise self._target
return self._target
@pytest.fixture(autouse=True)
def _isolated_registry(monkeypatch: pytest.MonkeyPatch) -> Any:
"""Every test gets a fresh entry-point cache and a registry snapshot restored after."""
_reset_entry_point_cache()
before = dict(_REGISTRY)
yield
_REGISTRY.clear()
_REGISTRY.update(before)
_reset_entry_point_cache()
def _patch_entry_points(monkeypatch: pytest.MonkeyPatch, *eps: _FakeEntryPoint) -> None:
def fake_entry_points(*, group: str) -> tuple[_FakeEntryPoint, ...]:
assert group == ENTRY_POINT_GROUP
return eps
monkeypatch.setattr("importlib.metadata.entry_points", fake_entry_points)
# --- entry-point discovery --------------------------------------------------------
def test_discovers_and_registers_an_entry_point_suite_instance(
monkeypatch: pytest.MonkeyPatch,
) -> None:
_patch_entry_points(monkeypatch, _FakeEntryPoint("stub-a", _StubSuite("stub-a")))
newly = load_entry_point_suites()
assert newly == ["stub-a"]
assert "stub-a" in available()
assert resolve_suites("stub-a")[0].name == "stub-a"
def test_discovers_a_zero_arg_factory_entry_point(monkeypatch: pytest.MonkeyPatch) -> None:
def build() -> _StubSuite:
return _StubSuite("stub-factory")
_patch_entry_points(monkeypatch, _FakeEntryPoint("stub-factory", build))
assert load_entry_point_suites() == ["stub-factory"]
assert "stub-factory" in available()
def test_discovery_is_cached_after_the_first_call(monkeypatch: pytest.MonkeyPatch) -> None:
calls = 0
def fake_entry_points(*, group: str) -> tuple[_FakeEntryPoint, ...]:
nonlocal calls
calls += 1
return (_FakeEntryPoint("stub-once", _StubSuite("stub-once")),)
monkeypatch.setattr("importlib.metadata.entry_points", fake_entry_points)
assert load_entry_point_suites() == ["stub-once"]
assert load_entry_point_suites() == [] # cached: no re-scan, nothing "newly" registered
assert calls == 1
def test_duplicate_suite_name_fails_closed(monkeypatch: pytest.MonkeyPatch) -> None:
register(_StubSuite("dup-name"))
_patch_entry_points(monkeypatch, _FakeEntryPoint("plugin-dup", _StubSuite("dup-name")))
with pytest.raises(ValueError, match="duplicate suite name"):
load_entry_point_suites()
def test_entry_point_colliding_with_a_builtin_suite_fails_closed(
monkeypatch: pytest.MonkeyPatch,
) -> None:
colliding = _FakeEntryPoint("plugin-groundedness", _StubSuite("groundedness"))
_patch_entry_points(monkeypatch, colliding)
with pytest.raises(ValueError, match="duplicate suite name"):
load_entry_point_suites()
def test_entry_point_that_fails_to_load_raises(monkeypatch: pytest.MonkeyPatch) -> None:
_patch_entry_points(monkeypatch, _FakeEntryPoint("broken", RuntimeError("boom")))
with pytest.raises(ImportError, match="failed to load suite entry point"):
load_entry_point_suites()
def test_entry_point_not_resolving_to_a_suite_raises(monkeypatch: pytest.MonkeyPatch) -> None:
_patch_entry_points(monkeypatch, _FakeEntryPoint("not-a-suite", object()))
with pytest.raises(TypeError, match="did not resolve to a Suite"):
load_entry_point_suites()
def test_resolve_suites_all_includes_a_discovered_plugin(monkeypatch: pytest.MonkeyPatch) -> None:
_patch_entry_points(monkeypatch, _FakeEntryPoint("stub-b", _StubSuite("stub-b")))
assert any(s.name == "stub-b" for s in resolve_suites("all"))
assert "stub-b" in available() # cached: the second call doesn't re-scan (or re-collide)
# --- the frozen public API surface (ADR-0019) --------------------------------------
def test_eval_package_exports_the_frozen_plugin_surface() -> None:
expected = {
"Dataset",
"DatasetItem",
"Judge",
"JudgeDecision",
"DeterministicJudge",
"build_judge",
"Suite",
"EvalContext",
"SuiteResult",
"MetricDefinition",
"ExampleOutcome",
"SegmentScore",
"Verdict",
"register",
"available",
"resolve_suites",
"load_entry_point_suites",
"ENTRY_POINT_GROUP",
"run_evaluation",
}
assert expected <= set(eval_pkg.__all__)
for name in expected:
assert hasattr(eval_pkg, name), f"sprout.eval.{name} is in __all__ but not importable"
def test_dataset_judge_suiteresult_metricdefinition_are_frozen() -> None:
assert Dataset.model_config.get("frozen") is True
assert SuiteResult.model_config.get("frozen") is True
assert MetricDefinition.model_config.get("frozen") is True
assert (
getattr(Suite, "_is_protocol", False) is True
) # still a Protocol, not accidentally a class
judge = DeterministicJudge()
assert isinstance(judge, Judge) # runtime_checkable Protocol conformance