blob: cedca663b90709795e63157ae35a7725572fc1f7 [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 <gtest/gtest.h>
#include <sys/stat.h>
#include <cstdio>
#include <fstream>
#include <sstream>
#include <string>
#include <vector>
#include "cli/run_cli.h"
#include "cli_test_util.h"
#include "format/input_format.h"
namespace {
const std::vector<std::string> kFormats = {"csv", "table", "ndjson"};
const std::vector<std::string> kNames = {"bad\xff", "bad\xfe",
"bad\xef\xbf\xbd"};
struct TreeDefinition {
std::string device;
std::vector<std::string> fields;
};
struct TableDefinition {
std::string table;
std::vector<std::string> tags;
std::vector<std::string> fields;
};
class CliUtf8CollisionTest : public ::testing::Test {
protected:
void SetUp() override { storage::libtsfile_init(); }
void TearDown() override {
for (const auto& path : files_) std::remove(path.c_str());
for (const auto& path : directories_) {
for (const auto& name :
{"_manifest.json", "0001.csv", "0002.csv", "0001.ndjson",
"0002.ndjson", "0001.txt", "0002.txt"}) {
std::remove((path + "/" + name).c_str());
}
#ifdef _WIN32
_rmdir(path.c_str());
#else
rmdir(path.c_str());
#endif
}
}
std::string temp_file(const std::string& extension = ".tsfile") {
std::string path =
tsfile_cli_test::unique_temp_path("utf8_collision", extension);
files_.push_back(path);
return path;
}
std::string temp_directory() {
std::string path =
tsfile_cli_test::unique_temp_path("utf8_collision_export", "");
directories_.push_back(path);
return path;
}
bool exists(const std::string& path) {
struct stat st;
return stat(path.c_str(), &st) == 0;
}
void write_tree(const std::string& path,
const std::vector<TreeDefinition>& definitions,
bool aligned = false) {
storage::WriteFile file;
int flags = O_WRONLY | O_CREAT | O_TRUNC;
#ifdef _WIN32
flags |= O_BINARY;
#endif
ASSERT_EQ(file.create(path, flags, 0666), common::E_OK);
storage::TsFileTreeWriter writer(&file);
for (const auto& definition : definitions) {
std::string device = definition.device;
std::vector<storage::MeasurementSchema*> schemas;
for (const auto& field : definition.fields) {
schemas.push_back(new storage::MeasurementSchema(
field, common::INT64, common::PLAIN, common::UNCOMPRESSED));
if (!aligned) {
ASSERT_EQ(
writer.register_timeseries(device, schemas.back()),
common::E_OK);
}
}
if (aligned) {
ASSERT_EQ(writer.register_timeseries(device, schemas),
common::E_OK);
}
storage::TsRecord record(device, 1000);
for (const auto& field : definition.fields) {
record.add_point(field, static_cast<int64_t>(42));
}
ASSERT_EQ(writer.write(record), common::E_OK);
if (!aligned) {
for (auto* schema : schemas) delete schema;
}
}
ASSERT_EQ(writer.flush(), common::E_OK);
ASSERT_EQ(writer.close(), common::E_OK);
}
void write_tables(const std::string& path,
const std::vector<TableDefinition>& definitions,
int rows = 2) {
storage::TsFileWriter writer;
ASSERT_EQ(writer.open(path), common::E_OK);
for (const auto& definition : definitions) {
std::vector<common::ColumnSchema> columns;
std::vector<std::string> names;
std::vector<common::TSDataType> types;
std::vector<common::ColumnCategory> categories;
for (const auto& tag : definition.tags) {
columns.push_back(common::ColumnSchema(
tag, common::STRING, common::UNCOMPRESSED, common::PLAIN,
common::ColumnCategory::TAG));
names.push_back(tag);
types.push_back(common::STRING);
categories.push_back(common::ColumnCategory::TAG);
}
for (const auto& field : definition.fields) {
columns.push_back(common::ColumnSchema(
field, common::INT64, common::UNCOMPRESSED, common::PLAIN,
common::ColumnCategory::FIELD));
names.push_back(field);
types.push_back(common::INT64);
categories.push_back(common::ColumnCategory::FIELD);
}
ASSERT_EQ(
writer.register_table(std::make_shared<storage::TableSchema>(
definition.table, columns)),
common::E_OK);
if (rows == 0) continue;
storage::Tablet tablet(definition.table, names, types, categories,
rows);
for (int row = 0; row < rows; ++row) {
tablet.add_timestamp(row, static_cast<int64_t>(1000 + row));
for (const auto& tag : definition.tags) {
tablet.add_value(row, tag, row == 0 ? "first" : "second");
}
for (const auto& field : definition.fields) {
tablet.add_value(row, field, static_cast<int64_t>(42));
}
}
ASSERT_EQ(writer.write_table(tablet), common::E_OK);
}
ASSERT_EQ(writer.flush(), common::E_OK);
ASSERT_EQ(writer.close(), common::E_OK);
}
void expect_collision(const std::vector<std::string>& args) {
std::ostringstream out, err;
EXPECT_EQ(tsfile_cli::run_cli(args, out, err), 2) << err.str();
EXPECT_TRUE(out.str().empty()) << out.str();
EXPECT_NE(err.str().find("after UTF-8 replacement"), std::string::npos)
<< err.str();
EXPECT_TRUE(tsfile_cli::is_valid_utf8(err.str()));
}
void expect_success(const std::vector<std::string>& args) {
std::ostringstream out, err;
EXPECT_EQ(tsfile_cli::run_cli(args, out, err), 0) << err.str();
EXPECT_TRUE(err.str().empty()) << err.str();
EXPECT_TRUE(tsfile_cli::is_valid_utf8(out.str()));
}
private:
std::vector<std::string> files_;
std::vector<std::string> directories_;
};
TEST_F(CliUtf8CollisionTest, TreeColumnsFailBeforeOutputAcrossFormats) {
for (bool aligned : {false, true}) {
const std::string path = temp_file();
write_tree(path, {{"root.d", kNames}}, aligned);
ASSERT_FALSE(HasFatalFailure());
for (const auto& format : kFormats) {
for (const auto& command :
{"schema", "count", "stats", "head", "cat"}) {
SCOPED_TRACE(std::string(command) + " " + format +
" aligned=" + std::to_string(aligned));
expect_collision({command, "-f", format, path});
}
expect_success({"ls", "-f", format, path});
}
// Sketch deliberately follows printSketch without collision checking.
expect_success({"sketch", path});
}
}
TEST_F(CliUtf8CollisionTest, TableFieldsAndTagsFailBeforeOutputAcrossFormats) {
const std::vector<TableDefinition> definitions = {
{"t", {"id"}, kNames},
{"t", kNames, {"value"}},
{"t", {kNames[0]}, {kNames[1]}},
{"t", {"id"}, {kNames[0], kNames[2]}}};
for (const auto& definition : definitions) {
for (int rows : {0, 2}) {
const std::string path = temp_file();
write_tables(path, {definition}, rows);
ASSERT_FALSE(HasFatalFailure());
for (const auto& format : kFormats) {
for (const auto& command :
{"schema", "count", "stats", "head", "cat"}) {
SCOPED_TRACE(std::string(command) + " " + format +
" rows=" + std::to_string(rows));
expect_collision({command, "-f", format, path});
}
expect_success({"ls", "-f", format, path});
}
}
}
}
TEST_F(CliUtf8CollisionTest, ObjectCollisionsDoNotRequireMatchingColumnNames) {
for (bool table : {false, true}) {
for (size_t second : {size_t(1), size_t(2)}) {
const std::string path = temp_file();
if (table) {
write_tables(path, {{kNames[0], {"id"}, {"left"}},
{kNames[second], {"id"}, {"right"}}});
} else {
write_tree(
path, {{kNames[0], {"left"}}, {kNames[second], {"right"}}});
}
ASSERT_FALSE(HasFatalFailure());
for (const auto& format : kFormats) {
for (const auto& command : {"ls", "schema", "count", "stats"}) {
SCOPED_TRACE(std::string(command) + " " + format);
expect_collision({command, "-f", format, path});
if (std::string(command) != "ls") {
expect_success({command, table ? "-t" : "-d", kNames[0],
"-f", format, path});
expect_success(
{command, "-m", "left", "-f", format, path});
}
}
}
}
}
}
TEST_F(CliUtf8CollisionTest, ProjectionAndSeparateColumnScopesRemainValid) {
for (bool table : {false, true}) {
const std::string path = temp_file();
const std::string scoped = temp_file();
if (table) {
write_tables(path, {{"a", {"id"}, kNames}});
write_tables(scoped, {{"a", {"id"}, {kNames[0], "value"}},
{"b", {"id"}, {kNames[1], "value"}}});
} else {
write_tree(path, {{"a", kNames}});
write_tree(scoped, {{"a", {kNames[0], "value"}},
{"b", {kNames[1], "value"}}});
}
ASSERT_FALSE(HasFatalFailure());
for (const auto& format : kFormats) {
for (const auto& command :
{"schema", "count", "stats", "head", "cat"}) {
expect_success({command, "-m", kNames[0], "-f", format, path});
if (std::string(command) != "head" &&
std::string(command) != "cat") {
expect_success({command, "-f", format, scoped});
}
}
const std::string output = temp_file(".out");
expect_success({"export", table ? "-t" : "-d", "a", "-m", kNames[0],
"--type", format, "-o", output, path});
EXPECT_TRUE(exists(output));
const std::string dir = temp_directory();
expect_success({"export", table ? "-t" : "-d", "a",
table ? "-t" : "-d", "b", "--type", format,
"--output-dir", dir, scoped});
EXPECT_TRUE(exists(dir + "/_manifest.json"));
}
}
}
TEST_F(CliUtf8CollisionTest, StatsChecksMergedTagHeadersAcrossTables) {
const std::string path = temp_file();
write_tables(
path, {{"a", {kNames[0]}, {"value"}}, {"b", {kNames[1]}, {"value"}}});
ASSERT_FALSE(HasFatalFailure());
for (const auto& format : kFormats) {
expect_collision({"stats", "-f", format, path});
for (const auto& command : {"schema", "count"}) {
expect_success({command, "-f", format, path});
}
expect_success({"stats", "-t", "a", "-f", format, path});
}
}
TEST_F(CliUtf8CollisionTest, EmptyRowWindowsCannotHideSelectedNameCollisions) {
const std::vector<std::vector<std::string>> windows = {
{"-n", "0"},
{"--start", "0", "-n", "0"},
{"--start", "2000"},
{"--end", "0"},
{"--offset", "1"}};
for (bool table : {false, true}) {
const std::string path = temp_file();
if (table)
write_tables(path, {{"t", {"id"}, kNames}});
else
write_tree(path, {{"root.d", kNames}});
ASSERT_FALSE(HasFatalFailure());
for (const auto& format : kFormats) {
for (const auto& window : windows) {
for (const auto& command : {"head", "cat", "export"}) {
SCOPED_TRACE(std::string(command) + " " + format);
std::vector<std::string> args = {command};
args.insert(args.end(), window.begin(), window.end());
const std::string output = temp_file(".out");
if (std::string(command) == "export") {
args.insert(
args.end(),
{table ? "-t" : "-d", table ? "t" : "root.d",
"--type", format, "-o", output});
} else {
args.insert(args.end(), {"-f", format});
}
args.push_back(path);
expect_collision(args);
EXPECT_FALSE(exists(output));
}
}
}
}
}
TEST_F(CliUtf8CollisionTest, ExportCollisionPreservesExistingTargets) {
for (bool table : {false, true}) {
const std::string path = temp_file();
if (table)
write_tables(path, {{"t", {"id"}, kNames}});
else
write_tree(path, {{"root.d", kNames}});
ASSERT_FALSE(HasFatalFailure());
for (const auto& format : kFormats) {
const std::string output = temp_file(".out");
expect_collision({"export", table ? "-t" : "-d",
table ? "t" : "root.d", "--type", format, "-o",
output, path});
EXPECT_FALSE(exists(output));
{
std::ofstream file(output.c_str(), std::ios::binary);
file << "keep existing output\n";
}
expect_collision({"export", table ? "-t" : "-d",
table ? "t" : "root.d", "--type", format, "-o",
output, path});
expect_collision({"export", table ? "-t" : "-d",
table ? "t" : "root.d", "--type", format, "-o",
output + "/unavailable", path});
expect_collision({"export", table ? "-t" : "-d",
table ? "t" : "root.d", "--type", format,
"--force", "-o", output, path});
std::ifstream file(output.c_str(), std::ios::binary);
std::ostringstream content;
content << file.rdbuf();
EXPECT_EQ(content.str(), "keep existing output\n");
}
}
}
TEST_F(CliUtf8CollisionTest,
MultiExportChecksEveryObjectBeforeCreatingDirectory) {
for (bool table : {false, true}) {
for (bool object_collision : {false, true}) {
const std::string first = object_collision ? kNames[0] : "a";
const std::string second = object_collision ? kNames[1] : "b";
const std::string path = temp_file();
if (table) {
write_tables(
path, {{first, {"id"}, {"left"}},
{second,
{"id"},
object_collision ? std::vector<std::string>{"right"}
: kNames}});
} else {
write_tree(path,
{{first, {"left"}},
{second, object_collision
? std::vector<std::string>{"right"}
: kNames}});
}
ASSERT_FALSE(HasFatalFailure());
for (const auto& format : kFormats) {
const std::string dir = temp_directory();
expect_collision({"export", table ? "-t" : "-d", first,
table ? "-t" : "-d", second, "--type", format,
"--output-dir", dir, path});
EXPECT_FALSE(exists(dir));
// A collision takes precedence over output-path errors as
// well, proving it is checked before publication is prepared.
const std::string occupied = temp_file(".out");
{
std::ofstream file(occupied.c_str());
file << "keep existing output\n";
}
expect_collision({"export", table ? "-t" : "-d", first,
table ? "-t" : "-d", second, "--type", format,
"--output-dir", occupied, path});
std::ifstream file(occupied.c_str());
std::ostringstream content;
content << file.rdbuf();
EXPECT_EQ(content.str(), "keep existing output\n");
}
}
}
}
} // namespace