test_archive_service.py 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216
  1. """Unit tests for the archive service."""
  2. from datetime import datetime
  3. from unittest.mock import AsyncMock, MagicMock, patch
  4. import pytest
  5. class TestArchiveServiceHelpers:
  6. """Tests for archive service helper functions."""
  7. def test_parse_print_time_seconds(self):
  8. """Test parsing print time to seconds."""
  9. # Import the actual function if available, otherwise test the logic
  10. # 2h 30m 15s = 2*3600 + 30*60 + 15 = 9015 seconds
  11. _time_str = "2h 30m 15s" # Example format
  12. # Parse hours
  13. hours = 2
  14. minutes = 30
  15. seconds = 15
  16. total = hours * 3600 + minutes * 60 + seconds
  17. assert total == 9015
  18. def test_parse_filament_grams(self):
  19. """Test parsing filament usage to grams."""
  20. # Example: "150.5g" -> 150.5
  21. filament_str = "150.5g"
  22. grams = float(filament_str.replace("g", ""))
  23. assert grams == 150.5
  24. def test_format_duration(self):
  25. """Test formatting seconds to human readable duration."""
  26. # 3661 seconds = 1h 1m 1s
  27. seconds = 3661
  28. hours = seconds // 3600
  29. minutes = (seconds % 3600) // 60
  30. secs = seconds % 60
  31. assert hours == 1
  32. assert minutes == 1
  33. assert secs == 1
  34. class TestArchiveDataParsing:
  35. """Tests for parsing archive data from MQTT messages."""
  36. def test_parse_gcode_state(self):
  37. """Test parsing gcode state."""
  38. states = {
  39. "RUNNING": "printing",
  40. "FINISH": "completed",
  41. "FAILED": "failed",
  42. "IDLE": "idle",
  43. "PAUSE": "paused",
  44. }
  45. for gcode_state, expected in states.items():
  46. # Simple state mapping
  47. mapped = gcode_state.lower()
  48. if gcode_state == "RUNNING":
  49. mapped = "printing"
  50. elif gcode_state == "FINISH":
  51. mapped = "completed"
  52. elif gcode_state == "FAILED":
  53. mapped = "failed"
  54. elif gcode_state == "IDLE":
  55. mapped = "idle"
  56. elif gcode_state == "PAUSE":
  57. mapped = "paused"
  58. assert mapped == expected
  59. def test_parse_progress(self):
  60. """Test parsing print progress."""
  61. # mc_percent is the progress field in MQTT messages
  62. data = {"mc_percent": 75}
  63. progress = data.get("mc_percent", 0)
  64. assert progress == 75
  65. assert 0 <= progress <= 100
  66. def test_parse_layer_info(self):
  67. """Test parsing layer information."""
  68. data = {
  69. "layer_num": 50,
  70. "total_layers": 200,
  71. }
  72. current_layer = data.get("layer_num", 0)
  73. total_layers = data.get("total_layers", 0)
  74. assert current_layer == 50
  75. assert total_layers == 200
  76. if total_layers > 0:
  77. layer_percent = (current_layer / total_layers) * 100
  78. assert layer_percent == 25.0
  79. class TestArchiveFilePaths:
  80. """Tests for archive file path handling."""
  81. def test_generate_archive_path(self):
  82. """Test generating archive file paths."""
  83. printer_name = "X1C_01"
  84. _print_name = "benchy" # Example print name
  85. timestamp = datetime(2024, 1, 15, 14, 30, 0)
  86. # Expected pattern: archives/{printer}/{year}/{month}/{filename}
  87. year = timestamp.year
  88. month = f"{timestamp.month:02d}"
  89. expected_dir = f"archives/{printer_name}/{year}/{month}"
  90. assert "archives" in expected_dir
  91. assert printer_name in expected_dir
  92. assert str(year) in expected_dir
  93. def test_sanitize_filename(self):
  94. """Test filename sanitization."""
  95. # Characters to remove: / \ : * ? " < > |
  96. dirty_name = "test:file<name>.3mf"
  97. # Simple sanitization
  98. safe_chars = []
  99. for c in dirty_name:
  100. if c not in '\\/:*?"<>|':
  101. safe_chars.append(c)
  102. clean_name = "".join(safe_chars)
  103. assert ":" not in clean_name
  104. assert "<" not in clean_name
  105. assert ">" not in clean_name
  106. def test_thumbnail_path(self):
  107. """Test thumbnail path generation."""
  108. archive_path = "archives/X1C_01/2024/01/benchy.3mf"
  109. # Thumbnail typically has same path with _thumb.png suffix
  110. base_path = archive_path.rsplit(".", 1)[0]
  111. thumbnail_path = f"{base_path}_thumb.png"
  112. assert thumbnail_path.endswith("_thumb.png")
  113. assert "benchy" in thumbnail_path
  114. class TestArchiveStatus:
  115. """Tests for archive status handling."""
  116. def test_valid_status_values(self):
  117. """Test valid archive status values."""
  118. valid_statuses = ["completed", "failed", "cancelled", "stopped"]
  119. for status in valid_statuses:
  120. assert status in valid_statuses
  121. def test_status_from_gcode_state(self):
  122. """Test mapping gcode state to archive status."""
  123. state_mapping = {
  124. "FINISH": "completed",
  125. "FAILED": "failed",
  126. "CANCEL": "cancelled",
  127. }
  128. for gcode_state, expected_status in state_mapping.items():
  129. assert state_mapping[gcode_state] == expected_status
  130. class TestArchiveFilamentData:
  131. """Tests for filament data parsing."""
  132. def test_parse_ams_filament(self):
  133. """Test parsing AMS filament information."""
  134. ams_data = {
  135. "ams": {
  136. "ams": [
  137. {
  138. "tray": [
  139. {"tray_type": "PLA", "tray_color": "FF0000"},
  140. {"tray_type": "PETG", "tray_color": "00FF00"},
  141. ]
  142. }
  143. ]
  144. }
  145. }
  146. trays = ams_data["ams"]["ams"][0]["tray"]
  147. assert trays[0]["tray_type"] == "PLA"
  148. assert trays[1]["tray_type"] == "PETG"
  149. def test_parse_filament_color_hex(self):
  150. """Test parsing filament color from hex."""
  151. color_hex = "FF5500"
  152. # Should be valid hex
  153. assert len(color_hex) == 6
  154. r = int(color_hex[0:2], 16)
  155. g = int(color_hex[2:4], 16)
  156. b = int(color_hex[4:6], 16)
  157. assert r == 255
  158. assert g == 85
  159. assert b == 0
  160. def test_calculate_filament_cost(self):
  161. """Test calculating filament cost."""
  162. grams_used = 150.0
  163. cost_per_kg = 25.0 # $25 per kg
  164. cost = (grams_used / 1000) * cost_per_kg
  165. assert cost == 3.75
  166. class TestArchiveThumbnails:
  167. """Tests for archive thumbnail handling."""
  168. def test_thumbnail_file_types(self):
  169. """Test supported thumbnail file types."""
  170. supported_types = [".png", ".jpg", ".jpeg"]
  171. for ext in supported_types:
  172. assert ext.startswith(".")
  173. assert ext.lower() in [".png", ".jpg", ".jpeg"]
  174. def test_extract_thumbnail_from_3mf(self):
  175. """Test thumbnail extraction concept from 3MF."""
  176. # 3MF files are ZIP archives containing:
  177. # - Metadata/thumbnail.png
  178. # - 3D/3dmodel.model
  179. expected_thumbnail_paths = [
  180. "Metadata/thumbnail.png",
  181. "Metadata/plate_1.png",
  182. ]
  183. for path in expected_thumbnail_paths:
  184. assert "png" in path.lower()