mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-10-19 21:51:06 +00:00
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.
373 lines
19 KiB
C++
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;"));
|
|
}
|