From c7546ea65e55143ff3d2d82d8c289bbac7fffe0f Mon Sep 17 00:00:00 2001
From: Nat Goodspeed <nat@lindenlab.com>
Date: Fri, 8 Sep 2023 14:14:09 -0400
Subject: SL-18837: Make llsdserialize_test debug output conditional.

Move hexdump() and hexmix() stream formatters to new hexdump.h for potential
use by other tests.

In toPythonUsing() helper function, add a temp file to receive Python script
debug output, and direct debug output to that file. On test failure, dump the
contents of that file to the log.

Give NamedTempFile::peep() an optional target std::ostream; refactor
implementation as peep_via() that accepts a callable to process each text
line. Add operator<<() to stream the contents of a NamedTempFile object to
ostream -- but don't use that with LL_DEBUGS(), as it flattens the file
contents into a single log line. Instead add peep_log(), which streams each
individual text line to LL_DEBUGS().
---
 indra/llcommon/tests/llsdserialize_test.cpp | 202 +++++++++++-----------------
 1 file changed, 75 insertions(+), 127 deletions(-)

(limited to 'indra/llcommon/tests')

diff --git a/indra/llcommon/tests/llsdserialize_test.cpp b/indra/llcommon/tests/llsdserialize_test.cpp
index ca63e74c6c..ac40125f75 100644
--- a/indra/llcommon/tests/llsdserialize_test.cpp
+++ b/indra/llcommon/tests/llsdserialize_test.cpp
@@ -52,12 +52,12 @@ typedef U32 uint32_t;
 #include "llformat.h"
 #include "llmemorystream.h"
 
+#include "../test/hexdump.h"
 #include "../test/lltut.h"
 #include "../test/namedtempfile.h"
 #include "stringize.h"
-#include <cctype>
+#include "StringVec.h"
 #include <functional>
-#include <iomanip>
 
 typedef std::function<void(const LLSD& data, std::ostream& str)> FormatterFunction;
 typedef std::function<bool(std::istream& istr, LLSD& data, llssize max_bytes)> ParserFunction;
@@ -67,81 +67,6 @@ std::vector<U8> string_to_vector(const std::string& str)
 	return std::vector<U8>(str.begin(), str.end());
 }
 
-// Format a given byte string as 2-digit hex values, no separators
-// Usage: std::cout << hexdump(somestring) << ...
-class hexdump
-{
-public:
-    hexdump(const char* data, size_t len):
-        hexdump(reinterpret_cast<const U8*>(data), len)
-    {}
-
-    hexdump(const U8* data, size_t len):
-        mData(data, data + len)
-    {}
-
-    hexdump(const hexdump&) = delete;
-    hexdump& operator=(const hexdump&) = delete;
-
-    friend std::ostream& operator<<(std::ostream& out, const hexdump& self)
-    {
-        auto oldfmt{ out.flags() };
-        auto oldfill{ out.fill() };
-        out.setf(std::ios_base::hex, std::ios_base::basefield);
-        out.fill('0');
-        for (auto c : self.mData)
-        {
-            out << std::setw(2) << unsigned(c);
-        }
-        out.setf(oldfmt, std::ios_base::basefield);
-        out.fill(oldfill);
-        return out;
-    }
-
-private:
-    std::vector<U8> mData;
-};
-
-// Format a given byte string as a mix of printable characters and, for each
-// non-printable character, "\xnn"
-// Usage: std::cout << hexmix(somestring) << ...
-class hexmix
-{
-public:
-    hexmix(const char* data, size_t len):
-        mData(data, len)
-    {}
-
-    hexmix(const hexmix&) = delete;
-    hexmix& operator=(const hexmix&) = delete;
-
-    friend std::ostream& operator<<(std::ostream& out, const hexmix& self)
-    {
-        auto oldfmt{ out.flags() };
-        auto oldfill{ out.fill() };
-        out.setf(std::ios_base::hex, std::ios_base::basefield);
-        out.fill('0');
-        for (auto c : self.mData)
-        {
-            // std::isprint() must be passed an unsigned char!
-            if (std::isprint(static_cast<unsigned char>(c)))
-            {
-                out << c;
-            }
-            else
-            {
-                out << "\\x" << std::setw(2) << unsigned(c);
-            }
-        }
-        out.setf(oldfmt, std::ios_base::basefield);
-        out.fill(oldfill);
-        return out;
-    }
-
-private:
-    std::string mData;
-};
-
 namespace tut
 {
 	struct sd_xml_data
@@ -1868,16 +1793,12 @@ namespace tut
     // helper for TestPythonCompatible
     static std::string import_llsd("import os.path\n"
                                    "import sys\n"
-                                   "try:\n"
-                                   // new freestanding llsd package
-                                   "    import llsd\n"
-                                   "except ImportError:\n"
-                                   // older llbase.llsd module
-                                   "    from llbase import llsd\n");
+                                   "import llsd\n");
 
     // helper for TestPythonCompatible
-    template <typename CONTENT>
-    void python(const std::string& desc, const CONTENT& script, int expect=0)
+    template <typename CONTENT, typename... ARGS>
+    void python_expect(const std::string& desc, const CONTENT& script, int expect=0,
+                       ARGS&&... args)
     {
         auto PYTHON(LLStringUtil::getenv("PYTHON"));
         ensure("Set $PYTHON to the Python interpreter", !PYTHON.empty());
@@ -1888,7 +1809,8 @@ namespace tut
         std::string q("\"");
         std::string qPYTHON(q + PYTHON + q);
         std::string qscript(q + scriptfile.getName() + q);
-        int rc = _spawnl(_P_WAIT, PYTHON.c_str(), qPYTHON.c_str(), qscript.c_str(), NULL);
+        int rc = _spawnl(_P_WAIT, PYTHON.c_str(), qPYTHON.c_str(), qscript.c_str(),
+                         std::forward<ARGS>(args)..., NULL);
         if (rc == -1)
         {
             char buffer[256];
@@ -1904,6 +1826,10 @@ namespace tut
         LLProcess::Params params;
         params.executable = PYTHON;
         params.args.add(scriptfile.getName());
+        for (const std::string& arg : StringVec{ std::forward<ARGS>(args)... })
+        {
+            params.args.add(arg);
+        }
         LLProcessPtr py(LLProcess::create(params));
         ensure(STRINGIZE("Couldn't launch " << desc << " script"), bool(py));
         // Implementing timeout would mean messing with alarm() and
@@ -1938,6 +1864,14 @@ namespace tut
 #endif
     }
 
+    // helper for TestPythonCompatible
+    template <typename CONTENT, typename... ARGS>
+    void python(const std::string& desc, const CONTENT& script, ARGS&&... args)
+    {
+        // plain python() expects rc 0
+        python_expect(desc, script, 0, std::forward<ARGS>(args)...);
+    }
+
     struct TestPythonCompatible
     {
         TestPythonCompatible() {}
@@ -1952,10 +1886,10 @@ namespace tut
     void TestPythonCompatibleObject::test<1>()
     {
         set_test_name("verify python()");
-        python("hello",
-               "import sys\n"
-               "sys.exit(17)\n",
-               17);                 // expect nonzero rc
+        python_expect("hello",
+                      "import sys\n"
+                      "sys.exit(17)\n",
+                      17);                 // expect nonzero rc
     }
 
     template<> template<>
@@ -1986,14 +1920,14 @@ namespace tut
             auto buffstr{ buffer.str() };
             int bufflen{ static_cast<int>(buffstr.length()) };
             out.write(reinterpret_cast<const char*>(&bufflen), sizeof(bufflen));
-            LL_DEBUGS("topy") << "Wrote length: "
-                                  << hexdump(reinterpret_cast<const char*>(&bufflen),
-                                             sizeof(bufflen))
-                                  << LL_ENDL;
+            LL_DEBUGS() << "Wrote length: "
+                        << hexdump(reinterpret_cast<const char*>(&bufflen),
+                                   sizeof(bufflen))
+                        << LL_ENDL;
             out.write(buffstr.c_str(), buffstr.length());
-            LL_DEBUGS("topy") << "Wrote data:   "
-                                  << hexmix(buffstr.c_str(), buffstr.length())
-                                  << LL_ENDL;
+            LL_DEBUGS() << "Wrote data:   "
+                        << hexmix(buffstr.c_str(), buffstr.length())
+                        << LL_ENDL;
         }
     }
 
@@ -2033,36 +1967,50 @@ namespace tut
                            (std::ostream& out)
                            { writeLLSDArray(serialize, out, cdata); });
 
-        python("read C++ " + desc,
-               [&](std::ostream& out){ out <<
-               import_llsd <<
-               "from functools import partial\n"
-               "import io\n"
-               "import struct\n"
-               "lenformat = struct.Struct('i')\n"
-               "def parse_each(inf):\n"
-               "    for rawlen in iter(partial(inf.read, lenformat.size), b''):\n"
-               "        print('Read length:', ''.join(('%02x' % b) for b in rawlen))\n"
-               "        len = lenformat.unpack(rawlen)[0]\n"
-               // Since llsd.parse() has no max_bytes argument, instead of
-               // passing the input stream directly to parse(), read the item
-               // into a distinct bytes object and parse that.
-               "        data = inf.read(len)\n"
-               "        print('Read data:  ', repr(data))\n"
-               "        try:\n"
-               "            frombytes = llsd.parse(data)\n"
-               "        except llsd.LLSDParseError as err:\n"
-               "            print(f'*** {err}')\n"
-               "            print(f'Bad content:\\n{data!r}')\n"
-               "            raise\n"
-               // Also try parsing from a distinct stream.
-               "        stream = io.BytesIO(data)\n"
-               "        fromstream = llsd.parse(stream)\n"
-               "        assert frombytes == fromstream\n"
-               "        yield frombytes\n"
-               << pydata <<
-               // Don't forget raw-string syntax for Windows pathnames.
-               "verify(parse_each(open(r'" << file.getName() << "', 'rb')))\n";});
+        // 'debug' starts empty because it's intended as an output file
+        NamedTempFile debug("debug", "");
+
+        try
+        {
+            python("read C++ " + desc,
+                   [&](std::ostream& out){ out <<
+                   import_llsd <<
+                   "from functools import partial\n"
+                   "import io\n"
+                   "import struct\n"
+                   "lenformat = struct.Struct('i')\n"
+                   "def parse_each(inf):\n"
+                   "    for rawlen in iter(partial(inf.read, lenformat.size), b''):\n"
+                   "        print('Read length:', ''.join(('%02x' % b) for b in rawlen),\n"
+                   "              file=debug)\n"
+                   "        len = lenformat.unpack(rawlen)[0]\n"
+                   // Since llsd.parse() has no max_bytes argument, instead of
+                   // passing the input stream directly to parse(), read the item
+                   // into a distinct bytes object and parse that.
+                   "        data = inf.read(len)\n"
+                   "        print('Read data:  ', repr(data), file=debug)\n"
+                   "        try:\n"
+                   "            frombytes = llsd.parse(data)\n"
+                   "        except llsd.LLSDParseError as err:\n"
+                   "            print(f'*** {err}')\n"
+                   "            print(f'Bad content:\\n{data!r}')\n"
+                   "            raise\n"
+                   // Also try parsing from a distinct stream.
+                   "        stream = io.BytesIO(data)\n"
+                   "        fromstream = llsd.parse(stream)\n"
+                   "        assert frombytes == fromstream\n"
+                   "        yield frombytes\n"
+                   << pydata <<
+                   // Don't forget raw-string syntax for Windows pathnames.
+                   "debug = open(r'" << debug.getName() << "', 'w')\n"
+                   "verify(parse_each(open(r'" << file.getName() << "', 'rb')))\n";});
+        }
+        catch (const failure&)
+        {
+            LL_DEBUGS() << "Script debug output:" << LL_ENDL;
+            debug.peep_log();
+            throw;
+        }
     }
 
     template<> template<>
-- 
cgit v1.2.3