From aa62868ccbc4cc2d296f263325adc5fe9ebfcec4 Mon Sep 17 00:00:00 2001
From: Vojtech Bubnik <bubnikv@gmail.com>
Date: Fri, 7 Jan 2022 12:23:15 +0100
Subject: [PATCH] WIP G-code find & replace: Unit tests and some bug fixes.

---
 src/libslic3r/GCode/FindReplace.cpp       |  38 +++-
 src/libslic3r/GCode/FindReplace.hpp       |   4 +-
 tests/fff_print/CMakeLists.txt            |   1 +
 tests/fff_print/test_gcodefindreplace.cpp | 218 ++++++++++++++++++++++
 4 files changed, 250 insertions(+), 11 deletions(-)
 create mode 100644 tests/fff_print/test_gcodefindreplace.cpp

diff --git a/src/libslic3r/GCode/FindReplace.cpp b/src/libslic3r/GCode/FindReplace.cpp
index d349632b5..480994da8 100644
--- a/src/libslic3r/GCode/FindReplace.cpp
+++ b/src/libslic3r/GCode/FindReplace.cpp
@@ -5,29 +5,47 @@
 
 namespace Slic3r {
 
-GCodeFindReplace::GCodeFindReplace(const PrintConfig &print_config)
+// Similar to https://npp-user-manual.org/docs/searching/#extended-search-mode
+const void unescape_extended_search_mode(std::string &s)
 {
-    const std::vector<std::string> &subst = print_config.gcode_substitutions.values;
+    boost::replace_all(s, "\\n", "\n"); // Line Feed control character LF (ASCII 0x0A)
+    boost::replace_all(s, "\\r", "\r"); // Carriage Return control character CR (ASCII 0x0D)
+    boost::replace_all(s, "\\t", "\t"); // TAB control character (ASCII 0x09)
+    boost::replace_all(s, "\\0", "\0x00"); // NUL control character (ASCII 0x00)
+    boost::replace_all(s, "\\\\", "\\"); // Backslash character (ASCII 0x5C)
 
-    if ((subst.size() % 3) != 0)
+// Notepad++ also supports:
+// \o: the octal representation of a byte, made of 3 digits in the 0-7 range
+// \d: the decimal representation of a byte, made of 3 digits in the 0-9 range
+// \x: the hexadecimal representation of a byte, made of 2 digits in the 0-9, A-F/a-f range.
+// \u: The hexadecimal representation of a two-byte character, made of 4 digits in the 0-9, A-F/a-f range.
+}
+
+GCodeFindReplace::GCodeFindReplace(const std::vector<std::string> &gcode_substitutions)
+{
+    if ((gcode_substitutions.size() % 3) != 0)
         throw RuntimeError("Invalid length of gcode_substitutions parameter");
 
-    m_substitutions.reserve(subst.size() / 3);
-    for (size_t i = 0; i < subst.size(); i += 3) {
+    m_substitutions.reserve(gcode_substitutions.size() / 3);
+    for (size_t i = 0; i < gcode_substitutions.size(); i += 3) {
         Substitution out;
         try {
-            out.plain_pattern    = subst[i];
-            out.format           = subst[i + 1];
-            const std::string &params = subst[i + 2];
+            out.plain_pattern    = gcode_substitutions[i];
+            out.format           = gcode_substitutions[i + 1];
+            const std::string &params = gcode_substitutions[i + 2];
             out.regexp           = strchr(params.c_str(), 'r') != nullptr || strchr(params.c_str(), 'R') != nullptr;
             out.case_insensitive = strchr(params.c_str(), 'i') != nullptr || strchr(params.c_str(), 'I') != nullptr;
             out.whole_word       = strchr(params.c_str(), 'w') != nullptr || strchr(params.c_str(), 'W') != nullptr;
-            if (out.regexp)
+            if (out.regexp) {
                 out.regexp_pattern.assign(
                     out.whole_word ? 
-                        std::string("\b") + out.plain_pattern + "\b" :
+                        std::string("\\b") + out.plain_pattern + "\\b" :
                         out.plain_pattern,
                     (out.case_insensitive ? boost::regex::icase : 0) | boost::regex::optimize);
+            } else {
+                unescape_extended_search_mode(out.plain_pattern);
+                unescape_extended_search_mode(out.format);
+            }
         } catch (const std::exception &ex) {
             throw RuntimeError(std::string("Invalid gcode_substitutions parameter, failed to compile regular expression: ") + ex.what());
         }
diff --git a/src/libslic3r/GCode/FindReplace.hpp b/src/libslic3r/GCode/FindReplace.hpp
index ff25700b9..2d12cf28b 100644
--- a/src/libslic3r/GCode/FindReplace.hpp
+++ b/src/libslic3r/GCode/FindReplace.hpp
@@ -9,7 +9,9 @@ namespace Slic3r {
 
 class GCodeFindReplace {
 public:
-    GCodeFindReplace(const PrintConfig &print_config);
+    GCodeFindReplace(const PrintConfig &print_config) : GCodeFindReplace(print_config.gcode_substitutions.values) {}
+    GCodeFindReplace(const std::vector<std::string> &gcode_substitutions);
+
 
     std::string process_layer(const std::string &gcode);
     
diff --git a/tests/fff_print/CMakeLists.txt b/tests/fff_print/CMakeLists.txt
index c69e722af..50b45e384 100644
--- a/tests/fff_print/CMakeLists.txt
+++ b/tests/fff_print/CMakeLists.txt
@@ -7,6 +7,7 @@ add_executable(${_TEST_NAME}_tests
 	test_fill.cpp
 	test_flow.cpp
 	test_gcode.cpp
+	test_gcodefindreplace.cpp
 	test_gcodewriter.cpp
 	test_model.cpp
 	test_print.cpp
diff --git a/tests/fff_print/test_gcodefindreplace.cpp b/tests/fff_print/test_gcodefindreplace.cpp
new file mode 100644
index 000000000..be0dd8a01
--- /dev/null
+++ b/tests/fff_print/test_gcodefindreplace.cpp
@@ -0,0 +1,218 @@
+#include <catch2/catch.hpp>
+
+#include <memory>
+
+#include "libslic3r/GCode/FindReplace.hpp"
+
+using namespace Slic3r;
+
+SCENARIO("Find/Replace with plain text", "[GCodeFindReplace]") {
+    GIVEN("G-code") {
+        const std::string gcode =
+            "G1 Z0; home\n"
+            "G1 Z1; move up\n"
+            "G1 X0 Y1 Z1; perimeter\n"
+            "G1 X13 Y32 Z1; infill\n"
+            "G1 X13 Y32 Z1; wipe\n";
+        WHEN("Replace \"move up\" with \"move down\", case sensitive") {
+            GCodeFindReplace find_replace({ "move up", "move down", "" });
+            REQUIRE(find_replace.process_layer(gcode) ==
+                "G1 Z0; home\n"
+                // substituted
+                "G1 Z1; move down\n"
+                "G1 X0 Y1 Z1; perimeter\n"
+                "G1 X13 Y32 Z1; infill\n"
+                "G1 X13 Y32 Z1; wipe\n");
+        }
+        WHEN("Replace \"move up\" with \"move down\", case insensitive") {
+            GCodeFindReplace find_replace({ "move up", "move down", "i" });
+            REQUIRE(find_replace.process_layer(gcode) ==
+                "G1 Z0; home\n"
+                // substituted
+                "G1 Z1; move down\n"
+                "G1 X0 Y1 Z1; perimeter\n"
+                "G1 X13 Y32 Z1; infill\n"
+                "G1 X13 Y32 Z1; wipe\n");
+        }
+        WHEN("Replace \"move UP\" with \"move down\", case insensitive") {
+            GCodeFindReplace find_replace({ "move UP", "move down", "i" });
+            REQUIRE(find_replace.process_layer(gcode) ==
+                "G1 Z0; home\n"
+                // substituted
+                "G1 Z1; move down\n"
+                "G1 X0 Y1 Z1; perimeter\n"
+                "G1 X13 Y32 Z1; infill\n"
+                "G1 X13 Y32 Z1; wipe\n");
+        }
+        WHEN("Replace \"move up\" with \"move down\", case sensitive") {
+            GCodeFindReplace find_replace({ "move UP", "move down", "" });
+            REQUIRE(find_replace.process_layer(gcode) == gcode);
+        }
+
+        // Whole word
+        WHEN("Replace \"move up\" with \"move down\", whole word") {
+            GCodeFindReplace find_replace({ "move up", "move down", "w" });
+            REQUIRE(find_replace.process_layer(gcode) ==
+                "G1 Z0; home\n"
+                // substituted
+                "G1 Z1; move down\n"
+                "G1 X0 Y1 Z1; perimeter\n"
+                "G1 X13 Y32 Z1; infill\n"
+                "G1 X13 Y32 Z1; wipe\n");
+        }
+        WHEN("Replace \"move u\" with \"move down\", whole word") {
+            GCodeFindReplace find_replace({ "move u", "move down", "w" });
+            REQUIRE(find_replace.process_layer(gcode) == gcode);
+        }
+        WHEN("Replace \"ove up\" with \"move down\", whole word") {
+            GCodeFindReplace find_replace({ "move u", "move down", "w" });
+            REQUIRE(find_replace.process_layer(gcode) == gcode);
+        }
+
+        // Multi-line replace
+        WHEN("Replace \"move up\\nG1 X0 \" with \"move down\\nG0 X1 \"") {
+            GCodeFindReplace find_replace({ "move up\\nG1 X0 ", "move down\\nG0 X1 ", "" });
+            REQUIRE(find_replace.process_layer(gcode) ==
+                "G1 Z0; home\n"
+                // substituted
+                "G1 Z1; move down\n"
+                "G0 X1 Y1 Z1; perimeter\n"
+                "G1 X13 Y32 Z1; infill\n"
+                "G1 X13 Y32 Z1; wipe\n");
+        }
+        // Multi-line replace, whole word.
+        WHEN("Replace \"move up\\nG1 X0\" with \"move down\\nG0 X1\", whole word") {
+            GCodeFindReplace find_replace({ "move up\\nG1 X0", "move down\\nG0 X1", "w" });
+            REQUIRE(find_replace.process_layer(gcode) ==
+                "G1 Z0; home\n"
+                // substituted
+                "G1 Z1; move down\n"
+                "G0 X1 Y1 Z1; perimeter\n"
+                "G1 X13 Y32 Z1; infill\n"
+                "G1 X13 Y32 Z1; wipe\n");
+        }
+        // Multi-line replace, whole word, fails.
+        WHEN("Replace \"move up\\nG1 X\" with \"move down\\nG0 X\", whole word") {
+            GCodeFindReplace find_replace({ "move up\\nG1 X", "move down\\nG0 X", "w" });
+            REQUIRE(find_replace.process_layer(gcode) == gcode);
+        }
+    }
+
+    GIVEN("G-code with decimals") {
+        const std::string gcode =
+            "G1 Z0.123; home\n"
+            "G1 Z1.21; move up\n"
+            "G1 X0 Y.33 Z.431 E1.2; perimeter\n";
+        WHEN("Regular expression NOT processed in non-regex mode") {
+            GCodeFindReplace find_replace({ "( [XYZEF]-?)\\.([0-9]+)", "\\10.\\2", "" });
+            REQUIRE(find_replace.process_layer(gcode) == gcode);
+        }
+    }
+}
+
+SCENARIO("Find/Replace with regexp", "[GCodeFindReplace]") {
+    GIVEN("G-code") {
+        const std::string gcode =
+            "G1 Z0; home\n"
+            "G1 Z1; move up\n"
+            "G1 X0 Y1 Z1; perimeter\n"
+            "G1 X13 Y32 Z1; infill\n"
+            "G1 X13 Y32 Z1; wipe\n";
+        WHEN("Replace \"move up\" with \"move down\", case sensitive") {
+            GCodeFindReplace find_replace({ "move up", "move down", "r" });
+            REQUIRE(find_replace.process_layer(gcode) ==
+                "G1 Z0; home\n"
+                // substituted
+                "G1 Z1; move down\n"
+                "G1 X0 Y1 Z1; perimeter\n"
+                "G1 X13 Y32 Z1; infill\n"
+                "G1 X13 Y32 Z1; wipe\n");
+        }
+        WHEN("Replace \"move up\" with \"move down\", case insensitive") {
+            GCodeFindReplace find_replace({ "move up", "move down", "ri" });
+            REQUIRE(find_replace.process_layer(gcode) ==
+                "G1 Z0; home\n"
+                // substituted
+                "G1 Z1; move down\n"
+                "G1 X0 Y1 Z1; perimeter\n"
+                "G1 X13 Y32 Z1; infill\n"
+                "G1 X13 Y32 Z1; wipe\n");
+        }
+        WHEN("Replace \"move UP\" with \"move down\", case insensitive") {
+            GCodeFindReplace find_replace({ "move UP", "move down", "ri" });
+            REQUIRE(find_replace.process_layer(gcode) ==
+                "G1 Z0; home\n"
+                // substituted
+                "G1 Z1; move down\n"
+                "G1 X0 Y1 Z1; perimeter\n"
+                "G1 X13 Y32 Z1; infill\n"
+                "G1 X13 Y32 Z1; wipe\n");
+        }
+        WHEN("Replace \"move up\" with \"move down\", case sensitive") {
+            GCodeFindReplace find_replace({ "move UP", "move down", "r" });
+            REQUIRE(find_replace.process_layer(gcode) == gcode);
+        }
+
+        // Whole word
+        WHEN("Replace \"move up\" with \"move down\", whole word") {
+            GCodeFindReplace find_replace({ "move up", "move down", "rw" });
+            REQUIRE(find_replace.process_layer(gcode) ==
+                "G1 Z0; home\n"
+                // substituted
+                "G1 Z1; move down\n"
+                "G1 X0 Y1 Z1; perimeter\n"
+                "G1 X13 Y32 Z1; infill\n"
+                "G1 X13 Y32 Z1; wipe\n");
+        }
+        WHEN("Replace \"move u\" with \"move down\", whole word") {
+            GCodeFindReplace find_replace({ "move u", "move down", "rw" });
+            REQUIRE(find_replace.process_layer(gcode) == gcode);
+        }
+        WHEN("Replace \"ove up\" with \"move down\", whole word") {
+            GCodeFindReplace find_replace({ "move u", "move down", "rw" });
+            REQUIRE(find_replace.process_layer(gcode) == gcode);
+        }
+
+        // Multi-line replace
+        WHEN("Replace \"move up\\nG1 X0 \" with \"move down\\nG0 X1 \"") {
+            GCodeFindReplace find_replace({ "move up\\nG1 X0 ", "move down\\nG0 X1 ", "r" });
+            REQUIRE(find_replace.process_layer(gcode) ==
+                "G1 Z0; home\n"
+                // substituted
+                "G1 Z1; move down\n"
+                "G0 X1 Y1 Z1; perimeter\n"
+                "G1 X13 Y32 Z1; infill\n"
+                "G1 X13 Y32 Z1; wipe\n");
+        }
+        // Multi-line replace, whole word.
+        WHEN("Replace \"move up\\nG1 X0\" with \"move down\\nG0 X1\", whole word") {
+            GCodeFindReplace find_replace({ "move up\\nG1 X0", "move down\\nG0 X1", "rw" });
+            REQUIRE(find_replace.process_layer(gcode) ==
+                "G1 Z0; home\n"
+                // substituted
+                "G1 Z1; move down\n"
+                "G0 X1 Y1 Z1; perimeter\n"
+                "G1 X13 Y32 Z1; infill\n"
+                "G1 X13 Y32 Z1; wipe\n");
+        }
+        // Multi-line replace, whole word, fails.
+        WHEN("Replace \"move up\\nG1 X\" with \"move down\\nG0 X\", whole word") {
+            GCodeFindReplace find_replace({ "move up\\nG1 X", "move down\\nG0 X", "rw" });
+            REQUIRE(find_replace.process_layer(gcode) == gcode);
+        }
+    }
+
+    GIVEN("G-code with decimals") {
+        const std::string gcode =
+            "G1 Z0.123; home\n"
+            "G1 Z1.21; move up\n"
+            "G1 X0 Y.33 Z.431 E1.2; perimeter\n";
+        WHEN("Missing zeros before dot filled in") {
+            GCodeFindReplace find_replace({ "( [XYZEF]-?)\\.([0-9]+)", "\\10.\\2", "r" });
+            REQUIRE(find_replace.process_layer(gcode) ==
+                "G1 Z0.123; home\n"
+                "G1 Z1.21; move up\n"
+                "G1 X0 Y0.33 Z0.431 E1.2; perimeter\n");
+        }
+    }
+}