mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-07-25 11:52:05 +00:00
This commit is contained in:
@@ -7,6 +7,7 @@ add_executable(${_TEST_NAME}_tests
|
||||
test_fill.cpp
|
||||
test_flow.cpp
|
||||
test_gcode.cpp
|
||||
test_gcode_timing.cpp
|
||||
test_gcodewriter.cpp
|
||||
test_model.cpp
|
||||
test_print.cpp
|
||||
|
||||
325
tests/fff_print/test_gcode_timing.cpp
Normal file
325
tests/fff_print/test_gcode_timing.cpp
Normal file
@@ -0,0 +1,325 @@
|
||||
#include <catch2/catch_all.hpp>
|
||||
|
||||
#include "libslic3r/libslic3r.h"
|
||||
#include "libslic3r/GCode/GCodeProcessor.hpp"
|
||||
#include "libslic3r/PrintConfig.hpp"
|
||||
|
||||
#include "test_utils.hpp"
|
||||
|
||||
#include <fstream>
|
||||
#include <map>
|
||||
|
||||
using namespace Slic3r;
|
||||
using Catch::Matchers::WithinAbs;
|
||||
|
||||
// Regression coverage for filament/tool-change time being folded into the first
|
||||
// pending motion block (an extrusion move) instead of the tool-change move, and
|
||||
// for that delay being dropped entirely when too few motion blocks precede the
|
||||
// change. See BambuStudio "seperate flush time from other types" (c54a8333c7)
|
||||
// and the follow-up "unprocessed addtional time" fix (27ef0b1bef).
|
||||
namespace {
|
||||
|
||||
constexpr size_t NORMAL = static_cast<size_t>(PrintEstimatedStatistics::ETimeMode::Normal);
|
||||
|
||||
FullPrintConfig make_config(double load_time, double unload_time, double tool_change_time)
|
||||
{
|
||||
FullPrintConfig config; // default-initialized with the built-in defaults
|
||||
config.gcode_flavor.value = gcfMarlinFirmware;
|
||||
// Two filaments, both assigned to the same (single) extruder, so a T1 after
|
||||
// T0 is a same-extruder filament swap that costs unload + load time.
|
||||
config.filament_diameter.values = {1.75, 1.75};
|
||||
config.filament_map.values = {1, 1};
|
||||
config.machine_load_filament_time.value = load_time;
|
||||
config.machine_unload_filament_time.value = unload_time;
|
||||
config.machine_tool_change_time.value = tool_change_time;
|
||||
return config;
|
||||
}
|
||||
|
||||
void run_processor(GCodeProcessor& proc, const FullPrintConfig& config, const char* gcode)
|
||||
{
|
||||
// reserved_tag() selects between two tag tables based on this shared static, and
|
||||
// other tests in the binary mutate it -- pin it so our "; FEATURE:" role tags are
|
||||
// parsed deterministically regardless of test execution order.
|
||||
GCodeProcessor::s_IsBBLPrinter = true;
|
||||
ScopedTemporaryFile temp(".gcode");
|
||||
{
|
||||
std::ofstream os(temp.string());
|
||||
os << gcode;
|
||||
}
|
||||
proc.apply_config(config);
|
||||
// No producer marker in the gcode, so process_file keeps our applied config.
|
||||
proc.process_file(temp.string());
|
||||
}
|
||||
|
||||
// Estimated time per extrusion role, grouped exactly the way libvgcode builds the
|
||||
// feature-type legend: sum MoveVertex.time over EMoveType::Extrude moves keyed by
|
||||
// extrusion_role (see ViewerImpl.cpp:1017 -- only Extrude moves are counted).
|
||||
std::map<ExtrusionRole, double> role_times(const GCodeProcessorResult& r)
|
||||
{
|
||||
std::map<ExtrusionRole, double> m;
|
||||
for (const auto& mv : r.moves)
|
||||
if (mv.type == EMoveType::Extrude)
|
||||
m[mv.extrusion_role] += mv.time[NORMAL];
|
||||
return m;
|
||||
}
|
||||
|
||||
// Sum of estimated time attributed to tool-change moves.
|
||||
double sum_tool_change_time(const GCodeProcessorResult& r)
|
||||
{
|
||||
double t = 0.0;
|
||||
for (const auto& mv : r.moves)
|
||||
if (mv.type == EMoveType::Tool_change)
|
||||
t += mv.time[NORMAL];
|
||||
return t;
|
||||
}
|
||||
|
||||
// Total filament-change delay, accumulated independently of the timing machinery.
|
||||
double filament_change_delay(const GCodeProcessorResult& r)
|
||||
{
|
||||
const auto& s = r.print_statistics;
|
||||
return s.total_filament_load_time + s.total_filament_unload_time + s.total_tool_change_time;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST_CASE("Filament-change time is attributed to tool-change moves, not extrusion roles", "[GCodeTiming]")
|
||||
{
|
||||
// Relative extrusion (M83) so every "E5" is a real 5mm extrusion move rather
|
||||
// than a zero-delta travel. Two real travels precede T0 so its delay is flushed
|
||||
// cleanly. The extrusions after T0 span several roles (Outer wall, Sparse infill,
|
||||
// Inner wall); the first pending block at T1 is an "Outer wall" move, so the
|
||||
// buggy code folds the T1 delay into that role. The per-role check below verifies
|
||||
// EVERY role stays clean, not just one, and catches any role-to-role misattribution.
|
||||
const char* gcode =
|
||||
"M83\n"
|
||||
"; FEATURE: Outer wall\n"
|
||||
"G1 X10 Y10 Z0.2 F600\n"
|
||||
"G1 X0 Y0 F6000\n"
|
||||
"T0\n"
|
||||
"; FEATURE: Outer wall\n"
|
||||
"G1 X50 Y0 E5 F1800\n"
|
||||
"G1 X50 Y50 E5\n"
|
||||
"; FEATURE: Sparse infill\n"
|
||||
"G1 X0 Y50 E5\n"
|
||||
"G1 X0 Y0 E5\n"
|
||||
"T1\n"
|
||||
"; FEATURE: Inner wall\n"
|
||||
"G1 X50 Y0 E5\n"
|
||||
"G1 X50 Y50 E5\n";
|
||||
|
||||
GCodeProcessor proc_zero;
|
||||
run_processor(proc_zero, make_config(0.0, 0.0, 0.0), gcode);
|
||||
const GCodeProcessorResult& r_zero = proc_zero.get_result();
|
||||
|
||||
const double load = 10.0;
|
||||
const double unload = 5.0;
|
||||
GCodeProcessor proc_delay;
|
||||
run_processor(proc_delay, make_config(load, unload, 0.0), gcode);
|
||||
const GCodeProcessorResult& r_delay = proc_delay.get_result();
|
||||
|
||||
const double delay = filament_change_delay(r_delay);
|
||||
|
||||
// Preconditions: the filament changes were charged, and cost nothing in the
|
||||
// zero-time baseline.
|
||||
REQUIRE(delay > 0.0);
|
||||
REQUIRE_THAT(filament_change_delay(r_zero), WithinAbs(0.0, 1e-9));
|
||||
|
||||
// The delay must not inflate the time of ANY extrusion role. Compare the full
|
||||
// per-role breakdown (exactly how the feature-type legend is built) between the
|
||||
// zero-delay and delayed runs -- every role must match to within tolerance.
|
||||
const auto roles_zero = role_times(r_zero);
|
||||
const auto roles_delay = role_times(r_delay);
|
||||
// Guard: the gcode must genuinely exercise multiple distinct roles (Outer wall,
|
||||
// Sparse infill, Inner wall), otherwise this check would silently cover only one.
|
||||
REQUIRE(roles_zero.size() >= 3);
|
||||
REQUIRE(roles_zero.size() == roles_delay.size());
|
||||
for (const auto& [role, zero_time] : roles_zero) {
|
||||
INFO("extrusion role index = " << static_cast<int>(role));
|
||||
REQUIRE(roles_delay.count(role) == 1);
|
||||
REQUIRE_THAT(roles_delay.at(role), WithinAbs(zero_time, 1e-2));
|
||||
}
|
||||
|
||||
// The delay must instead land on the tool-change moves, so per-move consumers
|
||||
// (layer-time view, layer slider) stay consistent.
|
||||
REQUIRE_THAT(sum_tool_change_time(r_delay), WithinAbs(delay, 1e-2));
|
||||
|
||||
// Both tool changes occur on layer 1, so the delay must also be reflected in
|
||||
// the first-layer time.
|
||||
const double first_layer_delta = proc_delay.get_first_layer_time(PrintEstimatedStatistics::ETimeMode::Normal)
|
||||
- proc_zero.get_first_layer_time(PrintEstimatedStatistics::ETimeMode::Normal);
|
||||
REQUIRE_THAT(first_layer_delta, WithinAbs(delay, 1e-2));
|
||||
}
|
||||
|
||||
TEST_CASE("Filament-change time is not dropped when few motion blocks precede the change", "[GCodeTiming]")
|
||||
{
|
||||
// Only a single motion block precedes T0, so the buggy code's "fewer than two
|
||||
// pending blocks" early-out discards that filament-change delay entirely,
|
||||
// making the total print time inconsistent with the reported statistics.
|
||||
const char* gcode =
|
||||
"; FEATURE: Outer wall\n"
|
||||
"G1 X10 Y10 Z0.2 F600\n"
|
||||
"T0\n"
|
||||
"G1 X50 Y0 E5 F1800\n"
|
||||
"G1 X50 Y50 E5\n"
|
||||
"T1\n"
|
||||
"G1 X0 Y50 E5\n"
|
||||
"G1 X0 Y0 E5\n";
|
||||
|
||||
GCodeProcessor proc_zero;
|
||||
run_processor(proc_zero, make_config(0.0, 0.0, 0.0), gcode);
|
||||
|
||||
const double load = 10.0;
|
||||
const double unload = 5.0;
|
||||
GCodeProcessor proc_delay;
|
||||
run_processor(proc_delay, make_config(load, unload, 0.0), gcode);
|
||||
const GCodeProcessorResult& r_delay = proc_delay.get_result();
|
||||
|
||||
const double delay = filament_change_delay(r_delay);
|
||||
REQUIRE(delay > 0.0);
|
||||
|
||||
// Every second of reported filament-change delay must be present in the total
|
||||
// estimated print time; none may be silently dropped.
|
||||
const double total_delta = proc_delay.get_time(PrintEstimatedStatistics::ETimeMode::Normal)
|
||||
- proc_zero.get_time(PrintEstimatedStatistics::ETimeMode::Normal);
|
||||
REQUIRE_THAT(total_delta, WithinAbs(delay, 1e-2));
|
||||
}
|
||||
|
||||
TEST_CASE("Back-to-back tool changes buffer then merge into one tool-change block", "[GCodeTiming]")
|
||||
{
|
||||
// T0 is the very first line: the block queue is empty when its delay is synchronized,
|
||||
// so with only the single (artificial) tool-change block queued the delay can't be
|
||||
// attributed yet and is buffered. T1 follows immediately with no motion between; its
|
||||
// synchronize now sees two tool-change blocks queued, so its own delay joins the buffered
|
||||
// T0 entry at application time, the two same-type entries merge into one, and the sum
|
||||
// lands entirely on the first tool-change block. The trailing travels leave both runs
|
||||
// with >= 2 blocks so their end-of-file flush is identical and cancels in every delta.
|
||||
const char* gcode =
|
||||
"T0\n" // first charged change (load only); empty queue -> buffers (Tool_change,10)
|
||||
"T1\n" // same-extruder swap (unload+load); merges with buffered T0 entry to (Tool_change,25)
|
||||
"G1 X10 Y0 Z0.2 F6000\n" // travels: keep >= 2 blocks queued at EOF (flushed identically by both runs)
|
||||
"G1 X10 Y10\n"
|
||||
"G1 X0 Y10\n";
|
||||
|
||||
GCodeProcessor proc_zero;
|
||||
run_processor(proc_zero, make_config(0.0, 0.0, 0.0), gcode);
|
||||
const GCodeProcessorResult& r_zero = proc_zero.get_result();
|
||||
|
||||
GCodeProcessor proc_delay;
|
||||
run_processor(proc_delay, make_config(10.0, 5.0, 0.0), gcode);
|
||||
const GCodeProcessorResult& r_delay = proc_delay.get_result();
|
||||
|
||||
// T0 load 10 + T1 unload 5 + T1 load 10 = 25.
|
||||
const double delay = filament_change_delay(r_delay);
|
||||
REQUIRE(delay > 0.0);
|
||||
REQUIRE_THAT(delay, WithinAbs(25.0, 1e-6));
|
||||
REQUIRE_THAT(filament_change_delay(r_zero), WithinAbs(0.0, 1e-9));
|
||||
|
||||
// The whole buffered-then-merged delay must reach the total print time.
|
||||
const double total_delta = proc_delay.get_time(PrintEstimatedStatistics::ETimeMode::Normal)
|
||||
- proc_zero.get_time(PrintEstimatedStatistics::ETimeMode::Normal);
|
||||
REQUIRE_THAT(total_delta, WithinAbs(delay, 1e-2));
|
||||
|
||||
// ...and must land on the tool-change moves, not on any extrusion role.
|
||||
REQUIRE_THAT(sum_tool_change_time(r_delay), WithinAbs(25.0, 1e-2));
|
||||
REQUIRE_THAT(sum_tool_change_time(r_zero), WithinAbs(0.0, 1e-9));
|
||||
|
||||
// Characterization (documents the current merge-collapse behavior, not a correctness
|
||||
// requirement): the two buffered same-type entries combine onto the FIRST artificial
|
||||
// tool-change block; the second receives nothing. Had the merge regressed, the 10 and 15
|
||||
// would land on separate moves instead of 25 and 0.
|
||||
std::vector<double> tc;
|
||||
for (const auto& mv : r_delay.moves)
|
||||
if (mv.type == EMoveType::Tool_change)
|
||||
tc.push_back(mv.time[NORMAL]);
|
||||
REQUIRE(tc.size() >= 2);
|
||||
REQUIRE_THAT(tc[0], WithinAbs(25.0, 1e-2));
|
||||
REQUIRE_THAT(tc[1], WithinAbs(0.0, 1e-9));
|
||||
}
|
||||
|
||||
TEST_CASE("Trailing tool change at end of file is drained, not dropped", "[GCodeTiming]")
|
||||
{
|
||||
// A tool change is the last line of the file, with only its single artificial block
|
||||
// queued. Its delay is buffered (fewer than two blocks) and there is no later motion to
|
||||
// flush it, so only the finalization pass can attribute it. Without the end-of-file drain
|
||||
// the delay would be stranded in the buffer and the total print time would disagree with
|
||||
// the reported filament-change statistics.
|
||||
const char* gcode =
|
||||
"G1 X10 Y0 Z0.2 F6000\n" // three travels -> three blocks queued (no E, so no filament is selected)
|
||||
"G1 X10 Y10\n"
|
||||
"G1 X0 Y10\n"
|
||||
"G4 S0\n" // dwell with S present -> full flush; queue and buffer now empty
|
||||
"T0\n"; // trailing change, nothing after: buffers (Tool_change,10), one block queued
|
||||
|
||||
GCodeProcessor proc_zero;
|
||||
run_processor(proc_zero, make_config(0.0, 0.0, 0.0), gcode);
|
||||
const GCodeProcessorResult& r_zero = proc_zero.get_result();
|
||||
|
||||
GCodeProcessor proc_delay;
|
||||
run_processor(proc_delay, make_config(10.0, 5.0, 0.0), gcode);
|
||||
const GCodeProcessorResult& r_delay = proc_delay.get_result();
|
||||
|
||||
// T0 is the first charged change on an empty extruder, so it costs the load time only.
|
||||
const double delay = filament_change_delay(r_delay);
|
||||
REQUIRE(delay > 0.0);
|
||||
REQUIRE_THAT(delay, WithinAbs(10.0, 1e-6));
|
||||
|
||||
// The trailing change's delay must survive to the total: the zero run buffers nothing and
|
||||
// drops its artificial block, so the motion cancels and the delta is exactly the drained delay.
|
||||
const double total_delta = proc_delay.get_time(PrintEstimatedStatistics::ETimeMode::Normal)
|
||||
- proc_zero.get_time(PrintEstimatedStatistics::ETimeMode::Normal);
|
||||
REQUIRE_THAT(total_delta, WithinAbs(delay, 1e-2));
|
||||
|
||||
// The size-1 drain runs the body, so the delay lands on the artificial tool-change move.
|
||||
REQUIRE_THAT(sum_tool_change_time(r_delay), WithinAbs(10.0, 1e-2));
|
||||
REQUIRE_THAT(sum_tool_change_time(r_zero), WithinAbs(0.0, 1e-9));
|
||||
}
|
||||
|
||||
TEST_CASE("Carried-forward tool-change delay reaches the total without polluting roles", "[GCodeTiming]")
|
||||
{
|
||||
// A wildcard dwell delay is buffered ahead of the tool-change delay, so when the blocks
|
||||
// are next flushed the dwell's (Noop) entry consumes the artificial tool-change block and
|
||||
// the tool-change entry finds no matching block and carries forward. It stays unmatched
|
||||
// through the remaining extrusion moves and is only resolved at finalization, where the
|
||||
// end-of-file fold adds it to the machine total and the custom-gcode cache -- never to a
|
||||
// move vertex, so it cannot leak into an extrusion role's time.
|
||||
const char* gcode =
|
||||
"M83\n"
|
||||
"G4 S3\n" // empty queue -> buffers (Noop,3) [wildcard delay]
|
||||
"T0\n" // one block queued -> buffers (Tool_change,10) behind the dwell
|
||||
"; FEATURE: Inner wall\n"
|
||||
"G1 X20 Y0 Z0.2 E5 F1800\n" // extrusion m1: queue is [artificial_TC0, m1]
|
||||
"G4 S0\n" // flush: (Noop,3) consumes artificial_TC0; (Tool_change,10) carries forward
|
||||
"G1 X20 Y20 E5\n" // extrusion m2
|
||||
"G1 X0 Y20 E5\n"; // extrusion m3: at EOF queue is [m2, m3], buffer is [(Tool_change,10)]
|
||||
|
||||
GCodeProcessor proc_zero;
|
||||
run_processor(proc_zero, make_config(0.0, 0.0, 0.0), gcode);
|
||||
const GCodeProcessorResult& r_zero = proc_zero.get_result();
|
||||
|
||||
GCodeProcessor proc_delay;
|
||||
run_processor(proc_delay, make_config(10.0, 5.0, 0.0), gcode);
|
||||
const GCodeProcessorResult& r_delay = proc_delay.get_result();
|
||||
|
||||
// T0 is the first charged change (load only); the fixed dwell delays are not in these counters.
|
||||
const double delay = filament_change_delay(r_delay);
|
||||
REQUIRE(delay > 0.0);
|
||||
REQUIRE_THAT(delay, WithinAbs(10.0, 1e-6));
|
||||
|
||||
// The stranded tool-change delay must be drained into the total, not dropped. The 3s dwell
|
||||
// is identical in both runs and cancels along with all motion, leaving exactly the delay.
|
||||
const double total_delta = proc_delay.get_time(PrintEstimatedStatistics::ETimeMode::Normal)
|
||||
- proc_zero.get_time(PrintEstimatedStatistics::ETimeMode::Normal);
|
||||
REQUIRE_THAT(total_delta, WithinAbs(delay, 1e-2));
|
||||
|
||||
// Pollution safety: the drained delay must NOT appear in any extrusion role. Every role's
|
||||
// time must match between the zero and delayed runs -- this is what the total-only fold buys.
|
||||
const auto rz = role_times(r_zero);
|
||||
const auto rd = role_times(r_delay);
|
||||
REQUIRE(rz.size() >= 1);
|
||||
REQUIRE(rz.size() == rd.size());
|
||||
for (const auto& [role, zero_time] : rz) {
|
||||
INFO("extrusion role index = " << static_cast<int>(role));
|
||||
REQUIRE(rd.count(role) == 1);
|
||||
REQUIRE_THAT(rd.at(role), WithinAbs(zero_time, 1e-2));
|
||||
}
|
||||
}
|
||||
@@ -183,6 +183,74 @@ void trigger_precise_wall_warning(DynamicPrintConfig& c)
|
||||
|
||||
} // namespace
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// {first_object_name} filename placeholder
|
||||
// ---------------------------------------------------------------------------
|
||||
namespace {
|
||||
|
||||
// Add a printable 20mm cube named `name` to `model`; returns it so the caller can tweak it.
|
||||
ModelObject* add_named_cube(Model& model, const std::string& name)
|
||||
{
|
||||
ModelObject* obj = model.add_object();
|
||||
obj->name = name;
|
||||
obj->add_volume(make_cube(20.0, 20.0, 20.0));
|
||||
obj->add_instance();
|
||||
obj->ensure_on_bed();
|
||||
return obj;
|
||||
}
|
||||
|
||||
// Resolve `format` to an output file name for a print of `model`. `filename_base`, when set,
|
||||
// is the saved-project name passed to output_filename().
|
||||
std::string resolved_output_name(Model& model, const std::string& format, const std::string& filename_base = {})
|
||||
{
|
||||
DynamicPrintConfig config = DynamicPrintConfig::full_print_config();
|
||||
config.set_key_value("filename_format", new ConfigOptionString(format));
|
||||
|
||||
Print print;
|
||||
for (ModelObject* obj : model.objects)
|
||||
print.auto_assign_extruders(obj);
|
||||
print.apply(model, config);
|
||||
return print.output_filename(filename_base);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST_CASE("Print: {first_object_name} names the first printable object on the plate", "[Print]")
|
||||
{
|
||||
Model model;
|
||||
|
||||
SECTION("uses the object's name") {
|
||||
add_named_cube(model, "WidgetPart");
|
||||
CHECK(resolved_output_name(model, "{first_object_name}") == "WidgetPart.gcode");
|
||||
}
|
||||
|
||||
SECTION("picks the first when several objects are printable") {
|
||||
add_named_cube(model, "FirstPart");
|
||||
add_named_cube(model, "SecondPart");
|
||||
CHECK(resolved_output_name(model, "{first_object_name}") == "FirstPart.gcode");
|
||||
}
|
||||
|
||||
SECTION("skips objects outside the print volume (e.g. on another plate)") {
|
||||
// First in model order, but not on the current plate, so is_printable() is false.
|
||||
add_named_cube(model, "OtherPlatePart")->instances.front()->print_volume_state = ModelInstancePVS_Fully_Outside;
|
||||
add_named_cube(model, "OnPlatePart");
|
||||
CHECK(resolved_output_name(model, "{first_object_name}") == "OnPlatePart.gcode");
|
||||
}
|
||||
|
||||
SECTION("is empty when the object has no name") {
|
||||
add_named_cube(model, "");
|
||||
CHECK(resolved_output_name(model, "part_{first_object_name}") == "part_.gcode");
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("Print: {first_object_name} is not replaced by the saved-project file name", "[Print]")
|
||||
{
|
||||
// Passing a saved-project file name as the filename_base must not change {first_object_name}.
|
||||
Model model;
|
||||
add_named_cube(model, "WidgetPart");
|
||||
CHECK(resolved_output_name(model, "{first_object_name}", "SavedProject") == "WidgetPart.gcode");
|
||||
}
|
||||
|
||||
TEST_CASE("Print::validate stacks independent warnings", "[Print][validate]")
|
||||
{
|
||||
// Two unrelated checks (region precise-wall + machine acceleration) must each
|
||||
|
||||
@@ -24,6 +24,7 @@ add_executable(${_TEST_NAME}_tests
|
||||
test_stl.cpp
|
||||
test_meshboolean.cpp
|
||||
test_marchingsquares.cpp
|
||||
test_utils.cpp
|
||||
test_timeutils.cpp
|
||||
test_voronoi.cpp
|
||||
test_optimizers.cpp
|
||||
|
||||
@@ -11,8 +11,8 @@ SCENARIO("Placeholder parser scripting", "[PlaceholderParser]") {
|
||||
|
||||
config.set_deserialize_strict( {
|
||||
{ "printer_notes", " PRINTER_VENDOR_PRUSA3D PRINTER_MODEL_MK2 " },
|
||||
{ "nozzle_diameter", "0.6;0.6;0.6;0.6" },
|
||||
{ "nozzle_temperature", "357;359;363;378" }
|
||||
{ "nozzle_diameter", "0.6,0.6,0.6,0.6" },
|
||||
{ "nozzle_temperature", "357,359,363,378" }
|
||||
});
|
||||
// To test the "min_width_top_surface" over "inner_wall_line_width".
|
||||
config.option<ConfigOptionFloatOrPercent>("inner_wall_line_width")->value = 150.;
|
||||
@@ -121,8 +121,8 @@ SCENARIO("Placeholder parser variables", "[PlaceholderParser]") {
|
||||
config.set_deserialize_strict({
|
||||
{ "filament_notes", "testnotes" },
|
||||
{ "enable_pressure_advance", "1" },
|
||||
{ "nozzle_diameter", "0.6;0.6;0.6;0.6" },
|
||||
{ "nozzle_temperature", "357;359;363;378" }
|
||||
{ "nozzle_diameter", "0.6,0.6,0.6,0.6" },
|
||||
{ "nozzle_temperature", "357,359,363,378" }
|
||||
});
|
||||
|
||||
PlaceholderParser::ContextData context_with_global_dict;
|
||||
@@ -241,3 +241,97 @@ SCENARIO("Placeholder parser variables", "[PlaceholderParser]") {
|
||||
}
|
||||
SECTION("if else completely empty") { REQUIRE(parser.process("{if false then elsif false then else endif}", 0, nullptr, nullptr, nullptr) == ""); }
|
||||
}
|
||||
|
||||
SCENARIO("Placeholder parser coFloatsOrPercents vector access", "[PlaceholderParser]") {
|
||||
PlaceholderParser parser;
|
||||
auto config = DynamicPrintConfig::full_print_config();
|
||||
|
||||
// outer_wall_speed is the ratio_over target for small_perimeter_speed.
|
||||
// Different values per extruder to verify parent resolves at the same element index.
|
||||
config.set_deserialize_strict({
|
||||
{ "outer_wall_speed", "60,70,80,90" },
|
||||
{ "nozzle_diameter", "0.4,0.4,0.4,0.4" },
|
||||
{ "pressure_advance", "1.5,2.0,3.0,4.0" } // coFloats non-nullable
|
||||
});
|
||||
// small_perimeter_speed:
|
||||
// [0] = 50% of outer_wall_speed[0] (= 60) → 30
|
||||
// [1] = 80% of outer_wall_speed[1] (= 70) → 56
|
||||
// [2] = 0 absolute
|
||||
// [3] = 50% of outer_wall_speed[3] (= 90) → 45
|
||||
config.option<ConfigOptionFloatsOrPercentsNullable>("small_perimeter_speed")->values = {
|
||||
FloatOrPercent{50.0, true}, // 50% of outer_wall_speed[0] (60) = 30
|
||||
FloatOrPercent{80.0, true}, // 80% of outer_wall_speed[1] (70) = 56
|
||||
FloatOrPercent{0.0, false}, // absolute: 0
|
||||
FloatOrPercent{50.0, true}, // 50% of outer_wall_speed[3] (90) = 45
|
||||
};
|
||||
|
||||
parser.apply_config(config);
|
||||
parser.set("foo", 0);
|
||||
parser.set("bar", 1);
|
||||
parser.set("baz", 3);
|
||||
parser.set("num_extruders", 4);
|
||||
|
||||
SECTION("Indexed access - percent resolved against parent at same index [0]") {
|
||||
// 50% of outer_wall_speed[0] (60) = 30
|
||||
REQUIRE(std::stod(parser.process("{small_perimeter_speed[0]}")) == Catch::Approx(30.0));
|
||||
}
|
||||
|
||||
SECTION("Indexed access - percent resolved against parent at same index [1]") {
|
||||
// 80% of outer_wall_speed[1] (70) = 56
|
||||
REQUIRE(std::stod(parser.process("{small_perimeter_speed[1]}")) == Catch::Approx(56.0));
|
||||
}
|
||||
|
||||
SECTION("Indexed access - percent resolved against parent at same index [3]") {
|
||||
// 50% of outer_wall_speed[3] (90) = 45
|
||||
REQUIRE(std::stod(parser.process("{small_perimeter_speed[3]}")) == Catch::Approx(45.0));
|
||||
}
|
||||
|
||||
SECTION("Variable-indexed access via foo (=0) - percent value") {
|
||||
// 50% of outer_wall_speed[0] (60) = 30
|
||||
REQUIRE(std::stod(parser.process("{small_perimeter_speed[foo]}")) == Catch::Approx(30.0));
|
||||
}
|
||||
|
||||
SECTION("Variable-indexed access via bar (=1) - percent value") {
|
||||
// 80% of outer_wall_speed[1] (70) = 56
|
||||
REQUIRE(std::stod(parser.process("{small_perimeter_speed[bar]}")) == Catch::Approx(56.0));
|
||||
}
|
||||
|
||||
SECTION("Variable-indexed access via baz (=3) - percent value") {
|
||||
// 50% of outer_wall_speed[3] (90) = 45
|
||||
REQUIRE(std::stod(parser.process("{small_perimeter_speed[baz]}")) == Catch::Approx(45.0));
|
||||
}
|
||||
|
||||
SECTION("Literal-indexed access - absolute value") {
|
||||
REQUIRE(std::stod(parser.process("{small_perimeter_speed[2]}")) == Catch::Approx(0.0));
|
||||
}
|
||||
|
||||
SECTION("No-index (extruder-based) access - percent resolved via current extruder") {
|
||||
// Extruder 0 = 50% of outer_wall_speed[0] (60) = 30
|
||||
REQUIRE(std::stod(parser.process("{small_perimeter_speed}")) == Catch::Approx(30.0));
|
||||
}
|
||||
|
||||
SECTION("Out-of-range index clamps to index 0") {
|
||||
// Index 99 is out of range, clamps to 0: 50% of outer_wall_speed[0] (60) = 30
|
||||
REQUIRE(std::stod(parser.process("{small_perimeter_speed[99]}")) == Catch::Approx(30.0));
|
||||
}
|
||||
|
||||
SECTION("coFloats no-index access - nullable (outer_wall_speed)") {
|
||||
// outer_wall_speed is ConfigOptionFloatsNullable, exercises the 'if' branch
|
||||
REQUIRE(std::stod(parser.process("{outer_wall_speed}")) == Catch::Approx(60.0));
|
||||
}
|
||||
|
||||
SECTION("coFloats indexed access - nullable") {
|
||||
// outer_wall_speed[2] = 80
|
||||
REQUIRE(std::stod(parser.process("{outer_wall_speed[2]}")) == Catch::Approx(80.0));
|
||||
}
|
||||
|
||||
SECTION("coFloats no-index access - non-nullable (pressure_advance)") {
|
||||
// pressure_advance is ConfigOptionFloats (non-nullable), exercises the 'else' branch
|
||||
REQUIRE(std::stod(parser.process("{pressure_advance}")) == Catch::Approx(1.5));
|
||||
}
|
||||
|
||||
SECTION("coFloats indexed access - non-nullable") {
|
||||
// pressure_advance[2] = 3.0
|
||||
REQUIRE(std::stod(parser.process("{pressure_advance[2]}")) == Catch::Approx(3.0));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
#include <boost/filesystem.hpp>
|
||||
|
||||
#include "libslic3r/PresetBundle.hpp"
|
||||
#include "libslic3r/AppConfig.hpp"
|
||||
|
||||
using namespace Slic3r;
|
||||
|
||||
@@ -298,3 +299,168 @@ TEST_CASE("Removed Generic parent is normalized into a loaded filament's inherit
|
||||
CHECK(bundle.filaments.get_preset_parent(*child)->name == "Generic PLA @System");
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
// A live reference to a preset's compatible_printers / compatible_prints list. Fetches the *stored*
|
||||
// preset (real=true) so writes and reads hit the same object; creates the option if absent.
|
||||
std::vector<std::string> &compatible_list(PresetCollection &coll, const std::string &preset_name, const char *field_key)
|
||||
{
|
||||
Preset *preset = coll.find_preset(preset_name, /*first_visible_if_not_found=*/false, /*real=*/true);
|
||||
REQUIRE(preset != nullptr);
|
||||
return preset->config.option<ConfigOptionStrings>(field_key, true)->values;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST_CASE("Renamed printer/process names are normalized into compatible lists on load", "[Preset][Rename]")
|
||||
{
|
||||
PresetBundle bundle;
|
||||
|
||||
// Current printer + process, each renamed from an older name.
|
||||
add_inmemory_preset(bundle.printers, "New Printer");
|
||||
set_renamed_from(bundle.printers, "New Printer", { "Old Printer" });
|
||||
add_inmemory_preset(bundle.prints, "New Process");
|
||||
set_renamed_from(bundle.prints, "New Process", { "Old Process" });
|
||||
|
||||
// A user process still compatible with the OLD printer name.
|
||||
add_inmemory_preset(bundle.prints, "My Process");
|
||||
compatible_list(bundle.prints, "My Process", "compatible_printers") = { "Old Printer" };
|
||||
|
||||
// A user filament referencing the OLD printer AND OLD process names, plus an unknown printer.
|
||||
add_inmemory_preset(bundle.filaments, "My Filament");
|
||||
compatible_list(bundle.filaments, "My Filament", "compatible_printers") = { "Old Printer", "Unknown Printer" };
|
||||
compatible_list(bundle.filaments, "My Filament", "compatible_prints") = { "Old Process" };
|
||||
|
||||
// Build the rename maps (done during system load in the real pipeline), then normalize.
|
||||
AppConfig app_config;
|
||||
bundle.load_installed_printers(app_config); // rebuilds every collection's rename map
|
||||
bundle.normalize_compatible_presets();
|
||||
|
||||
// The stale printer name in a process' compatible_printers is rewritten to the current name.
|
||||
CHECK(compatible_list(bundle.prints, "My Process", "compatible_printers") == std::vector<std::string>{ "New Printer" });
|
||||
|
||||
// The stale process name in a filament's compatible_prints is rewritten (this field has no
|
||||
// runtime rename fallback, so load-time normalization is the only fix).
|
||||
CHECK(compatible_list(bundle.filaments, "My Filament", "compatible_prints") == std::vector<std::string>{ "New Process" });
|
||||
|
||||
// The renamed printer is rewritten while the unknown/deleted name is preserved as-is.
|
||||
CHECK(compatible_list(bundle.filaments, "My Filament", "compatible_printers") ==
|
||||
(std::vector<std::string>{ "New Printer", "Unknown Printer" }));
|
||||
|
||||
// Normalizing rewrites config in place without flagging the preset dirty.
|
||||
CHECK_FALSE(bundle.prints.find_preset("My Process", false, true)->is_dirty);
|
||||
|
||||
// A system preset that already references the current name is left untouched (idempotent no-op).
|
||||
bundle.normalize_compatible_presets();
|
||||
CHECK(compatible_list(bundle.prints, "My Process", "compatible_printers") == std::vector<std::string>{ "New Printer" });
|
||||
}
|
||||
|
||||
TEST_CASE("Renamed names are normalized into a SYSTEM preset's compatible lists", "[Preset][Rename]")
|
||||
{
|
||||
PresetBundle bundle;
|
||||
|
||||
// Current printer + process, each renamed from an older name.
|
||||
add_inmemory_preset(bundle.printers, "New Printer");
|
||||
set_renamed_from(bundle.printers, "New Printer", { "Old Printer" });
|
||||
add_inmemory_preset(bundle.prints, "New Process");
|
||||
set_renamed_from(bundle.prints, "New Process", { "Old Process" });
|
||||
|
||||
// A *system* (vendor) filament whose own compatible lists still reference the OLD names. A vendor
|
||||
// profile can point at a sibling preset that was later renamed, so system presets must be
|
||||
// normalized too (they are skipped by neither collection walk).
|
||||
add_inmemory_preset(bundle.filaments, "System Filament").is_system = true;
|
||||
compatible_list(bundle.filaments, "System Filament", "compatible_printers") = { "Old Printer" };
|
||||
compatible_list(bundle.filaments, "System Filament", "compatible_prints") = { "Old Process" };
|
||||
|
||||
AppConfig app_config;
|
||||
bundle.load_installed_printers(app_config); // build the rename maps
|
||||
bundle.normalize_compatible_presets();
|
||||
|
||||
// The stale references in the system preset are rewritten to the current names.
|
||||
CHECK(compatible_list(bundle.filaments, "System Filament", "compatible_printers") ==
|
||||
std::vector<std::string>{ "New Printer" });
|
||||
CHECK(compatible_list(bundle.filaments, "System Filament", "compatible_prints") ==
|
||||
std::vector<std::string>{ "New Process" });
|
||||
|
||||
// The rewrite does not flag the system preset dirty, and is idempotent.
|
||||
CHECK_FALSE(bundle.filaments.find_preset("System Filament", false, true)->is_dirty);
|
||||
bundle.normalize_compatible_presets();
|
||||
CHECK(compatible_list(bundle.filaments, "System Filament", "compatible_printers") ==
|
||||
std::vector<std::string>{ "New Printer" });
|
||||
}
|
||||
|
||||
TEST_CASE("compatible_prints on SLA materials resolves against sla_prints, not prints", "[Preset][Rename]")
|
||||
{
|
||||
PresetBundle bundle;
|
||||
|
||||
// A renamed SLA process, and a same-named FFF process that must NOT be picked up: resolving the
|
||||
// SLA material's compatible_prints against `prints` would wrongly rewrite to "Wrong FFF Process".
|
||||
add_inmemory_preset(bundle.sla_prints, "New SLA Process");
|
||||
set_renamed_from(bundle.sla_prints, "New SLA Process", { "Old SLA Process" });
|
||||
add_inmemory_preset(bundle.prints, "Wrong FFF Process");
|
||||
set_renamed_from(bundle.prints, "Wrong FFF Process", { "Old SLA Process" });
|
||||
|
||||
add_inmemory_preset(bundle.sla_materials, "My SLA Material");
|
||||
compatible_list(bundle.sla_materials, "My SLA Material", "compatible_prints") = { "Old SLA Process" };
|
||||
|
||||
AppConfig app_config;
|
||||
bundle.load_installed_printers(app_config);
|
||||
bundle.normalize_compatible_presets();
|
||||
|
||||
CHECK(compatible_list(bundle.sla_materials, "My SLA Material", "compatible_prints") ==
|
||||
std::vector<std::string>{ "New SLA Process" });
|
||||
}
|
||||
|
||||
TEST_CASE("Profile validator flags dangling and renamed preset references", "[Preset][Validate]")
|
||||
{
|
||||
PresetBundle bundle;
|
||||
|
||||
// Current printers: a real one, and a renamed one (its old name resolves via renamed_from).
|
||||
add_inmemory_preset(bundle.printers, "Real Printer");
|
||||
add_inmemory_preset(bundle.printers, "New Printer");
|
||||
set_renamed_from(bundle.printers, "New Printer", { "Old Printer" });
|
||||
|
||||
// A real process, referenced from a filament's compatible_prints.
|
||||
add_inmemory_preset(bundle.prints, "Real Process").is_system = true;
|
||||
|
||||
// A fully valid system filament: references only current names.
|
||||
add_inmemory_preset(bundle.filaments, "Good Filament").is_system = true;
|
||||
compatible_list(bundle.filaments, "Good Filament", "compatible_printers") = { "Real Printer" };
|
||||
compatible_list(bundle.filaments, "Good Filament", "compatible_prints") = { "Real Process" };
|
||||
|
||||
AppConfig app_config;
|
||||
bundle.load_installed_printers(app_config); // build the rename maps
|
||||
|
||||
// With only valid references, the validator is clean.
|
||||
CHECK_FALSE(bundle.check_preset_references());
|
||||
|
||||
SECTION("deleted compatible_printers is flagged") {
|
||||
add_inmemory_preset(bundle.filaments, "Ghost Ref Filament").is_system = true;
|
||||
compatible_list(bundle.filaments, "Ghost Ref Filament", "compatible_printers") = { "Ghost Printer" };
|
||||
CHECK(bundle.check_preset_references());
|
||||
}
|
||||
|
||||
SECTION("renamed compatible_printers (old name) is flagged") {
|
||||
add_inmemory_preset(bundle.filaments, "Old Ref Filament").is_system = true;
|
||||
compatible_list(bundle.filaments, "Old Ref Filament", "compatible_printers") = { "Old Printer" };
|
||||
CHECK(bundle.check_preset_references());
|
||||
}
|
||||
|
||||
SECTION("deleted compatible_prints is flagged") {
|
||||
add_inmemory_preset(bundle.filaments, "Bad Process Ref").is_system = true;
|
||||
compatible_list(bundle.filaments, "Bad Process Ref", "compatible_prints") = { "Ghost Process" };
|
||||
CHECK(bundle.check_preset_references());
|
||||
}
|
||||
|
||||
SECTION("deleted inherits parent is flagged") {
|
||||
add_inmemory_preset(bundle.filaments, "Orphan Filament", "Ghost Parent").is_system = true;
|
||||
CHECK(bundle.check_preset_references());
|
||||
}
|
||||
|
||||
SECTION("non-system preset with a dangling reference is ignored") {
|
||||
add_inmemory_preset(bundle.filaments, "User Filament"); // is_system stays false
|
||||
compatible_list(bundle.filaments, "User Filament", "compatible_printers") = { "Ghost Printer" };
|
||||
CHECK_FALSE(bundle.check_preset_references());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
54
tests/libslic3r/test_utils.cpp
Normal file
54
tests/libslic3r/test_utils.cpp
Normal file
@@ -0,0 +1,54 @@
|
||||
#include <catch2/catch_all.hpp>
|
||||
|
||||
#include "libslic3r/Utils.hpp"
|
||||
|
||||
#ifndef _WIN32
|
||||
#include <unistd.h> // getuid
|
||||
#endif
|
||||
|
||||
using namespace Slic3r;
|
||||
|
||||
TEST_CASE("per_user_temp_dir composes a per-user temp root", "[utils]") {
|
||||
const std::string base = "/tmp";
|
||||
|
||||
SECTION("an empty id returns base unchanged") {
|
||||
REQUIRE(per_user_temp_dir(base, "") == base);
|
||||
}
|
||||
SECTION("a non-empty id is appended at the top level") {
|
||||
REQUIRE(per_user_temp_dir(base, "1000") == base + "/orcaslicer_1000");
|
||||
}
|
||||
SECTION("distinct ids produce distinct roots") {
|
||||
REQUIRE(per_user_temp_dir(base, "1000") != per_user_temp_dir(base, "1001"));
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("per_user_temp_id follows the platform contract", "[utils]") {
|
||||
const std::string id = per_user_temp_id();
|
||||
|
||||
SECTION("stable across calls") {
|
||||
REQUIRE(per_user_temp_id() == id);
|
||||
}
|
||||
#ifdef _WIN32
|
||||
SECTION("empty on Windows (its temp dir is already per-user)") {
|
||||
REQUIRE(id.empty());
|
||||
}
|
||||
#else
|
||||
SECTION("the current uid on Linux/macOS") {
|
||||
REQUIRE_FALSE(id.empty());
|
||||
REQUIRE(id == std::to_string(static_cast<unsigned long>(::getuid())));
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
// The end-to-end contract callers depend on: the temp root is left alone on
|
||||
// Windows and isolated per user on Linux/macOS.
|
||||
TEST_CASE("per-user temp root is unchanged on Windows, isolated elsewhere", "[utils]") {
|
||||
const std::string base = "/tmp";
|
||||
const std::string root = per_user_temp_dir(base, per_user_temp_id());
|
||||
#ifdef _WIN32
|
||||
REQUIRE(root == base);
|
||||
#else
|
||||
REQUIRE(root != base);
|
||||
REQUIRE_THAT(root, Catch::Matchers::StartsWith(base + "/orcaslicer_"));
|
||||
#endif
|
||||
}
|
||||
Reference in New Issue
Block a user