Przeglądaj źródła

test: add unit tests for extract_filament_usage_from_3mf function
- Valid filament extraction (single/multiple)
- Plate-specific filtering
- Error handling (invalid file, missing slice_info)
- Missing/invalid field handling

AneoPsy 3 miesięcy temu
rodzic
commit
e21283de1c
1 zmienionych plików z 161 dodań i 0 usunięć
  1. 161 0
      backend/tests/unit/test_threemf_tools.py

+ 161 - 0
backend/tests/unit/test_threemf_tools.py

@@ -4,15 +4,27 @@ Tests G-code parsing, filament length-to-weight conversion,
 and cumulative layer usage lookup.
 """
 
+import io
 import math
+import zipfile
 
 from backend.app.utils.threemf_tools import (
+    extract_filament_usage_from_3mf,
     get_cumulative_usage_at_layer,
     mm_to_grams,
     parse_gcode_layer_filament_usage,
 )
 
 
+def create_mock_3mf(slice_info_content: str) -> io.BytesIO:
+    """Create a mock 3MF file (ZIP) with slice_info.config content."""
+    buffer = io.BytesIO()
+    with zipfile.ZipFile(buffer, "w") as zf:
+        zf.writestr("Metadata/slice_info.config", slice_info_content)
+    buffer.seek(0)
+    return buffer
+
+
 class TestParseGcodeLayerFilamentUsage:
     """Tests for parse_gcode_layer_filament_usage()."""
 
@@ -247,3 +259,152 @@ class TestGetCumulativeUsageAtLayer:
         """Target layer 0."""
         data = {0: {0: 10.0}, 1: {0: 20.0}}
         assert get_cumulative_usage_at_layer(data, 0) == {0: 10.0}
+
+
+class TestExtractFilamentUsageFrom3mf:
+    """Tests for extract_filament_usage_from_3mf function."""
+
+    def test_extract_single_filament(self, tmp_path):
+        """Test extracting a single filament."""
+        xml_content = """<?xml version="1.0" encoding="UTF-8"?>
+        <config>
+            <filament id="1" used_g="50.5" type="PLA" color="#FF0000"/>
+        </config>
+        """
+        mock_3mf = create_mock_3mf(xml_content)
+        file_path = tmp_path / "test.3mf"
+        file_path.write_bytes(mock_3mf.read())
+
+        result = extract_filament_usage_from_3mf(file_path)
+
+        assert len(result) == 1
+        assert result[0]["slot_id"] == 1
+        assert result[0]["used_g"] == 50.5
+        assert result[0]["type"] == "PLA"
+        assert result[0]["color"] == "#FF0000"
+
+    def test_extract_multiple_filaments(self, tmp_path):
+        """Test extracting multiple filaments."""
+        xml_content = """<?xml version="1.0" encoding="UTF-8"?>
+        <config>
+            <filament id="1" used_g="50.5" type="PLA" color="#FF0000"/>
+            <filament id="2" used_g="30.2" type="PETG" color="#00FF00"/>
+            <filament id="3" used_g="10.0" type="ABS" color="#0000FF"/>
+        </config>
+        """
+        mock_3mf = create_mock_3mf(xml_content)
+        file_path = tmp_path / "test.3mf"
+        file_path.write_bytes(mock_3mf.read())
+
+        result = extract_filament_usage_from_3mf(file_path)
+
+        assert len(result) == 3
+        assert result[0]["slot_id"] == 1
+        assert result[1]["slot_id"] == 2
+        assert result[2]["slot_id"] == 3
+
+    def test_extract_filament_with_plate_id(self, tmp_path):
+        """Test extracting filament for a specific plate."""
+        xml_content = """<?xml version="1.0" encoding="UTF-8"?>
+        <config>
+            <plate>
+                <metadata key="index" value="1"/>
+                <filament id="1" used_g="25.0" type="PLA" color="#FF0000"/>
+            </plate>
+            <plate>
+                <metadata key="index" value="2"/>
+                <filament id="1" used_g="75.0" type="PETG" color="#00FF00"/>
+            </plate>
+        </config>
+        """
+        mock_3mf = create_mock_3mf(xml_content)
+        file_path = tmp_path / "test.3mf"
+        file_path.write_bytes(mock_3mf.read())
+
+        result = extract_filament_usage_from_3mf(file_path, plate_id=2)
+
+        assert len(result) == 1
+        assert result[0]["used_g"] == 75.0
+        assert result[0]["type"] == "PETG"
+
+    def test_missing_slice_info_returns_empty(self, tmp_path):
+        """Test that missing slice_info.config returns empty list."""
+        buffer = io.BytesIO()
+        with zipfile.ZipFile(buffer, "w") as zf:
+            zf.writestr("other_file.txt", "content")
+        buffer.seek(0)
+
+        file_path = tmp_path / "test.3mf"
+        file_path.write_bytes(buffer.read())
+
+        result = extract_filament_usage_from_3mf(file_path)
+
+        assert result == []
+
+    def test_invalid_file_returns_empty(self, tmp_path):
+        """Test that invalid file returns empty list."""
+        file_path = tmp_path / "invalid.3mf"
+        file_path.write_text("not a zip file")
+
+        result = extract_filament_usage_from_3mf(file_path)
+
+        assert result == []
+
+    def test_nonexistent_file_returns_empty(self, tmp_path):
+        """Test that nonexistent file returns empty list."""
+        file_path = tmp_path / "nonexistent.3mf"
+
+        result = extract_filament_usage_from_3mf(file_path)
+
+        assert result == []
+
+    def test_filament_without_id_is_skipped(self, tmp_path):
+        """Test that filament without id is skipped."""
+        xml_content = """<?xml version="1.0" encoding="UTF-8"?>
+        <config>
+            <filament used_g="50.5" type="PLA" color="#FF0000"/>
+            <filament id="2" used_g="30.0" type="PETG" color="#00FF00"/>
+        </config>
+        """
+        mock_3mf = create_mock_3mf(xml_content)
+        file_path = tmp_path / "test.3mf"
+        file_path.write_bytes(mock_3mf.read())
+
+        result = extract_filament_usage_from_3mf(file_path)
+
+        assert len(result) == 1
+        assert result[0]["slot_id"] == 2
+
+    def test_invalid_used_g_is_skipped(self, tmp_path):
+        """Test that filament with invalid used_g is skipped."""
+        xml_content = """<?xml version="1.0" encoding="UTF-8"?>
+        <config>
+            <filament id="1" used_g="invalid" type="PLA" color="#FF0000"/>
+            <filament id="2" used_g="30.0" type="PETG" color="#00FF00"/>
+        </config>
+        """
+        mock_3mf = create_mock_3mf(xml_content)
+        file_path = tmp_path / "test.3mf"
+        file_path.write_bytes(mock_3mf.read())
+
+        result = extract_filament_usage_from_3mf(file_path)
+
+        assert len(result) == 1
+        assert result[0]["slot_id"] == 2
+
+    def test_missing_optional_fields(self, tmp_path):
+        """Test that missing type and color default to empty string."""
+        xml_content = """<?xml version="1.0" encoding="UTF-8"?>
+        <config>
+            <filament id="1" used_g="50.5"/>
+        </config>
+        """
+        mock_3mf = create_mock_3mf(xml_content)
+        file_path = tmp_path / "test.3mf"
+        file_path.write_bytes(mock_3mf.read())
+
+        result = extract_filament_usage_from_3mf(file_path)
+
+        assert len(result) == 1
+        assert result[0]["type"] == ""
+        assert result[0]["color"] == ""