| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731 |
- """Unit tests for the archive service."""
- from datetime import datetime
- class TestArchiveServiceHelpers:
- """Tests for archive service helper functions."""
- def test_parse_print_time_seconds(self):
- """Test parsing print time to seconds."""
- # Import the actual function if available, otherwise test the logic
- # 2h 30m 15s = 2*3600 + 30*60 + 15 = 9015 seconds
- _time_str = "2h 30m 15s" # Example format
- # Parse hours
- hours = 2
- minutes = 30
- seconds = 15
- total = hours * 3600 + minutes * 60 + seconds
- assert total == 9015
- def test_parse_filament_grams(self):
- """Test parsing filament usage to grams."""
- # Example: "150.5g" -> 150.5
- filament_str = "150.5g"
- grams = float(filament_str.replace("g", ""))
- assert grams == 150.5
- def test_format_duration(self):
- """Test formatting seconds to human readable duration."""
- # 3661 seconds = 1h 1m 1s
- seconds = 3661
- hours = seconds // 3600
- minutes = (seconds % 3600) // 60
- secs = seconds % 60
- assert hours == 1
- assert minutes == 1
- assert secs == 1
- class TestArchiveDataParsing:
- """Tests for parsing archive data from MQTT messages."""
- def test_parse_gcode_state(self):
- """Test parsing gcode state."""
- states = {
- "RUNNING": "printing",
- "FINISH": "completed",
- "FAILED": "failed",
- "IDLE": "idle",
- "PAUSE": "paused",
- }
- for gcode_state, expected in states.items():
- # Simple state mapping
- mapped = gcode_state.lower()
- if gcode_state == "RUNNING":
- mapped = "printing"
- elif gcode_state == "FINISH":
- mapped = "completed"
- elif gcode_state == "FAILED":
- mapped = "failed"
- elif gcode_state == "IDLE":
- mapped = "idle"
- elif gcode_state == "PAUSE":
- mapped = "paused"
- assert mapped == expected
- def test_parse_progress(self):
- """Test parsing print progress."""
- # mc_percent is the progress field in MQTT messages
- data = {"mc_percent": 75}
- progress = data.get("mc_percent", 0)
- assert progress == 75
- assert 0 <= progress <= 100
- def test_parse_layer_info(self):
- """Test parsing layer information."""
- data = {
- "layer_num": 50,
- "total_layers": 200,
- }
- current_layer = data.get("layer_num", 0)
- total_layers = data.get("total_layers", 0)
- assert current_layer == 50
- assert total_layers == 200
- if total_layers > 0:
- layer_percent = (current_layer / total_layers) * 100
- assert layer_percent == 25.0
- class TestArchiveFilePaths:
- """Tests for archive file path handling."""
- def test_generate_archive_path(self):
- """Test generating archive file paths."""
- printer_name = "X1C_01"
- _print_name = "benchy" # Example print name
- timestamp = datetime(2024, 1, 15, 14, 30, 0)
- # Expected pattern: archives/{printer}/{year}/{month}/{filename}
- year = timestamp.year
- month = f"{timestamp.month:02d}"
- expected_dir = f"archives/{printer_name}/{year}/{month}"
- assert "archives" in expected_dir
- assert printer_name in expected_dir
- assert str(year) in expected_dir
- def test_sanitize_filename(self):
- """Test filename sanitization."""
- # Characters to remove: / \ : * ? " < > |
- dirty_name = "test:file<name>.3mf"
- # Simple sanitization
- safe_chars = []
- for c in dirty_name:
- if c not in '\\/:*?"<>|':
- safe_chars.append(c)
- clean_name = "".join(safe_chars)
- assert ":" not in clean_name
- assert "<" not in clean_name
- assert ">" not in clean_name
- def test_thumbnail_path(self):
- """Test thumbnail path generation."""
- archive_path = "archives/X1C_01/2024/01/benchy.3mf"
- # Thumbnail typically has same path with _thumb.png suffix
- base_path = archive_path.rsplit(".", 1)[0]
- thumbnail_path = f"{base_path}_thumb.png"
- assert thumbnail_path.endswith("_thumb.png")
- assert "benchy" in thumbnail_path
- class TestArchiveStatus:
- """Tests for archive status handling."""
- def test_valid_status_values(self):
- """Test valid archive status values."""
- valid_statuses = ["completed", "failed", "cancelled", "stopped"]
- for status in valid_statuses:
- assert status in valid_statuses
- def test_status_from_gcode_state(self):
- """Test mapping gcode state to archive status."""
- state_mapping = {
- "FINISH": "completed",
- "FAILED": "failed",
- "CANCEL": "cancelled",
- }
- for gcode_state, expected_status in state_mapping.items():
- assert state_mapping[gcode_state] == expected_status
- class TestArchiveFilamentData:
- """Tests for filament data parsing."""
- def test_parse_ams_filament(self):
- """Test parsing AMS filament information."""
- ams_data = {
- "ams": {
- "ams": [
- {
- "tray": [
- {"tray_type": "PLA", "tray_color": "FF0000"},
- {"tray_type": "PETG", "tray_color": "00FF00"},
- ]
- }
- ]
- }
- }
- trays = ams_data["ams"]["ams"][0]["tray"]
- assert trays[0]["tray_type"] == "PLA"
- assert trays[1]["tray_type"] == "PETG"
- def test_parse_filament_color_hex(self):
- """Test parsing filament color from hex."""
- color_hex = "FF5500"
- # Should be valid hex
- assert len(color_hex) == 6
- r = int(color_hex[0:2], 16)
- g = int(color_hex[2:4], 16)
- b = int(color_hex[4:6], 16)
- assert r == 255
- assert g == 85
- assert b == 0
- def test_calculate_filament_cost(self):
- """Test calculating filament cost."""
- grams_used = 150.0
- cost_per_kg = 25.0 # $25 per kg
- cost = (grams_used / 1000) * cost_per_kg
- assert cost == 3.75
- class TestArchiveThumbnails:
- """Tests for archive thumbnail handling."""
- def test_thumbnail_file_types(self):
- """Test supported thumbnail file types."""
- supported_types = [".png", ".jpg", ".jpeg"]
- for ext in supported_types:
- assert ext.startswith(".")
- assert ext.lower() in [".png", ".jpg", ".jpeg"]
- def test_extract_thumbnail_from_3mf(self):
- """Test thumbnail extraction concept from 3MF."""
- # 3MF files are ZIP archives containing:
- # - Metadata/thumbnail.png
- # - 3D/3dmodel.model
- expected_thumbnail_paths = [
- "Metadata/thumbnail.png",
- "Metadata/plate_1.png",
- ]
- for path in expected_thumbnail_paths:
- assert "png" in path.lower()
- def test_extract_thumbnail_falls_back_to_auxiliaries(self, tmp_path):
- """#1493 follow-up: when BambuStudio's CLI runs with --arrange it
- rearranges objects but doesn't always emit a fresh
- ``Metadata/plate_N.png`` for the rearranged plate. The project-wide
- thumbnail under ``Auxiliaries/.thumbnails/`` survives though, and
- we use it as a cover-image fallback so re-sliced archive cards
- still render with a thumbnail."""
- import zipfile
- from backend.app.services.archive import ThreeMFParser
- threemf_path = tmp_path / "sliced.3mf"
- with zipfile.ZipFile(threemf_path, "w") as zf:
- zf.writestr("3D/3dmodel.model", "<model/>")
- # No Metadata/plate_1.png / thumbnail.png — only the
- # Auxiliaries project-wide thumbnail (what arranged slices
- # carry in practice).
- zf.writestr("Auxiliaries/.thumbnails/thumbnail_middle.png", b"PNGMIDDLE")
- parser = ThreeMFParser(str(threemf_path), plate_number=1)
- parsed = parser.parse()
- assert parsed.get("_thumbnail_data") == b"PNGMIDDLE"
- assert parsed.get("_thumbnail_ext") == ".png"
- def test_per_plate_png_wins_over_auxiliaries_fallback(self, tmp_path):
- """Order matters: when BOTH the per-plate preview and the
- Auxiliaries fallback are present, the per-plate one wins because
- it reflects the actual sliced layout."""
- import zipfile
- from backend.app.services.archive import ThreeMFParser
- threemf_path = tmp_path / "sliced.3mf"
- with zipfile.ZipFile(threemf_path, "w") as zf:
- zf.writestr("3D/3dmodel.model", "<model/>")
- zf.writestr("Metadata/plate_1.png", b"PLATE1")
- zf.writestr("Auxiliaries/.thumbnails/thumbnail_middle.png", b"PROJECT_WIDE")
- parser = ThreeMFParser(str(threemf_path), plate_number=1)
- parsed = parser.parse()
- assert parsed.get("_thumbnail_data") == b"PLATE1"
- class TestPrintableObjectsExtraction:
- """Tests for extracting printable objects count from 3MF files."""
- def test_extract_printable_objects_from_slice_info(self):
- """Test parsing printable objects from slice_info.config XML."""
- from defusedxml import ElementTree as ET
- # Example slice_info.config content with 4 objects
- slice_info_xml = """<?xml version="1.0" encoding="UTF-8"?>
- <config>
- <plate plate_idx="1">
- <metadata key="prediction" value="3600" />
- <metadata key="weight" value="50.5" />
- <object identify_id="1" name="Part_A" skipped="false" />
- <object identify_id="2" name="Part_B" skipped="false" />
- <object identify_id="3" name="Part_C" skipped="false" />
- <object identify_id="4" name="Part_D" skipped="true" />
- </plate>
- </config>
- """
- root = ET.fromstring(slice_info_xml)
- plate = root.find(".//plate")
- # Count non-skipped objects (should be 3, not 4)
- count = 0
- for obj in plate.findall("object"):
- skipped = obj.get("skipped", "false")
- if skipped.lower() != "true":
- count += 1
- assert count == 3 # 3 objects (Part_D is skipped)
- def test_extract_printable_objects_empty_plate(self):
- """Test handling plate with no objects."""
- from defusedxml import ElementTree as ET
- slice_info_xml = """<?xml version="1.0" encoding="UTF-8"?>
- <config>
- <plate plate_idx="1">
- <metadata key="prediction" value="0" />
- </plate>
- </config>
- """
- root = ET.fromstring(slice_info_xml)
- plate = root.find(".//plate")
- count = 0
- for obj in plate.findall("object"):
- skipped = obj.get("skipped", "false")
- if skipped.lower() != "true":
- count += 1
- assert count == 0
- def test_extract_printable_objects_all_skipped(self):
- """Test handling plate where all objects are skipped."""
- from defusedxml import ElementTree as ET
- slice_info_xml = """<?xml version="1.0" encoding="UTF-8"?>
- <config>
- <plate plate_idx="1">
- <object identify_id="1" name="Part_A" skipped="true" />
- <object identify_id="2" name="Part_B" skipped="true" />
- </plate>
- </config>
- """
- root = ET.fromstring(slice_info_xml)
- plate = root.find(".//plate")
- count = 0
- for obj in plate.findall("object"):
- skipped = obj.get("skipped", "false")
- if skipped.lower() != "true":
- count += 1
- assert count == 0 # All objects skipped
- class TestThreeMFPlateIndexExtraction:
- """Tests for extracting plate index from multi-plate 3MF exports (Issue #92)."""
- def test_extract_plate_index_from_slice_info(self):
- """Test parsing plate index from slice_info.config metadata."""
- from defusedxml import ElementTree as ET
- # Single-plate export from plate 5 of a multi-plate project
- slice_info_xml = """<?xml version="1.0" encoding="UTF-8"?>
- <config>
- <plate>
- <metadata key="index" value="5" />
- <metadata key="prediction" value="3600" />
- <metadata key="weight" value="50.5" />
- <object identify_id="1" name="Part_A" skipped="false" />
- </plate>
- </config>
- """
- root = ET.fromstring(slice_info_xml)
- plate = root.find(".//plate")
- plate_index = None
- for meta in plate.findall("metadata"):
- if meta.get("key") == "index":
- plate_index = int(meta.get("value"))
- break
- assert plate_index == 5
- def test_extract_plate_index_plate_1(self):
- """Test parsing plate index when it's plate 1."""
- from defusedxml import ElementTree as ET
- slice_info_xml = """<?xml version="1.0" encoding="UTF-8"?>
- <config>
- <plate>
- <metadata key="index" value="1" />
- <metadata key="prediction" value="1800" />
- </plate>
- </config>
- """
- root = ET.fromstring(slice_info_xml)
- plate = root.find(".//plate")
- plate_index = None
- for meta in plate.findall("metadata"):
- if meta.get("key") == "index":
- plate_index = int(meta.get("value"))
- break
- assert plate_index == 1
- def test_thumbnail_path_uses_plate_number(self):
- """Test that thumbnail path correctly uses the extracted plate number."""
- plate_number = 5
- thumbnail_paths = []
- if plate_number:
- thumbnail_paths.append(f"Metadata/plate_{plate_number}.png")
- thumbnail_paths.extend(
- [
- "Metadata/plate_1.png",
- "Metadata/thumbnail.png",
- ]
- )
- # First priority should be plate_5.png
- assert thumbnail_paths[0] == "Metadata/plate_5.png"
- @staticmethod
- def _enhance_print_name(print_name: str, plate_index: int) -> str:
- """Apply plate name enhancement logic from archive.py."""
- if plate_index and plate_index > 1:
- if print_name and f"Plate {plate_index}" not in print_name:
- print_name = f"{print_name} - Plate {plate_index}"
- return print_name
- def test_print_name_enhanced_for_plate_greater_than_1(self):
- """Test that print_name is enhanced with plate info for plate > 1."""
- assert self._enhance_print_name("Benchy", 5) == "Benchy - Plate 5"
- def test_print_name_not_enhanced_for_plate_1(self):
- """Test that print_name is NOT enhanced for plate 1."""
- assert self._enhance_print_name("Benchy", 1) == "Benchy"
- def test_print_name_not_duplicated(self):
- """Test that plate info is not added if already present in print_name."""
- assert self._enhance_print_name("Benchy - Plate 5", 5) == "Benchy - Plate 5"
- def test_high_plate_number_extraction(self):
- """Test extracting high plate numbers (e.g., plate 28)."""
- from defusedxml import ElementTree as ET
- slice_info_xml = """<?xml version="1.0" encoding="UTF-8"?>
- <config>
- <plate>
- <metadata key="index" value="28" />
- <metadata key="prediction" value="7200" />
- </plate>
- </config>
- """
- root = ET.fromstring(slice_info_xml)
- plate = root.find(".//plate")
- plate_index = None
- for meta in plate.findall("metadata"):
- if meta.get("key") == "index":
- plate_index = int(meta.get("value"))
- break
- assert plate_index == 28
- # Verify thumbnail would use correct plate
- thumbnail_path = f"Metadata/plate_{plate_index}.png"
- assert thumbnail_path == "Metadata/plate_28.png"
- class TestMultiPlate3MFParsing:
- """Tests for parsing multi-plate 3MF files (Issue #93)."""
- def test_parse_multiple_plates_from_slice_info(self):
- """Test parsing multiple plates from slice_info.config."""
- from defusedxml import ElementTree as ET
- # Multi-plate 3MF with 3 plates
- slice_info_xml = """<?xml version="1.0" encoding="UTF-8"?>
- <config>
- <plate>
- <metadata key="index" value="1" />
- <metadata key="prediction" value="3600" />
- <metadata key="weight" value="50.0" />
- <filament id="1" type="PLA" color="#FF0000" used_g="25.0" used_m="8.5" />
- <object identify_id="1" name="Part_A" skipped="false" />
- </plate>
- <plate>
- <metadata key="index" value="2" />
- <metadata key="prediction" value="7200" />
- <metadata key="weight" value="100.0" />
- <filament id="2" type="PETG" color="#00FF00" used_g="50.0" used_m="17.0" />
- <object identify_id="2" name="Part_B" skipped="false" />
- </plate>
- <plate>
- <metadata key="index" value="3" />
- <metadata key="prediction" value="1800" />
- <metadata key="weight" value="25.0" />
- <filament id="1" type="PLA" color="#FF0000" used_g="12.5" used_m="4.2" />
- <filament id="3" type="TPU" color="#0000FF" used_g="12.5" used_m="4.2" />
- <object identify_id="3" name="Part_C" skipped="false" />
- </plate>
- </config>
- """
- root = ET.fromstring(slice_info_xml)
- plates = root.findall(".//plate")
- assert len(plates) == 3
- # Parse each plate
- plate_data = []
- for plate_elem in plates:
- plate_info = {"index": None, "filaments": []}
- for meta in plate_elem.findall("metadata"):
- if meta.get("key") == "index":
- plate_info["index"] = int(meta.get("value"))
- for filament_elem in plate_elem.findall("filament"):
- used_g = float(filament_elem.get("used_g", "0"))
- if used_g > 0:
- plate_info["filaments"].append(
- {
- "slot_id": int(filament_elem.get("id")),
- "type": filament_elem.get("type"),
- "color": filament_elem.get("color"),
- "used_grams": used_g,
- }
- )
- plate_data.append(plate_info)
- # Verify plate 1
- assert plate_data[0]["index"] == 1
- assert len(plate_data[0]["filaments"]) == 1
- assert plate_data[0]["filaments"][0]["slot_id"] == 1
- assert plate_data[0]["filaments"][0]["type"] == "PLA"
- # Verify plate 2
- assert plate_data[1]["index"] == 2
- assert len(plate_data[1]["filaments"]) == 1
- assert plate_data[1]["filaments"][0]["slot_id"] == 2
- assert plate_data[1]["filaments"][0]["type"] == "PETG"
- # Verify plate 3 (has 2 filaments)
- assert plate_data[2]["index"] == 3
- assert len(plate_data[2]["filaments"]) == 2
- filament_types = {f["type"] for f in plate_data[2]["filaments"]}
- assert filament_types == {"PLA", "TPU"}
- def test_filter_filaments_by_plate_id(self):
- """Test filtering filaments for a specific plate."""
- from defusedxml import ElementTree as ET
- slice_info_xml = """<?xml version="1.0" encoding="UTF-8"?>
- <config>
- <plate>
- <metadata key="index" value="1" />
- <filament id="1" type="PLA" color="#FF0000" used_g="25.0" />
- </plate>
- <plate>
- <metadata key="index" value="2" />
- <filament id="2" type="PETG" color="#00FF00" used_g="50.0" />
- </plate>
- </config>
- """
- root = ET.fromstring(slice_info_xml)
- # Filter for plate 2 only
- target_plate_id = 2
- filaments = []
- for plate_elem in root.findall(".//plate"):
- plate_index = None
- for meta in plate_elem.findall("metadata"):
- if meta.get("key") == "index":
- plate_index = int(meta.get("value", "0"))
- break
- if plate_index == target_plate_id:
- for filament_elem in plate_elem.findall("filament"):
- used_g = float(filament_elem.get("used_g", "0"))
- if used_g > 0:
- filaments.append(
- {
- "slot_id": int(filament_elem.get("id")),
- "type": filament_elem.get("type"),
- }
- )
- break
- # Should only have plate 2's filament
- assert len(filaments) == 1
- assert filaments[0]["slot_id"] == 2
- assert filaments[0]["type"] == "PETG"
- def test_detect_multi_plate_from_gcode_files(self):
- """Test detecting multiple plates from gcode file presence."""
- # Simulate namelist from a multi-plate 3MF
- namelist = [
- "Metadata/plate_1.gcode",
- "Metadata/plate_2.gcode",
- "Metadata/plate_3.gcode",
- "Metadata/plate_1.png",
- "Metadata/plate_2.png",
- "Metadata/plate_3.png",
- "Metadata/slice_info.config",
- "3D/3dmodel.model",
- ]
- # Extract plate indices from gcode files
- gcode_files = [n for n in namelist if n.startswith("Metadata/plate_") and n.endswith(".gcode")]
- plate_indices = []
- for gf in gcode_files:
- plate_str = gf[15:-6] # Remove "Metadata/plate_" and ".gcode"
- plate_indices.append(int(plate_str))
- plate_indices.sort()
- assert len(plate_indices) == 3
- assert plate_indices == [1, 2, 3]
- # Verify it's a multi-plate file
- is_multi_plate = len(plate_indices) > 1
- assert is_multi_plate is True
- def test_single_plate_export_not_multi_plate(self):
- """Test that single-plate exports are not detected as multi-plate."""
- # Simulate namelist from a single-plate export (plate 5 only)
- namelist = [
- "Metadata/plate_5.gcode",
- "Metadata/plate_1.png",
- "Metadata/plate_2.png",
- "Metadata/plate_3.png",
- "Metadata/plate_4.png",
- "Metadata/plate_5.png", # All thumbnails present
- "Metadata/slice_info.config",
- "3D/3dmodel.model",
- ]
- # Extract plate indices from gcode files (not thumbnails!)
- gcode_files = [n for n in namelist if n.startswith("Metadata/plate_") and n.endswith(".gcode")]
- plate_indices = []
- for gf in gcode_files:
- plate_str = gf[15:-6]
- plate_indices.append(int(plate_str))
- # Only one gcode file = single plate export
- assert len(plate_indices) == 1
- assert plate_indices[0] == 5
- is_multi_plate = len(plate_indices) > 1
- assert is_multi_plate is False
- class TestReprintCostCalculation:
- """Tests for reprint cost calculation."""
- def test_cost_addition_logic(self):
- """Test that reprint costs are added correctly."""
- # Simulate the cost addition logic
- existing_cost = 5.25 # Original print cost
- filament_grams = 100.0
- cost_per_kg = 25.0 # Default cost
- # Calculate additional cost for reprint
- additional_cost = round((filament_grams / 1000) * cost_per_kg, 2)
- assert additional_cost == 2.50
- # Add to existing cost
- new_total = round(existing_cost + additional_cost, 2)
- assert new_total == 7.75
- def test_cost_addition_with_none_existing(self):
- """Test cost addition when existing cost is None."""
- existing_cost = None
- filament_grams = 200.0
- cost_per_kg = 15.0
- additional_cost = round((filament_grams / 1000) * cost_per_kg, 2)
- assert additional_cost == 3.0
- # When existing is None, just use additional
- new_total = additional_cost if existing_cost is None else round(existing_cost + additional_cost, 2)
- assert new_total == 3.0
- def test_cost_with_custom_filament_price(self):
- """Test cost calculation with custom filament price."""
- filament_grams = 150.0
- custom_cost_per_kg = 35.0 # More expensive filament
- cost = round((filament_grams / 1000) * custom_cost_per_kg, 2)
- assert cost == 5.25
- def test_multiple_reprints_accumulate(self):
- """Test that multiple reprints accumulate costs correctly."""
- filament_grams = 100.0
- cost_per_kg = 20.0
- single_print_cost = round((filament_grams / 1000) * cost_per_kg, 2)
- assert single_print_cost == 2.0
- # After 3 prints (1 original + 2 reprints)
- total_after_3_prints = round(single_print_cost * 3, 2)
- assert total_after_3_prints == 6.0
- class TestGcodeHeaderFilamentUsage:
- """ThreeMFParser pulls total filament usage from the produced 3MF's G-code
- header. Some slicer-sidecar builds leave the X-Filament-Used-* response
- headers unset, so the slice would otherwise report "0 g" for a real
- multi-hour print."""
- @staticmethod
- def _make_3mf(gcode_header: str) -> str:
- import tempfile
- import zipfile
- fd, path = tempfile.mkstemp(suffix=".3mf")
- import os
- os.close(fd)
- with zipfile.ZipFile(path, "w", zipfile.ZIP_DEFLATED) as zf:
- zf.writestr("3D/3dmodel.model", "<model/>")
- zf.writestr("Metadata/plate_1.gcode", gcode_header + "\nG1 X0 Y0\n")
- return path
- def test_extracts_filament_weight_and_length_from_header(self):
- from backend.app.services.archive import ThreeMFParser
- header = (
- "; HEADER_BLOCK_START\n"
- "; BambuStudio 02.06.00.51\n"
- "; total layer number: 503\n"
- "; total filament length [mm] : 41661.40\n"
- "; total filament volume [cm^3] : 100207.42\n"
- "; total filament weight [g] : 126.26\n"
- )
- meta = ThreeMFParser(self._make_3mf(header)).parse()
- assert meta.get("filament_used_grams") == 126.26
- assert meta.get("filament_used_mm") == 41661.40
- assert meta.get("total_layers") == 503
- def test_no_filament_keys_when_header_lacks_them(self):
- from backend.app.services.archive import ThreeMFParser
- meta = ThreeMFParser(self._make_3mf("; total layer number: 10\n")).parse()
- assert "filament_used_grams" not in meta
- assert "filament_used_mm" not in meta
|