Files
OrcaSlicer/tests/fff_print
SoftFever e98750e713 fix(engine): real-change-only toolchange ordinals and a single M1020 toolchange command
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>
2026-07-12 21:30:12 +08:00
..
2025-12-08 22:42:11 +08:00

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]")
  • 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.

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_sequence does).
  • 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 a TriangleMesh from tests/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), and gcode(print).
    • Two-cube placement: slice_two_cubes_arranged(...) (arranger-positioned), and place_two_cubes_apart(...) / slice_two_cubes_apart(...) (a fixed gap, not arranged).
    • Meshes: cube(size) / make_cube(...) for simple shapes; the TestMesh enum with mesh(...) 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 example brim_count in test_skirt_brim).

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)

  1. Find the subsystem's file in the tables; create test_<subsystem>.cpp if missing.
  2. Build the print with a harness helper; set only the config keys the behavior needs.
  3. Assert the behavior, in-memory or via parsed G-code, whichever is clearest.
  4. Name it as a behavioral sentence and tag it [Subsystem].
  5. 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]"