test_slicer_preset_values.py 3.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687
  1. """Tests for resolving a preset's effective values via the sidecar.
  2. The slice modal's settings panel needs the values a preset actually sets, not
  3. the option schema's compiled-in defaults. Only the sidecar can answer that: a
  4. "Standard" pick is a ``{inherits: ...}`` stub on our side, and local/cloud
  5. presets are deltas whose remainder lives in the sidecar's bundled profile tree.
  6. """
  7. import json
  8. import httpx
  9. import pytest
  10. from backend.app.services.slicer_api import SlicerApiService, SlicerApiUnavailableError
  11. PROCESS_STUB = json.dumps({"inherits": "0.20mm Standard @BBL X1C", "from": "system"})
  12. def _service(handler) -> SlicerApiService:
  13. transport = httpx.MockTransport(handler)
  14. client = httpx.AsyncClient(transport=transport)
  15. return SlicerApiService("http://sidecar:3003", client=client)
  16. class TestResolveProfile:
  17. @pytest.mark.asyncio
  18. async def test_returns_the_flattened_values(self):
  19. def handler(request: httpx.Request) -> httpx.Response:
  20. assert request.url.path == "/profiles/resolve"
  21. body = json.loads(request.content)
  22. assert body["category"] == "process"
  23. # The stub goes out as an object, not a JSON string.
  24. assert body["profile"]["inherits"] == "0.20mm Standard @BBL X1C"
  25. return httpx.Response(200, json={"profile": {"line_width": "0.42", "wall_loops": "2"}})
  26. service = _service(handler)
  27. assert await service.resolve_profile(PROCESS_STUB, "process") == {
  28. "line_width": "0.42",
  29. "wall_loops": "2",
  30. }
  31. @pytest.mark.asyncio
  32. async def test_a_sidecar_without_the_endpoint_is_not_an_error(self):
  33. # Older images 404 here. The panel degrades to schema defaults and says
  34. # so; failing would make the modal unusable against an old sidecar.
  35. service = _service(lambda request: httpx.Response(404, json={"message": "Not Found"}))
  36. assert await service.resolve_profile(PROCESS_STUB, "process") is None
  37. @pytest.mark.asyncio
  38. async def test_a_sidecar_error_degrades_rather_than_raising(self):
  39. service = _service(lambda request: httpx.Response(500, json={"message": "boom"}))
  40. assert await service.resolve_profile(PROCESS_STUB, "process") is None
  41. @pytest.mark.asyncio
  42. async def test_unreachable_sidecar_still_raises(self):
  43. # Distinct from "too old": the caller reports this as slicing being
  44. # unavailable rather than silently showing defaults forever.
  45. def handler(request: httpx.Request) -> httpx.Response:
  46. raise httpx.ConnectError("refused")
  47. with pytest.raises(SlicerApiUnavailableError):
  48. await _service(handler).resolve_profile(PROCESS_STUB, "process")
  49. @pytest.mark.asyncio
  50. async def test_unparseable_preset_content_returns_none(self):
  51. service = _service(lambda request: httpx.Response(200, json={"profile": {}}))
  52. assert await service.resolve_profile("not json", "process") is None
  53. @pytest.mark.asyncio
  54. async def test_a_response_without_a_profile_object_returns_none(self):
  55. # Guards against reading a differently-shaped body as if it were values.
  56. service = _service(lambda request: httpx.Response(200, json={"ok": True}))
  57. assert await service.resolve_profile(PROCESS_STUB, "process") is None
  58. @pytest.mark.asyncio
  59. async def test_an_already_flat_preset_round_trips(self):
  60. flat = json.dumps({"line_width": "0.45", "type": "process"})
  61. def handler(request: httpx.Request) -> httpx.Response:
  62. body = json.loads(request.content)
  63. assert "inherits" not in body["profile"]
  64. return httpx.Response(200, json={"profile": json.loads(flat)})
  65. assert await _service(handler).resolve_profile(flat, "process") == {
  66. "line_width": "0.45",
  67. "type": "process",
  68. }