|
| 1 | +"""Unit tests for the Endpoint and Step Pydantic schemas.""" |
| 2 | + |
| 3 | +from __future__ import annotations |
| 4 | + |
| 5 | +import pytest |
| 6 | +from pydantic import ValidationError |
| 7 | + |
| 8 | +from app.config.constants import ( |
| 9 | + EndpointStepCPU, |
| 10 | + EndpointStepIO, |
| 11 | + EndpointStepRAM, |
| 12 | + Metrics, |
| 13 | +) |
| 14 | +from app.schemas.system_topology_schema.endpoint_schema import Endpoint, Step |
| 15 | + |
| 16 | + |
| 17 | +# --------------------------------------------------------------------------- # |
| 18 | +# Helper functions to build minimal valid Step objects |
| 19 | +# --------------------------------------------------------------------------- # |
| 20 | +def cpu_step(value: float = 0.1) -> Step: |
| 21 | + """Return a minimal valid CPU-bound Step.""" |
| 22 | + return Step( |
| 23 | + kind=EndpointStepCPU.CPU_BOUND_OPERATION, |
| 24 | + step_metrics={Metrics.CPU_TIME: value}, |
| 25 | + ) |
| 26 | + |
| 27 | + |
| 28 | +def ram_step(value: int = 128) -> Step: |
| 29 | + """Return a minimal valid RAM Step.""" |
| 30 | + return Step( |
| 31 | + kind=EndpointStepRAM.RAM, |
| 32 | + step_metrics={Metrics.NECESSARY_RAM: value}, |
| 33 | + ) |
| 34 | + |
| 35 | + |
| 36 | +def io_step(value: float = 0.05) -> Step: |
| 37 | + """Return a minimal valid I/O Step.""" |
| 38 | + return Step( |
| 39 | + kind=EndpointStepIO.WAIT, |
| 40 | + step_metrics={Metrics.IO_WAITING_TIME: value}, |
| 41 | + ) |
| 42 | + |
| 43 | + |
| 44 | +# --------------------------------------------------------------------------- # |
| 45 | +# Positive test cases |
| 46 | +# --------------------------------------------------------------------------- # |
| 47 | +def test_valid_cpu_step() -> None: |
| 48 | + """Test that a CPU step with correct 'cpu_time' metric passes validation.""" |
| 49 | + step = cpu_step() |
| 50 | + # The metric value must match the input |
| 51 | + assert step.step_metrics[Metrics.CPU_TIME] == 0.1 |
| 52 | + |
| 53 | + |
| 54 | +def test_valid_ram_step() -> None: |
| 55 | + """Test that a RAM step with correct 'necessary_ram' metric passes validation.""" |
| 56 | + step = ram_step() |
| 57 | + assert step.step_metrics[Metrics.NECESSARY_RAM] == 128 |
| 58 | + |
| 59 | + |
| 60 | +def test_valid_io_step() -> None: |
| 61 | + """Test that an I/O step with correct 'io_waiting_time' metric passes validation.""" |
| 62 | + step = io_step() |
| 63 | + assert step.step_metrics[Metrics.IO_WAITING_TIME] == 0.05 |
| 64 | + |
| 65 | + |
| 66 | +def test_endpoint_with_mixed_steps() -> None: |
| 67 | + """Test that an Endpoint with multiple valid Step instances normalizes the name.""" |
| 68 | + ep = Endpoint( |
| 69 | + endpoint_name="/Predict", |
| 70 | + steps=[cpu_step(), ram_step(), io_step()], |
| 71 | + ) |
| 72 | + # endpoint_name should be lowercased by the validator |
| 73 | + assert ep.endpoint_name == "/predict" |
| 74 | + # All steps should be present in the list |
| 75 | + assert len(ep.steps) == 3 |
| 76 | + |
| 77 | + |
| 78 | +# --------------------------------------------------------------------------- # |
| 79 | +# Negative test cases |
| 80 | +# --------------------------------------------------------------------------- # |
| 81 | +@pytest.mark.parametrize( |
| 82 | + ("kind", "bad_metrics"), |
| 83 | + [ |
| 84 | + # CPU step with RAM metric |
| 85 | + (EndpointStepCPU.CPU_BOUND_OPERATION, {Metrics.NECESSARY_RAM: 64}), |
| 86 | + # RAM step with CPU metric |
| 87 | + (EndpointStepRAM.RAM, {Metrics.CPU_TIME: 0.2}), |
| 88 | + # I/O step with CPU metric |
| 89 | + (EndpointStepIO.DB, {Metrics.CPU_TIME: 0.05}), |
| 90 | + ], |
| 91 | +) |
| 92 | +def test_incoherent_kind_metric_pair( |
| 93 | + kind: EndpointStepCPU | EndpointStepRAM | EndpointStepIO, |
| 94 | + bad_metrics: dict[Metrics, float | int], |
| 95 | +) -> None: |
| 96 | + """Test that mismatched kind and metric combinations raise ValidationError.""" |
| 97 | + with pytest.raises(ValidationError): |
| 98 | + Step(kind=kind, step_metrics=bad_metrics) |
| 99 | + |
| 100 | + |
| 101 | +def test_multiple_metrics_not_allowed() -> None: |
| 102 | + """Test that providing multiple metrics in a single Step raises ValidationError.""" |
| 103 | + with pytest.raises(ValidationError): |
| 104 | + Step( |
| 105 | + kind=EndpointStepCPU.CPU_BOUND_OPERATION, |
| 106 | + step_metrics={ |
| 107 | + Metrics.CPU_TIME: 0.1, |
| 108 | + Metrics.NECESSARY_RAM: 64, |
| 109 | + }, |
| 110 | + ) |
| 111 | + |
| 112 | + |
| 113 | +def test_empty_metrics_rejected() -> None: |
| 114 | + """Test that an empty metrics dict is rejected by the validator.""" |
| 115 | + with pytest.raises(ValidationError): |
| 116 | + Step(kind=EndpointStepCPU.CPU_BOUND_OPERATION, step_metrics={}) |
| 117 | + |
| 118 | + |
| 119 | +def test_wrong_metric_name_for_io() -> None: |
| 120 | + """Test that an I/O step with a non-I/O metric key is rejected.""" |
| 121 | + with pytest.raises(ValidationError): |
| 122 | + Step( |
| 123 | + kind=EndpointStepIO.CACHE, |
| 124 | + step_metrics={Metrics.NECESSARY_RAM: 64}, |
| 125 | + ) |
0 commit comments