Files
firmware/qa/static/test_exclude_object.py
T
Oleksandr Shyshatskyi 80a4d64ffd Prepare next firmware release
Add Fluidd and custom boot scripts, migrate DC24V control, update the web UIs and filament macros, improve startup status reporting, and expand upgrade and replica coverage.
2026-09-24 11:37:28 +03:00

183 lines
5.9 KiB
Python

"""Regression tests for the multi-extruder exclude-object transform.
Upstream stores the skipped-move offset by extruder, but stores the maxima
used for retraction compensation globally. The old code therefore turns the
final XY-only move below into +99 mm of physical E after T2 -> T0.
"""
import importlib.util
import os
import pytest
from lib.paths import ROOT
pytestmark = pytest.mark.static
MODULE = os.path.join(
ROOT, "pkgs", "klipper", "payload", "klipper", "klippy", "extras",
"exclude_object.py")
def load_module():
spec = importlib.util.spec_from_file_location(
"anvil_exclude_object", MODULE)
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
return module
class Extruder:
def __init__(self, name):
self.name = name
def get_name(self):
return self.name
class Toolhead:
def __init__(self, name):
self.extruder = Extruder(name)
def get_extruder(self):
return self.extruder
def select(self, name):
self.extruder = Extruder(name)
class Downstream:
def __init__(self, position):
self.position = list(position)
self.moves = []
def get_position(self):
return list(self.position)
def move(self, position, speed):
self.position = list(position)
self.moves.append((list(position), speed))
def build_transform(tool="extruder2", position=(0., 0., 0., 100.)):
module = load_module()
eo = module.ExcludeObject.__new__(module.ExcludeObject)
eo.toolhead = Toolhead(tool)
eo.next_transform = Downstream(position)
eo.xyz_offset = [0., 0., 0.]
eo.physical_xyz = None
eo.last_excluded_xyz = [0., 0., 0.]
eo.e_states = {}
eo.active_extruder = None
eo.initial_extrusion_moves = 0
eo.last_position = [0., 0., 0., 0.]
eo.current_object = None
eo.excluded_objects = ["BAD"]
eo.get_position()
return eo
def select(eo, name, physical_e):
eo.toolhead.select(name)
eo.next_transform.position[3] = physical_e
eo.get_position()
def test_tool_change_does_not_put_old_tools_e_history_on_xy_travel():
eo = build_transform()
# T2's E=100 must never enter T0's retraction calculation.
select(eo, "extruder", 0.)
eo.move([0., 0., 0., 1.], 20.)
eo.current_object = "BAD"
eo.move([10., 10., 0., 2.], 20.) # suppressed
assert eo.next_transform.position == [0., 0., 0., 1.]
eo.current_object = None
eo.move([20., 20., 0., 2.], 500.) # XY-only exit travel
assert eo.next_transform.position == [20., 20., 0., 1.]
assert eo.e_states["extruder2"]["max_normal"] == 100.
# The virtual E=2 travel becomes the new normal high-water mark for T0,
# but its downstream E correctly remains 1. It must not affect T2.
assert eo.e_states["extruder"]["max_normal"] == 2.
def test_each_tool_keeps_its_own_pending_retraction_adjustment():
eo = build_transform(tool="extruder", position=(0., 0., 0., 10.))
eo.move([0., 0., 0., 8.], 20.) # T0 retracted by 2
eo.current_object = "BAD"
eo.move([5., 5., 0., 7.], 20.) # skipped retract
eo.move([6., 6., 0., 9.], 20.) # skipped partial unretract
# T1 may consume the shared spatial offset, but not T0's E correction.
eo.current_object = None
select(eo, "extruder1", 0.)
eo.move([20., 20., 0., 0.], 100.)
assert eo.next_transform.position[3] == 0.
# Returning to T0 does not put its pending correction on an XY-only move.
# It is retained until T0's own next E change.
select(eo, "extruder", 8.)
eo.move([21., 20., 0., 9.], 100.)
assert eo.next_transform.position[3] == 8.
assert eo.e_states["extruder"]["pending_adjustment"] == -1.
assert eo.e_states["extruder1"]["pending_adjustment"] == 0.
eo.move([21., 20., 0., 10.], 20.)
assert eo.next_transform.position[3] == 10.
assert eo.e_states["extruder"]["pending_adjustment"] == 0.
def test_first_tool_state_uses_its_real_negative_e_origin():
eo = build_transform(tool="extruder", position=(0., 0., 0., -20.))
state = eo.e_states["extruder"]
assert state["last_normal"] == -20.
assert state["max_normal"] == -20.
assert state["last_excluded"] == -20.
assert state["max_excluded"] == -20.
def test_single_extruder_retraction_compensation_is_preserved():
eo = build_transform(tool="extruder", position=(0., 0., 0., 10.))
eo.move([0., 0., 0., 8.], 20.) # normal retract: 2 mm debt
eo.current_object = "BAD"
eo.move([5., 5., 0., 7.], 20.) # suppressed retract
eo.move([6., 6., 0., 9.], 20.) # suppressed partial unretract
eo.current_object = None
eo.move([20., 20., 0., 9.], 100.) # leaving object, XY only
# Excluded debt is 1 mm and normal debt was 2 mm, so the -1 adjustment
# combines with the skipped net +1 and virtual E9 maps to physical E9.
assert eo.next_transform.position == [20., 20., 0., 9.]
assert eo.e_states["extruder"]["pending_adjustment"] == 0.
def test_initial_five_extrusions_remain_one_global_grace_period():
eo = build_transform(tool="extruder", position=(0., 0., 0., 0.))
eo.initial_extrusion_moves = 5
eo.current_object = "BAD"
for e in range(1, 6):
eo.move([float(e), 0., 0., float(e)], 20.)
assert len(eo.next_transform.moves) == 5
select(eo, "extruder1", 0.)
eo.move([6., 0., 0., 1.], 20.)
assert len(eo.next_transform.moves) == 5
def test_large_adjustment_logs_complete_diagnostic(caplog):
eo = build_transform(tool="extruder", position=(0., 0., 0., 0.))
state = eo.e_states["extruder"]
state.update(max_normal=20., last_normal=0.,
max_excluded=1., last_excluded=1., was_ignored=True)
eo.move([1., 1., 0., 0.], 100.)
messages = "\n".join(record.getMessage() for record in caplog.records)
assert "large E correction -20.000000 mm for extruder" in messages
assert "normal=0.000000/20.000000" in messages
assert "xyz_offset=" in messages