feat: add field-specific bbox expansion strategies for YOLO training

Implement center-point based bbox scaling with directional compensation
to capture field labels that typically appear above or to the left of
field values. This improves YOLO training data quality by including
contextual information around field values.

Key changes:
- Add shared.bbox module with ScaleStrategy dataclass and expand_bbox function
- Define field-specific strategies (ocr_number, bankgiro, invoice_date, etc.)
- Support manual_mode for minimal padding (no scaling)
- Integrate expand_bbox into AnnotationGenerator
- Add FIELD_TO_CLASS mapping for field_name to class_name lookup
- Comprehensive tests with 100% coverage (45 tests)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Yaojia Wang
2026-02-04 22:56:52 +01:00
parent 8723ef4653
commit 0990239e9c
13 changed files with 1424 additions and 18 deletions

View File

@@ -0,0 +1 @@
"""Tests for shared.bbox module."""

View File

@@ -0,0 +1,556 @@
"""
Tests for expand_bbox function.
Tests verify that bbox expansion works correctly with center-point scaling,
directional compensation, max padding clamping, and image boundary handling.
"""
import pytest
from shared.bbox import (
expand_bbox,
ScaleStrategy,
FIELD_SCALE_STRATEGIES,
DEFAULT_STRATEGY,
)
class TestExpandBboxCenterScaling:
"""Tests for center-point based scaling."""
def test_center_scaling_expands_symmetrically(self):
"""Verify bbox expands symmetrically around center when no extra ratios."""
# 100x50 bbox at (100, 200)
bbox = (100, 200, 200, 250)
strategy = ScaleStrategy(
scale_x=1.2, # 20% wider
scale_y=1.4, # 40% taller
max_pad_x=1000, # Large to avoid clamping
max_pad_y=1000,
)
result = expand_bbox(
bbox=bbox,
image_width=1000,
image_height=1000,
field_type="test_field",
strategies={"test_field": strategy},
)
# Original: width=100, height=50
# New: width=120, height=70
# Center: (150, 225)
# Expected: x0=150-60=90, x1=150+60=210, y0=225-35=190, y1=225+35=260
assert result[0] == 90 # x0
assert result[1] == 190 # y0
assert result[2] == 210 # x1
assert result[3] == 260 # y1
def test_no_scaling_returns_original(self):
"""Verify scale=1.0 with no extras returns original bbox."""
bbox = (100, 200, 200, 250)
strategy = ScaleStrategy(
scale_x=1.0,
scale_y=1.0,
max_pad_x=1000,
max_pad_y=1000,
)
result = expand_bbox(
bbox=bbox,
image_width=1000,
image_height=1000,
field_type="test_field",
strategies={"test_field": strategy},
)
assert result == (100, 200, 200, 250)
class TestExpandBboxDirectionalCompensation:
"""Tests for directional compensation (extra ratios)."""
def test_extra_top_expands_upward(self):
"""Verify extra_top_ratio adds expansion toward top."""
bbox = (100, 200, 200, 250) # width=100, height=50
strategy = ScaleStrategy(
scale_x=1.0,
scale_y=1.0,
extra_top_ratio=0.5, # Add 50% of height to top
max_pad_x=1000,
max_pad_y=1000,
)
result = expand_bbox(
bbox=bbox,
image_width=1000,
image_height=1000,
field_type="test_field",
strategies={"test_field": strategy},
)
# extra_top = 50 * 0.5 = 25
assert result[0] == 100 # x0 unchanged
assert result[1] == 175 # y0 = 200 - 25
assert result[2] == 200 # x1 unchanged
assert result[3] == 250 # y1 unchanged
def test_extra_left_expands_leftward(self):
"""Verify extra_left_ratio adds expansion toward left."""
bbox = (100, 200, 200, 250) # width=100
strategy = ScaleStrategy(
scale_x=1.0,
scale_y=1.0,
extra_left_ratio=0.8, # Add 80% of width to left
max_pad_x=1000,
max_pad_y=1000,
)
result = expand_bbox(
bbox=bbox,
image_width=1000,
image_height=1000,
field_type="test_field",
strategies={"test_field": strategy},
)
# extra_left = 100 * 0.8 = 80
assert result[0] == 20 # x0 = 100 - 80
assert result[1] == 200 # y0 unchanged
assert result[2] == 200 # x1 unchanged
assert result[3] == 250 # y1 unchanged
def test_extra_right_expands_rightward(self):
"""Verify extra_right_ratio adds expansion toward right."""
bbox = (100, 200, 200, 250) # width=100
strategy = ScaleStrategy(
scale_x=1.0,
scale_y=1.0,
extra_right_ratio=0.3, # Add 30% of width to right
max_pad_x=1000,
max_pad_y=1000,
)
result = expand_bbox(
bbox=bbox,
image_width=1000,
image_height=1000,
field_type="test_field",
strategies={"test_field": strategy},
)
# extra_right = 100 * 0.3 = 30
assert result[0] == 100 # x0 unchanged
assert result[1] == 200 # y0 unchanged
assert result[2] == 230 # x1 = 200 + 30
assert result[3] == 250 # y1 unchanged
def test_extra_bottom_expands_downward(self):
"""Verify extra_bottom_ratio adds expansion toward bottom."""
bbox = (100, 200, 200, 250) # height=50
strategy = ScaleStrategy(
scale_x=1.0,
scale_y=1.0,
extra_bottom_ratio=0.4, # Add 40% of height to bottom
max_pad_x=1000,
max_pad_y=1000,
)
result = expand_bbox(
bbox=bbox,
image_width=1000,
image_height=1000,
field_type="test_field",
strategies={"test_field": strategy},
)
# extra_bottom = 50 * 0.4 = 20
assert result[0] == 100 # x0 unchanged
assert result[1] == 200 # y0 unchanged
assert result[2] == 200 # x1 unchanged
assert result[3] == 270 # y1 = 250 + 20
def test_combined_scaling_and_directional(self):
"""Verify scale + directional compensation work together."""
bbox = (100, 200, 200, 250) # width=100, height=50
strategy = ScaleStrategy(
scale_x=1.2, # 20% wider -> 120 width
scale_y=1.0, # no height change
extra_left_ratio=0.5, # Add 50% of width to left
max_pad_x=1000,
max_pad_y=1000,
)
result = expand_bbox(
bbox=bbox,
image_width=1000,
image_height=1000,
field_type="test_field",
strategies={"test_field": strategy},
)
# Center: x=150
# After scale: width=120 -> x0=150-60=90, x1=150+60=210
# After extra_left: x0 = 90 - (100 * 0.5) = 40
assert result[0] == 40 # x0
assert result[2] == 210 # x1
class TestExpandBboxMaxPadClamping:
"""Tests for max padding clamping."""
def test_max_pad_x_limits_horizontal_expansion(self):
"""Verify max_pad_x limits expansion on left and right."""
bbox = (100, 200, 200, 250) # width=100
strategy = ScaleStrategy(
scale_x=2.0, # Double width (would add 50 each side)
scale_y=1.0,
max_pad_x=30, # Limit to 30 pixels each side
max_pad_y=1000,
)
result = expand_bbox(
bbox=bbox,
image_width=1000,
image_height=1000,
field_type="test_field",
strategies={"test_field": strategy},
)
# Scale would make: x0=100, x1=200 -> x0=50, x1=250 (50px each side)
# But max_pad_x=30 limits to: x0=70, x1=230
assert result[0] == 70 # x0 = 100 - 30
assert result[2] == 230 # x1 = 200 + 30
def test_max_pad_y_limits_vertical_expansion(self):
"""Verify max_pad_y limits expansion on top and bottom."""
bbox = (100, 200, 200, 250) # height=50
strategy = ScaleStrategy(
scale_x=1.0,
scale_y=3.0, # Triple height (would add 50 each side)
max_pad_x=1000,
max_pad_y=20, # Limit to 20 pixels each side
)
result = expand_bbox(
bbox=bbox,
image_width=1000,
image_height=1000,
field_type="test_field",
strategies={"test_field": strategy},
)
# Scale would make: y0=175, y1=275 (50px each side)
# But max_pad_y=20 limits to: y0=180, y1=270
assert result[1] == 180 # y0 = 200 - 20
assert result[3] == 270 # y1 = 250 + 20
def test_max_pad_preserves_asymmetry(self):
"""Verify max_pad clamping preserves asymmetric expansion."""
bbox = (100, 200, 200, 250) # width=100
strategy = ScaleStrategy(
scale_x=1.0,
scale_y=1.0,
extra_left_ratio=1.0, # 100px left expansion
extra_right_ratio=0.0, # No right expansion
max_pad_x=50, # Limit to 50 pixels
max_pad_y=1000,
)
result = expand_bbox(
bbox=bbox,
image_width=1000,
image_height=1000,
field_type="test_field",
strategies={"test_field": strategy},
)
# Left would expand 100, clamped to 50
# Right stays at 0
assert result[0] == 50 # x0 = 100 - 50
assert result[2] == 200 # x1 unchanged
class TestExpandBboxImageBoundaryClamping:
"""Tests for image boundary clamping."""
def test_clamps_to_left_boundary(self):
"""Verify x0 is clamped to 0."""
bbox = (10, 200, 110, 250) # Close to left edge
strategy = ScaleStrategy(
scale_x=1.0,
scale_y=1.0,
extra_left_ratio=0.5, # Would push x0 below 0
max_pad_x=1000,
max_pad_y=1000,
)
result = expand_bbox(
bbox=bbox,
image_width=1000,
image_height=1000,
field_type="test_field",
strategies={"test_field": strategy},
)
assert result[0] == 0 # Clamped to 0
def test_clamps_to_top_boundary(self):
"""Verify y0 is clamped to 0."""
bbox = (100, 10, 200, 60) # Close to top edge
strategy = ScaleStrategy(
scale_x=1.0,
scale_y=1.0,
extra_top_ratio=0.5, # Would push y0 below 0
max_pad_x=1000,
max_pad_y=1000,
)
result = expand_bbox(
bbox=bbox,
image_width=1000,
image_height=1000,
field_type="test_field",
strategies={"test_field": strategy},
)
assert result[1] == 0 # Clamped to 0
def test_clamps_to_right_boundary(self):
"""Verify x1 is clamped to image_width."""
bbox = (900, 200, 990, 250) # Close to right edge
strategy = ScaleStrategy(
scale_x=1.0,
scale_y=1.0,
extra_right_ratio=0.5, # Would push x1 beyond image_width
max_pad_x=1000,
max_pad_y=1000,
)
result = expand_bbox(
bbox=bbox,
image_width=1000,
image_height=1000,
field_type="test_field",
strategies={"test_field": strategy},
)
assert result[2] == 1000 # Clamped to image_width
def test_clamps_to_bottom_boundary(self):
"""Verify y1 is clamped to image_height."""
bbox = (100, 940, 200, 990) # Close to bottom edge
strategy = ScaleStrategy(
scale_x=1.0,
scale_y=1.0,
extra_bottom_ratio=0.5, # Would push y1 beyond image_height
max_pad_x=1000,
max_pad_y=1000,
)
result = expand_bbox(
bbox=bbox,
image_width=1000,
image_height=1000,
field_type="test_field",
strategies={"test_field": strategy},
)
assert result[3] == 1000 # Clamped to image_height
class TestExpandBboxUnknownField:
"""Tests for unknown field handling."""
def test_unknown_field_uses_default_strategy(self):
"""Verify unknown field types use DEFAULT_STRATEGY."""
bbox = (100, 200, 200, 250)
result = expand_bbox(
bbox=bbox,
image_width=1000,
image_height=1000,
field_type="unknown_field_xyz",
)
# DEFAULT_STRATEGY: scale_x=1.15, scale_y=1.15
# Original: width=100, height=50
# New: width=115, height=57.5
# Center: (150, 225)
# x0 = 150 - 57.5 = 92.5 -> 92
# x1 = 150 + 57.5 = 207.5 -> 207
# y0 = 225 - 28.75 = 196.25 -> 196
# y1 = 225 + 28.75 = 253.75 -> 253
# But max_pad_x=50 may clamp...
# Left pad = 100 - 92.5 = 7.5 (< 50, ok)
# Right pad = 207.5 - 200 = 7.5 (< 50, ok)
assert result[0] == 92
assert result[2] == 207
class TestExpandBboxWithRealStrategies:
"""Tests using actual FIELD_SCALE_STRATEGIES."""
def test_ocr_number_expands_significantly_upward(self):
"""Verify ocr_number field gets significant upward expansion."""
bbox = (100, 200, 200, 230) # Small height=30
result = expand_bbox(
bbox=bbox,
image_width=1000,
image_height=1000,
field_type="ocr_number",
)
# extra_top_ratio=0.60 -> 30 * 0.6 = 18 extra top
# y0 should decrease significantly
assert result[1] < 200 - 10 # At least 10px upward expansion
def test_bankgiro_expands_significantly_leftward(self):
"""Verify bankgiro field gets significant leftward expansion."""
bbox = (200, 200, 300, 230) # width=100
result = expand_bbox(
bbox=bbox,
image_width=1000,
image_height=1000,
field_type="bankgiro",
)
# extra_left_ratio=0.80 -> 100 * 0.8 = 80 extra left
# x0 should decrease significantly
assert result[0] < 200 - 30 # At least 30px leftward expansion
def test_amount_expands_rightward(self):
"""Verify amount field gets rightward expansion for currency."""
bbox = (100, 200, 200, 230) # width=100
result = expand_bbox(
bbox=bbox,
image_width=1000,
image_height=1000,
field_type="amount",
)
# extra_right_ratio=0.30 -> 100 * 0.3 = 30 extra right
# x1 should increase
assert result[2] > 200 + 10 # At least 10px rightward expansion
class TestExpandBboxReturnType:
"""Tests for return type and value format."""
def test_returns_tuple_of_four_ints(self):
"""Verify return type is tuple of 4 integers."""
bbox = (100.5, 200.3, 200.7, 250.9)
result = expand_bbox(
bbox=bbox,
image_width=1000,
image_height=1000,
field_type="invoice_number",
)
assert isinstance(result, tuple)
assert len(result) == 4
assert all(isinstance(v, int) for v in result)
def test_returns_valid_bbox_format(self):
"""Verify returned bbox has x0 < x1 and y0 < y1."""
bbox = (100, 200, 200, 250)
result = expand_bbox(
bbox=bbox,
image_width=1000,
image_height=1000,
field_type="invoice_number",
)
x0, y0, x1, y1 = result
assert x0 < x1, "x0 should be less than x1"
assert y0 < y1, "y0 should be less than y1"
class TestManualLabelMode:
"""Tests for manual_mode parameter."""
def test_manual_mode_uses_minimal_padding(self):
"""Verify manual_mode uses MANUAL_LABEL_STRATEGY with minimal padding."""
bbox = (100, 200, 200, 250) # width=100, height=50
result = expand_bbox(
bbox=bbox,
image_width=1000,
image_height=1000,
field_type="bankgiro", # Would normally expand left significantly
manual_mode=True,
)
# MANUAL_LABEL_STRATEGY: scale=1.0, max_pad=10
# Should only add 10px padding each side (but scale=1.0 means no scaling)
# Actually with scale=1.0, no extra ratios, we get 0 expansion from scaling
# Only max_pad=10 applies as a limit, but there's no expansion to limit
# So result should be same as original
assert result == (100, 200, 200, 250)
def test_manual_mode_ignores_field_type(self):
"""Verify manual_mode ignores field-specific strategies."""
bbox = (100, 200, 200, 250)
# Different fields should give same result in manual_mode
result_bankgiro = expand_bbox(
bbox=bbox,
image_width=1000,
image_height=1000,
field_type="bankgiro",
manual_mode=True,
)
result_ocr = expand_bbox(
bbox=bbox,
image_width=1000,
image_height=1000,
field_type="ocr_number",
manual_mode=True,
)
assert result_bankgiro == result_ocr
def test_manual_mode_vs_auto_mode_different(self):
"""Verify manual_mode produces different results than auto mode."""
bbox = (100, 200, 200, 250)
auto_result = expand_bbox(
bbox=bbox,
image_width=1000,
image_height=1000,
field_type="bankgiro", # Has extra_left_ratio=0.80
manual_mode=False,
)
manual_result = expand_bbox(
bbox=bbox,
image_width=1000,
image_height=1000,
field_type="bankgiro",
manual_mode=True,
)
# Auto mode should expand more (especially to the left for bankgiro)
assert auto_result[0] < manual_result[0] # Auto x0 is more left
def test_manual_mode_clamps_to_image_bounds(self):
"""Verify manual_mode still respects image boundaries."""
bbox = (5, 5, 50, 50) # Close to top-left corner
result = expand_bbox(
bbox=bbox,
image_width=1000,
image_height=1000,
field_type="test",
manual_mode=True,
)
# Should clamp to 0
assert result[0] >= 0
assert result[1] >= 0

View File

@@ -0,0 +1,192 @@
"""
Tests for ScaleStrategy configuration.
Tests verify that scale strategies are properly defined, immutable,
and cover all required fields.
"""
import pytest
from shared.bbox import (
ScaleStrategy,
DEFAULT_STRATEGY,
MANUAL_LABEL_STRATEGY,
FIELD_SCALE_STRATEGIES,
)
from shared.fields import CLASS_NAMES
class TestScaleStrategyDataclass:
"""Tests for ScaleStrategy dataclass behavior."""
def test_default_strategy_values(self):
"""Verify default strategy has expected default values."""
strategy = ScaleStrategy()
assert strategy.scale_x == 1.15
assert strategy.scale_y == 1.15
assert strategy.extra_top_ratio == 0.0
assert strategy.extra_bottom_ratio == 0.0
assert strategy.extra_left_ratio == 0.0
assert strategy.extra_right_ratio == 0.0
assert strategy.max_pad_x == 50
assert strategy.max_pad_y == 50
def test_scale_strategy_immutability(self):
"""Verify ScaleStrategy is frozen (immutable)."""
strategy = ScaleStrategy()
with pytest.raises(AttributeError):
strategy.scale_x = 2.0 # type: ignore
def test_custom_strategy_values(self):
"""Verify custom values are properly set."""
strategy = ScaleStrategy(
scale_x=1.5,
scale_y=1.8,
extra_top_ratio=0.6,
extra_left_ratio=0.8,
max_pad_x=100,
max_pad_y=150,
)
assert strategy.scale_x == 1.5
assert strategy.scale_y == 1.8
assert strategy.extra_top_ratio == 0.6
assert strategy.extra_left_ratio == 0.8
assert strategy.max_pad_x == 100
assert strategy.max_pad_y == 150
class TestDefaultStrategy:
"""Tests for DEFAULT_STRATEGY constant."""
def test_default_strategy_is_scale_strategy(self):
"""Verify DEFAULT_STRATEGY is a ScaleStrategy instance."""
assert isinstance(DEFAULT_STRATEGY, ScaleStrategy)
def test_default_strategy_matches_default_values(self):
"""Verify DEFAULT_STRATEGY has same values as ScaleStrategy()."""
expected = ScaleStrategy()
assert DEFAULT_STRATEGY == expected
class TestManualLabelStrategy:
"""Tests for MANUAL_LABEL_STRATEGY constant."""
def test_manual_label_strategy_is_scale_strategy(self):
"""Verify MANUAL_LABEL_STRATEGY is a ScaleStrategy instance."""
assert isinstance(MANUAL_LABEL_STRATEGY, ScaleStrategy)
def test_manual_label_strategy_has_no_scaling(self):
"""Verify MANUAL_LABEL_STRATEGY has scale factors of 1.0."""
assert MANUAL_LABEL_STRATEGY.scale_x == 1.0
assert MANUAL_LABEL_STRATEGY.scale_y == 1.0
def test_manual_label_strategy_has_no_directional_expansion(self):
"""Verify MANUAL_LABEL_STRATEGY has no directional expansion."""
assert MANUAL_LABEL_STRATEGY.extra_top_ratio == 0.0
assert MANUAL_LABEL_STRATEGY.extra_bottom_ratio == 0.0
assert MANUAL_LABEL_STRATEGY.extra_left_ratio == 0.0
assert MANUAL_LABEL_STRATEGY.extra_right_ratio == 0.0
def test_manual_label_strategy_has_small_max_pad(self):
"""Verify MANUAL_LABEL_STRATEGY has small max padding."""
assert MANUAL_LABEL_STRATEGY.max_pad_x <= 15
assert MANUAL_LABEL_STRATEGY.max_pad_y <= 15
class TestFieldScaleStrategies:
"""Tests for FIELD_SCALE_STRATEGIES dictionary."""
def test_all_class_names_have_strategies(self):
"""Verify all field class names have defined strategies."""
for class_name in CLASS_NAMES:
assert class_name in FIELD_SCALE_STRATEGIES, (
f"Missing strategy for field: {class_name}"
)
def test_strategies_are_scale_strategy_instances(self):
"""Verify all strategies are ScaleStrategy instances."""
for field_name, strategy in FIELD_SCALE_STRATEGIES.items():
assert isinstance(strategy, ScaleStrategy), (
f"Strategy for {field_name} is not a ScaleStrategy"
)
def test_scale_values_are_greater_than_one(self):
"""Verify all scale values are >= 1.0 (expansion, not contraction)."""
for field_name, strategy in FIELD_SCALE_STRATEGIES.items():
assert strategy.scale_x >= 1.0, (
f"{field_name} scale_x should be >= 1.0"
)
assert strategy.scale_y >= 1.0, (
f"{field_name} scale_y should be >= 1.0"
)
def test_extra_ratios_are_non_negative(self):
"""Verify all extra ratios are >= 0."""
for field_name, strategy in FIELD_SCALE_STRATEGIES.items():
assert strategy.extra_top_ratio >= 0, (
f"{field_name} extra_top_ratio should be >= 0"
)
assert strategy.extra_bottom_ratio >= 0, (
f"{field_name} extra_bottom_ratio should be >= 0"
)
assert strategy.extra_left_ratio >= 0, (
f"{field_name} extra_left_ratio should be >= 0"
)
assert strategy.extra_right_ratio >= 0, (
f"{field_name} extra_right_ratio should be >= 0"
)
def test_max_pad_values_are_positive(self):
"""Verify all max_pad values are > 0."""
for field_name, strategy in FIELD_SCALE_STRATEGIES.items():
assert strategy.max_pad_x > 0, (
f"{field_name} max_pad_x should be > 0"
)
assert strategy.max_pad_y > 0, (
f"{field_name} max_pad_y should be > 0"
)
class TestSpecificFieldStrategies:
"""Tests for specific field strategy configurations."""
def test_ocr_number_expands_upward(self):
"""Verify ocr_number strategy expands upward to capture label."""
strategy = FIELD_SCALE_STRATEGIES["ocr_number"]
assert strategy.extra_top_ratio > 0.0
assert strategy.extra_top_ratio >= 0.5 # Significant upward expansion
def test_bankgiro_expands_leftward(self):
"""Verify bankgiro strategy expands leftward to capture prefix."""
strategy = FIELD_SCALE_STRATEGIES["bankgiro"]
assert strategy.extra_left_ratio > 0.0
assert strategy.extra_left_ratio >= 0.5 # Significant leftward expansion
def test_plusgiro_expands_leftward(self):
"""Verify plusgiro strategy expands leftward to capture prefix."""
strategy = FIELD_SCALE_STRATEGIES["plusgiro"]
assert strategy.extra_left_ratio > 0.0
assert strategy.extra_left_ratio >= 0.5
def test_amount_expands_rightward(self):
"""Verify amount strategy expands rightward for currency symbol."""
strategy = FIELD_SCALE_STRATEGIES["amount"]
assert strategy.extra_right_ratio > 0.0
def test_invoice_date_expands_upward(self):
"""Verify invoice_date strategy expands upward to capture label."""
strategy = FIELD_SCALE_STRATEGIES["invoice_date"]
assert strategy.extra_top_ratio > 0.0
def test_invoice_due_date_expands_upward_and_leftward(self):
"""Verify invoice_due_date strategy expands both up and left."""
strategy = FIELD_SCALE_STRATEGIES["invoice_due_date"]
assert strategy.extra_top_ratio > 0.0
assert strategy.extra_left_ratio > 0.0
def test_payment_line_has_minimal_expansion(self):
"""Verify payment_line has conservative expansion (machine code)."""
strategy = FIELD_SCALE_STRATEGIES["payment_line"]
# Payment line is machine-readable, needs minimal expansion
assert strategy.scale_x <= 1.2
assert strategy.scale_y <= 1.3

View File

@@ -16,6 +16,7 @@ from shared.fields import (
FIELD_CLASSES,
FIELD_CLASS_IDS,
CLASS_TO_FIELD,
FIELD_TO_CLASS,
CSV_TO_CLASS_MAPPING,
TRAINING_FIELD_CLASSES,
NUM_CLASSES,
@@ -133,6 +134,20 @@ class TestMappingConsistency:
assert fd.field_name in TRAINING_FIELD_CLASSES
assert TRAINING_FIELD_CLASSES[fd.field_name] == fd.class_id
def test_field_to_class_is_inverse_of_class_to_field(self):
"""Verify FIELD_TO_CLASS and CLASS_TO_FIELD are proper inverses."""
for class_name, field_name in CLASS_TO_FIELD.items():
assert FIELD_TO_CLASS[field_name] == class_name
for field_name, class_name in FIELD_TO_CLASS.items():
assert CLASS_TO_FIELD[class_name] == field_name
def test_field_to_class_has_all_fields(self):
"""Verify FIELD_TO_CLASS has mapping for all field names."""
for fd in FIELD_DEFINITIONS:
assert fd.field_name in FIELD_TO_CLASS
assert FIELD_TO_CLASS[fd.field_name] == fd.class_name
class TestSpecificFieldDefinitions:
"""Tests for specific field definitions to catch common mistakes."""