|
3 | 3 | from __future__ import annotations |
4 | 4 |
|
5 | 5 | import tempfile |
| 6 | +import warnings |
6 | 7 | from datetime import datetime, timedelta, timezone |
7 | 8 | from unittest.mock import MagicMock, call |
8 | 9 |
|
9 | 10 | import pytest |
| 11 | +from sift.test_reports.v1.test_reports_pb2 import ( |
| 12 | + TestMeasurement as TestMeasurementProto, |
| 13 | +) |
10 | 14 |
|
| 15 | +from sift_client.sift_types.channel import Channel, ChannelDataType |
11 | 16 | from sift_client.sift_types.test_report import ( |
12 | 17 | ErrorInfo, |
13 | 18 | NumericBounds, |
14 | 19 | TestMeasurement, |
| 20 | + TestMeasurementCreate, |
15 | 21 | TestMeasurementType, |
16 | 22 | TestReport, |
17 | 23 | TestStatus, |
@@ -84,6 +90,9 @@ def mock_test_measurement(mock_client): |
84 | 90 | unit="Celsius", |
85 | 91 | passed=True, |
86 | 92 | timestamp=simulated_time, |
| 93 | + description="Expected nominal: 25.0C", |
| 94 | + metadata={"part_number": "PN-001", "serial_number": "SN-42"}, |
| 95 | + channel_names=["temperature_celsius"], |
87 | 96 | ) |
88 | 97 | test_measurement._apply_client_to_instance(mock_client) |
89 | 98 | return test_measurement |
@@ -298,3 +307,135 @@ def test_upload_attachment(self, mock_test_report, mock_test_step, mock_client): |
298 | 307 | ), |
299 | 308 | ] |
300 | 309 | ) |
| 310 | + |
| 311 | + def test_measurement_description_truncates_with_warning(self): |
| 312 | + """Description over the server limit is truncated and a UserWarning is raised.""" |
| 313 | + over_limit = "x" * 2001 |
| 314 | + with pytest.warns(UserWarning, match="exceeds 2000 characters"): |
| 315 | + create = TestMeasurementCreate( |
| 316 | + test_step_id="step_123", |
| 317 | + name="m", |
| 318 | + passed=True, |
| 319 | + timestamp=datetime.now(timezone.utc), |
| 320 | + numeric_value=1.0, |
| 321 | + description=over_limit, |
| 322 | + ) |
| 323 | + assert create.description is not None |
| 324 | + assert len(create.description) == 2000 |
| 325 | + |
| 326 | + def test_measurement_description_at_limit_is_not_truncated(self): |
| 327 | + """A description exactly at the limit should not warn or truncate.""" |
| 328 | + at_limit = "x" * 2000 |
| 329 | + with warnings.catch_warnings(): |
| 330 | + warnings.simplefilter("error") # any warning fails the test |
| 331 | + create = TestMeasurementCreate( |
| 332 | + test_step_id="step_123", |
| 333 | + name="m", |
| 334 | + passed=True, |
| 335 | + timestamp=datetime.now(timezone.utc), |
| 336 | + numeric_value=1.0, |
| 337 | + description=at_limit, |
| 338 | + ) |
| 339 | + assert create.description == at_limit |
| 340 | + |
| 341 | + def test_measurement_channel_names_accepts_strings(self): |
| 342 | + """channel_names accepts a homogeneous list of channel name strings.""" |
| 343 | + create = TestMeasurementCreate( |
| 344 | + test_step_id="step_123", |
| 345 | + name="m", |
| 346 | + passed=True, |
| 347 | + timestamp=datetime.now(timezone.utc), |
| 348 | + numeric_value=1.0, |
| 349 | + channel_names=["temperature_celsius", "pressure_psi"], |
| 350 | + ) |
| 351 | + assert create.channel_names == ["temperature_celsius", "pressure_psi"] |
| 352 | + proto = create.to_proto() |
| 353 | + assert list(proto.channel_names) == ["temperature_celsius", "pressure_psi"] |
| 354 | + |
| 355 | + def test_measurement_channel_names_accepts_channels(self): |
| 356 | + """channel_names accepts a homogeneous list of Channel instances; names are extracted at serialization.""" |
| 357 | + now = datetime.now(timezone.utc) |
| 358 | + |
| 359 | + def _channel(name: str) -> Channel: |
| 360 | + return Channel( |
| 361 | + proto=MagicMock(), |
| 362 | + id_=f"channel_{name}", |
| 363 | + name=name, |
| 364 | + data_type=ChannelDataType.DOUBLE, |
| 365 | + description="", |
| 366 | + unit="", |
| 367 | + bit_field_elements=[], |
| 368 | + enum_types={}, |
| 369 | + asset_id="asset_1", |
| 370 | + created_date=now, |
| 371 | + modified_date=now, |
| 372 | + created_by_user_id="user1", |
| 373 | + modified_by_user_id="user1", |
| 374 | + ) |
| 375 | + |
| 376 | + create = TestMeasurementCreate( |
| 377 | + test_step_id="step_123", |
| 378 | + name="m", |
| 379 | + passed=True, |
| 380 | + timestamp=now, |
| 381 | + numeric_value=1.0, |
| 382 | + channel_names=[_channel("temperature_celsius"), _channel("pressure_psi")], |
| 383 | + ) |
| 384 | + proto = create.to_proto() |
| 385 | + assert list(proto.channel_names) == ["temperature_celsius", "pressure_psi"] |
| 386 | + |
| 387 | + def test_measurement_create_to_proto_writes_new_fields(self): |
| 388 | + """to_proto carries description, metadata, and channel_names onto the proto.""" |
| 389 | + create = TestMeasurementCreate( |
| 390 | + test_step_id="step_123", |
| 391 | + name="m", |
| 392 | + passed=True, |
| 393 | + timestamp=datetime.now(timezone.utc), |
| 394 | + numeric_value=1.0, |
| 395 | + description="note", |
| 396 | + metadata={"pn": "PN-001", "count": 3, "flag": True}, |
| 397 | + channel_names=["chan_a", "chan_b"], |
| 398 | + ) |
| 399 | + proto = create.to_proto() |
| 400 | + assert proto.description == "note" |
| 401 | + assert list(proto.channel_names) == ["chan_a", "chan_b"] |
| 402 | + proto_keys = {m.key.name for m in proto.metadata} |
| 403 | + assert proto_keys == {"pn", "count", "flag"} |
| 404 | + |
| 405 | + def test_measurement_from_proto_round_trips_new_fields(self): |
| 406 | + """A proto with the new fields populated round-trips into TestMeasurement.""" |
| 407 | + ts = datetime.now(timezone.utc) |
| 408 | + source = TestMeasurementCreate( |
| 409 | + test_step_id="step_123", |
| 410 | + name="m", |
| 411 | + passed=True, |
| 412 | + timestamp=ts, |
| 413 | + numeric_value=1.0, |
| 414 | + description="note", |
| 415 | + metadata={"pn": "PN-001", "count": 3}, |
| 416 | + channel_names=["chan_a"], |
| 417 | + ).to_proto() |
| 418 | + source.measurement_id = "measurement_456" |
| 419 | + source.test_report_id = "report_789" |
| 420 | + |
| 421 | + measurement = TestMeasurement._from_proto(source) |
| 422 | + |
| 423 | + assert measurement.description == "note" |
| 424 | + assert measurement.metadata == {"pn": "PN-001", "count": 3} |
| 425 | + assert measurement.channel_names == ["chan_a"] |
| 426 | + |
| 427 | + def test_measurement_from_proto_handles_absent_new_fields(self): |
| 428 | + """Proto with unset description/metadata/channel_names yields None on the model.""" |
| 429 | + proto = TestMeasurementProto( |
| 430 | + measurement_id="measurement_abc", |
| 431 | + measurement_type=TestMeasurementType.DOUBLE.value, |
| 432 | + name="m", |
| 433 | + test_step_id="step_123", |
| 434 | + test_report_id="report_789", |
| 435 | + passed=True, |
| 436 | + ) |
| 437 | + proto.timestamp.FromDatetime(datetime.now(timezone.utc)) |
| 438 | + measurement = TestMeasurement._from_proto(proto) |
| 439 | + assert measurement.description is None |
| 440 | + assert measurement.metadata is None |
| 441 | + assert measurement.channel_names is None |
0 commit comments