blob: aaf910691f0d2e87cb2b760deba0bae1c282db9a [file]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* License); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
#include "format/output_format.h"
#include <gtest/gtest.h>
#include <sstream>
#include <streambuf>
#include <utility>
#include <vector>
#include "common/db_common.h"
#include "utils/errno_define.h"
using tsfile_cli::OutputFormat;
using tsfile_cli::ParsedArgs;
using tsfile_cli::RowWriter;
namespace {
class FailingStreamBuf : public std::streambuf {
protected:
std::streamsize xsputn(const char*, std::streamsize) override { return 0; }
int_type overflow(int_type) override { return traits_type::eof(); }
};
class FlushFailingStreamBuf : public std::stringbuf {
protected:
int sync() override { return -1; }
};
} // namespace
TEST(ErrorCodeMessageTest, KnownCodesMapToReadablePhrases) {
EXPECT_STREQ(tsfile_cli::error_code_message(common::E_TABLE_NOT_EXIST),
"table does not exist");
EXPECT_STREQ(tsfile_cli::error_code_message(common::E_DEVICE_NOT_EXIST),
"device does not exist");
EXPECT_STREQ(
tsfile_cli::error_code_message(common::E_MEASUREMENT_NOT_EXIST),
"measurement does not exist");
EXPECT_STREQ(tsfile_cli::error_code_message(common::E_TSFILE_CORRUPTED),
"file is corrupted");
EXPECT_STREQ(tsfile_cli::error_code_message(common::E_OUT_OF_ORDER),
"data is out of order");
EXPECT_STREQ(tsfile_cli::error_code_message(common::E_DECODE_ERR),
"failed to decode data");
EXPECT_STREQ(tsfile_cli::error_code_message(common::E_FILE_MAP_ERR),
"failed to memory-map file");
}
TEST(ErrorCodeMessageTest, UnknownCodeFallsBackToInternalError) {
EXPECT_STREQ(tsfile_cli::error_code_message(987654), "internal error");
// The phrase is always a non-empty, printable string (never a bare code).
EXPECT_GT(std::string(tsfile_cli::error_code_message(-1)).size(), 0u);
}
TEST(ResolveFormatTest, AutoAlwaysUsesTable) {
EXPECT_EQ(tsfile_cli::resolve_format(ParsedArgs::Format::kAuto, true),
OutputFormat::kTable);
EXPECT_EQ(tsfile_cli::resolve_format(ParsedArgs::Format::kAuto, false),
OutputFormat::kTable);
EXPECT_EQ(tsfile_cli::resolve_format(ParsedArgs::Format::kJson, true),
OutputFormat::kJson);
}
TEST(CsvEscapeTest, QuotesWhenSpecialCharsPresent) {
EXPECT_EQ(tsfile_cli::csv_escape("plain"), "plain");
EXPECT_EQ(tsfile_cli::csv_escape("a,b"), "\"a,b\"");
EXPECT_EQ(tsfile_cli::csv_escape("she said \"hi\""),
"\"she said \"\"hi\"\"\"");
EXPECT_EQ(tsfile_cli::csv_escape("line\nbreak"), "\"line\nbreak\"");
}
TEST(JsonEscapeTest, EscapesQuotesBackslashAndControls) {
EXPECT_EQ(tsfile_cli::json_escape("a\"b\\c"), "a\\\"b\\\\c");
EXPECT_EQ(tsfile_cli::json_escape("tab\there"), "tab\\there");
}
TEST(Utf8OutputTest, PreservesValidSequencesAcrossFormats) {
const std::string text =
"ASCII\xc2\x80\xdf\xbf\xe0\xa0\x80\xe4\xb8\xad"
"\xed\x9f\xbf\xee\x80\x80\xef\xbb\xbf\xf0\x90\x80\x80"
"\xf0\x9f\x98\x80\xf4\x8f\xbf\xbf";
EXPECT_EQ(tsfile_cli::csv_escape(text), text);
EXPECT_EQ(tsfile_cli::json_escape(text), text);
EXPECT_EQ(tsfile_cli::table_escape(text), text);
}
TEST(Utf8OutputTest, ReplacesMaximalInvalidSubpartsAcrossFormats) {
const std::string replacement = "\xef\xbf\xbd";
const std::vector<std::pair<std::string, std::string>> cases = {
{"\xff", replacement},
{"\x80\xbf", replacement + replacement},
{"\xc0\xaf", replacement + replacement},
{"\xe0\x80\xbf", replacement + replacement + replacement},
{"\xed\xa0\x80", replacement + replacement + replacement},
{"\xf4\x91\x92\x93",
replacement + replacement + replacement + replacement},
{"\xc2", replacement},
{"\xe1\x80", replacement},
{"\xf0\x91\x92", replacement},
{"a\xc2"
"b",
"a" + replacement + "b"},
{"\xe1\x80"
"A",
replacement + "A"},
{"\xf1\xbf\xe4\xb8\xad", replacement + "\xe4\xb8\xad"},
{"\xe1\x80\xe2\xf0\x91\x92\xf1\xbf"
"A",
replacement + replacement + replacement + replacement + "A"},
};
for (const auto& test : cases) {
SCOPED_TRACE(::testing::PrintToString(test.first));
EXPECT_EQ(tsfile_cli::csv_escape(test.first), test.second);
EXPECT_EQ(tsfile_cli::json_escape(test.first), test.second);
EXPECT_EQ(tsfile_cli::table_escape(test.first), test.second);
}
}
TEST(Utf8OutputTest, PreservesDelimitersAfterAnInvalidSequence) {
const std::string text = "\xe1\x80\",\n";
const std::string replacement = "\xef\xbf\xbd";
EXPECT_EQ(tsfile_cli::csv_escape(text), "\"" + replacement + "\"\",\n\"");
EXPECT_EQ(tsfile_cli::json_escape(text), replacement + "\\\",\\n");
EXPECT_EQ(tsfile_cli::table_escape(text), replacement + "\",\\n");
}
TEST(TableEscapeTest, EscapesBackslashAndNamedControls) {
EXPECT_EQ(tsfile_cli::table_escape("a\\b"), "a\\\\b");
EXPECT_EQ(tsfile_cli::table_escape("line\nbreak"), "line\\nbreak");
EXPECT_EQ(tsfile_cli::table_escape("car\rret"), "car\\rret");
EXPECT_EQ(tsfile_cli::table_escape("tab\there"), "tab\\there");
}
TEST(TableEscapeTest, OtherControlsBecomeUnicodeEscapes) {
EXPECT_EQ(tsfile_cli::table_escape(std::string("a\bb", 3)), "a\\u0008b");
EXPECT_EQ(tsfile_cli::table_escape(std::string("a\fb", 3)), "a\\u000cb");
EXPECT_EQ(tsfile_cli::table_escape(std::string("\x01\x1f", 2)),
"\\u0001\\u001f");
}
TEST(TableEscapeTest, ControlByteBoundaries) {
// NUL is the lowest C0 control; DEL (0x7f) is the only non-C0 control in
// the C iscntrl() set and must also become \uXXXX rather than pass through.
EXPECT_EQ(tsfile_cli::table_escape(std::string("\x00", 1)), "\\u0000");
EXPECT_EQ(tsfile_cli::table_escape(std::string("\x7f", 1)), "\\u007f");
EXPECT_EQ(tsfile_cli::table_escape(std::string("a\177b", 3)), "a\\u007fb");
// Space (0x20) is printable: it must survive verbatim, never be escaped.
EXPECT_EQ(tsfile_cli::table_escape("a b"), "a b");
// Well-formed UTF-8 must pass through unchanged.
EXPECT_EQ(tsfile_cli::table_escape("\xe4\xb8\xad"), "\xe4\xb8\xad");
}
TEST(TypeNameTest, KnownTypesMapToNames) {
EXPECT_STREQ(tsfile_cli::tsdatatype_name(common::INT64), "INT64");
EXPECT_STREQ(tsfile_cli::tsdatatype_name(common::STRING), "STRING");
EXPECT_STREQ(tsfile_cli::tsdatatype_name(common::BOOLEAN), "BOOLEAN");
}
TEST(EncodingNameTest, KnownEncodings) {
EXPECT_STREQ(tsfile_cli::tsencoding_name(common::PLAIN), "PLAIN");
EXPECT_STREQ(tsfile_cli::tsencoding_name(common::TS_2DIFF), "TS_2DIFF");
EXPECT_STREQ(tsfile_cli::tsencoding_name(common::SPRINTZ), "SPRINTZ");
EXPECT_STREQ(tsfile_cli::tsencoding_name(common::CHIMP), "CHIMP");
EXPECT_STREQ(tsfile_cli::tsencoding_name(common::RLBE), "RLBE");
EXPECT_STREQ(tsfile_cli::tsencoding_name(common::CAMEL), "CAMEL");
}
TEST(CompressionNameTest, KnownCompressors) {
EXPECT_STREQ(tsfile_cli::compression_name(common::UNCOMPRESSED),
"UNCOMPRESSED");
EXPECT_STREQ(tsfile_cli::compression_name(common::SNAPPY), "SNAPPY");
EXPECT_STREQ(tsfile_cli::compression_name(common::LZ4), "LZ4");
EXPECT_STREQ(tsfile_cli::compression_name(common::ZSTD), "ZSTD");
EXPECT_STREQ(tsfile_cli::compression_name(common::LZMA2), "LZMA2");
}
TEST(RowWriterTest, TsvWritesHeaderThenRows) {
std::ostringstream out;
RowWriter w(out, OutputFormat::kTsv, {"time", "s1"},
{common::INT64, common::INT64}, false);
w.write({"1", "10"}, {false, false});
w.write({"2", ""}, {false, true});
w.finish();
EXPECT_EQ(out.str(), "time\ts1\n1\t10\n2\t\n");
}
TEST(RowWriterTest, NoHeaderSuppressesHeader) {
std::ostringstream out;
RowWriter w(out, OutputFormat::kTsv, {"name"}, {common::STRING}, true);
w.write({"table1"}, {false});
w.finish();
EXPECT_EQ(out.str(), "table1\n");
}
TEST(RowWriterTest, CsvEscapesCells) {
std::ostringstream out;
RowWriter w(out, OutputFormat::kCsv, {"name", "note"},
{common::STRING, common::STRING}, false);
w.write({"a,b", ""}, {false, true});
w.write({"", ""}, {false, false});
w.finish();
EXPECT_EQ(out.str(), "name,note\n\"a,b\",\\N\n\"\",\"\"\n");
}
TEST(RowWriterTest, JsonQuotesInt64TimestampAndLeavesSmallNumbersBare) {
std::ostringstream out;
RowWriter w(
out, OutputFormat::kJson, {"time", "small", "ts", "name"},
{common::INT64, common::INT32, common::TIMESTAMP, common::STRING},
false);
w.write({"5", "10", "1700000000000", "dev1"}, {false, false, false, false});
w.write({"6", "11", "1700000000001", ""}, {false, false, false, true});
w.finish();
EXPECT_EQ(out.str(),
"{\"time\":\"5\",\"small\":10,\"ts\":\"1700000000000\","
"\"name\":\"dev1\"}\n"
"{\"time\":\"6\",\"small\":11,\"ts\":\"1700000000001\","
"\"name\":null}\n");
}
TEST(RowWriterTest, BlobCellsUseLowercaseHexLexeme) {
std::ostringstream json;
RowWriter jw(json, OutputFormat::kJson, {"payload"}, {common::BLOB}, false);
jw.write({std::string("A\0z", 3)}, {false});
jw.finish();
EXPECT_EQ(json.str(), "{\"payload\":\"0x41007a\"}\n");
std::ostringstream csv;
RowWriter cw(csv, OutputFormat::kCsv, {"payload"}, {common::BLOB}, false);
cw.write({"hello"}, {false});
cw.finish();
EXPECT_EQ(csv.str(), "payload\n0x68656c6c6f\n");
}
TEST(RowWriterTest, TableAlignsColumns) {
std::ostringstream out;
RowWriter w(out, OutputFormat::kTable, {"name", "type"},
{common::STRING, common::STRING}, false);
w.write({"s1", "INT64"}, {false, false});
w.write({"longname", "BOOLEAN"}, {false, false});
w.finish();
EXPECT_EQ(out.str(),
"name type\n"
"s1 INT64\n"
"longname BOOLEAN\n");
}
TEST(RowWriterTest, TableEscapesControlCharactersOnOneLine) {
std::ostringstream out;
RowWriter w(out, OutputFormat::kTable, {"time", "note"},
{common::INT64, common::STRING}, false);
w.write({"1000", "line1\nline2"}, {false, false});
w.write({"2000", "tab\there"}, {false, false});
w.write({"3000", "back\\slash"}, {false, false});
w.finish();
EXPECT_EQ(out.str(),
"time note\n"
"1000 line1\\nline2\n"
"2000 tab\\there\n"
"3000 back\\\\slash\n");
}
TEST(RowWriterTest, TableEscapingKeepsColumnAlignment) {
// When several cells in one row contain controls, each expanded escape
// must still be padded to the same visual width computed at write time.
std::ostringstream out;
RowWriter w(out, OutputFormat::kTable, {"a", "b"},
{common::STRING, common::STRING}, false);
w.write({"x\ty", "long\nvalue"}, {false, false});
w.write({"p", "q"}, {false, false});
w.finish();
EXPECT_EQ(out.str(),
"a b\n"
"x\\ty long\\nvalue\n"
"p q\n");
}
TEST(RowWriterTest, ReportsStreamWriteFailure) {
FailingStreamBuf buffer;
std::ostream out(&buffer);
RowWriter writer(out, OutputFormat::kCsv, {"name"}, {common::STRING},
false);
EXPECT_FALSE(writer.write({"value"}, {false}));
EXPECT_FALSE(writer.finish());
}
TEST(RowWriterTest, ReportsFlushFailure) {
FlushFailingStreamBuf buffer;
std::ostream out(&buffer);
RowWriter writer(out, OutputFormat::kCsv, {"name"}, {common::STRING},
false);
ASSERT_TRUE(writer.write({"value"}, {false}));
EXPECT_FALSE(writer.finish());
}
TEST(RowWriterTest, ReplacesInvalidUtf8InHeadersAndValues) {
const std::string header = "name\xff";
const std::string value = "value\xe1\x80";
const std::string replacement = "\xef\xbf\xbd";
const OutputFormat formats[] = {OutputFormat::kCsv, OutputFormat::kJson,
OutputFormat::kTable};
for (OutputFormat format : formats) {
std::ostringstream out;
RowWriter writer(out, format, {header}, {common::STRING}, false);
ASSERT_TRUE(writer.write({value}, {false}));
ASSERT_TRUE(writer.finish());
const std::string expected =
format == OutputFormat::kJson
? "{\"name" + replacement + "\":\"value" + replacement + "\"}\n"
: "name" + replacement + "\nvalue" + replacement + "\n";
EXPECT_EQ(out.str(), expected);
}
}
TEST(RowWriterTest, PreservesNonUtf8BlobBytesAsHex) {
const OutputFormat formats[] = {OutputFormat::kCsv, OutputFormat::kJson,
OutputFormat::kTable};
for (OutputFormat format : formats) {
std::ostringstream out;
RowWriter writer(out, format, {"payload"}, {common::BLOB}, false);
ASSERT_TRUE(writer.write({std::string("\xff\0\x80", 3)}, {false}));
ASSERT_TRUE(writer.finish());
EXPECT_NE(out.str().find("0xff0080"), std::string::npos);
}
}
TEST(RowWriterTest, RejectsColumnCollisionsAfterUtf8Replacement) {
const std::vector<std::vector<std::string>> headers = {
{"bad\xff", "bad\xfe"},
{"bad\xff", "bad\xef\xbf\xbd"},
{"same", "same"},
};
for (const auto& header : headers) {
for (OutputFormat format : {OutputFormat::kJson, OutputFormat::kCsv,
OutputFormat::kTable, OutputFormat::kTsv}) {
for (bool no_header : {false, true}) {
std::ostringstream out;
RowWriter writer(out, format, header,
{common::INT64, common::INT64}, no_header);
EXPECT_FALSE(writer.write({"42", "7"}, {false, false}));
EXPECT_FALSE(writer.finish());
EXPECT_TRUE(out.str().empty());
}
}
}
}
TEST(RowWriterTest, RejectsColumnCollisionsWithoutRows) {
for (OutputFormat format : {OutputFormat::kJson, OutputFormat::kCsv,
OutputFormat::kTable, OutputFormat::kTsv}) {
std::ostringstream out;
RowWriter writer(out, format, {"bad\xff", "bad\xfe"},
{common::STRING, common::STRING}, false);
EXPECT_FALSE(writer.finish());
EXPECT_TRUE(out.str().empty());
}
}
TEST(RowWriterTest, CsvEscapesLeadingBackslashesOnlyInTextValues) {
std::ostringstream out;
RowWriter writer(out, OutputFormat::kCsv, {R"(\header)", "note"},
{common::STRING, common::TEXT}, false);
ASSERT_TRUE(writer.write({R"(\N)", R"(\a,b)"}, {false, false}));
ASSERT_TRUE(writer.write({R"(back\slash)", ""}, {false, true}));
ASSERT_TRUE(writer.finish());
EXPECT_EQ(out.str(), R"csv(\header,note
\\N,"\\a,b"
back\slash,\N
)csv");
}