diff --git a/tests/fff_print/CMakeLists.txt b/tests/fff_print/CMakeLists.txt index abddfebe5e..ac1b12d61f 100644 --- a/tests/fff_print/CMakeLists.txt +++ b/tests/fff_print/CMakeLists.txt @@ -8,6 +8,7 @@ add_executable(${_TEST_NAME}_tests test_fill.cpp test_flow.cpp test_gcode_timing.cpp + test_gcodeprocessor.cpp test_gcodewriter.cpp test_model.cpp test_multifilament.cpp diff --git a/tests/fff_print/test_gcodeprocessor.cpp b/tests/fff_print/test_gcodeprocessor.cpp new file mode 100644 index 0000000000..b8ce32e05c --- /dev/null +++ b/tests/fff_print/test_gcodeprocessor.cpp @@ -0,0 +1,105 @@ +#include + +#include "libslic3r/GCode/GCodeProcessor.hpp" + +#include "test_utils.hpp" + +#include +#include + +#include +#include +#include + +using namespace Slic3r; + +namespace { + +// Writes the bytes verbatim: the G-code export is binary, so no newline translation must creep in +// here either, otherwise the CRLF cases below would silently test LF. +void write_gcode(const std::string &path, const std::string &content) +{ + boost::nowide::ofstream f(path, std::ios::binary); + f << content; +} + +// Cuts line `id` (1-based) out of the file the way GCodeViewer's G-code window does: the text it +// shows is bytes [lines_ends[id - 2], lines_ends[id - 1]). Reproducing that here is what makes these +// assertions about the rendered result rather than about the offsets themselves. +std::string line_at(const std::string &path, const std::vector &lines_ends, size_t id) +{ + boost::nowide::ifstream f(path, std::ios::binary); + const std::string bytes((std::istreambuf_iterator(f)), std::istreambuf_iterator()); + const size_t start = id == 1 ? 0 : lines_ends[id - 2]; + return bytes.substr(start, lines_ends[id - 1] - start); +} + +} // namespace + +TEST_CASE("rebuild_lines_ends realigns the line offsets after the G-code is rewritten in place", "[GCodeProcessor]") +{ + ScopedTemporaryFile tmp(".gcode"); + GCodeProcessorResult result; + result.filename = tmp.string(); + + write_gcode(tmp.string(), "G1 X1\nG1 X2\nG1 X3\n"); + REQUIRE(result.rebuild_lines_ends()); + REQUIRE(result.lines_ends.size() == 3); + CHECK(line_at(tmp.string(), result.lines_ends, 2) == "G1 X2\n"); + + // What a post-processing script that opens the file in Python text mode on Windows does: prepend a + // comment and translate every LF to CRLF. Every byte offset shifts, and cumulatively. + write_gcode(tmp.string(), ";EDITED\r\nG1 X1\r\nG1 X2\r\nG1 X3\r\n"); + CHECK(line_at(tmp.string(), result.lines_ends, 2) != "G1 X1\r\n"); + + REQUIRE(result.rebuild_lines_ends()); + REQUIRE(result.lines_ends.size() == 4); + CHECK(line_at(tmp.string(), result.lines_ends, 1) == ";EDITED\r\n"); + CHECK(line_at(tmp.string(), result.lines_ends, 2) == "G1 X1\r\n"); + CHECK(line_at(tmp.string(), result.lines_ends, 4) == "G1 X3\r\n"); +} + +TEST_CASE("rebuild_lines_ends maps a file longer than one read chunk", "[GCodeProcessor]") +{ + ScopedTemporaryFile tmp(".gcode"); + GCodeProcessorResult result; + result.filename = tmp.string(); + + const size_t line_count = 20000; + std::string content; + for (size_t i = 0; i < line_count; ++i) + content += "G1 X" + std::to_string(i) + "\n"; + // The scan reads the file in chunks, so it has to span at least one chunk boundary to be meaningful. + REQUIRE(content.size() > 65536); + write_gcode(tmp.string(), content); + + REQUIRE(result.rebuild_lines_ends()); + REQUIRE(result.lines_ends.size() == line_count); + CHECK(result.lines_ends.back() == content.size()); + CHECK(line_at(tmp.string(), result.lines_ends, line_count) == "G1 X" + std::to_string(line_count - 1) + "\n"); +} + +TEST_CASE("rebuild_lines_ends ignores a trailing line that has no newline", "[GCodeProcessor]") +{ + ScopedTemporaryFile tmp(".gcode"); + GCodeProcessorResult result; + result.filename = tmp.string(); + + // Matches the export pass, which only ever records the offset one past a '\n'. + write_gcode(tmp.string(), "G1 X1\nG1 X2"); + REQUIRE(result.rebuild_lines_ends()); + CHECK(result.lines_ends.size() == 1); + CHECK(line_at(tmp.string(), result.lines_ends, 1) == "G1 X1\n"); +} + +TEST_CASE("rebuild_lines_ends empties the map when the G-code file cannot be read", "[GCodeProcessor]") +{ + // A partially rebuilt map would have the G-code window slicing lines at wrong offsets again; an + // empty one just hides the window. + GCodeProcessorResult result; + result.filename = (boost::filesystem::temp_directory_path() / "orca-nonexistent-gcode-file.gcode").string(); + result.lines_ends = {1, 2, 3}; + + CHECK_FALSE(result.rebuild_lines_ends()); + CHECK(result.lines_ends.empty()); +}