Files
OrcaSlicer/tests/fff_print/test_cooling.cpp
T
Hanif Koh 32d92a55d3 Make Unit Tests That Could Never Fail Check Real Behavior
Rework tests whose assertions held whatever the production code did: inverted or self-referential checks, checks on values the test had just set, branches that never ran, symmetric fixtures that hid the behavior under test, font paths that skipped silently, and Voronoi reproducers that only exercised boost. Each now asserts the behavior its name describes.
2026-10-19 17:02:49 +08:00

196 lines
8.5 KiB
C++

#include <catch2/catch_all.hpp>
#include <catch2/catch_test_macros.hpp>
#include "catch2/catch_message.hpp"
#include "test_helpers.hpp"
#include <algorithm>
#include <cstddef>
#include "libslic3r/PrintConfig.hpp"
#include "libslic3r/Config.hpp"
#include "libslic3r/TriangleMesh.hpp"
#include "libslic3r/libslic3r.h"
#include <sstream>
#include <string>
#include <utility>
#include <vector>
using namespace Slic3r;
using namespace Slic3r::Test;
// The fan stays off while printing the first close_fan_the_first_x_layers layers and runs afterwards.
TEST_CASE("Fan is held off for the initial layers", "[Cooling]")
{
const int fan_off_layers = 5;
const std::string gcode = slice({ cube(20) }, {
{ "close_fan_the_first_x_layers", fan_off_layers },
{ "full_fan_speed_layer", 0 },
{ "fan_min_speed", 50 },
{ "fan_max_speed", 100 },
{ "fan_cooling_layer_time", 1000 }, // every layer is short enough to run the fan
{ "layer_height", 0.2 },
{ "initial_layer_print_height", 0.2 },
{ "layer_change_gcode", ";TEST_LAYER_CHANGE" },
});
// Record, for each layer, whether any extrusion was printed with the part fan running.
std::vector<bool> extruded_with_fan;
int layer = -1;
bool fan_on = false;
std::istringstream input(gcode);
std::string line;
while (std::getline(input, line)) {
if (line.rfind(";TEST_LAYER_CHANGE", 0) == 0) {
++layer;
extruded_with_fan.push_back(false);
} else if (line.rfind("M107", 0) == 0) {
fan_on = false;
} else if (line.rfind("M106 ", 0) == 0 && line.find(" P") == std::string::npos) {
const size_t s = line.find(" S");
fan_on = s != std::string::npos && std::stod(line.substr(s + 2)) > 0.;
} else if (layer >= 0 && fan_on && line.rfind("G1 ", 0) == 0 && line.find(" E") != std::string::npos &&
(line.find(" X") != std::string::npos || line.find(" Y") != std::string::npos)) {
extruded_with_fan[layer] = true;
}
}
// A 20 mm cube at 0.2 mm layers has 100 layers.
REQUIRE(extruded_with_fan.size() == 100);
for (int i = 0; i < fan_off_layers; ++i) {
CAPTURE(i);
CHECK_FALSE(extruded_with_fan[i]);
}
CHECK(extruded_with_fan[fan_off_layers]);
}
// The cooling pass resolves and strips its internal speed placeholders; none leak into
// the final G-code.
TEST_CASE("Cooling consumes its internal speed markers", "[Cooling]")
{
const std::string gcode = slice({ cube(20) }, { { "layer_height", 0.2 } });
CHECK(gcode.find(";_EXTRUDE_SET_SPEED") == std::string::npos);
}
TEST_CASE("Arc fitting remains active with overhang fan control", "[Cooling][Regression]")
{
DynamicPrintConfig config = DynamicPrintConfig::full_print_config();
config.set_deserialize_strict({
{ "enable_arc_fitting", true },
{ "enable_overhang_bridge_fan", true },
{ "enable_overhang_speed", true },
{ "initial_layer_print_height", 0.2 },
{ "layer_height", 0.2 },
{ "slow_down_for_layer_cooling", false },
});
config.set_key_value("fan_max_speed", new ConfigOptionFloats{20.0});
config.set_key_value("fan_min_speed", new ConfigOptionFloats{20.0});
config.set_key_value("overhang_fan_speed", new ConfigOptionInts{100});
config.set_key_value("overhang_fan_threshold", new ConfigOptionEnumsGeneric{Overhang_threshold_2_4});
config.set_key_value("layer_change_gcode", new ConfigOptionString{";TEST_LAYER=[layer_num]"});
const auto has_arc_after_first_layer = [](const std::string &gcode) {
std::istringstream input(gcode);
std::string line;
size_t layers_seen = 0;
while (std::getline(input, line)) {
if (line.rfind(";TEST_LAYER=", 0) == 0)
++layers_seen;
else if (layers_seen > 1 && (line.rfind("G2 ", 0) == 0 || line.rfind("G3 ", 0) == 0))
return true;
}
return false;
};
const auto has_arc_with_overhang_fan = [](const std::string &gcode) {
std::istringstream input(gcode);
std::string line;
bool overhang_fan_active = false;
bool outer_wall = false;
while (std::getline(input, line)) {
if (line.rfind("M106 S255", 0) == 0)
overhang_fan_active = true;
else if (line.rfind("M106 S0", 0) == 0 || line.rfind("M107", 0) == 0)
overhang_fan_active = false;
else if (line.rfind(";TYPE:", 0) == 0)
outer_wall = line == ";TYPE:Outer wall";
else if (overhang_fan_active && outer_wall && (line.rfind("G2 ", 0) == 0 || line.rfind("G3 ", 0) == 0))
return true;
}
return false;
};
const std::string with_overhang_speed = slice({ make_cylinder(25.0, 10.0, 2.0 * PI / 72.0) }, config);
CHECK(has_arc_after_first_layer(with_overhang_speed));
config.set_deserialize_strict({ { "enable_overhang_speed", false } });
const std::string without_overhang_speed = slice({ make_cylinder(25.0, 10.0, 2.0 * PI / 72.0) }, config);
CHECK(has_arc_after_first_layer(without_overhang_speed));
const std::string uniform_overhang_gcode = slice({ make_sphere(50.0, 2.0 * PI / 72.0) }, config);
CHECK(has_arc_with_overhang_fan(uniform_overhang_gcode));
}
TEST_CASE("Overhang fan transitions do not depend on overhang speed", "[Cooling][Regression]")
{
DynamicPrintConfig config = DynamicPrintConfig::full_print_config();
config.set_deserialize_strict({
{ "bridge_speed", 2.0 },
{ "enable_arc_fitting", false },
{ "enable_overhang_bridge_fan", true },
{ "enable_overhang_speed", false },
{ "initial_layer_print_height", 0.3 },
{ "inner_wall_speed", 30.0 },
{ "layer_height", 0.3 },
{ "outer_wall_speed", 30.0 },
{ "overhang_1_4_speed", "30" },
{ "overhang_2_4_speed", "29" },
{ "overhang_3_4_speed", "6" },
{ "overhang_4_4_speed", "3" },
{ "slow_down_for_layer_cooling", false },
{ "slowdown_for_curled_perimeters", false },
});
config.set_key_value("fan_max_speed", new ConfigOptionFloats{20.0});
config.set_key_value("fan_min_speed", new ConfigOptionFloats{20.0});
config.set_key_value("overhang_fan_speed", new ConfigOptionInts{100});
config.set_key_value("overhang_fan_threshold", new ConfigOptionEnumsGeneric{Overhang_threshold_2_4});
config.set_key_value("layer_change_gcode", new ConfigOptionString{";TEST_LAYER_Z=[layer_z]"});
const auto fan_commands = [](const std::string &gcode) {
std::vector<std::pair<std::string, std::string>> commands;
std::istringstream input(gcode);
std::string layer;
std::string line;
while (std::getline(input, line)) {
if (line.rfind(";TEST_LAYER_Z=", 0) == 0)
layer = line;
else if (!layer.empty() && (line.rfind("M106", 0) == 0 || line.rfind("M107", 0) == 0))
commands.emplace_back(layer, line);
}
return commands;
};
const auto feedrates = [](const std::string &gcode) {
std::vector<std::string> values;
std::istringstream input(gcode);
std::string word;
while (input >> word)
if (!word.empty() && word.front() == 'F')
values.push_back(word);
return values;
};
constexpr double sphere_radius = 50.0; // 100 mm diameter.
const std::string without_speed_gcode = slice({make_sphere(sphere_radius, PI / 24.0)}, config);
config.set_deserialize_strict({{"enable_overhang_speed", true}});
const std::string with_speed_gcode = slice({make_sphere(sphere_radius, PI / 24.0)}, config);
const auto without_speed_fan = fan_commands(without_speed_gcode);
const auto with_speed_fan = fan_commands(with_speed_gcode);
const auto without_speed_feedrates = feedrates(without_speed_gcode);
const auto with_speed_feedrates = feedrates(with_speed_gcode);
REQUIRE_FALSE(without_speed_fan.empty());
REQUIRE(std::any_of(without_speed_fan.begin(), without_speed_fan.end(),
[](const auto &command) { return command.second.find("S255") != std::string::npos; }));
REQUIRE(with_speed_feedrates != without_speed_feedrates);
CHECK(with_speed_fan == without_speed_fan);
}