blob: 197f80744ff0ca7e24cc47da8143a4779324c27e [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 <cstdio>
#include <fstream>
#include <sstream>
#include <string>
#ifndef _WIN32
#include <sys/stat.h>
#include <unistd.h>
#endif
#include "cli/run_cli.h"
#include "cli_test_util.h"
#include "reader/tsfile_reader.h"
#ifndef TSFILE_CPP_SOURCE_DIR
#define TSFILE_CPP_SOURCE_DIR "."
#endif
namespace {
struct TableFixture {
std::string path = tsfile_cli_test::write_table_fixture();
~TableFixture() { std::remove(path.c_str()); }
};
struct MultiTableFixture {
std::string path = tsfile_cli_test::write_multi_table_fixture();
~MultiTableFixture() { std::remove(path.c_str()); }
};
bool file_exists(const std::string& path) {
std::ifstream in(path.c_str());
return in.good();
}
std::string read_file(const std::string& path) {
std::ifstream in(path.c_str(), std::ios::binary);
std::ostringstream buf;
buf << in.rdbuf();
return buf.str();
}
} // namespace
TEST(CliRequirements, HelpListsExactlyCurrentCommandSurface) {
std::ostringstream out;
std::ostringstream err;
int code = tsfile_cli::run_cli({"--help"}, out, err);
EXPECT_EQ(code, 0);
EXPECT_NE(out.str().find(
"ls schema meta stats count sketch head cat export write"),
std::string::npos)
<< out.str();
EXPECT_EQ(out.str().find("sample"), std::string::npos) << out.str();
EXPECT_TRUE(err.str().empty());
}
TEST(CliRequirements, CommandHelpIsSpecificAndDocumentsSyntaxFieldsExamples) {
const std::vector<std::string> commands = {
"ls", "schema", "meta", "stats", "count",
"sketch", "head", "cat", "export", "write"};
for (const auto& command : commands) {
std::ostringstream out;
std::ostringstream err;
EXPECT_EQ(tsfile_cli::run_cli({command, "--help"}, out, err), 0)
<< command;
EXPECT_TRUE(err.str().empty()) << command << ": " << err.str();
EXPECT_NE(out.str().find("Usage: tsfile-cli " + command),
std::string::npos)
<< command << ": " << out.str();
EXPECT_NE(out.str().find("Result fields:"), std::string::npos)
<< command << ": " << out.str();
EXPECT_NE(out.str().find("Examples:"), std::string::npos)
<< command << ": " << out.str();
EXPECT_EQ(out.str().find("Commands:"), std::string::npos)
<< command << ": " << out.str();
}
std::ostringstream meta_out;
std::ostringstream meta_err;
EXPECT_EQ(tsfile_cli::run_cli({"meta", "--help"}, meta_out, meta_err), 0);
EXPECT_TRUE(meta_err.str().empty());
EXPECT_NE(meta_out.str().find("Usage: tsfile-cli meta"), std::string::npos)
<< meta_out.str();
EXPECT_NE(meta_out.str().find("Result fields:"), std::string::npos)
<< meta_out.str();
EXPECT_NE(meta_out.str().find("size_bytes,format_version,model"),
std::string::npos)
<< meta_out.str();
EXPECT_NE(meta_out.str().find("Examples:"), std::string::npos)
<< meta_out.str();
EXPECT_EQ(meta_out.str().find("Commands:"), std::string::npos)
<< meta_out.str();
std::ostringstream write_out;
std::ostringstream write_err;
EXPECT_EQ(tsfile_cli::run_cli({"write", "-h"}, write_out, write_err), 0);
EXPECT_TRUE(write_err.str().empty());
EXPECT_NE(write_out.str().find("Usage: tsfile-cli write"),
std::string::npos)
<< write_out.str();
EXPECT_NE(write_out.str().find("--field <name> <type>"), std::string::npos)
<< write_out.str();
EXPECT_NE(write_out.str().find("Default: success is silent"),
std::string::npos)
<< write_out.str();
EXPECT_NE(write_out.str().find("Examples:"), std::string::npos)
<< write_out.str();
EXPECT_EQ(write_out.str().find("Commands:"), std::string::npos)
<< write_out.str();
}
TEST(CliRequirements, SkillShipsRequiredReferenceFiles) {
const std::string root = std::string(TSFILE_CPP_SOURCE_DIR) +
"/tools/skills/tsfile-cli/references/";
const std::vector<std::string> refs = {"commands.md", "errors.md",
"examples.md"};
for (const auto& ref : refs) {
std::ifstream in(root + ref);
ASSERT_TRUE(in.good()) << ref;
std::ostringstream body;
body << in.rdbuf();
EXPECT_NE(body.str().find("tsfile-cli"), std::string::npos)
<< root + ref;
}
}
TEST(CliRequirements, SampleIsNotACommand) {
std::ostringstream out;
std::ostringstream err;
int code = tsfile_cli::run_cli({"sample", "x.tsfile"}, out, err);
EXPECT_EQ(code, 1);
EXPECT_TRUE(out.str().empty());
EXPECT_NE(err.str().find("Unknown command"), std::string::npos)
<< err.str();
}
TEST(CliRequirements, VersionIncludesConcreteBuildMetadata) {
std::ostringstream out;
std::ostringstream err;
int code = tsfile_cli::run_cli({"--version"}, out, err);
EXPECT_EQ(code, 0);
EXPECT_TRUE(err.str().empty());
EXPECT_NE(out.str().find("tsfile-cli "), std::string::npos) << out.str();
EXPECT_NE(out.str().find(" tsfile="), std::string::npos) << out.str();
EXPECT_NE(out.str().find(" commit="), std::string::npos) << out.str();
EXPECT_NE(out.str().find(" built="), std::string::npos) << out.str();
EXPECT_EQ(out.str().find("unknown"), std::string::npos) << out.str();
}
TEST(CliRequirements, FormatVocabularyIsTableNdjsonCsvOnly) {
TableFixture f;
std::ostringstream ndjson_out;
std::ostringstream ndjson_err;
EXPECT_EQ(tsfile_cli::run_cli({"cat", "-m", "s1", "--start", "0", "--end",
"0", "-f", "ndjson", f.path},
ndjson_out, ndjson_err),
0)
<< ndjson_err.str();
EXPECT_EQ(ndjson_out.str(),
"{\"time\":\"0\",\"id1\":\"id1_field_1\",\"id2\":"
"\"id2_field_2\",\"s1\":\"0\"}\n");
std::ostringstream json_out;
std::ostringstream json_err;
EXPECT_EQ(
tsfile_cli::run_cli({"cat", "-f", "json", f.path}, json_out, json_err),
1);
EXPECT_TRUE(json_out.str().empty());
std::ostringstream tsv_out;
std::ostringstream tsv_err;
EXPECT_EQ(
tsfile_cli::run_cli({"cat", "-f", "tsv", f.path}, tsv_out, tsv_err), 1);
EXPECT_TRUE(tsv_out.str().empty());
}
TEST(CliRequirements, DuplicateSingletonOptionsAreUsageErrors) {
TableFixture f;
std::ostringstream out;
std::ostringstream err;
int code = tsfile_cli::run_cli({"cat", "-f", "csv", "-f", "ndjson", f.path},
out, err);
EXPECT_EQ(code, 1);
EXPECT_TRUE(out.str().empty());
EXPECT_NE(err.str().find("--format specified more than once"),
std::string::npos)
<< err.str();
const std::vector<std::vector<std::string>> duplicate_singletons = {
{"cat", "-n", "1", "--limit", "2", f.path},
{"cat", "--offset", "0", "--offset", "1", f.path},
{"cat", "--start", "0", "--start", "1", f.path},
{"cat", "--end", "1", "--end", "2", f.path},
{"export", "-t", "table1", "--type", "csv", "-o", "a.csv", "--output",
"b.csv", f.path},
{"export", "-t", "table1", "--type", "csv", "--output-dir", "a",
"--output-dir", "b", f.path},
{"export", "-t", "table1", "--type", "csv", "-o", "a.csv", "--force",
"--force", f.path},
};
for (const std::vector<std::string>& args : duplicate_singletons) {
std::ostringstream dup_out;
std::ostringstream dup_err;
EXPECT_EQ(tsfile_cli::run_cli(args, dup_out, dup_err), 1)
<< dup_err.str();
EXPECT_TRUE(dup_out.str().empty());
EXPECT_NE(dup_err.str().find("specified more than once"),
std::string::npos)
<< dup_err.str();
}
}
TEST(CliRequirements, PositionalFileMustBeFinalUnlessAfterDoubleDash) {
TableFixture f;
std::ostringstream bad_out;
std::ostringstream bad_err;
int bad_code =
tsfile_cli::run_cli({"cat", f.path, "-f", "csv"}, bad_out, bad_err);
EXPECT_EQ(bad_code, 1);
EXPECT_TRUE(bad_out.str().empty());
EXPECT_NE(bad_err.str().find("Unexpected argument after file"),
std::string::npos)
<< bad_err.str();
std::ostringstream ok_out;
std::ostringstream ok_err;
int ok_code =
tsfile_cli::run_cli({"cat", "-m", "s1", "--", f.path}, ok_out, ok_err);
EXPECT_EQ(ok_code, 0) << ok_err.str();
EXPECT_TRUE(ok_err.str().empty());
}
TEST(CliRequirements, MeasurementOptionRepeatsAndRejectsCommaLists) {
TableFixture f;
std::ostringstream comma_out;
std::ostringstream comma_err;
EXPECT_EQ(tsfile_cli::run_cli({"cat", "-m", "s1,s2", f.path}, comma_out,
comma_err),
1);
std::ostringstream dup_out;
std::ostringstream dup_err;
EXPECT_EQ(tsfile_cli::run_cli({"cat", "-m", "s1", "-m", "s1", f.path},
dup_out, dup_err),
1);
EXPECT_NE(dup_err.str().find("specified more than once"), std::string::npos)
<< dup_err.str();
}
TEST(CliRequirements, MetaOnlyReturnsSizeFormatVersionAndModel) {
TableFixture f;
std::ostringstream out;
std::ostringstream err;
int code = tsfile_cli::run_cli({"meta", "-f", "csv", f.path}, out, err);
EXPECT_EQ(code, 0) << err.str();
EXPECT_TRUE(err.str().empty());
EXPECT_EQ(out.str().substr(
0, std::string("size_bytes,format_version,model\n").size()),
"size_bytes,format_version,model\n")
<< out.str();
EXPECT_EQ(out.str().find("path"), std::string::npos) << out.str();
EXPECT_EQ(out.str().find("device_count"), std::string::npos) << out.str();
}
TEST(CliRequirements, LsReturnsModelAndObjectFields) {
TableFixture f;
std::ostringstream out;
std::ostringstream err;
int code = tsfile_cli::run_cli({"ls", "-f", "csv", f.path}, out, err);
EXPECT_EQ(code, 0) << err.str();
EXPECT_EQ(out.str(), "model,object\ntable,table1\n");
}
TEST(CliRequirements, SchemaReturnsFixedSevenFieldContract) {
TableFixture f;
std::ostringstream out;
std::ostringstream err;
int code = tsfile_cli::run_cli({"schema", "-f", "csv", f.path}, out, err);
EXPECT_EQ(code, 0) << err.str();
EXPECT_EQ(out.str().substr(
0, std::string("model,object,column,category,data_type,"
"encoding,compression\n")
.size()),
"model,object,column,category,data_type,encoding,compression\n")
<< out.str();
EXPECT_NE(out.str().find("table,table1,id1,TAG,STRING"), std::string::npos)
<< out.str();
EXPECT_NE(out.str().find("table,table1,s1,FIELD,INT64"), std::string::npos)
<< out.str();
}
TEST(CliRequirements, HeadCatAndExportRejectOffsetWithZeroLimit) {
TableFixture f;
std::string out_path = tsfile_cli_test::unique_temp_path(
"tsfile_cli_zero_limit_export", ".csv");
std::ostringstream head_out;
std::ostringstream head_err;
EXPECT_EQ(tsfile_cli::run_cli({"head", "-n", "0", "--offset", "1", f.path},
head_out, head_err),
1);
EXPECT_TRUE(head_out.str().empty());
std::ostringstream cat_out;
std::ostringstream cat_err;
EXPECT_EQ(tsfile_cli::run_cli({"cat", "-n", "0", "--offset", "1", f.path},
cat_out, cat_err),
1);
EXPECT_TRUE(cat_out.str().empty());
std::ostringstream export_out;
std::ostringstream export_err;
EXPECT_EQ(
tsfile_cli::run_cli({"export", "-t", "table1", "--type", "csv", "-o",
out_path, "-n", "0", "--offset", "1", f.path},
export_out, export_err),
1);
EXPECT_TRUE(export_out.str().empty());
EXPECT_FALSE(file_exists(out_path));
std::remove(out_path.c_str());
}
TEST(CliRequirements, HeadAndCatRequireScopeForMultiObjectFiles) {
MultiTableFixture f;
std::ostringstream cat_out;
std::ostringstream cat_err;
EXPECT_EQ(tsfile_cli::run_cli({"cat", "-m", "s1", "-f", "csv", f.path},
cat_out, cat_err),
1);
EXPECT_TRUE(cat_out.str().empty());
EXPECT_NE(cat_err.str().find("requires -t/--table"), std::string::npos)
<< cat_err.str();
std::ostringstream head_out;
std::ostringstream head_err;
EXPECT_EQ(tsfile_cli::run_cli({"head", "-m", "s1", "-f", "csv", f.path},
head_out, head_err),
1);
EXPECT_TRUE(head_out.str().empty());
EXPECT_NE(head_err.str().find("requires -t/--table"), std::string::npos)
<< head_err.str();
}
TEST(CliRequirements, WriteUsesExplicitTagAndFieldOptions) {
std::string csv =
tsfile_cli_test::unique_temp_path("tsfile_cli_req_in", ".csv");
{
std::ofstream o(csv.c_str());
o << "time,id1,s1\n0,dev,0\n1,dev,10\n";
}
std::string out_path =
tsfile_cli_test::unique_temp_path("tsfile_cli_req_out", ".tsfile");
std::ostringstream wout;
std::ostringstream werr;
int wc = tsfile_cli::run_cli(
{"write", "--table", "t1", "--tag", "id1", "STRING", "--field", "s1",
"INT64", "-i", csv, "-o", out_path},
wout, werr);
EXPECT_EQ(wc, 0) << werr.str();
EXPECT_TRUE(file_exists(out_path));
std::ostringstream rout;
std::ostringstream rerr;
EXPECT_EQ(tsfile_cli::run_cli({"cat", "-m", "s1", "-f", "csv", out_path},
rout, rerr),
0)
<< rerr.str();
EXPECT_EQ(rout.str(), "time,id1,s1\n0,dev,0\n1,dev,10\n");
std::remove(csv.c_str());
std::remove(out_path.c_str());
}
TEST(CliRequirements, WriteMapsInputByHeaderName) {
std::string csv =
tsfile_cli_test::unique_temp_path("tsfile_cli_header_order", ".csv");
{
std::ofstream o(csv.c_str());
o << "temp,time,site\n21.5,1000,beijing\n22.0,2000,shanghai\n";
}
std::string out_path = tsfile_cli_test::unique_temp_path(
"tsfile_cli_header_order_out", ".tsfile");
std::ostringstream wout;
std::ostringstream werr;
int wc = tsfile_cli::run_cli(
{"write", "--table", "sensors", "--tag", "site", "STRING", "--field",
"temp", "FLOAT", "-i", csv, "-o", out_path},
wout, werr);
EXPECT_EQ(wc, 0) << werr.str();
std::ostringstream rout;
std::ostringstream rerr;
EXPECT_EQ(tsfile_cli::run_cli(
{"cat", "-t", "sensors", "-f", "csv", out_path}, rout, rerr),
0)
<< rerr.str();
EXPECT_EQ(rout.str(),
"time,site,temp\n1000,beijing,21.5\n2000,shanghai,22\n");
std::remove(csv.c_str());
std::remove(out_path.c_str());
}
TEST(CliRequirements, WriteRejectsHeaderShapeErrorsBeforeCreatingOutput) {
struct Case {
const char* name;
const char* content;
const char* needle;
};
const Case cases[] = {
{"missing", "time,site\n1000,beijing\n", "missing required column"},
{"undeclared", "time,site,temp,status\n1000,beijing,21.5,true\n",
"undeclared column"},
{"duplicate", "time,site,temp,TEMP\n1000,beijing,21.5,22.0\n",
"conflict"},
};
for (const Case& c : cases) {
std::string csv = tsfile_cli_test::unique_temp_path(
std::string("tsfile_cli_header_") + c.name, ".csv");
{
std::ofstream o(csv.c_str());
o << c.content;
}
std::string out_path = tsfile_cli_test::unique_temp_path(
std::string("tsfile_cli_header_out_") + c.name, ".tsfile");
std::ostringstream out;
std::ostringstream err;
int code = tsfile_cli::run_cli(
{"write", "--table", "sensors", "--tag", "site", "STRING",
"--field", "temp", "FLOAT", "-i", csv, "-o", out_path},
out, err);
EXPECT_EQ(code, 2) << c.name << " " << err.str();
EXPECT_TRUE(out.str().empty());
EXPECT_NE(err.str().find(c.needle), std::string::npos)
<< c.name << " " << err.str();
EXPECT_FALSE(file_exists(out_path));
std::remove(csv.c_str());
std::remove(out_path.c_str());
}
}
TEST(CliRequirements, WriteAppliesAndValidatesTypePhysicalOverrides) {
std::string csv =
tsfile_cli_test::unique_temp_path("tsfile_cli_type_override", ".csv");
{
std::ofstream o(csv.c_str());
o << "time,site,temp,humidity\n"
"1000,beijing,21.0,40.5\n"
"2000,beijing,21.5,41.0\n";
}
std::string out_path = tsfile_cli_test::unique_temp_path(
"tsfile_cli_type_override_out", ".tsfile");
std::ostringstream wout;
std::ostringstream werr;
int wc = tsfile_cli::run_cli(
{"write", "--table", "sensors", "--tag",
"site", "STRING", "--field", "temp",
"FLOAT", "--field", "humidity", "FLOAT",
"--encoding", "FLOAT", "GORILLA", "--compression",
"FLOAT", "UNCOMPRESSED", "-i", csv,
"-o", out_path},
wout, werr);
EXPECT_EQ(wc, 0) << werr.str();
std::ostringstream schema_out;
std::ostringstream schema_err;
EXPECT_EQ(
tsfile_cli::run_cli({"schema", "-t", "sensors", "-f", "csv", out_path},
schema_out, schema_err),
0)
<< schema_err.str();
EXPECT_NE(schema_out.str().find(
"table,sensors,temp,FIELD,FLOAT,GORILLA,UNCOMPRESSED\n"),
std::string::npos)
<< schema_out.str();
EXPECT_NE(schema_out.str().find(
"table,sensors,humidity,FIELD,FLOAT,GORILLA,UNCOMPRESSED\n"),
std::string::npos)
<< schema_out.str();
std::ostringstream dup_out;
std::ostringstream dup_err;
EXPECT_EQ(tsfile_cli::run_cli(
{"write", "--table", "sensors", "--field", "temp", "FLOAT",
"--encoding", "FLOAT", "GORILLA", "--encoding", "FLOAT",
"PLAIN", "--stdin", "-o", "unused.tsfile"},
dup_out, dup_err),
1);
EXPECT_NE(dup_err.str().find("specified more than once"), std::string::npos)
<< dup_err.str();
std::ostringstream unused_out;
std::ostringstream unused_err;
EXPECT_EQ(tsfile_cli::run_cli({"write", "--table", "sensors", "--field",
"temp", "FLOAT", "--encoding", "DOUBLE",
"GORILLA", "--stdin", "-o", "unused.tsfile"},
unused_out, unused_err),
1);
EXPECT_NE(unused_err.str().find("not used"), std::string::npos)
<< unused_err.str();
std::ostringstream bad_out;
std::ostringstream bad_err;
EXPECT_EQ(tsfile_cli::run_cli({"write", "--table", "binary_data", "--field",
"payload", "BLOB", "--encoding", "BLOB",
"GORILLA", "--stdin", "-o", "unused.tsfile"},
bad_out, bad_err),
1);
EXPECT_NE(bad_err.str().find("not supported"), std::string::npos)
<< bad_err.str();
std::remove(csv.c_str());
std::remove(out_path.c_str());
}
TEST(CliRequirements, WriteRejectsExistingOutputWithoutTruncating) {
std::string csv =
tsfile_cli_test::unique_temp_path("tsfile_cli_existing_in", ".csv");
{
std::ofstream o(csv.c_str());
o << "time,s1\n0,10\n";
}
std::string out_path =
tsfile_cli_test::unique_temp_path("tsfile_cli_existing_out", ".tsfile");
{
std::ofstream o(out_path.c_str());
o << "keep-me";
}
std::ostringstream out;
std::ostringstream err;
int code = tsfile_cli::run_cli({"write", "--table", "t1", "--field", "s1",
"INT64", "-i", csv, "-o", out_path},
out, err);
EXPECT_EQ(code, 3);
EXPECT_EQ(read_file(out_path), "keep-me");
std::remove(csv.c_str());
std::remove(out_path.c_str());
}
TEST(CliRequirements, WriteRejectsNonRegularInputBeforeCreatingOutput) {
#ifndef _WIN32
std::string dir =
tsfile_cli_test::unique_temp_path("tsfile_cli_input_dir", "");
ASSERT_EQ(mkdir(dir.c_str(), 0777), 0);
std::string out_path = tsfile_cli_test::unique_temp_path(
"tsfile_cli_input_dir_out", ".tsfile");
std::ostringstream out;
std::ostringstream err;
int code = tsfile_cli::run_cli({"write", "--table", "t1", "--field", "s1",
"INT64", "-i", dir, "-o", out_path},
out, err);
EXPECT_EQ(code, 2);
EXPECT_FALSE(file_exists(out_path));
rmdir(dir.c_str());
#endif
}
TEST(CliRequirements, WriteRejectsNonCanonicalTimeLexemesAsInputErrors) {
const char* bad_times[] = {"+1", "01", "-0"};
for (const char* bad_time : bad_times) {
std::string csv =
tsfile_cli_test::unique_temp_path("tsfile_cli_bad_time", ".csv");
{
std::ofstream o(csv.c_str());
o << "time,s1\n" << bad_time << ",10\n";
}
std::string out_path = tsfile_cli_test::unique_temp_path(
"tsfile_cli_bad_time_out", ".tsfile");
std::ostringstream out;
std::ostringstream err;
int code =
tsfile_cli::run_cli({"write", "--table", "t1", "--field", "s1",
"INT64", "-i", csv, "-o", out_path},
out, err);
EXPECT_EQ(code, 2) << bad_time << " " << err.str();
EXPECT_FALSE(file_exists(out_path)) << bad_time;
std::remove(csv.c_str());
std::remove(out_path.c_str());
}
}
TEST(CliRequirements, WriteTargetFailuresAreRuntimeErrors) {
std::string csv =
tsfile_cli_test::unique_temp_path("tsfile_cli_target", ".csv");
{
std::ofstream o(csv.c_str(), std::ios::binary);
o << "time,s1\n0,1\n";
}
std::ostringstream same_out;
std::ostringstream same_err;
EXPECT_EQ(tsfile_cli::run_cli({"write", "--table", "t1", "--field", "s1",
"INT64", "-i", csv, "-o", csv},
same_out, same_err),
3);
EXPECT_TRUE(same_out.str().empty());
EXPECT_NE(same_err.str().find("same as the input"), std::string::npos)
<< same_err.str();
EXPECT_EQ(read_file(csv), "time,s1\n0,1\n");
std::string parent =
tsfile_cli_test::unique_temp_path("tsfile_cli_missing_parent", "");
std::string child = parent + "/out.tsfile";
std::ostringstream parent_out;
std::ostringstream parent_err;
EXPECT_EQ(tsfile_cli::run_cli({"write", "--table", "t1", "--field", "s1",
"INT64", "-i", csv, "-o", child},
parent_out, parent_err),
3);
EXPECT_TRUE(parent_out.str().empty());
EXPECT_NE(parent_err.str().find("cannot create output"), std::string::npos)
<< parent_err.str();
EXPECT_FALSE(file_exists(child));
std::remove(csv.c_str());
}
TEST(CliRequirements, WriteCsvNullAndEmptyStringRemainDistinctTags) {
std::string csv =
tsfile_cli_test::unique_temp_path("tsfile_cli_tag_null", ".csv");
{
std::ofstream o(csv.c_str());
o << "time,site,temp\n1000,\\N,21.0\n1000,\"\",22.0\n";
}
std::string out_path =
tsfile_cli_test::unique_temp_path("tsfile_cli_tag_null_out", ".tsfile");
std::ostringstream out;
std::ostringstream err;
ASSERT_EQ(tsfile_cli::run_cli(
{"write", "--table", "sensors", "--tag", "site", "STRING",
"--field", "temp", "FLOAT", "-i", csv, "-o", out_path},
out, err),
0)
<< err.str();
std::ostringstream cat_out;
std::ostringstream cat_err;
ASSERT_EQ(
tsfile_cli::run_cli({"cat", "-t", "sensors", "-f", "ndjson", out_path},
cat_out, cat_err),
0)
<< cat_err.str();
EXPECT_NE(cat_out.str().find("\"site\":null"), std::string::npos)
<< cat_out.str();
EXPECT_NE(cat_out.str().find("\"site\":\"\""), std::string::npos)
<< cat_out.str();
EXPECT_NE(cat_out.str().find("\"temp\":21"), std::string::npos)
<< cat_out.str();
EXPECT_NE(cat_out.str().find("\"temp\":22"), std::string::npos)
<< cat_out.str();
std::ostringstream count_out;
std::ostringstream count_err;
ASSERT_EQ(tsfile_cli::run_cli({"count", "-t", "sensors", "-m", "site", "-f",
"csv", out_path},
count_out, count_err),
0)
<< count_err.str();
EXPECT_NE(count_out.str().find("table,sensors,site,TAG,2,2"),
std::string::npos)
<< count_out.str();
std::remove(csv.c_str());
std::remove(out_path.c_str());
}
TEST(CliRequirements, WriteRejectsUnterminatedQuotedCsvField) {
std::string csv =
tsfile_cli_test::unique_temp_path("tsfile_cli_unclosed_quote", ".csv");
{
std::ofstream o(csv.c_str());
o << "time,site,temp\n1000,\"beijing,21.0\n";
}
std::string out_path = tsfile_cli_test::unique_temp_path(
"tsfile_cli_unclosed_quote_out", ".tsfile");
std::ostringstream out;
std::ostringstream err;
EXPECT_EQ(tsfile_cli::run_cli(
{"write", "--table", "sensors", "--tag", "site", "STRING",
"--field", "temp", "FLOAT", "-i", csv, "-o", out_path},
out, err),
2);
EXPECT_TRUE(out.str().empty());
EXPECT_NE(err.str().find("unterminated quoted CSV field"),
std::string::npos)
<< err.str();
EXPECT_FALSE(file_exists(out_path));
std::remove(csv.c_str());
std::remove(out_path.c_str());
}
TEST(CliRequirements, LegacyColumnsOptionIsRejected) {
std::ostringstream out;
std::ostringstream err;
int code = tsfile_cli::run_cli({"write", "--table", "t1", "--columns",
"legacy", "-o", "x.tsfile", "--stdin"},
out, err);
EXPECT_EQ(code, 1);
EXPECT_TRUE(out.str().empty());
EXPECT_NE(err.str().find("Unknown flag: --columns"), std::string::npos)
<< err.str();
}
TEST(CliRequirements, WriteRejectsImplicitInputAndFormatFlag) {
std::ostringstream implicit_out;
std::ostringstream implicit_err;
EXPECT_EQ(tsfile_cli::run_cli({"write", "--table", "t1", "--field", "s1",
"INT64", "-o", "x.tsfile", "in.csv"},
implicit_out, implicit_err),
1);
EXPECT_NE(
implicit_err.str().find("choose exactly one of --input or --stdin"),
std::string::npos)
<< implicit_err.str();
std::ostringstream format_out;
std::ostringstream format_err;
EXPECT_EQ(
tsfile_cli::run_cli({"write", "--table", "t1", "--field", "s1", "INT64",
"-f", "csv", "--stdin", "-o", "x.tsfile"},
format_out, format_err),
1);
EXPECT_NE(format_err.str().find("--format is not valid"), std::string::npos)
<< format_err.str();
}
TEST(CliRequirements, ExportWritesSingleObjectAtomically) {
TableFixture f;
std::string out_path =
tsfile_cli_test::unique_temp_path("tsfile_cli_export", ".csv");
std::ostringstream out;
std::ostringstream err;
int code = tsfile_cli::run_cli({"export", "-t", "table1", "-o", out_path,
"--type", "csv", "-m", "s1", f.path},
out, err);
EXPECT_EQ(code, 0) << err.str();
EXPECT_TRUE(out.str().empty());
std::ostringstream cat_out;
std::ostringstream cat_err;
EXPECT_EQ(tsfile_cli::run_cli(
{"cat", "-t", "table1", "-m", "s1", "-f", "csv", f.path},
cat_out, cat_err),
0)
<< cat_err.str();
EXPECT_EQ(read_file(out_path), cat_out.str());
std::remove(out_path.c_str());
}
#ifndef _WIN32
TEST(CliRequirements, ExportForceRejectsSymlinkAndSpecialTargets) {
TableFixture f;
std::string sentinel =
tsfile_cli_test::unique_temp_path("tsfile_cli_export_sentinel", ".txt");
std::string symlink_path =
tsfile_cli_test::unique_temp_path("tsfile_cli_export_link", ".csv");
{
std::ofstream file(sentinel.c_str());
file << "sentinel";
}
ASSERT_EQ(symlink(sentinel.c_str(), symlink_path.c_str()), 0);
std::ostringstream link_out;
std::ostringstream link_err;
EXPECT_EQ(tsfile_cli::run_cli({"export", "-t", "table1", "-o", symlink_path,
"--type", "csv", "--force", f.path},
link_out, link_err),
3);
EXPECT_EQ(read_file(sentinel), "sentinel");
std::string fifo_path =
tsfile_cli_test::unique_temp_path("tsfile_cli_export_fifo", ".csv");
ASSERT_EQ(mkfifo(fifo_path.c_str(), 0600), 0);
std::ostringstream fifo_out;
std::ostringstream fifo_err;
EXPECT_EQ(tsfile_cli::run_cli({"export", "-t", "table1", "-o", fifo_path,
"--type", "csv", "--force", f.path},
fifo_out, fifo_err),
3);
std::remove(symlink_path.c_str());
std::remove(sentinel.c_str());
std::remove(fifo_path.c_str());
}
#endif
TEST(CliRequirements, ExportWritesMultiObjectManifestAndNumberedFiles) {
MultiTableFixture f;
std::string dir =
tsfile_cli_test::unique_temp_path("tsfile_cli_multi_export", "");
std::ostringstream out;
std::ostringstream err;
int code = tsfile_cli::run_cli(
{"export", "-t", "sensors_a", "-t", "sensors_b", "--output-dir", dir,
"--type", "csv", "-m", "s1", f.path},
out, err);
EXPECT_EQ(code, 0) << err.str();
EXPECT_TRUE(out.str().empty());
EXPECT_EQ(read_file(dir + "/0001.csv"),
"time,id1,s1\n0,sensors_a_tag,10\n");
EXPECT_EQ(read_file(dir + "/0002.csv"),
"time,id1,s1\n0,sensors_b_tag,20\n");
std::string manifest = read_file(dir + "/_manifest.json");
EXPECT_NE(manifest.find("\"complete\": true"), std::string::npos)
<< manifest;
EXPECT_NE(manifest.find("\"file\":\"0001.csv\""), std::string::npos)
<< manifest;
EXPECT_NE(manifest.find("\"object\":\"sensors_b\""), std::string::npos)
<< manifest;
EXPECT_NE(manifest.find("\"rows\":\"1\""), std::string::npos) << manifest;
std::remove((dir + "/0001.csv").c_str());
std::remove((dir + "/0002.csv").c_str());
std::remove((dir + "/_manifest.json").c_str());
#ifndef _WIN32
rmdir(dir.c_str());
#endif
}
TEST(CliRequirements, MultipleTagFiltersRequireAndHonorTagMatch) {
std::string path = tsfile_cli_test::write_tag_filter_fixture();
std::ostringstream missing_out;
std::ostringstream missing_err;
EXPECT_EQ(tsfile_cli::run_cli(
{"cat", "-m", "s1", "--tag-filter", "id1", "eq", "dev_a",
"--tag-filter", "id1", "eq", "dev_c", "-f", "csv", path},
missing_out, missing_err),
1);
std::ostringstream out;
std::ostringstream err;
EXPECT_EQ(
tsfile_cli::run_cli({"cat", "-m", "s1", "--tag-filter", "id1", "eq",
"dev_a", "--tag-filter", "id1", "eq", "dev_c",
"--tag-match", "any", "-f", "csv", path},
out, err),
0)
<< err.str();
EXPECT_EQ(out.str(), "time,id1,s1\n0,dev_a,10\n3,dev_c,40\n");
std::remove(path.c_str());
}
TEST(CliRequirements, SketchRejectsRegularResultFormat) {
TableFixture f;
std::ostringstream out;
std::ostringstream err;
int code = tsfile_cli::run_cli({"sketch", "-f", "csv", f.path}, out, err);
EXPECT_EQ(code, 1);
EXPECT_TRUE(out.str().empty());
EXPECT_NE(err.str().find("sketch does not accept --format"),
std::string::npos)
<< err.str();
}
TEST(CliRequirements, SketchWritesStdoutAndAtomicOutput) {
TableFixture f;
std::ostringstream stdout_out;
std::ostringstream stdout_err;
ASSERT_EQ(tsfile_cli::run_cli({"sketch", f.path}, stdout_out, stdout_err),
0)
<< stdout_err.str();
EXPECT_NE(stdout_out.str().find("TsFile Sketch"), std::string::npos)
<< stdout_out.str();
EXPECT_NE(stdout_out.str().find("model: table"), std::string::npos)
<< stdout_out.str();
std::string output =
tsfile_cli_test::unique_temp_path("tsfile_cli_sketch", ".txt");
std::ostringstream file_out;
std::ostringstream file_err;
ASSERT_EQ(tsfile_cli::run_cli({"sketch", "-o", output, f.path}, file_out,
file_err),
0)
<< file_err.str();
EXPECT_TRUE(file_out.str().empty());
EXPECT_EQ(read_file(output), stdout_out.str());
const std::string sentinel = "do not replace";
{
std::ofstream existing(output.c_str(), std::ios::binary);
existing << sentinel;
}
std::ostringstream no_force_out;
std::ostringstream no_force_err;
EXPECT_EQ(tsfile_cli::run_cli({"sketch", "-o", output, f.path},
no_force_out, no_force_err),
3);
EXPECT_EQ(read_file(output), sentinel);
std::ostringstream force_out;
std::ostringstream force_err;
ASSERT_EQ(tsfile_cli::run_cli({"sketch", "-o", output, "--force", f.path},
force_out, force_err),
0)
<< force_err.str();
EXPECT_EQ(read_file(output), stdout_out.str());
const std::string source_before = read_file(f.path);
std::ostringstream alias_out;
std::ostringstream alias_err;
EXPECT_EQ(tsfile_cli::run_cli({"sketch", "-o", f.path, "--force", f.path},
alias_out, alias_err),
3);
EXPECT_EQ(read_file(f.path), source_before);
std::remove(output.c_str());
}
TEST(CliRequirements, ModelDetectionPrefersTableSchemaAndFallsBackToTree) {
TableFixture table;
storage::TsFileReader table_reader;
ASSERT_EQ(table_reader.open(table.path), common::E_OK);
EXPECT_FALSE(table_reader.get_all_table_schemas().empty());
EXPECT_FALSE(table_reader.get_all_device_ids().empty());
table_reader.close();
std::ostringstream table_out;
std::ostringstream table_err;
ASSERT_EQ(tsfile_cli::run_cli({"ls", "-f", "csv", table.path}, table_out,
table_err),
0)
<< table_err.str();
EXPECT_EQ(table_out.str(), "model,object\ntable,table1\n");
std::string tree_path = tsfile_cli_test::write_sparse_tree_fixture();
storage::TsFileReader tree_reader;
ASSERT_EQ(tree_reader.open(tree_path), common::E_OK);
EXPECT_TRUE(tree_reader.get_all_table_schemas().empty());
EXPECT_FALSE(tree_reader.get_all_device_ids().empty());
tree_reader.close();
std::ostringstream tree_out;
std::ostringstream tree_err;
ASSERT_EQ(
tsfile_cli::run_cli({"ls", "-f", "csv", tree_path}, tree_out, tree_err),
0)
<< tree_err.str();
EXPECT_EQ(tree_out.str(), "model,object\ntree,root.test.d1\n");
std::remove(tree_path.c_str());
}