57 lines
2.9 KiB
Python
57 lines
2.9 KiB
Python
"""Meaningful scorer and frozen-provenance checks; no inference."""
|
|||
|
|
import importlib.util
|
||
|
|
from pathlib import Path
|
||
|
|
import tempfile
|
||
|
|
import unittest
|
||
|
|
|
||
|
|
spec = importlib.util.spec_from_file_location('mixed_eval', Path(__file__).with_name('evaluate.py'))
|
||
|
|
evaluate = importlib.util.module_from_spec(spec); spec.loader.exec_module(evaluate)
|
||
|
|
|
||
|
|
|
||
|
|
class EvaluationTests(unittest.TestCase):
|
||
|
|
def test_frozen_real_assets_verify(self):
|
||
|
|
fixture, _ = evaluate.load_fixture()
|
||
|
|
self.assertEqual(len(fixture['assets']), 32)
|
||
|
|
self.assertEqual(len(fixture['queries']), 91)
|
||
|
|
self.assertTrue(any(a.get('source_repository') for a in fixture['assets']))
|
||
|
|
|
||
|
|
def test_multiple_relevant_files_and_duplicate_hits(self):
|
||
|
|
query = {'expected': ['a', 'b'], 'hard_negatives': ['x']}
|
||
|
|
result = evaluate.score(query, [{'asset': x} for x in ['x', 'a', 'a', 'c', 'b']])
|
||
|
|
self.assertEqual(result['first_relevant_rank'], 2)
|
||
|
|
self.assertEqual(result['recall_at_5'], 1)
|
||
|
|
self.assertEqual(result['reciprocal_rank'], .5)
|
||
|
|
self.assertTrue(result['hard_negative_in_top5'])
|
||
|
|
|
||
|
|
def test_no_answer_is_not_counted_as_positive_recall(self):
|
||
|
|
result = evaluate.score({'expected': []}, [{'asset': 'unrelated'}])
|
||
|
|
self.assertTrue(result['no_answer_returned_any'])
|
||
|
|
self.assertIsNone(result['recall_at_5'])
|
||
|
|
self.assertIsNone(result['top1'])
|
||
|
|
self.assertIsNone(result['reciprocal_rank'])
|
||
|
|
|
||
|
|
def test_temporal_labels_file_relevance_only(self):
|
||
|
|
result = evaluate.score({'expected': [{'asset': 'speech', 'start_s': 20, 'end_s': 25}]}, [{'asset': 'speech', 'source': {'number': 0}}])
|
||
|
|
self.assertTrue(result['top1']) # Deliberately no invented timestamp success metric.
|
||
|
|
|
||
|
|
def test_label_tampering_rejected_before_assets(self):
|
||
|
|
with tempfile.TemporaryDirectory() as directory:
|
||
|
|
path = Path(directory)
|
||
|
|
(path / 'fixture.json').write_text('{"assets": []}')
|
||
|
|
(path / 'labels.lock.json').write_text('{"fixture_sha256": "wrong"}')
|
||
|
|
with self.assertRaisesRegex(ValueError, 'Frozen labels changed'):
|
||
|
|
evaluate.load_fixture(path)
|
||
|
|
|
||
|
|
def test_summary_separates_scope_and_language(self):
|
||
|
|
rows = []
|
||
|
|
for scope, lang, expected, found in [('all_files', 'en', ['a'], ['b']), ('code', 'en', ['a'], ['a']), ('all_files', 'de', [], ['b'])]:
|
||
|
|
query = {'expected': expected, 'language': lang, 'category': 'code'}
|
||
|
|
rows.append({'mode': 'hybrid', 'scope': scope, 'query': query, 'score': evaluate.score(query, [{'asset': a} for a in found])})
|
||
|
|
groups = evaluate.summarize(rows)
|
||
|
|
self.assertEqual(len(groups), 3)
|
||
|
|
self.assertEqual(next(g for g in groups if g['scope'] == 'code')['top1'], 1)
|
||
|
|
self.assertEqual(next(g for g in groups if g['language'] == 'de')['no_answer_returned_any'], 1)
|
||
|
|
|
||
|
|
|
||
|
|
if __name__ == '__main__':
|
||
|
|
unittest.main()
|