Files
OrcaSlicer/tests/fff_print/test_imex_mode_gcode.cpp
T
Hanif Koh e57945179b Remove Disabled, Duplicate and Library-Only Unit Tests
Delete test cases that never run (#if 0 blocks, [NotWorking] cases, commented-out Perl ports, blocks behind undefined macros), cases that cannot fail, cases that only exercise boost::polygon directly, and cases whose assertions another test already makes on the same path. Remove the helpers, includes and fixture only those cases used.
2026-10-19 15:40:06 +08:00

373 lines
19 KiB
C++

#include <catch2/catch_all.hpp>
#include "catch2/catch_test_macros.hpp"
#include "catch2/generators/catch_generators.hpp"
#include "catch2/catch_message.hpp"
#include "libslic3r/Config.hpp"
#include "libslic3r/PrintConfig.hpp"
#include "test_helpers.hpp"
#include "test_utils.hpp"
#include <cstddef>
#include <string>
#include <vector>
using namespace Slic3r;
using namespace Slic3r::Test;
// The IMEX mode placeholders ({imex_mode}, {imex_mode_index}, {imex_mode_gcode}) and the mode
// script that consumes them are set up in GCode.cpp's _do_export, immediately before
// machine_start_gcode is processed. Two contracts live in that ordering:
// * the three placeholders exist for every G-code script the export runs, IMEX or not;
// * the mode script is itself run through the placeholder parser, and it runs FIRST, so a
// {global ...} it declares is in scope by the time machine_start_gcode is processed.
// Nothing else in the suite exercises them.
// Offset of the first line of `gcode` that equals `expected` once trailing whitespace is
// stripped, or npos. Whole-line matching is deliberate on both counts: prefix matching would
// let ";IMEX_MODE_INDEX:2" pass for an emitted index of 20, and the config block the exporter
// appends to every file repeats machine_start_gcode and imex_mode_gcodes verbatim (as
// "; key = value"), which would make any substring search for the templates below trivially
// true and every "not emitted" assertion trivially false.
static std::size_t find_exact_line(const std::string &gcode, const std::string &expected)
{
std::size_t pos = 0;
while (pos <= gcode.size()) {
const std::size_t eol = gcode.find('\n', pos);
const std::size_t end = (eol == std::string::npos) ? gcode.size() : eol;
std::string line = gcode.substr(pos, end - pos);
while (!line.empty() && (line.back() == '\r' || line.back() == ' ' || line.back() == '\t'))
line.pop_back();
if (line == expected)
return pos;
if (eol == std::string::npos)
break;
pos = eol + 1;
}
return std::string::npos;
}
static bool has_line(const std::string &gcode, const std::string &expected)
{
return find_exact_line(gcode, expected) != std::string::npos;
}
// Any command line -- not a comment -- containing `token`. Used where the assertion is that a
// carriage was addressed at all, rather than with a particular value: the config block the
// exporter appends restates every setting as "; key = value", so the comment lines have to be
// excluded or a search for a setting's own name matches itself.
static bool has_command_containing(const std::string &gcode, const std::string &token)
{
std::size_t pos = 0;
while (pos < gcode.size()) {
const std::size_t eol = gcode.find('\n', pos);
const std::string line = gcode.substr(pos, (eol == std::string::npos ? gcode.size() : eol) - pos);
if (!line.empty() && line.front() != ';' && line.find(token) != std::string::npos)
return true;
if (eol == std::string::npos)
break;
pos = eol + 1;
}
return false;
}
// Mode scripts. Free of placeholders so that a test can tell "the script was emitted" apart
// from "the script was expanded"; the expansion case below supplies its own template.
static const char *kCopyScript = "SET_DUAL_CARRIAGE MODE=COPY";
static const char *kQuadScript = "SET_QUAD_CARRIAGE MODE=COPY";
// Reports all three placeholders on their own lines so find_exact_line can pin each value.
static const char *kReportStartGcode = ";IMEX_MODE:{imex_mode}\n"
";IMEX_MODE_INDEX:{imex_mode_index}\n"
";IMEX_MODE_GCODE:{imex_mode_gcode}\n";
// Shared IMEX printer geometry: 7 logical extruders across 4 physical heads, three modes.
// physical_extruder_map is only honoured when its length matches the nozzle count
// (PrintApply feeds effective_physical_extruder_map the nozzle_diameter size), so the nozzle
// keys must be sized to 7 or the map is silently replaced with the identity and IMEX quietly
// stops happening at all. Mirrors imex_7x4_printer() in test_multifilament.cpp; `iq-copy` is
// carried over from the IQEX case there, which is a mode combination known to slice.
//
// imex_mode_gcodes is left to each test: the position a mode's script occupies in this list is
// exactly what {imex_mode_index} is asserted against.
static void imex_7x4_printer(DynamicPrintConfig &config)
{
config.set_deserialize_strict({
{ "nozzle_diameter", "0.4,0.4,0.4,0.4,0.4,0.4,0.4" },
{ "printer_extruder_id", "1,2,3,4,5,6,7" },
{ "printer_extruder_variant", "Direct Drive Standard,Direct Drive Standard,Direct Drive Standard,"
"Direct Drive Standard,Direct Drive Standard,Direct Drive Standard,"
"Direct Drive Standard" },
{ "extruder_printable_height", "0,0,0,0,0,0,0" },
{ "physical_extruder_map", "0,0,0,0,1,2,3" },
// Authored 1..n, as every production path does: PresetBundle writes it in
// full_fff_config(), and config load synthesizes it when absent. The all-1s default is
// not a neutral placeholder -- get_config_index_base() keys the slot-to-column lookup
// on it, and with every entry equal each per-filament vector collapses to filament 1's
// value, leaving a per-slot assertion comparing a value against itself.
{ "filament_self_index", "1,2,3,4,5,6,7" },
{ "is_imex", "1" },
{ "imex_mode_names", "primary;copy;iq-copy" },
{ "imex_mode_active_tools", "0:P;0:P,1:C;0:P,1:C,2:C,3:C" },
{ "skirt_loops", "0" },
{ "brim_type", "no_brim" },
// Klipper skips the bed/extruder temperature block that would otherwise be written
// between the mode script and machine_start_gcode, so the ordering assertion below
// compares the two scripts and nothing else.
{ "gcode_flavor", "klipper" },
});
size_flush_to_nozzles(config);
}
// Set the mode scripts positionally, one per name in imex_mode_names. Assigning the vector
// beats set_deserialize_strict here: a coStrings round trip through a ';'-joined string has to
// quote and escape empty entries and embedded braces, and the empty first entry (primary, which
// deliberately has no script) is exactly the entry that would be lost.
static void set_mode_gcodes(DynamicPrintConfig &config, const std::vector<std::string> &gcodes)
{
config.option<ConfigOptionStrings>("imex_mode_gcodes", true)->values = gcodes;
}
// Route every region to one filament. An unset *_filament_id is not "inherit":
// clamp_feature_filament_to_valid rewrites <=0 to 1, which would drag tool 0 into
// tool_ordering. Filament 1 is logical slot 0, which pem routes to head 0 -- the head every
// mode here declares Primary -- so Print::validate() accepts the plate.
static void all_regions_on_filament(DynamicPrintConfig &config, int filament_1based)
{
for (const char *key : { "outer_wall_filament_id", "inner_wall_filament_id",
"sparse_infill_filament_id", "internal_solid_filament_id",
"top_surface_filament_id", "bottom_surface_filament_id" })
config.set_deserialize_strict({ { key, std::to_string(filament_1based) } });
}
// {imex_mode} is the plate's active mode, {imex_mode_index} its position in imex_mode_names and
// {imex_mode_gcode} the raw script at that same position. Two modes are exercised because a
// single one cannot tell a resolved index apart from a constant: `copy` sits at 1 and `iq-copy`
// at 2, so an index that stopped tracking imex_mode_names fails at least one of them.
TEST_CASE("IMEX mode placeholders resolve to the active mode's name, index and script",
"[ImexModeGcode][IMEX]")
{
auto [mode, mode_index, mode_script] = GENERATE(table<std::string, int, std::string>({
{ "copy", 1, kCopyScript },
{ "iq-copy", 2, kQuadScript },
}));
INFO("active mode: " << mode);
DynamicPrintConfig config = multifilament_config(7);
imex_7x4_printer(config);
all_regions_on_filament(config, 1);
set_mode_gcodes(config, { "", kCopyScript, kQuadScript });
config.set_deserialize_strict({
{ "imex_parallel_mode", mode },
{ "machine_start_gcode", kReportStartGcode },
});
const std::string gcode = slice({ cube(20) }, config);
CHECK(has_line(gcode, ";IMEX_MODE:" + mode));
CHECK(has_line(gcode, ";IMEX_MODE_INDEX:" + std::to_string(mode_index)));
CHECK(has_line(gcode, ";IMEX_MODE_GCODE:" + mode_script));
// The script for the active mode -- and only that one -- reaches the file.
CHECK(has_line(gcode, mode_script));
CHECK_FALSE(has_line(gcode, mode == "copy" ? kQuadScript : kCopyScript));
}
// The mode script is a G-code template, not a literal: it goes through
// placeholder_parser_process like machine_start_gcode does. Because the three IMEX placeholders
// are set before that call, the script can also read its own mode -- which is what
// distinguishes "the script is processed" from "the script is processed too early to see them".
TEST_CASE("A placeholder inside an IMEX mode's G-code script is expanded",
"[ImexModeGcode][IMEX]")
{
const std::string templ = "SET_DUAL_CARRIAGE MODE={imex_mode} INDEX={imex_mode_index}"
" TEMP={nozzle_temperature_initial_layer[0]}";
DynamicPrintConfig config = multifilament_config(7);
imex_7x4_printer(config);
all_regions_on_filament(config, 1);
set_mode_gcodes(config, { "", templ, kQuadScript });
config.set_deserialize_strict({
{ "imex_parallel_mode", "copy" },
// The multi-extruder normalization collapses the per-filament temperature vector to a
// single value, so [0] is this literal 200 rather than a per-slot default.
{ "nozzle_temperature_initial_layer", "200" },
{ "machine_start_gcode", ";START\n" },
});
const std::string gcode = slice({ cube(20) }, config);
CHECK(has_line(gcode, "SET_DUAL_CARRIAGE MODE=copy INDEX=1 TEMP=200"));
// If the script were written straight to the file the braces would survive verbatim.
CHECK_FALSE(has_line(gcode, templ));
}
// The ordering guarantee, and the reason the placeholder block sits where it does: the mode
// script is processed BEFORE machine_start_gcode, so a {global} the mode declares is in scope
// for machine_start_gcode. Reorder the two and machine_start_gcode references an undefined
// variable, which the placeholder parser raises as an error out of the export -- so a
// regression here fails this test whether the export throws or merely writes nothing useful.
TEST_CASE("A global declared in the IMEX mode G-code is visible to machine_start_gcode",
"[ImexModeGcode][IMEX]")
{
DynamicPrintConfig config = multifilament_config(7);
imex_7x4_printer(config);
all_regions_on_filament(config, 1);
set_mode_gcodes(config, { "", std::string("{global imex_carriage_count = 2}") + kCopyScript, kQuadScript });
config.set_deserialize_strict({
{ "imex_parallel_mode", "copy" },
{ "machine_start_gcode", ";IMEX_CARRIAGES:{imex_carriage_count}\n" },
});
const std::string gcode = slice({ cube(20) }, config);
// The value is the mode script's, not a default: nothing else in this config defines it.
const std::size_t start_gcode_at = find_exact_line(gcode, ";IMEX_CARRIAGES:2");
REQUIRE(start_gcode_at != std::string::npos);
// ...and the two land in the file in that same order. Ordering is the contract here, so it
// is asserted directly rather than left implicit in the global resolving.
const std::size_t mode_script_at = find_exact_line(gcode, kCopyScript);
REQUIRE(mode_script_at != std::string::npos);
CHECK(mode_script_at < start_gcode_at);
}
// Primary is single-carriage printing: it names a real mode (position 0 of imex_mode_names, not
// the not-found fallback) but carries no script, so the export must add nothing. This is what
// keeps the mode machinery out of the way of an IMEX printer that is not printing in parallel.
TEST_CASE("Primary-mode IMEX prints emit no mode G-code", "[ImexModeGcode][IMEX]")
{
DynamicPrintConfig config = multifilament_config(7);
imex_7x4_printer(config);
all_regions_on_filament(config, 1);
set_mode_gcodes(config, { "", kCopyScript, kQuadScript });
config.set_deserialize_strict({
{ "imex_parallel_mode", "primary" },
{ "machine_start_gcode", kReportStartGcode },
});
const std::string gcode = slice({ cube(20) }, config);
CHECK(has_line(gcode, ";IMEX_MODE:primary"));
CHECK(has_line(gcode, ";IMEX_MODE_INDEX:0"));
CHECK(has_line(gcode, ";IMEX_MODE_GCODE:"));
CHECK_FALSE(has_line(gcode, kCopyScript));
CHECK_FALSE(has_line(gcode, kQuadScript));
}
// A plate stores its IMEX mode as the mode's *name* and the exporter resolves that name against
// the printer's imex_mode_names at slice time, so the two drift apart whenever the mode is
// renamed or deleted after a plate was set to it, or the project is opened against a printer
// preset that names its modes differently. A name matching no row must be treated as Primary --
// not as a parallel mode whose every name-keyed lookup happens to come back empty, which is how
// it used to behave: no mode script (there is none to find), no active-tool roster, and yet the
// parallel branches taken all the way through the export.
//
// Primary is a row like any other, so falling back to it means running its script too; a
// non-empty script is used here so "fell back to Primary" is distinguishable from "resolved
// nothing and emitted nothing".
TEST_CASE("An IMEX mode the printer no longer defines falls back to Primary",
"[ImexModeGcode][IMEX]")
{
static const char *kPrimaryScript = "SET_DUAL_CARRIAGE MODE=PRIMARY";
DynamicPrintConfig config = multifilament_config(7);
imex_7x4_printer(config);
all_regions_on_filament(config, 1);
set_mode_gcodes(config, { kPrimaryScript, kCopyScript, kQuadScript });
config.set_deserialize_strict({
// A name no row in imex_mode_names carries -- what `copy` becomes once it is renamed.
{ "imex_parallel_mode", "copy-renamed" },
{ "machine_start_gcode", kReportStartGcode },
});
const std::string gcode = slice({ cube(20) }, config);
// The placeholders report the mode the export actually ran, so the stale name must not
// survive into them -- a mode script keyed on {imex_mode} would otherwise set the printer
// up for a mode the slicer did not slice for.
CHECK(has_line(gcode, ";IMEX_MODE:primary"));
CHECK(has_line(gcode, ";IMEX_MODE_INDEX:0"));
CHECK(has_line(gcode, ";IMEX_MODE_GCODE:" + std::string(kPrimaryScript)));
// Primary's own script runs; neither parallel mode's does.
CHECK(has_line(gcode, kPrimaryScript));
CHECK_FALSE(has_line(gcode, kCopyScript));
CHECK_FALSE(has_line(gcode, kQuadScript));
}
// The whole block is gated on is_imex. A printer preset can carry a filled-in mode table and
// still not be an IMEX machine -- and a plate config can still carry a stale imex_parallel_mode
// -- so with the flag off the placeholders stay empty and no mode script is injected. Run on an
// ordinary single-extruder printer on purpose: the guard is one boolean, and the multi-head
// geometry the other cases need would only add ways for this one to fail for another reason.
TEST_CASE("A printer with IMEX disabled resolves the mode placeholders to nothing",
"[ImexModeGcode][IMEX]")
{
DynamicPrintConfig config = DynamicPrintConfig::full_print_config();
set_mode_gcodes(config, { "", kCopyScript, kQuadScript });
config.set_deserialize_strict({
{ "imex_mode_names", "primary;copy;iq-copy" },
{ "imex_mode_active_tools", "0:P;0:P,1:C;0:P,1:C,2:C,3:C" },
{ "is_imex", "0" },
{ "imex_parallel_mode", "copy" },
{ "skirt_loops", "0" },
{ "brim_type", "no_brim" },
{ "gcode_flavor", "klipper" },
{ "machine_start_gcode", kReportStartGcode },
});
const std::string gcode = slice({ cube(20) }, config);
CHECK(has_line(gcode, ";IMEX_MODE:"));
CHECK(has_line(gcode, ";IMEX_MODE_INDEX:0"));
CHECK(has_line(gcode, ";IMEX_MODE_GCODE:"));
CHECK_FALSE(has_line(gcode, kCopyScript));
CHECK_FALSE(has_line(gcode, kQuadScript));
}
// In a parallel mode no tool changes occur, so every active carriage has to be addressed
// explicitly: once before the print for pressure advance, and again at the second layer for the
// drop from the initial-layer temperature. Nothing else in the suite covers either emission, and
// a carriage that falls out of one of those loops emits nothing at all rather than emitting
// something wrong -- it silently holds the initial-layer temperature, or whatever pressure
// advance the firmware was last told, for the whole job. So the property worth pinning is that
// the set of carriages addressed is exactly the set the mode declares active.
TEST_CASE("Every carriage a parallel mode declares active is addressed, and no other",
"[ImexModeGcode][IMEX]")
{
DynamicPrintConfig config = multifilament_config(7);
imex_7x4_printer(config);
all_regions_on_filament(config, 1);
set_mode_gcodes(config, { "", kCopyScript, kQuadScript });
config.set_deserialize_strict({
{ "imex_parallel_mode", "copy" },
{ "enable_pressure_advance", "1,1,1,1,1,1,1" },
// Distinct per slot, so each assertion below names one filament and no other. copy
// mode's secondary rides physical 1, which physical_extruder_map routes to the fifth
// slot -- 0.05 and 244, not the primary's 0.01 and 240.
{ "pressure_advance", "0.010,0.020,0.030,0.040,0.050,0.060,0.070" },
{ "nozzle_temperature_initial_layer", "235,235,235,235,235,235,235" },
{ "nozzle_temperature", "240,241,242,243,244,245,246" },
});
const std::string gcode = slice({ cube(20) }, config);
// imex_mode_active_tools declares copy as "0:P,1:C", so exactly these two carriages run, and
// each transitions to the temperature of the filament its own head is routed to. The other
// two heads this printer has must stay untouched.
CHECK(has_command_containing(gcode, "M104 S240 T0 ; set IMEX tool temperature"));
CHECK(has_command_containing(gcode, "M104 S244 T1 ; set IMEX tool temperature"));
CHECK_FALSE(has_command_containing(gcode, "T2 ; set IMEX tool temperature"));
CHECK_FALSE(has_command_containing(gcode, "T3 ; set IMEX tool temperature"));
// The companion loop, which skips the primary because set_extruder() has already emitted it
// on the ordinary path. The qualified line is therefore the one that belongs to this loop:
// Klipper names the first carriage as the unnumbered "extruder" and the rest by index, so
// dropping the secondary removes `extruder1` and leaves `extruder` in place.
CHECK(has_command_containing(gcode, "SET_PRESSURE_ADVANCE ADVANCE=0.05 EXTRUDER=extruder1;"));
CHECK(has_command_containing(gcode, "SET_PRESSURE_ADVANCE ADVANCE=0.01 EXTRUDER=extruder;"));
CHECK_FALSE(has_command_containing(gcode, "EXTRUDER=extruder2;"));
}