From e21283de1c12f41835167b63cd7a155ffcae4943 Mon Sep 17 00:00:00 2001 From: AneoPsy Date: Sat, 14 Feb 2026 14:49:32 -1000 Subject: [PATCH] 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 --- backend/tests/unit/test_threemf_tools.py | 161 +++++++++++++++++++++++ 1 file changed, 161 insertions(+) diff --git a/backend/tests/unit/test_threemf_tools.py b/backend/tests/unit/test_threemf_tools.py index b07bd749d..bacbed307 100644 --- a/backend/tests/unit/test_threemf_tools.py +++ b/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 = """ + + + + """ + 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 = """ + + + + + + """ + 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 = """ + + + + + + + + + + + """ + 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 = """ + + + + + """ + 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 = """ + + + + + """ + 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 = """ + + + + """ + 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"] == ""