A dual-nozzle H2C print with support filament hangs at its first nozzle
switch. The emitted file shows the change-filament block's M620 O ordinal
jumping from O1 straight to O230, plus a duplicate "M1020 S<n>" toolchange
command right after every change block. Two causes, fixed together because
they interlock (the ordinal check keys off the same toolchange detection
that suppresses the duplicate):
- append_tcr incremented m_toolchange_count once per prime-tower visit
(roughly once per layer), while the change-filament template only emits
its M620 O{toolchange_count + 1} line on real filament changes. With 229
change-less sparse tower layers below the first support layer, the first
real change reported ordinal 230. The counter now advances only when the
expanded change block really contains a toolchange command, and the
placeholder exposes the upcoming change's ordinal (count + 1). The
set_extruder path already counted per real change and is unchanged.
- toolchange_prefix() returned "M1020 S" for BBL printers, so the
custom_gcode_changes_tool() dedup could never match the stock profiles'
line-leading "T[next_filament_id] ..." commands and the writer's own
toolchange was appended after every change block on dual-extruder
machines. The prefix is now the plain "T" (the manual-filament-change tag
branch stays first), and the M1020 form moved into GCodeWriter::toolchange()
as an explicit branch that also carries the nozzle:
"M1020 S<filament> H<nozzle>". The nozzle parameter is signed on purpose:
the null-safe nozzle lookup legitimately yields -1, matching the stock
templates' own H-1 convention.
The prefix change also lets the CoolingBuffer recognize the change blocks'
T commands as tool boundaries on BBL printers (its per-filament attribution
previously keyed off the duplicate M1020, or nothing at all on
single-extruder models); its existing out-of-range guard ignores
T1000-class machine commands.
Verification: full suites green (libslic3r 48998 assertions / 169 cases;
fff_print 692 / 65 including three new scenarios - writer emission per
printer kind, dedup + ordinal progression on sequential prints, and a
prime-tower regression scenario verified to fail against the old per-visit
counting). Byte gate: 18 of 20 fixtures bit-identical; the sequential repro
differs by exactly its 3 removed duplicate M1020 lines, deterministic
across two runs. Reslicing the field project that exposed the hang yields
M620 O1 followed by a gapless O2..O59 and zero duplicate M1020 lines.
Co-authored-by: songwei.li <songwei.li@bambulab.com>
fff_print test suite
Component- and pipeline-level tests for FFF slicing: the path from a Model plus config, through Print / PrintObject, to emitted G-code.
For Catch2 mechanics (assertions, generators, matchers, random ordering, thread-safety), see ../CLAUDE.md. This document is the organizing contract for the suite: where a test goes, and how it is named.
Organizing principle
One file per subsystem. A subsystem is usually a single production class (Flow, PrintObject), but may be a cohesive feature that spans several (skirt/brim lives in Brim.cpp, Print.cpp, and GCode.cpp). That file owns every test for the subsystem: in-memory-state assertions and emitted-G-code assertions alike.
A test's home is decided by what production code it exercises, never by how it observes the result. A skirt test that inspects print.skirt() and one that greps the G-code for ; skirt live in the same file.
If you touched code in a subsystem, its test file is where your test goes. If a subsystem has no file yet, add test_<subsystem>.cpp and list it in CMakeLists.txt.
File ownership
Building blocks (one class, exercised through its API)
| File | Source (src/libslic3r/) |
Covers |
|---|---|---|
test_trianglemesh |
TriangleMesh.{c,h}pp |
mesh stats, transforms, slicing, split/merge/cut |
test_flow |
Flow.{c,h}pp |
extrusion width / area math |
test_extrusion_entity |
ExtrusionEntity.{c,h}pp |
extrusion-collection geometry |
test_gcodewriter |
GCodeWriter.{c,h}pp, GCode.cpp |
low-level G-code emit primitives, origin |
test_model |
Model.{c,h}pp |
object / volume / instance construction |
Slicing pipeline (build a Print, then assert state or G-code)
| File | Source (src/libslic3r/) |
Covers |
|---|---|---|
test_printobject |
PrintObject.cpp |
layer heights, perimeter generation |
test_fill |
Fill/ |
infill patterns and infill G-code |
test_skirt_brim |
Brim.cpp, Print.cpp |
skirt/brim loop counts, grouping, brim ears, emission order |
test_support_material |
Support/ |
support & raft layers, contact distance |
test_cooling |
GCode/CoolingBuffer.cpp |
fan control, speed-marker consumption |
test_multifilament |
GCode/ToolOrdering.cpp |
per-feature and per-object filament routing |
test_print |
Print.{c,h}pp |
validate(), solid-shell behavior, sequential printing, custom G-code & config comments, default-slice smoke |
Paths are under src/libslic3r/. A trailing / is a directory of related files; otherwise it is a single class. {c,h}pp means the .cpp/.hpp pair.
Naming and tags
- File:
test_<subsystem>.cpp. - Test name: a plain behavioral sentence, present tense, stating the contract the test pins down. No
Subsystem:prefix (the tag carries that).- Good:
TEST_CASE("Skirt is emitted once per layer it spans", "[SkirtBrim]") - Avoid:
TEST_CASE("Print: Skirt generation", "[Print]")
- Good:
- Tags:
- Exactly one subsystem tag, PascalCase, matching the file (
[SkirtBrim],[PrintObject],[Fill]). This is the grouping / filter key. - Optional cross-cutting tags for a concern that genuinely spans files (
[validate],[Regression]). - Status tags:
[NotWorking]marks a test disabled for a known, documented reason; CI excludes it via~[NotWorking](it does not hide itself). Use[.]to hide a test from default runs entirely. Either way, say why in a one-line comment.
- Exactly one subsystem tag, PascalCase, matching the file (
Test style
Prefer a flat TEST_CASE per behavior, with GENERATE for parameterized cases and shared setup factored into helpers. The test name carries the behavior, so the BDD scaffolding is usually redundant. Reserve SCENARIO / GIVEN / WHEN / THEN for a test with genuine shared setup that branches into a few closely related variations, and never let a SCENARIO accumulate unrelated WHENs: that grab-bag is what this contract exists to prevent (and it hides failures behind a single coarse test case).
Robust tests
A test should fail only when the behavior it names breaks, not from unrelated changes (the "change-detector" anti-pattern). Test behavior, not incidentals, and aim for one reason to fail. Concretely:
- Don't depend on or assert defaults: set the config keys the behavior needs, and derive expected values from those inputs (a 20mm cube at 0.2mm = 100 layers), not from a default that may change.
- Assert the defining property, not an incidental value: prefer "skirt present", "at least 2 brim loops", or "ears vs none" over exact coordinates, extrusion amounts, line counts, or byte sizes.
- Compare floats with a tolerance (
WithinAbs/WithinRel), never==. - Match the meaningful G-code token (
; skirt), not whole lines, whitespace, or comment wording. - Rely on ordering only when it is the contract (as
role_sequencedoes). - Keep tests self-contained: no shared state, green under
--order rand.
Helpers
Reuse these instead of building a Print or parsing G-code by hand.
- Global (
tests/test_utils.hpp, available to every suite):load_model("file.obj"): load aTriangleMeshfromtests/data/.ScopedTemporaryFile: an RAII temp-file path, removed on scope exit.
- Suite harness (
fff_print/test_helpers.{hpp,cpp}):- Build and run:
init_print(...),init_and_process_print(...),slice(...)(returns the G-code string), andgcode(print). - Two-cube placement:
slice_two_cubes_arranged(...)(arranger-positioned), andplace_two_cubes_apart(...)/slice_two_cubes_apart(...)(a fixed gap, not arranged). - Meshes:
cube(size)/make_cube(...)for simple shapes; theTestMeshenum withmesh(...)for named fixtures. - G-code analysis:
layers_with_role(gcode, role),max_z(gcode),role_passes(gcode, role),role_sequence(gcode, roles). Subsystem-specific checks stay local (for examplebrim_countintest_skirt_brim).
- Build and run:
Promote a helper into the suite harness when it is a general test primitive (not tied to one subsystem's logic), even if only one file uses it today; keep genuinely subsystem-specific helpers local (file-static). Reuse potential, not current usage count, is the test.
Adding a test (checklist)
- Find the subsystem's file in the tables; create
test_<subsystem>.cppif missing. - Build the print with a harness helper; set only the config keys the behavior needs.
- Assert the behavior, in-memory or via parsed G-code, whichever is clearest.
- Name it as a behavioral sentence and tag it
[Subsystem]. - For a bug fix, add the regression test in the owning file. Name it for the behavior it protects; the test must stand on its own without relying on an external issue or PR for meaning.
Running
ctest --test-dir build/tests/fff_print
build/tests/fff_print/<config>/fff_print_tests --order rand "~[NotWorking]"