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().master
parent
d459b3a1ca
commit
c7546ea65e
|
|
@ -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<>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,97 @@
|
|||
/**
|
||||
* @file hexdump.h
|
||||
* @author Nat Goodspeed
|
||||
* @date 2023-09-08
|
||||
* @brief Provide hexdump() and hexmix() ostream formatters
|
||||
*
|
||||
* $LicenseInfo:firstyear=2023&license=viewerlgpl$
|
||||
* Copyright (c) 2023, Linden Research, Inc.
|
||||
* $/LicenseInfo$
|
||||
*/
|
||||
|
||||
#if ! defined(LL_HEXDUMP_H)
|
||||
#define LL_HEXDUMP_H
|
||||
|
||||
#include <cctype>
|
||||
#include <iomanip>
|
||||
#include <iostream>
|
||||
#include <string_view>
|
||||
|
||||
// Format a given byte string as 2-digit hex values, no separators
|
||||
// Usage: std::cout << hexdump(somestring) << ...
|
||||
class hexdump
|
||||
{
|
||||
public:
|
||||
hexdump(const std::string_view& data):
|
||||
hexdump(data.data(), data.length())
|
||||
{}
|
||||
|
||||
hexdump(const char* data, size_t len):
|
||||
hexdump(reinterpret_cast<const unsigned char*>(data), len)
|
||||
{}
|
||||
|
||||
hexdump(const unsigned char* data, size_t len):
|
||||
mData(data, data + len)
|
||||
{}
|
||||
|
||||
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<unsigned char> 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 std::string_view& data):
|
||||
mData(data)
|
||||
{}
|
||||
|
||||
hexmix(const char* data, size_t len):
|
||||
mData(data, len)
|
||||
{}
|
||||
|
||||
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;
|
||||
};
|
||||
|
||||
#endif /* ! defined(LL_HEXDUMP_H) */
|
||||
|
|
@ -67,14 +67,31 @@ public:
|
|||
|
||||
std::string getName() const { return mPath.string(); }
|
||||
|
||||
void peep()
|
||||
template <typename CALLABLE>
|
||||
void peep_via(CALLABLE&& callable) const
|
||||
{
|
||||
std::cout << "File '" << mPath << "' contains:\n";
|
||||
std::forward<CALLABLE>(callable)(stringize("File '", mPath, "' contains:"));
|
||||
boost::filesystem::ifstream reader(mPath, std::ios::binary);
|
||||
std::string line;
|
||||
while (std::getline(reader, line))
|
||||
std::cout << line << '\n';
|
||||
std::cout << "---\n";
|
||||
std::forward<CALLABLE>(callable)(line);
|
||||
std::forward<CALLABLE>(callable)("---");
|
||||
}
|
||||
|
||||
void peep_log() const
|
||||
{
|
||||
peep_via([](const std::string& line){ LL_DEBUGS() << line << LL_ENDL; });
|
||||
}
|
||||
|
||||
void peep(std::ostream& out=std::cout) const
|
||||
{
|
||||
peep_via([&out](const std::string& line){ out << line << '\n'; });
|
||||
}
|
||||
|
||||
friend std::ostream& operator<<(std::ostream& out, const NamedTempFile& self)
|
||||
{
|
||||
self.peep(out);
|
||||
return out;
|
||||
}
|
||||
|
||||
static boost::filesystem::path temp_path(const std::string_view& pfx="",
|
||||
|
|
|
|||
Loading…
Reference in New Issue