Files
bambuddy/backend/tests/unit/services/test_archive_service.py
T
maziggy b99536cc33 Remove unused imports, variables, and fix minor CodeQL findings
- Remove 28 unused imports across 22 test files
- Prefix 4 unused local variables with _ in app code
  (archives, bambu_mqtt, main) and remove 1 dead store
- Consolidate import/import-from in test_plate_detection.py
- Fix unreachable statement in test_archive_service.py
- Simplify redundant comparison in timelapse_processor.py

Resolves ~50 CodeQL py/unused-import, py/unused-local-variable,
py/import-and-import-from, py/unreachable-statement, and
py/redundant-comparison findings.
2026-02-06 12:19:17 +01:00

646 lines
23 KiB
Python

"""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()
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