mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-09-16 21:42:43 +00:00
Merge branch 'main' into feat/printer-agent-infra
This commit is contained in:
@@ -15,6 +15,7 @@
|
||||
#include "libslic3r/Geometry.hpp"
|
||||
#include "libslic3r/Layer.hpp"
|
||||
#include "libslic3r/Print.hpp"
|
||||
#include "libslic3r/PrintConfig.hpp"
|
||||
#include "libslic3r/SVG.hpp"
|
||||
#include "libslic3r/libslic3r.h"
|
||||
|
||||
@@ -676,6 +677,73 @@ TEST_CASE("Ironing follows the solid infill rotation template", "[Fill]")
|
||||
REQUIRE(compared > int(ironing.size()) / 2);
|
||||
}
|
||||
|
||||
|
||||
namespace {
|
||||
|
||||
PrintRegionConfig ironing_config(IroningType type,
|
||||
int top_surface_filament_id = 1,
|
||||
int top_shell_layers = 3,
|
||||
int bottom_shell_layers = 1)
|
||||
{
|
||||
PrintRegionConfig cfg;
|
||||
cfg.ironing_type.value = type;
|
||||
cfg.top_surface_filament_id.value = top_surface_filament_id;
|
||||
cfg.top_shell_layers.value = top_shell_layers;
|
||||
cfg.bottom_shell_layers.value = bottom_shell_layers;
|
||||
cfg.outer_wall_filament_id.value = 1;
|
||||
cfg.wall_loops.value = 2;
|
||||
return cfg;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST_CASE("Ironing an all-solid region uses the top surface filament on every layer", "[Fill]")
|
||||
{
|
||||
const PrintRegionConfig cfg = ironing_config(IroningType::AllSolid, /*top_surface_filament_id=*/2);
|
||||
const bool is_topmost_layer = GENERATE(false, true);
|
||||
CAPTURE(is_topmost_layer);
|
||||
REQUIRE(Layer::choose_ironing_extruder(cfg, /*spiral_mode=*/false, is_topmost_layer) == 2);
|
||||
}
|
||||
|
||||
TEST_CASE("Ironing top surfaces uses the top surface filament when the region has top shells", "[Fill]")
|
||||
{
|
||||
const PrintRegionConfig cfg = ironing_config(IroningType::TopSurfaces,
|
||||
/*top_surface_filament_id=*/3,
|
||||
/*top_shell_layers=*/2);
|
||||
REQUIRE(Layer::choose_ironing_extruder(cfg, /*spiral_mode=*/false, /*is_topmost_layer=*/false) == 3);
|
||||
}
|
||||
|
||||
TEST_CASE("Ironing top surfaces without top shells needs spiral mode and more than one bottom shell", "[Fill]")
|
||||
{
|
||||
const PrintRegionConfig one_bottom_shell = ironing_config(IroningType::TopSurfaces,
|
||||
/*top_surface_filament_id=*/1,
|
||||
/*top_shell_layers=*/0,
|
||||
/*bottom_shell_layers=*/1);
|
||||
const PrintRegionConfig two_bottom_shells = ironing_config(IroningType::TopSurfaces,
|
||||
/*top_surface_filament_id=*/1,
|
||||
/*top_shell_layers=*/0,
|
||||
/*bottom_shell_layers=*/2);
|
||||
|
||||
REQUIRE(Layer::choose_ironing_extruder(two_bottom_shells, /*spiral_mode=*/true, /*is_topmost_layer=*/false) == 1);
|
||||
REQUIRE(Layer::choose_ironing_extruder(one_bottom_shell, /*spiral_mode=*/true, /*is_topmost_layer=*/false) == -1);
|
||||
REQUIRE(Layer::choose_ironing_extruder(two_bottom_shells, /*spiral_mode=*/false, /*is_topmost_layer=*/false) == -1);
|
||||
}
|
||||
|
||||
TEST_CASE("Ironing the topmost surface only applies to the topmost layer", "[Fill]")
|
||||
{
|
||||
const PrintRegionConfig cfg = ironing_config(IroningType::TopmostOnly, /*top_surface_filament_id=*/4);
|
||||
REQUIRE(Layer::choose_ironing_extruder(cfg, /*spiral_mode=*/false, /*is_topmost_layer=*/true) == 4);
|
||||
REQUIRE(Layer::choose_ironing_extruder(cfg, /*spiral_mode=*/false, /*is_topmost_layer=*/false) == -1);
|
||||
}
|
||||
|
||||
TEST_CASE("A region with ironing turned off is never ironed", "[Fill]")
|
||||
{
|
||||
const PrintRegionConfig cfg = ironing_config(IroningType::NoIroning);
|
||||
const bool spiral_mode = GENERATE(false, true);
|
||||
CAPTURE(spiral_mode);
|
||||
REQUIRE(Layer::choose_ironing_extruder(cfg, spiral_mode, /*is_topmost_layer=*/true) == -1);
|
||||
}
|
||||
|
||||
TEST_CASE("Solid infill direction offsets every layer when no template is set", "[Fill]")
|
||||
{
|
||||
auto angles_for = [](int direction) {
|
||||
|
||||
@@ -15,6 +15,8 @@
|
||||
#include <boost/nowide/fstream.hpp>
|
||||
#include <nlohmann/json.hpp>
|
||||
|
||||
#include <sstream>
|
||||
|
||||
using namespace Slic3r;
|
||||
|
||||
SCENARIO("Generic config validation performs as expected.", "[Config]") {
|
||||
@@ -488,6 +490,59 @@ TEST_CASE("save_to_json round-trips plugin capability references as strings", "[
|
||||
CHECK(reloaded.option<ConfigOptionStrings>("slicing_pipeline_plugin")->values == refs);
|
||||
}
|
||||
|
||||
TEST_CASE("save_to_json writes the same document to a stream as to a file", "[Config]") {
|
||||
DynamicPrintConfig config;
|
||||
config.set_key_value("layer_height", new ConfigOptionFloat(0.2));
|
||||
config.set_key_value("wall_loops", new ConfigOptionInt(3));
|
||||
config.set_key_value("filament_type", new ConfigOptionStrings({ "PLA", "PETG" }));
|
||||
config.set_key_value("machine_start_gcode", new ConfigOptionString("G28\nG1 Z5"));
|
||||
|
||||
ScopedTemporaryFile tmp(".json");
|
||||
config.save_to_json(tmp.string(), "test_preset", "User", "1.0.0.0");
|
||||
std::string file_contents;
|
||||
{
|
||||
boost::nowide::ifstream ifs(tmp.string());
|
||||
file_contents.assign(std::istreambuf_iterator<char>(ifs), std::istreambuf_iterator<char>());
|
||||
}
|
||||
// The file format: one tab per nesting level and a trailing newline.
|
||||
REQUIRE_FALSE(file_contents.empty());
|
||||
CHECK(file_contents.rfind("{\n\t\"", 0) == 0);
|
||||
CHECK(file_contents.back() == '\n');
|
||||
|
||||
std::ostringstream strict, replaced;
|
||||
config.save_to_json(strict, "test_preset", "User", "1.0.0.0");
|
||||
config.save_to_json(replaced, "test_preset", "User", "1.0.0.0", true);
|
||||
CHECK(strict.str() == file_contents);
|
||||
CHECK(replaced.str() == file_contents);
|
||||
CHECK(nlohmann::json::parse(strict.str())["machine_start_gcode"] == "G28\nG1 Z5");
|
||||
}
|
||||
|
||||
TEST_CASE("save_to_json replaces invalid UTF-8 in a stream only when asked", "[Config]") {
|
||||
DynamicPrintConfig config;
|
||||
config.set_key_value("machine_start_gcode", new ConfigOptionString("G28 ; \xff"));
|
||||
|
||||
std::ostringstream strict, replaced;
|
||||
CHECK_THROWS_AS(config.save_to_json(strict, "test_preset", "User", "1.0.0.0"), nlohmann::json::type_error);
|
||||
REQUIRE_NOTHROW(config.save_to_json(replaced, "test_preset", "User", "1.0.0.0", true));
|
||||
CHECK(nlohmann::json::parse(replaced.str())["machine_start_gcode"] == "G28 ; \xEF\xBF\xBD");
|
||||
}
|
||||
|
||||
TEST_CASE("save_to_json leaves an existing file untouched when the config cannot be serialized", "[Config]") {
|
||||
DynamicPrintConfig config;
|
||||
config.set_key_value("machine_start_gcode", new ConfigOptionString("G28 ; \xff"));
|
||||
|
||||
ScopedTemporaryFile tmp(".json");
|
||||
{
|
||||
boost::nowide::ofstream ofs(tmp.string());
|
||||
ofs << "previous";
|
||||
}
|
||||
CHECK_THROWS_AS(config.save_to_json(tmp.string(), "test_preset", "User", "1.0.0.0"), nlohmann::json::type_error);
|
||||
|
||||
boost::nowide::ifstream ifs(tmp.string());
|
||||
const std::string contents((std::istreambuf_iterator<char>(ifs)), std::istreambuf_iterator<char>());
|
||||
CHECK(contents == "previous");
|
||||
}
|
||||
|
||||
TEST_CASE("plugin capability references survive string-map serialization", "[Config][plugins]") {
|
||||
const std::vector<std::string> refs = {
|
||||
"master_plugin;;header-stamp",
|
||||
|
||||
@@ -484,6 +484,34 @@ TEST_CASE("update_values_to_printer_extruders_for_multiple_filaments resolves pe
|
||||
REQUIRE(config.option<ConfigOptionFloats>("filament_max_volumetric_speed")->values == std::vector<double>({12., 21.}));
|
||||
REQUIRE(config.option<ConfigOptionInts>("filament_self_index")->values == std::vector<int>({1, 2}));
|
||||
}
|
||||
|
||||
SECTION("a variant option shorter than the filament slots keeps its first value instead of zero") {
|
||||
DynamicPrintConfig config;
|
||||
config.option<ConfigOptionEnumsGeneric>("extruder_type", true)->values = {etDirectDrive, etDirectDrive};
|
||||
config.option<ConfigOptionEnumsGeneric>("nozzle_volume_type", true)->values = {nvtStandard, nvtHighFlow};
|
||||
config.option<ConfigOptionStrings>("extruder_variant_list", true)->values = {"Direct Drive Standard,Direct Drive High Flow",
|
||||
"Direct Drive Standard,Direct Drive High Flow"};
|
||||
make_filament_arrays(config);
|
||||
config.option<ConfigOptionInts>("filament_map", true)->values = {1, 2};
|
||||
// no loaded preset carries the key, so only its single registered default is present
|
||||
config.option<ConfigOptionFloatsNullable>("filament_cooling_before_tower", true)->values = {10.};
|
||||
// only the first filament's two variant columns were loaded
|
||||
config.option<ConfigOptionFloatsNullable>("filament_ramming_volumetric_speed", true)->values = {-1., -2.};
|
||||
|
||||
std::vector<std::vector<NozzleVolumeType>> nozzle_volume_types;
|
||||
int extruder_count = 2;
|
||||
int count = config.get_extruder_nozzle_volume_count(extruder_count, nozzle_volume_types);
|
||||
|
||||
config.update_values_to_printer_extruders_for_multiple_filaments(config, extruder_count, count, filament_keys,
|
||||
"filament_self_index", "filament_extruder_variant");
|
||||
|
||||
// filament 2 resolves to column 3 (its extruder's High Flow column), past the end of both vectors
|
||||
REQUIRE_THAT(config.option<ConfigOptionFloatsNullable>("filament_cooling_before_tower")->values,
|
||||
Catch::Matchers::Approx(std::vector<double>({10., 10.})));
|
||||
REQUIRE_THAT(config.option<ConfigOptionFloatsNullable>("filament_ramming_volumetric_speed")->values,
|
||||
Catch::Matchers::Approx(std::vector<double>({-1., -1.})));
|
||||
REQUIRE(config.option<ConfigOptionFloats>("filament_max_volumetric_speed")->values == std::vector<double>({12., 21.}));
|
||||
}
|
||||
}
|
||||
|
||||
// update_values_from_multi_to_multi_2 walks the DESTINATION PRINTER's variant list while writing
|
||||
|
||||
@@ -987,6 +987,193 @@ TEST_CASE("Resolution terminates when no vendor manifest exists", "[Preset][Bund
|
||||
CHECK(error == "Preset was not found in the loaded bundle");
|
||||
}
|
||||
|
||||
TEST_CASE("Manifest-backed resolution reuses the vendor tree it already loaded", "[Preset][Bundle][Regression]")
|
||||
{
|
||||
ScopedTemporaryDir dir;
|
||||
const fs::path process_dir = dir.path() / "Acme" / "process";
|
||||
fs::create_directories(process_dir);
|
||||
std::ofstream((dir.path() / "Acme.json").string())
|
||||
<< R"({"version":"1.0.0","name":"Acme","process_list":[)"
|
||||
<< R"({"name":"fdm_process_common","sub_path":"process/base.json"},)"
|
||||
<< R"({"name":"Acme First","sub_path":"process/first.json"},)"
|
||||
<< R"({"name":"Acme Second","sub_path":"process/second.json"}]})";
|
||||
auto write_base = [&](double travel_speed) {
|
||||
std::ofstream((process_dir / "base.json").string())
|
||||
<< R"({"type":"process","name":"fdm_process_common","from":"system",)"
|
||||
<< R"("instantiation":"false","travel_speed":[")" << travel_speed << R"("]})";
|
||||
};
|
||||
auto write_child = [&](const std::string &file, const std::string &name) {
|
||||
std::ofstream((process_dir / file).string())
|
||||
<< R"({"type":"process","name":")" << name << R"(","from":"system",)"
|
||||
<< R"("instantiation":"true","inherits":"fdm_process_common"})";
|
||||
};
|
||||
write_base(111.0);
|
||||
write_child("first.json", "Acme First");
|
||||
write_child("second.json", "Acme Second");
|
||||
|
||||
auto travel_speed = [&](PresetBundle &bundle, const std::string &file) {
|
||||
DynamicPrintConfig raw;
|
||||
raw.option<ConfigOptionString>(BBL_JSON_KEY_INHERITS, true)->value = "fdm_process_common";
|
||||
std::string error;
|
||||
REQUIRE(bundle.resolve_preset_config(raw, Preset::TYPE_PRINT, (process_dir / file).string(),
|
||||
ForwardCompatibilitySubstitutionRule::EnableSilent, error));
|
||||
return raw.option<ConfigOptionFloats>("travel_speed")->values.front();
|
||||
};
|
||||
|
||||
PresetBundle bundle;
|
||||
CHECK_THAT(travel_speed(bundle, "first.json"), Catch::Matchers::WithinAbs(111.0, 1e-6));
|
||||
|
||||
// Only a reload would see this change.
|
||||
write_base(222.0);
|
||||
CHECK_THAT(travel_speed(bundle, "second.json"), Catch::Matchers::WithinAbs(111.0, 1e-6));
|
||||
|
||||
PresetBundle fresh;
|
||||
CHECK_THAT(travel_speed(fresh, "second.json"), Catch::Matchers::WithinAbs(222.0, 1e-6));
|
||||
}
|
||||
|
||||
TEST_CASE("Manifest-backed resolution does not keep a vendor tree that failed to load", "[Preset][Bundle][Regression]")
|
||||
{
|
||||
ScopedTemporaryDir dir;
|
||||
const fs::path child_file = dir.path() / "Acme" / "process" / "child.json";
|
||||
auto write_manifest = [&](const std::string &leading_entry) {
|
||||
std::ofstream((dir.path() / "Acme.json").string())
|
||||
<< R"({"version":"1.0.0","name":"Acme","process_list":[)" << leading_entry
|
||||
<< R"({"name":"Acme Process","sub_path":"process/child.json"}]})";
|
||||
};
|
||||
write_manifest("123,");
|
||||
fs::create_directories(child_file.parent_path());
|
||||
std::ofstream(child_file.string())
|
||||
<< R"({"type":"process","name":"Acme Process","from":"system",)"
|
||||
<< R"("instantiation":"true","layer_height":"0.2"})";
|
||||
|
||||
PresetBundle bundle;
|
||||
auto resolve = [&](std::string &error) {
|
||||
DynamicPrintConfig raw;
|
||||
raw.option<ConfigOptionString>(BBL_JSON_KEY_INHERITS, true)->value = "fdm_process_common";
|
||||
return bundle.resolve_preset_config(raw, Preset::TYPE_PRINT, child_file.string(),
|
||||
ForwardCompatibilitySubstitutionRule::EnableSilent, error);
|
||||
};
|
||||
|
||||
std::string error;
|
||||
CHECK_FALSE(resolve(error));
|
||||
CHECK_FALSE(error.empty());
|
||||
|
||||
write_manifest("");
|
||||
error.clear();
|
||||
CHECK(resolve(error));
|
||||
CHECK(error.empty());
|
||||
}
|
||||
|
||||
TEST_CASE("Manifest-backed resolution reuses the library base for type-probed files", "[Preset][Bundle][Regression]")
|
||||
{
|
||||
ScopedTemporaryDir dir;
|
||||
const fs::path library_pet = dir.path() / PresetBundle::ORCA_FILAMENT_LIBRARY / "filament" / "pet.json";
|
||||
const fs::path filament_dir = dir.path() / "Acme" / "filament";
|
||||
|
||||
std::ofstream((dir.path() / (std::string(PresetBundle::ORCA_FILAMENT_LIBRARY) + ".json")).string())
|
||||
<< R"({"version":"1.0.0","name":"OrcaFilamentLibrary","filament_list":[)"
|
||||
<< R"({"name":"fdm_filament_pet","sub_path":"filament/pet.json","filament_id":"GFL99"}]})";
|
||||
fs::create_directories(library_pet.parent_path());
|
||||
auto write_library_pet = [&](double density) {
|
||||
std::ofstream(library_pet.string())
|
||||
<< R"({"type":"filament","name":"fdm_filament_pet","from":"system",)"
|
||||
<< R"("filament_id":"GFL99","instantiation":"false",)"
|
||||
<< R"("filament_type":["PETG"],"filament_density":[")" << density << R"("]})";
|
||||
};
|
||||
write_library_pet(1.27);
|
||||
|
||||
std::ofstream((dir.path() / "Acme.json").string())
|
||||
<< R"({"version":"1.0.0","name":"Acme","filament_list":[)"
|
||||
<< R"({"name":"Acme PETG","sub_path":"filament/petg.json","filament_id":"GFA00"},)"
|
||||
<< R"({"name":"Acme PETG Matte","sub_path":"filament/petg_matte.json","filament_id":"GFA01"}]})";
|
||||
fs::create_directories(filament_dir);
|
||||
auto write_child = [&](const std::string &file, const std::string &name, const std::string &filament_id) {
|
||||
std::ofstream((filament_dir / file).string())
|
||||
<< R"({"type":"filament","name":")" << name << R"(","from":"system",)"
|
||||
<< R"("filament_id":")" << filament_id << R"(","instantiation":"true","inherits":"fdm_filament_pet"})";
|
||||
};
|
||||
write_child("petg.json", "Acme PETG", "GFA00");
|
||||
write_child("petg_matte.json", "Acme PETG Matte", "GFA01");
|
||||
|
||||
auto density = [](const DynamicPrintConfig &config) {
|
||||
return config.option<ConfigOptionFloats>("filament_density")->values.front();
|
||||
};
|
||||
|
||||
PresetBundle bundle;
|
||||
DynamicPrintConfig first;
|
||||
first.option<ConfigOptionString>(BBL_JSON_KEY_INHERITS, true)->value = "fdm_filament_pet";
|
||||
std::string error;
|
||||
REQUIRE(bundle.resolve_preset_config(first, Preset::TYPE_FILAMENT, (filament_dir / "petg.json").string(),
|
||||
ForwardCompatibilitySubstitutionRule::EnableSilent, error));
|
||||
CHECK_THAT(density(first), Catch::Matchers::WithinAbs(1.27, 1e-6));
|
||||
|
||||
// Only a reload would see this change.
|
||||
write_library_pet(1.5);
|
||||
|
||||
DynamicPrintConfig second;
|
||||
Preset::Type type = Preset::TYPE_INVALID;
|
||||
REQUIRE(bundle.resolve_preset_config_type(second, type, (filament_dir / "petg_matte.json").string(),
|
||||
ForwardCompatibilitySubstitutionRule::EnableSilent, error));
|
||||
CHECK(type == Preset::TYPE_FILAMENT);
|
||||
CHECK_THAT(density(second), Catch::Matchers::WithinAbs(1.27, 1e-6));
|
||||
}
|
||||
|
||||
TEST_CASE("Manifest-backed resolution shares the library between vendors under one root", "[Preset][Bundle][Regression]")
|
||||
{
|
||||
ScopedTemporaryDir dir;
|
||||
const fs::path library_dir = dir.path() / PresetBundle::ORCA_FILAMENT_LIBRARY / "filament";
|
||||
|
||||
std::ofstream((dir.path() / (std::string(PresetBundle::ORCA_FILAMENT_LIBRARY) + ".json")).string())
|
||||
<< R"({"version":"1.0.0","name":"OrcaFilamentLibrary","filament_list":[)"
|
||||
<< R"({"name":"fdm_filament_pet","sub_path":"filament/pet.json","filament_id":"GFL99"},)"
|
||||
<< R"({"name":"Generic PETG","sub_path":"filament/generic_petg.json","filament_id":"GFL98"}]})";
|
||||
fs::create_directories(library_dir);
|
||||
auto write_library_pet = [&](double density) {
|
||||
std::ofstream((library_dir / "pet.json").string())
|
||||
<< R"({"type":"filament","name":"fdm_filament_pet","from":"system",)"
|
||||
<< R"("filament_id":"GFL99","instantiation":"false",)"
|
||||
<< R"("filament_type":["PETG"],"filament_density":[")" << density << R"("]})";
|
||||
};
|
||||
write_library_pet(1.27);
|
||||
std::ofstream((library_dir / "generic_petg.json").string())
|
||||
<< R"({"type":"filament","name":"Generic PETG","from":"system",)"
|
||||
<< R"("filament_id":"GFL98","instantiation":"true","inherits":"fdm_filament_pet"})";
|
||||
|
||||
auto write_vendor = [&](const std::string &vendor, const std::string &filament_id) {
|
||||
const fs::path filament_dir = dir.path() / vendor / "filament";
|
||||
fs::create_directories(filament_dir);
|
||||
std::ofstream((dir.path() / (vendor + ".json")).string())
|
||||
<< R"({"version":"1.0.0","name":")" << vendor << R"(","filament_list":[)"
|
||||
<< R"({"name":")" << vendor << R"( PETG","sub_path":"filament/petg.json","filament_id":")" << filament_id << R"("}]})";
|
||||
std::ofstream((filament_dir / "petg.json").string())
|
||||
<< R"({"type":"filament","name":")" << vendor << R"( PETG","from":"system",)"
|
||||
<< R"("filament_id":")" << filament_id << R"(","instantiation":"true","inherits":"fdm_filament_pet"})";
|
||||
return filament_dir / "petg.json";
|
||||
};
|
||||
const fs::path acme_petg = write_vendor("Acme", "GFA00");
|
||||
const fs::path beta_petg = write_vendor("Beta", "GFB00");
|
||||
|
||||
auto density = [&](PresetBundle &bundle, const fs::path &file) {
|
||||
DynamicPrintConfig raw;
|
||||
raw.option<ConfigOptionString>(BBL_JSON_KEY_INHERITS, true)->value = "fdm_filament_pet";
|
||||
std::string error;
|
||||
REQUIRE(bundle.resolve_preset_config(raw, Preset::TYPE_FILAMENT, file.string(),
|
||||
ForwardCompatibilitySubstitutionRule::EnableSilent, error));
|
||||
return raw.option<ConfigOptionFloats>("filament_density")->values.front();
|
||||
};
|
||||
|
||||
PresetBundle bundle;
|
||||
CHECK_THAT(density(bundle, acme_petg), Catch::Matchers::WithinAbs(1.27, 1e-6));
|
||||
|
||||
// Only a reload would see this change.
|
||||
write_library_pet(1.5);
|
||||
CHECK_THAT(density(bundle, beta_petg), Catch::Matchers::WithinAbs(1.27, 1e-6));
|
||||
CHECK_THAT(density(bundle, library_dir / "generic_petg.json"), Catch::Matchers::WithinAbs(1.27, 1e-6));
|
||||
|
||||
PresetBundle fresh;
|
||||
CHECK_THAT(density(fresh, beta_petg), Catch::Matchers::WithinAbs(1.5, 1e-6));
|
||||
}
|
||||
|
||||
// Orca: a filament in the Orca Filament Library that names its compatible printers has to hide the generic
|
||||
// library filament sharing its alias, the same way a vendor owned filament does. Otherwise both are compatible
|
||||
// with that printer and the plater combo box lists the shared alias twice.
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
|
||||
#include "test_utils.hpp"
|
||||
|
||||
#include <boost/filesystem.hpp>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <fstream>
|
||||
@@ -88,3 +90,65 @@ TEST_CASE("copy_file reports the OS error when the destination cannot be written
|
||||
REQUIRE(std::all_of(code.begin(), code.end(), [](unsigned char c) { return std::isdigit(c) != 0; }));
|
||||
#endif // _WIN32
|
||||
}
|
||||
|
||||
TEST_CASE("A resolved input path still names the same file after the working directory changes", "[utils]") {
|
||||
ScopedTemporaryFile model(".3mf");
|
||||
{ std::ofstream out(model.string()); out << "3mf"; }
|
||||
const std::string name = model.path().filename().string();
|
||||
|
||||
// Resolve the bare name from the directory holding the file, then move away from it. The guard
|
||||
// restores the directory the test started in, wherever this leaves it.
|
||||
ScopedWorkingDirectory cwd(model.path().parent_path());
|
||||
const std::string resolved = resolve_cli_input_path(name);
|
||||
boost::filesystem::current_path(boost::filesystem::path(TEST_DATA_DIR));
|
||||
|
||||
REQUIRE(boost::filesystem::exists(resolved));
|
||||
REQUIRE(boost::filesystem::equivalent(resolved, model.path()));
|
||||
// Control: the bare name finds nothing from here, so resolving it this late would have failed.
|
||||
REQUIRE_FALSE(boost::filesystem::exists(name));
|
||||
}
|
||||
|
||||
TEST_CASE("resolve_cli_input_path completes a relative path against the working directory", "[utils]") {
|
||||
ScopedWorkingDirectory cwd(boost::filesystem::temp_directory_path());
|
||||
// Read back rather than reusing temp_directory_path(): changing to it resolves any symlink.
|
||||
const boost::filesystem::path here = boost::filesystem::current_path();
|
||||
|
||||
SECTION("a bare name") {
|
||||
REQUIRE(resolve_cli_input_path("model.3mf") == (here / "model.3mf").make_preferred().string());
|
||||
}
|
||||
SECTION("a ./ prefix is dropped") {
|
||||
REQUIRE(resolve_cli_input_path("./model.3mf") == (here / "model.3mf").make_preferred().string());
|
||||
}
|
||||
SECTION("a ../ traversal is collapsed") {
|
||||
REQUIRE(resolve_cli_input_path("../model.3mf") == (here.parent_path() / "model.3mf").make_preferred().string());
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("resolve_cli_input_path leaves inputs that must not be completed unchanged", "[utils]") {
|
||||
SECTION("an absolute path") {
|
||||
const boost::filesystem::path absolute = (boost::filesystem::temp_directory_path() / "model.3mf").make_preferred();
|
||||
REQUIRE(resolve_cli_input_path(absolute.string()) == absolute.string());
|
||||
}
|
||||
#ifdef _WIN32
|
||||
// Every absolute form Windows accepts opens today, so each must come back byte for byte:
|
||||
// normalizing them would rewrite the forward slashes and rebuild the \\?\ and UNC prefixes.
|
||||
SECTION("an absolute Windows path of any form") {
|
||||
for (const std::string absolute : {R"(C:\models\model.3mf)",
|
||||
R"(C:/models/model.3mf)",
|
||||
R"(\\server\share\model.3mf)",
|
||||
R"(\\?\C:\models\model.3mf)"})
|
||||
REQUIRE(resolve_cli_input_path(absolute) == absolute);
|
||||
}
|
||||
#endif
|
||||
// These are downloaded rather than opened, and completing one would produce a path, not a URL.
|
||||
SECTION("a custom open protocol URL") {
|
||||
for (const std::string url : {"orcaslicer://open/?file=https://example.com/model.3mf",
|
||||
"prusaslicer://open/?file=https://example.com/model.3mf",
|
||||
"bambustudio://open/?file=https://example.com/model.3mf",
|
||||
"cura://open/?file=https://example.com/model.3mf"})
|
||||
REQUIRE(resolve_cli_input_path(url) == url);
|
||||
}
|
||||
SECTION("an empty argument") {
|
||||
REQUIRE(resolve_cli_input_path("").empty());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -176,4 +176,22 @@ inline void write_debug_stream([[maybe_unused]] const std::string &name, [[maybe
|
||||
#endif
|
||||
}
|
||||
|
||||
// Changes the working directory and restores the previous one on scope exit, including when an
|
||||
// assertion throws. It is process wide state shared with every other test.
|
||||
class ScopedWorkingDirectory
|
||||
{
|
||||
public:
|
||||
explicit ScopedWorkingDirectory(const boost::filesystem::path &dir)
|
||||
: m_previous(boost::filesystem::current_path())
|
||||
{
|
||||
boost::filesystem::current_path(dir);
|
||||
}
|
||||
~ScopedWorkingDirectory() { boost::system::error_code ec; boost::filesystem::current_path(m_previous, ec); }
|
||||
ScopedWorkingDirectory(const ScopedWorkingDirectory &) = delete;
|
||||
ScopedWorkingDirectory &operator=(const ScopedWorkingDirectory &) = delete;
|
||||
|
||||
private:
|
||||
boost::filesystem::path m_previous;
|
||||
};
|
||||
|
||||
#endif // SLIC3R_TEST_UTILS
|
||||
|
||||
Reference in New Issue
Block a user