blob: 0830009918bb544418b688de226f6de5bde7da7d [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>
#include "cli/run_cli.h"
#include "cli_test_util.h"
namespace {
struct Fixture {
std::string path = tsfile_cli_test::write_table_fixture();
~Fixture() { std::remove(path.c_str()); }
};
struct TagFilterFixture {
std::string path = tsfile_cli_test::write_tag_filter_fixture();
~TagFilterFixture() { std::remove(path.c_str()); }
};
struct MultiTableFixture {
std::string path = tsfile_cli_test::write_multi_table_fixture();
~MultiTableFixture() { std::remove(path.c_str()); }
};
struct DisjointTableFixture {
std::string path = tsfile_cli_test::write_disjoint_table_fixture();
~DisjointTableFixture() { std::remove(path.c_str()); }
};
struct SparseTreeFixture {
std::string path = tsfile_cli_test::write_sparse_tree_fixture();
~SparseTreeFixture() { std::remove(path.c_str()); }
};
struct DisjointTreeFixture {
std::string path = tsfile_cli_test::write_disjoint_tree_fixture();
~DisjointTreeFixture() { std::remove(path.c_str()); }
};
class FlushFailingStreamBuf : public std::stringbuf {
protected:
int sync() override { return -1; }
};
size_t count_lines(const std::string& s) {
size_t n = 0;
for (char c : s) {
if (c == '\n') {
++n;
}
}
return n;
}
} // namespace
TEST(CliE2E, LsListsTableNameTsv) {
Fixture f;
std::ostringstream out;
std::ostringstream err;
int code = tsfile_cli::run_cli({"ls", "-f", "csv", f.path}, out, err);
EXPECT_EQ(code, 0);
EXPECT_EQ(out.str(), "model,object\ntable,table1\n");
EXPECT_TRUE(err.str().empty());
}
TEST(CliE2E, LsRejectsNoHeader) {
Fixture f;
std::ostringstream out;
std::ostringstream err;
int code = tsfile_cli::run_cli({"ls", "-f", "csv", "--no-header", f.path},
out, err);
EXPECT_EQ(code, 1);
EXPECT_TRUE(out.str().empty());
EXPECT_NE(err.str().find("--no-header"), std::string::npos) << err.str();
}
TEST(CliE2E, OpenMissingFileReturnsFileError) {
std::ostringstream out;
std::ostringstream err;
int code =
tsfile_cli::run_cli({"ls", "definitely_missing.tsfile"}, out, err);
EXPECT_EQ(code, 2);
EXPECT_FALSE(err.str().empty());
}
TEST(CliE2E, SchemaShowsFieldColumnAndType) {
Fixture f;
std::ostringstream out;
std::ostringstream err;
int code = tsfile_cli::run_cli({"schema", "-f", "csv", f.path}, out, err);
EXPECT_EQ(code, 0);
EXPECT_NE(out.str().find("model,object,column,category,data_type,encoding,"
"compression"),
std::string::npos);
EXPECT_NE(out.str().find("s1"), std::string::npos);
EXPECT_NE(out.str().find("INT64"), std::string::npos);
}
TEST(CliE2E, SchemaTableMeasurementFilterOnlyShowsRequestedColumn) {
Fixture f;
std::ostringstream out;
std::ostringstream err;
int code = tsfile_cli::run_cli({"schema", "-m", "s1", "-f", "csv", f.path},
out, err);
EXPECT_EQ(code, 0);
EXPECT_NE(out.str().find("table,table1,s1,FIELD,INT64"), std::string::npos);
EXPECT_EQ(out.str().find("table1,id1"), std::string::npos);
EXPECT_EQ(out.str().find("table1,id2"), std::string::npos);
}
TEST(CliE2E, SchemaTableMeasurementFilterIsCaseInsensitive) {
Fixture f;
std::ostringstream out;
std::ostringstream err;
int code = tsfile_cli::run_cli({"schema", "-m", "S1", "-f", "csv", f.path},
out, err);
EXPECT_EQ(code, 0) << err.str();
EXPECT_NE(out.str().find("table,table1,s1,FIELD,INT64"), std::string::npos)
<< out.str();
}
TEST(CliE2E, SchemaRejectsMissingTableBeforeWritingOutput) {
Fixture f;
std::ostringstream out;
std::ostringstream err;
int code = tsfile_cli::run_cli(
{"schema", "-t", "missing", "-f", "csv", f.path}, out, err);
EXPECT_EQ(code, 1);
EXPECT_TRUE(out.str().empty());
EXPECT_NE(err.str().find("does not exist"), std::string::npos) << err.str();
}
TEST(CliE2E, StatsReportsCountAndTimeRange) {
Fixture f;
std::ostringstream out;
std::ostringstream err;
int code = tsfile_cli::run_cli({"stats", "-f", "csv", f.path}, out, err);
EXPECT_EQ(code, 0);
EXPECT_NE(out.str().find("model,object,tag.id1,tag.id2,field,data_type,"
"non_null_count,null_count,min_time,max_time,min,"
"max,first,last,sum,stats_source"),
std::string::npos)
<< out.str();
EXPECT_NE(out.str().find("table,table1,id1_field_1,id2_field_2,s1,INT64,"
"5,\\N,0,4,0,40,0,40,\\N,statistics"),
std::string::npos)
<< out.str();
}
TEST(CliE2E, HeadProjectsAndLimits) {
Fixture f;
std::ostringstream out;
std::ostringstream err;
int code = tsfile_cli::run_cli(
{"head", "-m", "s1", "-n", "2", "-f", "csv", f.path}, out, err);
EXPECT_EQ(code, 0);
EXPECT_EQ(out.str(),
"time,id1,id2,s1\n0,id1_field_1,id2_field_2,0\n1,id1_field_1,"
"id2_field_2,10\n");
}
TEST(CliE2E, CatReturnsAllRows) {
Fixture f;
std::ostringstream out;
std::ostringstream err;
int code =
tsfile_cli::run_cli({"cat", "-m", "s1", "-f", "csv", f.path}, out, err);
EXPECT_EQ(code, 0);
EXPECT_EQ(count_lines(out.str()), 6u);
EXPECT_NE(out.str().find("time,id1,id2,s1\n"), std::string::npos);
}
TEST(CliE2E, CatRejectsMissingOrNonFieldProjectionBeforeQuery) {
Fixture f;
for (const char* measurement : {"missing", "id1"}) {
std::ostringstream out;
std::ostringstream err;
int code = tsfile_cli::run_cli(
{"cat", "-m", measurement, "-f", "csv", f.path}, out, err);
EXPECT_EQ(code, 1) << measurement << " " << err.str();
EXPECT_TRUE(out.str().empty()) << measurement;
EXPECT_NE(err.str().find("FIELD"), std::string::npos)
<< measurement << " " << err.str();
}
}
TEST(CliE2E, CatTableProjectionIsCaseInsensitive) {
Fixture f;
std::ostringstream out;
std::ostringstream err;
int code =
tsfile_cli::run_cli({"cat", "-m", "S1", "-f", "csv", f.path}, out, err);
EXPECT_EQ(code, 0) << err.str();
EXPECT_EQ(out.str(),
"time,id1,id2,s1\n0,id1_field_1,id2_field_2,0\n1,id1_field_1,"
"id2_field_2,10\n2,id1_field_1,id2_field_2,20\n3,id1_field_1,"
"id2_field_2,30\n4,id1_field_1,id2_field_2,40\n");
}
TEST(CliE2E, CatPushesDownOffsetAndLimit) {
Fixture f;
std::ostringstream out;
std::ostringstream err;
int code = tsfile_cli::run_cli(
{"cat", "-m", "s1", "--offset", "2", "-n", "2", "-f", "csv", f.path},
out, err);
EXPECT_EQ(code, 0);
EXPECT_EQ(out.str(),
"time,id1,id2,s1\n2,id1_field_1,id2_field_2,20\n3,id1_field_1,"
"id2_field_2,30\n");
}
TEST(CliE2E, HeadPushesDownOffsetAndLimit) {
Fixture f;
std::ostringstream out;
std::ostringstream err;
int code = tsfile_cli::run_cli(
{"head", "-m", "s1", "--offset", "1", "-n", "3", "-f", "csv", f.path},
out, err);
EXPECT_EQ(code, 0);
EXPECT_EQ(out.str(),
"time,id1,id2,s1\n1,id1_field_1,id2_field_2,10\n2,id1_field_1,"
"id2_field_2,20\n3,id1_field_1,id2_field_2,30\n");
}
TEST(CliE2E, CatWithTimeRange) {
Fixture f;
std::ostringstream out;
std::ostringstream err;
int code = tsfile_cli::run_cli(
{"cat", "-m", "s1", "--start", "2", "--end", "3", "-f", "csv", f.path},
out, err);
EXPECT_EQ(code, 0);
EXPECT_EQ(out.str(),
"time,id1,id2,s1\n2,id1_field_1,id2_field_2,20\n3,id1_field_1,"
"id2_field_2,30\n");
}
TEST(CliE2E, CatAppliesOffsetAfterTimeRange) {
Fixture f;
std::ostringstream out;
std::ostringstream err;
int code =
tsfile_cli::run_cli({"cat", "-m", "s1", "--start", "1", "--end", "4",
"--offset", "1", "-n", "2", "-f", "csv", f.path},
out, err);
EXPECT_EQ(code, 0);
EXPECT_EQ(out.str(),
"time,id1,id2,s1\n2,id1_field_1,id2_field_2,20\n3,id1_field_1,"
"id2_field_2,30\n");
}
TEST(CliE2E, CatFiltersRowsByTagEq) {
TagFilterFixture f;
std::ostringstream out;
std::ostringstream err;
int code = tsfile_cli::run_cli({"cat", "-m", "s1", "--tag-filter", "id1",
"eq", "dev_b", "-f", "csv", f.path},
out, err);
EXPECT_EQ(code, 0) << err.str();
EXPECT_EQ(out.str(), "time,id1,s1\n1,dev_b,20\n2,dev_b,30\n");
}
TEST(CliE2E, HeadFiltersRowsByTagRegexp) {
TagFilterFixture f;
std::ostringstream out;
std::ostringstream err;
int code = tsfile_cli::run_cli(
{"head", "-m", "s1", "--tag-filter", "id1", "regexp", "dev_[bc]", "-n",
"10", "-f", "csv", f.path},
out, err);
EXPECT_EQ(code, 0) << err.str();
EXPECT_EQ(out.str(), "time,id1,s1\n1,dev_b,20\n2,dev_b,30\n3,dev_c,40\n");
}
TEST(CliE2E, TagFilterRejectsFieldColumn) {
TagFilterFixture f;
std::ostringstream out;
std::ostringstream err;
int code = tsfile_cli::run_cli({"cat", "-m", "s1", "--tag-filter", "s1",
"eq", "20", "-f", "csv", f.path},
out, err);
EXPECT_EQ(code, 1);
EXPECT_NE(err.str().find("invalid tag filter column"), std::string::npos)
<< err.str();
}
TEST(CliE2E, TagFiltersDistinguishNullEmptyAndLiteralNull) {
std::string path = tsfile_cli_test::write_nullable_tag_filter_fixture();
struct Case {
std::vector<std::string> filter;
std::string expected_row;
};
const std::vector<Case> cases = {
{{"id1", "is-null"}, "0,\\N,10\n"},
{{"id1", "eq", ""}, "1,\"\",20\n"},
{{"id1", "eq", "null"}, "2,null,30\n"},
};
for (const Case& test_case : cases) {
std::vector<std::string> args = {"cat", "-m", "s1", "--tag-filter"};
args.insert(args.end(), test_case.filter.begin(),
test_case.filter.end());
args.insert(args.end(), {"-f", "csv", path});
std::ostringstream out;
std::ostringstream err;
ASSERT_EQ(tsfile_cli::run_cli(args, out, err), 0) << err.str();
EXPECT_EQ(out.str(), "time,id1,s1\n" + test_case.expected_row);
}
std::ostringstream not_null_out;
std::ostringstream not_null_err;
ASSERT_EQ(tsfile_cli::run_cli({"cat", "-m", "s1", "--tag-filter", "id1",
"not-null", "-f", "csv", path},
not_null_out, not_null_err),
0)
<< not_null_err.str();
EXPECT_EQ(not_null_out.str().find("0,\\N,10"), std::string::npos)
<< not_null_out.str();
EXPECT_NE(not_null_out.str().find("1,\"\",20"), std::string::npos)
<< not_null_out.str();
std::ostringstream neq_out;
std::ostringstream neq_err;
ASSERT_EQ(tsfile_cli::run_cli({"cat", "-m", "s1", "--tag-filter", "id1",
"neq", "dev_a", "-f", "csv", path},
neq_out, neq_err),
0)
<< neq_err.str();
EXPECT_EQ(neq_out.str().find("0,\\N,10"), std::string::npos)
<< neq_out.str();
EXPECT_EQ(neq_out.str().find("3,dev_a,40"), std::string::npos)
<< neq_out.str();
EXPECT_NE(neq_out.str().find("4,dev_b,50"), std::string::npos)
<< neq_out.str();
std::remove(path.c_str());
}
TEST(CliE2E, TagRegexpUsesFullValueAndRejectsInvalidPatterns) {
std::string path = tsfile_cli_test::write_nullable_tag_filter_fixture();
std::ostringstream substring_out;
std::ostringstream substring_err;
ASSERT_EQ(tsfile_cli::run_cli({"cat", "-m", "s1", "--tag-filter", "id1",
"regexp", "dev", "-f", "csv", path},
substring_out, substring_err),
0)
<< substring_err.str();
EXPECT_EQ(substring_out.str(), "time,id1,s1\n");
std::ostringstream full_out;
std::ostringstream full_err;
ASSERT_EQ(tsfile_cli::run_cli({"cat", "-m", "s1", "--tag-filter", "id1",
"regexp", "dev_.*", "-f", "csv", path},
full_out, full_err),
0)
<< full_err.str();
EXPECT_NE(full_out.str().find("3,dev_a,40"), std::string::npos)
<< full_out.str();
EXPECT_NE(full_out.str().find("4,dev_b,50"), std::string::npos)
<< full_out.str();
std::ostringstream invalid_out;
std::ostringstream invalid_err;
EXPECT_EQ(tsfile_cli::run_cli({"cat", "-m", "s1", "--tag-filter", "id1",
"regexp", "[", "-f", "csv", path},
invalid_out, invalid_err),
1);
EXPECT_TRUE(invalid_out.str().empty());
EXPECT_NE(invalid_err.str().find("invalid regular expression"),
std::string::npos)
<< invalid_err.str();
std::remove(path.c_str());
}
TEST(CliE2E, MultipleTagFiltersHonorAll) {
std::string path = tsfile_cli_test::write_nullable_tag_filter_fixture();
std::ostringstream out;
std::ostringstream err;
ASSERT_EQ(
tsfile_cli::run_cli({"cat", "-m", "s1", "--tag-filter", "id1", "neq",
"dev_a", "--tag-filter", "id1", "regexp", "dev_.*",
"--tag-match", "all", "-f", "csv", path},
out, err),
0)
<< err.str();
EXPECT_EQ(out.str(), "time,id1,s1\n4,dev_b,50\n");
std::remove(path.c_str());
}
TEST(CliE2E, RowWindowDistinguishesExactAndExcessiveOffset) {
Fixture f;
std::ostringstream exact_out;
std::ostringstream exact_err;
EXPECT_EQ(tsfile_cli::run_cli(
{"cat", "-m", "s1", "--offset", "5", "-f", "csv", f.path},
exact_out, exact_err),
0)
<< exact_err.str();
EXPECT_EQ(exact_out.str(), "time,id1,id2,s1\n");
std::ostringstream excessive_out;
std::ostringstream excessive_err;
EXPECT_EQ(tsfile_cli::run_cli(
{"cat", "-m", "s1", "--offset", "6", "-f", "csv", f.path},
excessive_out, excessive_err),
1);
EXPECT_TRUE(excessive_out.str().empty());
EXPECT_NE(excessive_err.str().find("offset exceeds matched row count"),
std::string::npos)
<< excessive_err.str();
}
TEST(CliE2E, ZeroLimitUsesEachFormatsZeroRowContract) {
Fixture f;
std::ostringstream csv_out;
std::ostringstream csv_err;
EXPECT_EQ(
tsfile_cli::run_cli({"cat", "-m", "s1", "-n", "0", "-f", "csv", f.path},
csv_out, csv_err),
0)
<< csv_err.str();
EXPECT_EQ(csv_out.str(), "time,id1,id2,s1\n");
std::ostringstream ndjson_out;
std::ostringstream ndjson_err;
EXPECT_EQ(tsfile_cli::run_cli(
{"cat", "-m", "s1", "-n", "0", "-f", "ndjson", f.path},
ndjson_out, ndjson_err),
0)
<< ndjson_err.str();
EXPECT_TRUE(ndjson_out.str().empty());
std::ostringstream table_out;
std::ostringstream table_err;
EXPECT_EQ(tsfile_cli::run_cli(
{"cat", "-m", "s1", "-n", "0", "-f", "table", f.path},
table_out, table_err),
0)
<< table_err.str();
EXPECT_NE(table_out.str().find("time"), std::string::npos)
<< table_out.str();
EXPECT_NE(table_out.str().find("s1"), std::string::npos) << table_out.str();
EXPECT_EQ(table_out.str().find("id1_field_1"), std::string::npos)
<< table_out.str();
}
TEST(CliE2E, CatJsonIsNdjson) {
Fixture f;
std::ostringstream out;
std::ostringstream err;
int code = tsfile_cli::run_cli({"cat", "-m", "s1", "--start", "0", "--end",
"0", "-f", "ndjson", f.path},
out, err);
EXPECT_EQ(code, 0);
EXPECT_EQ(out.str(),
"{\"time\":\"0\",\"id1\":\"id1_field_1\",\"id2\":"
"\"id2_field_2\",\"s1\":\"0\"}\n");
}
TEST(CliE2E, StdoutFlushFailureReturnsRuntimeError) {
Fixture f;
FlushFailingStreamBuf buffer;
std::ostream out(&buffer);
std::ostringstream err;
EXPECT_EQ(tsfile_cli::run_cli({"cat", "-f", "csv", f.path}, out, err), 3);
EXPECT_NE(err.str().find("failed to read rows"), std::string::npos)
<< err.str();
}
TEST(CliE2E, MetaReportsFileSummary) {
Fixture f;
std::ostringstream out;
std::ostringstream err;
int code = tsfile_cli::run_cli({"meta", "-f", "csv", f.path}, out, err);
EXPECT_EQ(code, 0);
EXPECT_TRUE(err.str().empty());
EXPECT_NE(out.str().find("size_bytes,format_version,model\n"),
std::string::npos)
<< out.str();
EXPECT_NE(out.str().find(",4,table\n"), std::string::npos) << out.str();
}
TEST(CliE2E, MetaReportsFileHeaderVersionWithoutConstrainingReader) {
SparseTreeFixture f;
std::fstream file(f.path.c_str(),
std::ios::in | std::ios::out | std::ios::binary);
ASSERT_TRUE(file.good());
file.seekp(storage::MAGIC_STRING_TSFILE_LEN);
file.put(static_cast<char>(3));
file.close();
std::ostringstream out;
std::ostringstream err;
EXPECT_EQ(tsfile_cli::run_cli({"meta", "-f", "csv", f.path}, out, err), 0)
<< err.str();
EXPECT_TRUE(err.str().empty());
EXPECT_NE(out.str().find(",3,tree\n"), std::string::npos) << out.str();
}
TEST(CliE2E, CountReportsColumnCountsWithoutSummaryRows) {
Fixture f;
std::ostringstream out;
std::ostringstream err;
int code = tsfile_cli::run_cli({"count", "-f", "csv", f.path}, out, err);
EXPECT_EQ(code, 0);
EXPECT_TRUE(err.str().empty());
EXPECT_NE(out.str().find("model,object,column,category,row_count,entity_"
"count,non_null_count,null_count,min_time,"
"max_time,time_source"),
std::string::npos)
<< out.str();
EXPECT_NE(out.str().find("table,table1,id1,TAG,5,1,5,0,0,4,scan"),
std::string::npos)
<< out.str();
EXPECT_NE(out.str().find("table,table1,s1,FIELD,5,1,5,0,0,4,scan"),
std::string::npos)
<< out.str();
EXPECT_EQ(out.str().find("total"), std::string::npos) << out.str();
}
TEST(CliE2E, CountAndStatsDefaultToAllTables) {
MultiTableFixture f;
std::ostringstream count_out;
std::ostringstream count_err;
EXPECT_EQ(tsfile_cli::run_cli({"count", "-f", "csv", f.path}, count_out,
count_err),
0)
<< count_err.str();
EXPECT_NE(count_out.str().find("table,sensors_a,s1,FIELD,1,1,1,0,0,0,scan"),
std::string::npos)
<< count_out.str();
EXPECT_NE(count_out.str().find("table,sensors_b,s1,FIELD,1,1,1,0,0,0,scan"),
std::string::npos)
<< count_out.str();
std::ostringstream stats_out;
std::ostringstream stats_err;
EXPECT_EQ(tsfile_cli::run_cli({"stats", "-f", "csv", f.path}, stats_out,
stats_err),
0)
<< stats_err.str();
EXPECT_NE(stats_out.str().find("table,sensors_a,sensors_a_tag,s1,INT64"),
std::string::npos)
<< stats_out.str();
EXPECT_NE(stats_out.str().find("table,sensors_b,sensors_b_tag,s1,INT64"),
std::string::npos)
<< stats_out.str();
}
TEST(CliE2E, CountAndStatsProjectionMayMatchOnlySomeTables) {
DisjointTableFixture f;
std::ostringstream count_out;
std::ostringstream count_err;
ASSERT_EQ(tsfile_cli::run_cli({"count", "-m", "S1", "-f", "csv", f.path},
count_out, count_err),
0)
<< count_err.str();
EXPECT_NE(count_out.str().find("table,sensors_a,s1,FIELD"),
std::string::npos)
<< count_out.str();
EXPECT_EQ(count_out.str().find("table,sensors_b"), std::string::npos)
<< count_out.str();
std::ostringstream stats_out;
std::ostringstream stats_err;
ASSERT_EQ(tsfile_cli::run_cli({"stats", "-m", "S1", "-f", "csv", f.path},
stats_out, stats_err),
0)
<< stats_err.str();
EXPECT_NE(stats_out.str().find("table,sensors_a,sensors_a_tag,s1,INT64"),
std::string::npos)
<< stats_out.str();
EXPECT_EQ(stats_out.str().find("table,sensors_b"), std::string::npos)
<< stats_out.str();
}
TEST(CliE2E, CountAndStatsProjectionMayMatchOnlySomeTreeDevices) {
DisjointTreeFixture f;
std::ostringstream count_out;
std::ostringstream count_err;
ASSERT_EQ(tsfile_cli::run_cli({"count", "-m", "left", "-f", "csv", f.path},
count_out, count_err),
0)
<< count_err.str();
EXPECT_NE(count_out.str().find("tree,root.test.d1,left,FIELD"),
std::string::npos)
<< count_out.str();
EXPECT_EQ(count_out.str().find("tree,root.test.d2"), std::string::npos)
<< count_out.str();
std::ostringstream stats_out;
std::ostringstream stats_err;
ASSERT_EQ(tsfile_cli::run_cli({"stats", "-m", "left", "-f", "csv", f.path},
stats_out, stats_err),
0)
<< stats_err.str();
EXPECT_NE(stats_out.str().find("tree,root.test.d1,left,INT32"),
std::string::npos)
<< stats_out.str();
EXPECT_EQ(stats_out.str().find("tree,root.test.d2"), std::string::npos)
<< stats_out.str();
}
TEST(CliE2E, TreeCountAndStatsUseDeviceTimestampUnion) {
SparseTreeFixture f;
std::ostringstream count_out;
std::ostringstream count_err;
ASSERT_EQ(tsfile_cli::run_cli({"count", "-f", "csv", f.path}, count_out,
count_err),
0)
<< count_err.str();
EXPECT_NE(count_out.str().find(
"tree,root.test.d1,left,FIELD,3,\\N,2,1,0,2,statistics"),
std::string::npos)
<< count_out.str();
EXPECT_NE(count_out.str().find(
"tree,root.test.d1,right,FIELD,3,\\N,2,1,1,2,statistics"),
std::string::npos)
<< count_out.str();
std::ostringstream stats_out;
std::ostringstream stats_err;
ASSERT_EQ(tsfile_cli::run_cli({"stats", "-f", "csv", f.path}, stats_out,
stats_err),
0)
<< stats_err.str();
EXPECT_NE(stats_out.str().find(
"tree,root.test.d1,left,INT32,2,1,0,2,10,20,10,20,30,"
"statistics"),
std::string::npos)
<< stats_out.str();
EXPECT_NE(stats_out.str().find(
"tree,root.test.d1,right,BOOLEAN,2,1,1,2,\\N,\\N,true,false,"
"1,statistics"),
std::string::npos)
<< stats_out.str();
}
TEST(CliE2E, MetadataTableFilterIsCaseInsensitive) {
Fixture f;
std::ostringstream schema_out;
std::ostringstream schema_err;
EXPECT_EQ(
tsfile_cli::run_cli({"schema", "-t", "TABLE1", "-f", "csv", f.path},
schema_out, schema_err),
0);
EXPECT_NE(schema_out.str().find("table,table1,s1,FIELD,INT64"),
std::string::npos)
<< schema_out.str();
std::ostringstream count_out;
std::ostringstream count_err;
EXPECT_EQ(
tsfile_cli::run_cli({"count", "-t", "TABLE1", "-f", "csv", f.path},
count_out, count_err),
0);
EXPECT_NE(count_out.str().find("table,table1,s1,FIELD,5,1,5,0,0,4,scan"),
std::string::npos)
<< count_out.str();
std::ostringstream stats_out;
std::ostringstream stats_err;
EXPECT_EQ(
tsfile_cli::run_cli({"stats", "-t", "TABLE1", "-f", "csv", f.path},
stats_out, stats_err),
0);
EXPECT_NE(stats_out.str().find("table,table1,id1_field_1,id2_field_2,s1,"
"INT64,5,\\N,0,4,0,40,0,40,\\N,statistics"),
std::string::npos)
<< stats_out.str();
}
TEST(CliE2E, WriteThenReadRoundTrip) {
std::string csv_path =
tsfile_cli_test::unique_temp_path("tsfile_cli_write_in", ".csv");
{
std::ofstream o(csv_path.c_str());
o << "time,id1,s1\n0,dev,0\n1,dev,10\n2,dev,20\n";
}
std::string out_path =
tsfile_cli_test::unique_temp_path("tsfile_cli_write_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_path, "-o", out_path},
wout, werr);
EXPECT_EQ(wc, 0) << werr.str();
std::ostringstream cout_;
std::ostringstream cerr_;
int cc =
tsfile_cli::run_cli({"count", "-f", "csv", out_path}, cout_, cerr_);
EXPECT_EQ(cc, 0);
EXPECT_NE(cout_.str().find("table,t1,s1,FIELD,3,1,3,0,0,2,scan"),
std::string::npos)
<< cout_.str();
std::ostringstream rout;
std::ostringstream rerr;
int rc = tsfile_cli::run_cli({"cat", "-m", "s1", "-f", "csv", out_path},
rout, rerr);
EXPECT_EQ(rc, 0);
EXPECT_EQ(rout.str(), "time,id1,s1\n0,dev,0\n1,dev,10\n2,dev,20\n");
std::remove(csv_path.c_str());
std::remove(out_path.c_str());
}
TEST(CliE2E, WriteThenReadFloatDoubleRoundTripLossless) {
std::string csv_path =
tsfile_cli_test::unique_temp_path("tsfile_cli_fp_in", ".csv");
{
std::ofstream o(csv_path.c_str());
o << "time,id1,f1,d1\n0,dev,0.1,0.1\n1,dev,3.4028235,"
"3.141592653589793\n";
}
std::string out_path =
tsfile_cli_test::unique_temp_path("tsfile_cli_fp_out", ".tsfile");
std::ostringstream wout;
std::ostringstream werr;
int wc = tsfile_cli::run_cli(
{"write", "--table", "t1", "--tag", "id1", "STRING", "--field", "f1",
"FLOAT", "--field", "d1", "DOUBLE", "-i", csv_path, "-o", out_path},
wout, werr);
ASSERT_EQ(wc, 0) << werr.str();
std::ostringstream rout;
std::ostringstream rerr;
int rc = tsfile_cli::run_cli({"cat", "-f", "ndjson", out_path}, rout, rerr);
ASSERT_EQ(rc, 0) << rerr.str();
// Default ostream precision (6 sig digits) would print 0.1 / 3.40282 and
// lose bits; max_digits10 keeps every digit needed to round-trip.
EXPECT_NE(rout.str().find("0.100000001"), std::string::npos) << rout.str();
EXPECT_NE(rout.str().find("3.40282345"), std::string::npos) << rout.str();
EXPECT_NE(rout.str().find("3.1415926535897931"), std::string::npos)
<< rout.str();
std::remove(csv_path.c_str());
std::remove(out_path.c_str());
}
TEST(CliE2E, WriteImportsQuotedFieldWithEmbeddedNewline) {
std::string csv_path =
tsfile_cli_test::unique_temp_path("tsfile_cli_nl_in", ".csv");
{
std::ofstream o(csv_path.c_str());
// The note field on the first row spans two physical lines inside
// quotes; it must import as a single row, not be split into two.
o << "time,id1,note\n0,dev,\"line one\nline two\"\n1,dev,plain\n";
}
std::string out_path =
tsfile_cli_test::unique_temp_path("tsfile_cli_nl_out", ".tsfile");
std::ostringstream wout;
std::ostringstream werr;
int wc = tsfile_cli::run_cli(
{"write", "--table", "t1", "--tag", "id1", "STRING", "--field", "note",
"TEXT", "-i", csv_path, "-o", out_path},
wout, werr);
ASSERT_EQ(wc, 0) << werr.str();
std::ostringstream cout_;
std::ostringstream cerr_;
ASSERT_EQ(
tsfile_cli::run_cli({"count", "-f", "csv", out_path}, cout_, cerr_), 0);
EXPECT_NE(cout_.str().find("table,t1,note,FIELD,2,1,2,0,0,1,scan"),
std::string::npos)
<< cout_.str();
std::ostringstream rout;
std::ostringstream rerr;
ASSERT_EQ(
tsfile_cli::run_cli({"cat", "-f", "ndjson", out_path}, rout, rerr), 0);
EXPECT_NE(rout.str().find("line one\\nline two"), std::string::npos)
<< rout.str();
std::remove(csv_path.c_str());
std::remove(out_path.c_str());
}
TEST(CliE2E, WriteMissingColumnsIsUsageError) {
std::ostringstream out;
std::ostringstream err;
int code = tsfile_cli::run_cli(
{"write", "--table", "t1", "-o", "x.tsfile", "in.csv"}, out, err);
EXPECT_EQ(code, 1);
EXPECT_NE(err.str().find("--field"), std::string::npos);
}
namespace {
bool path_exists(const std::string& p) {
std::ifstream in(p.c_str());
return in.good();
}
} // namespace
TEST(CliE2E, WriteRejectsOutOfOrderTimestampsAndLeavesNoOutput) {
std::string csv =
tsfile_cli_test::unique_temp_path("tsfile_cli_ooo", ".csv");
{
std::ofstream o(csv.c_str());
o << "time,s1\n5,50\n1,10\n";
}
std::string out_path =
tsfile_cli_test::unique_temp_path("tsfile_cli_ooo_out", ".tsfile");
std::ostringstream out;
std::ostringstream err;
int code = tsfile_cli::run_cli({"write", "--table", "t", "--field", "s1",
"INT64", "-i", csv, "-o", out_path},
out, err);
EXPECT_EQ(code, 2);
EXPECT_NE(err.str().find("strictly increasing"), std::string::npos)
<< err.str();
EXPECT_NE(err.str().find("line 3"), std::string::npos) << err.str();
EXPECT_FALSE(path_exists(out_path)) << "failed import must leave no output";
std::remove(csv.c_str());
std::remove(out_path.c_str());
}
TEST(CliE2E, WriteAllowsSameTimestampAcrossDevices) {
std::string csv =
tsfile_cli_test::unique_temp_path("tsfile_cli_md", ".csv");
{
std::ofstream o(csv.c_str());
o << "time,id,s1\n1,A,10\n1,B,20\n2,A,30\n";
}
std::string out_path =
tsfile_cli_test::unique_temp_path("tsfile_cli_md_out", ".tsfile");
std::ostringstream out;
std::ostringstream err;
int code = tsfile_cli::run_cli(
{"write", "--table", "t", "--tag", "id", "STRING", "--field", "s1",
"INT64", "-i", csv, "-o", out_path},
out, err);
EXPECT_EQ(code, 0) << err.str();
std::ostringstream cout_;
std::ostringstream cerr_;
tsfile_cli::run_cli({"count", "-f", "csv", out_path}, cout_, cerr_);
EXPECT_NE(cout_.str().find("table,t,s1,FIELD,3,2,3,0,1,2,scan"),
std::string::npos)
<< cout_.str();
std::remove(csv.c_str());
std::remove(out_path.c_str());
}
TEST(CliE2E, WriteRejectsOutputEqualsInput) {
std::string csv =
tsfile_cli_test::unique_temp_path("tsfile_cli_alias", ".csv");
{
std::ofstream o(csv.c_str());
o << "time,s1\n0,1\n";
}
std::ostringstream out;
std::ostringstream err;
int code = tsfile_cli::run_cli({"write", "--table", "t", "--field", "s1",
"INT64", "-i", csv, "-o", csv},
out, err);
EXPECT_EQ(code, 3);
EXPECT_NE(err.str().find("same as the input"), std::string::npos)
<< err.str();
// The input file must be untouched.
std::ifstream in(csv.c_str());
std::stringstream buf;
buf << in.rdbuf();
EXPECT_EQ(buf.str(), "time,s1\n0,1\n");
std::remove(csv.c_str());
}
TEST(CliE2E, WriteFailureOnBadValueLeavesNoOutput) {
std::string csv =
tsfile_cli_test::unique_temp_path("tsfile_cli_badval", ".csv");
{
std::ofstream o(csv.c_str());
o << "time,s1\n0,notanumber\n";
}
std::string out_path =
tsfile_cli_test::unique_temp_path("tsfile_cli_badval_out", ".tsfile");
std::ostringstream out;
std::ostringstream err;
int code = tsfile_cli::run_cli({"write", "--table", "t", "--field", "s1",
"INT64", "-i", csv, "-o", out_path},
out, err);
EXPECT_EQ(code, 2);
EXPECT_FALSE(path_exists(out_path));
std::remove(csv.c_str());
std::remove(out_path.c_str());
}
TEST(CliE2E, WriteRejectsDuplicateColumnNames) {
std::ostringstream out;
std::ostringstream err;
int code = tsfile_cli::run_cli(
{"write", "--table", "t", "--field", "s1", "INT64", "--field", "s1",
"INT64", "--stdin", "-o", "x.tsfile"},
out, err);
EXPECT_EQ(code, 1);
EXPECT_NE(err.str().find("duplicate column"), std::string::npos)
<< err.str();
}
TEST(CliE2E, WriteRejectsTagOnlySchema) {
std::ostringstream out;
std::ostringstream err;
int code = tsfile_cli::run_cli({"write", "--table", "t", "--tag", "id",
"STRING", "--stdin", "-o", "x.tsfile"},
out, err);
EXPECT_EQ(code, 1);
EXPECT_NE(err.str().find("--field"), std::string::npos) << err.str();
}
TEST(CliE2E, WriteRejectsNonStringTag) {
std::ostringstream out;
std::ostringstream err;
int code = tsfile_cli::run_cli(
{"write", "--table", "t", "--tag", "id", "INT64", "--field", "s1",
"INT64", "--stdin", "-o", "x.tsfile"},
out, err);
EXPECT_EQ(code, 1);
EXPECT_NE(err.str().find("must use STRING"), std::string::npos)
<< err.str();
}
TEST(CliE2E, WriteNormalizesNamesAndHeaderCase) {
std::string csv =
tsfile_cli_test::unique_temp_path("tsfile_cli_case", ".csv");
{
std::ofstream o(csv.c_str());
o << "time,ID,S1\n0,a,1\n";
}
std::string output =
tsfile_cli_test::unique_temp_path("tsfile_cli_case_out", ".tsfile");
std::ostringstream out;
std::ostringstream err;
EXPECT_EQ(tsfile_cli::run_cli(
{"write", "--table", "Mixed", "--tag", "Id", "STRING",
"--field", "s1", "INT64", "-i", csv, "-o", output},
out, err),
0)
<< err.str();
std::ostringstream schema_out;
std::ostringstream schema_err;
EXPECT_EQ(tsfile_cli::run_cli({"schema", "-f", "csv", output}, schema_out,
schema_err),
0);
EXPECT_NE(schema_out.str().find("table,mixed,id,TAG,STRING"),
std::string::npos);
std::remove(csv.c_str());
std::remove(output.c_str());
}
TEST(CliE2E, WriteRejectsHeaderMatchWithNoHeader) {
std::ostringstream out;
std::ostringstream err;
int code = tsfile_cli::run_cli(
{"write", "--table", "t", "--field", "s1", "INT64", "-o", "x.tsfile",
"--stdin", "--no-header", "--header-match"},
out, err);
EXPECT_EQ(code, 1);
EXPECT_NE(err.str().find("--header-match"), std::string::npos) << err.str();
}
TEST(CliE2E, ReadRejectsWriteOnlyFlag) {
Fixture f;
std::ostringstream out;
std::ostringstream err;
int code = tsfile_cli::run_cli({"ls", "-o", "x.tsfile", f.path}, out, err);
EXPECT_EQ(code, 1);
EXPECT_NE(err.str().find("only valid for write"), std::string::npos)
<< err.str();
}
TEST(CliE2E, ReadRejectsWriteColumnFlags) {
Fixture f;
std::ostringstream out;
std::ostringstream err;
int code =
tsfile_cli::run_cli({"ls", "--field", "s1", "INT64", f.path}, out, err);
EXPECT_EQ(code, 1);
EXPECT_NE(err.str().find("--tag/--field are only valid for write"),
std::string::npos)
<< err.str();
}
TEST(CliE2E, MetaRejectsDeviceScopeFlag) {
Fixture f;
std::ostringstream out;
std::ostringstream err;
int code = tsfile_cli::run_cli({"meta", "-d", "dev", f.path}, out, err);
EXPECT_EQ(code, 1);
EXPECT_NE(err.str().find("not valid for meta"), std::string::npos)
<< err.str();
}
TEST(CliE2E, SchemaTableShowsEncodingAndCompression) {
Fixture f;
std::ostringstream out;
std::ostringstream err;
int code = tsfile_cli::run_cli({"schema", "-f", "csv", f.path}, out, err);
EXPECT_EQ(code, 0);
// Table-model schema must report the fixture's configured encoding and
// compression rather than blanks.
EXPECT_NE(out.str().find(",s1,FIELD,INT64,PLAIN,UNCOMPRESSED\n"),
std::string::npos)
<< out.str();
}
namespace {
// Run a one-row `write` whose single value cell is `value`, declaring the
// column as `type`. Returns the exit code; captures stderr into `err`.
int write_one_value(const std::string& type, const std::string& value,
std::string& err_out) {
std::string csv =
tsfile_cli_test::unique_temp_path("tsfile_cli_ovf", ".csv");
{
std::ofstream o(csv.c_str());
o << "time,s1\n0," << value << "\n";
}
std::string out_path =
tsfile_cli_test::unique_temp_path("tsfile_cli_ovf_out", ".tsfile");
std::ostringstream out;
std::ostringstream err;
int code = tsfile_cli::run_cli({"write", "--table", "t", "--field", "s1",
type, "-i", csv, "-o", out_path},
out, err);
err_out = err.str();
std::remove(csv.c_str());
std::remove(out_path.c_str());
return code;
}
} // namespace
TEST(CliE2E, WriteRejectsInt32Overflow) {
std::string err;
EXPECT_EQ(write_one_value("INT32", "3000000000", err), 2);
EXPECT_NE(err.find("INT32 out of range"), std::string::npos) << err;
}
TEST(CliE2E, WriteAcceptsInt32Boundary) {
std::string err;
EXPECT_EQ(write_one_value("INT32", "2147483647", err), 0) << err;
}
TEST(CliE2E, WriteRejectsInt64Overflow) {
std::string err;
EXPECT_EQ(write_one_value("INT64", "99999999999999999999999999", err), 2);
EXPECT_NE(err.find("INT64 out of range"), std::string::npos) << err;
}
TEST(CliE2E, WriteRejectsDoubleOverflow) {
std::string err;
EXPECT_EQ(write_one_value("DOUBLE", "1e400", err), 2);
EXPECT_NE(err.find("DOUBLE out of range"), std::string::npos) << err;
}
TEST(CliE2E, WriteRejectsNonNumericInt64) {
std::string err;
EXPECT_EQ(write_one_value("INT64", "12abc", err), 2);
EXPECT_NE(err.find("bad INT64"), std::string::npos) << err;
}
TEST(CliE2E, WriteDateRequiresStrictIsoLexicalForm) {
std::string err;
EXPECT_EQ(write_one_value("DATE", "2024-1-1", err), 2);
EXPECT_NE(err.find("want YYYY-MM-DD"), std::string::npos) << err;
EXPECT_EQ(write_one_value("DATE", "2024-02-30", err), 2);
EXPECT_EQ(write_one_value("DATE", "2024-02-29", err), 0) << err;
}
TEST(CliE2E, WriteAcceptsOneLeadingUtf8Bom) {
std::string csv =
tsfile_cli_test::unique_temp_path("tsfile_cli_bom", ".csv");
{
std::ofstream o(csv.c_str(), std::ios::binary);
o << "\xEF\xBB\xBFtime,s1\n0,1\n";
}
std::string output =
tsfile_cli_test::unique_temp_path("tsfile_cli_bom_out", ".tsfile");
std::ostringstream out;
std::ostringstream err;
EXPECT_EQ(tsfile_cli::run_cli({"write", "--table", "t", "--field", "s1",
"INT64", "-i", csv, "-o", output},
out, err),
0)
<< err.str();
std::remove(csv.c_str());
std::remove(output.c_str());
}
TEST(CliE2E, WriteRejectsInvalidUtf8AndMisplacedBomWithoutOutput) {
const std::string invalid_utf8 =
std::string("time,s1\n0,") + static_cast<char>(0xC3) + "(\n";
const std::string misplaced_bom =
"time,s1\n0,\xEF\xBB\xBF"
"1\n";
const std::string inputs[] = {invalid_utf8, misplaced_bom};
for (size_t i = 0; i < 2; ++i) {
std::string csv =
tsfile_cli_test::unique_temp_path("tsfile_cli_bad_utf8", ".csv");
std::string output = tsfile_cli_test::unique_temp_path(
"tsfile_cli_bad_utf8_out", ".tsfile");
{
std::ofstream file(csv.c_str(), std::ios::binary);
file.write(inputs[i].data(),
static_cast<std::streamsize>(inputs[i].size()));
}
std::ostringstream out;
std::ostringstream err;
EXPECT_EQ(tsfile_cli::run_cli({"write", "--table", "t", "--field", "s1",
"INT64", "-i", csv, "-o", output},
out, err),
2)
<< err.str();
std::ifstream target(output.c_str(), std::ios::binary);
EXPECT_FALSE(target.good());
std::remove(csv.c_str());
std::remove(output.c_str());
}
}
TEST(CliE2E, WriteRejectsReservedAndControlCharacterNames) {
std::ostringstream out;
std::ostringstream err;
EXPECT_EQ(tsfile_cli::run_cli({"write", "--table", "t", "--field", "time",
"INT64", "--stdin", "-o", "unused.tsfile"},
out, err),
1);
EXPECT_NE(err.str().find("reserved"), std::string::npos) << err.str();
out.str("");
out.clear();
err.str("");
err.clear();
EXPECT_EQ(
tsfile_cli::run_cli({"write", "--table", "bad\nname", "--field", "s1",
"INT64", "--stdin", "-o", "unused.tsfile"},
out, err),
1);
EXPECT_NE(err.str().find("control"), std::string::npos) << err.str();
}
TEST(CliE2E, WriteFailurePreservesExistingTarget) {
std::string csv =
tsfile_cli_test::unique_temp_path("tsfile_cli_existing", ".csv");
std::string output =
tsfile_cli_test::unique_temp_path("tsfile_cli_existing_out", ".tsfile");
{
std::ofstream o(csv.c_str());
o << "time,s1\n0,1\n";
}
{
std::ofstream o(output.c_str());
o << "sentinel";
}
std::ostringstream out;
std::ostringstream err;
EXPECT_EQ(tsfile_cli::run_cli({"write", "--table", "t", "--field", "s1",
"INT64", "-i", csv, "-o", output},
out, err),
3);
std::ifstream existing(output.c_str());
std::stringstream content;
content << existing.rdbuf();
EXPECT_EQ(content.str(), "sentinel");
std::remove(csv.c_str());
std::remove(output.c_str());
}
TEST(CliE2E, WriteRejectsOutOfOrderAcrossBatches) {
// More than one 1024-row batch of ascending rows, then a violating
// timestamp. The first batch is already flushed by the time the bad row is
// read, so this proves both that per-device tracking survives a batch flush
// and that the already-written output is removed on failure.
std::string csv =
tsfile_cli_test::unique_temp_path("tsfile_cli_xbatch", ".csv");
{
std::ofstream o(csv.c_str());
o << "time,s1\n";
for (int i = 1; i <= 1100; ++i) {
o << i << "," << i << "\n";
}
o << "500,999\n"; // <= the last timestamp for the tag-less device
}
std::string out_path =
tsfile_cli_test::unique_temp_path("tsfile_cli_xbatch_out", ".tsfile");
std::ostringstream out;
std::ostringstream err;
int code = tsfile_cli::run_cli({"write", "--table", "t", "--field", "s1",
"INT64", "-i", csv, "-o", out_path},
out, err);
EXPECT_EQ(code, 2);
EXPECT_NE(err.str().find("strictly increasing"), std::string::npos)
<< err.str();
EXPECT_FALSE(path_exists(out_path));
std::remove(csv.c_str());
std::remove(out_path.c_str());
}
TEST(CliE2E, WriteStreamsLargeInputRoundTrips) {
std::string csv =
tsfile_cli_test::unique_temp_path("tsfile_cli_large", ".csv");
{
std::ofstream o(csv.c_str());
o << "time,s1\n";
for (int i = 1; i <= 3000; ++i) {
o << i << "," << (i * 2) << "\n";
}
}
std::string out_path =
tsfile_cli_test::unique_temp_path("tsfile_cli_large_out", ".tsfile");
std::ostringstream out;
std::ostringstream err;
int code = tsfile_cli::run_cli({"write", "--table", "big", "--field", "s1",
"INT64", "-i", csv, "-o", out_path},
out, err);
EXPECT_EQ(code, 0) << err.str();
std::ostringstream cout_;
std::ostringstream cerr_;
tsfile_cli::run_cli({"count", "-f", "csv", out_path}, cout_, cerr_);
EXPECT_NE(cout_.str().find("table,big,s1,FIELD,3000,1,3000,0,1,3000,scan"),
std::string::npos)
<< cout_.str();
std::remove(csv.c_str());
std::remove(out_path.c_str());
}
TEST(CliE2E, HelpWithPositionalFileIsUsageError) {
Fixture f;
std::ostringstream out;
std::ostringstream err;
int code = tsfile_cli::run_cli({"cat", "--help", f.path}, out, err);
EXPECT_EQ(code, 1);
EXPECT_TRUE(out.str().empty());
EXPECT_NE(err.str().find("--help"), std::string::npos) << err.str();
}
TEST(CliE2E, StatsRejectsRowOnlyFlag) {
Fixture f;
std::ostringstream out;
std::ostringstream err;
int code = tsfile_cli::run_cli({"stats", "--start", "1", f.path}, out, err);
EXPECT_EQ(code, 1);
EXPECT_NE(err.str().find("only valid for head/cat"), std::string::npos)
<< err.str();
}
TEST(CliE2E, LsRejectsMeasurementsFlag) {
Fixture f;
std::ostringstream out;
std::ostringstream err;
int code = tsfile_cli::run_cli({"ls", "-m", "s1", f.path}, out, err);
EXPECT_EQ(code, 1);
EXPECT_NE(err.str().find("not valid for ls"), std::string::npos)
<< err.str();
}
TEST(CliE2E, WriteRoundTripsTimestampDateBlob) {
std::string csv =
tsfile_cli_test::unique_temp_path("tsfile_cli_tdb", ".csv");
{
std::ofstream o(csv.c_str());
o << "time,id1,ts1,d1,b1\n"
"0,dev,1700000000000,2024-01-15,hello\n"
"1,dev,1700000000001,2024-12-31,world\n";
}
std::string out_path =
tsfile_cli_test::unique_temp_path("tsfile_cli_tdb_out", ".tsfile");
std::ostringstream wout;
std::ostringstream werr;
int wc = tsfile_cli::run_cli(
{"write", "--table", "t1", "--tag", "id1", "STRING", "--field", "ts1",
"TIMESTAMP", "--field", "d1", "DATE", "--field", "b1", "BLOB", "-o",
out_path, "-i", csv},
wout, werr);
ASSERT_EQ(wc, 0) << werr.str();
std::ostringstream rout;
std::ostringstream rerr;
ASSERT_EQ(tsfile_cli::run_cli({"cat", "-f", "csv", out_path}, rout, rerr),
0)
<< rerr.str();
// TIMESTAMP stays a decimal string, DATE uses YYYY-MM-DD, and BLOB uses
// the external 0x-prefixed lowercase hex lexeme.
EXPECT_NE(rout.str().find("1700000000000"), std::string::npos)
<< rout.str();
EXPECT_NE(rout.str().find("2024-01-15"), std::string::npos) << rout.str();
EXPECT_NE(rout.str().find("2024-12-31"), std::string::npos) << rout.str();
EXPECT_NE(rout.str().find("0x68656c6c6f"), std::string::npos) << rout.str();
EXPECT_NE(rout.str().find("0x776f726c64"), std::string::npos) << rout.str();
std::remove(csv.c_str());
std::remove(out_path.c_str());
}
TEST(CliE2E, WriteRejectsBadDate) {
std::string err;
EXPECT_EQ(write_one_value("DATE", "not-a-date", err), 2);
EXPECT_NE(err.find("bad DATE"), std::string::npos) << err;
}
TEST(CliE2E, WriteVerboseEchoesConfig) {
std::string csv =
tsfile_cli_test::unique_temp_path("tsfile_cli_vb", ".csv");
{
std::ofstream o(csv.c_str());
o << "time,s1\n0,1\n";
}
std::string out_path =
tsfile_cli_test::unique_temp_path("tsfile_cli_vb_out", ".tsfile");
std::ostringstream out;
std::ostringstream err;
int code = tsfile_cli::run_cli({"write", "--table", "vt", "--field", "s1",
"INT64", "-v", "-i", csv, "-o", out_path},
out, err);
EXPECT_EQ(code, 0) << err.str();
EXPECT_NE(err.str().find("created model=table object=vt rows=1 output="),
std::string::npos)
<< err.str();
EXPECT_NE(err.str().find("column=s1 category=FIELD data_type=INT64"),
std::string::npos)
<< err.str();
EXPECT_NE(err.str().find("source=default"), std::string::npos) << err.str();
std::remove(csv.c_str());
std::remove(out_path.c_str());
}
TEST(CliE2E, WriteRejectsHeaderMatch) {
std::string csv =
tsfile_cli_test::unique_temp_path("tsfile_cli_hm", ".csv");
{
std::ofstream o(csv.c_str());
o << "time,wrong\n0,1\n";
}
std::string out_path =
tsfile_cli_test::unique_temp_path("tsfile_cli_hm_out", ".tsfile");
std::ostringstream out;
std::ostringstream err;
int code =
tsfile_cli::run_cli({"write", "--table", "t", "--field", "s1", "INT64",
"--header-match", "-i", csv, "-o", out_path},
out, err);
EXPECT_EQ(code, 1);
EXPECT_NE(err.str().find("--header-match"), std::string::npos) << err.str();
std::remove(csv.c_str());
std::remove(out_path.c_str());
}
// Every column carries a distinct, type-specific value, so the JSON output pins
// each column name to exactly one value. This is what guards the by-index
// add_value mapping in cmd_write: if any two columns were written to the wrong
// slot, a key/value pair below would mismatch.
TEST(CliE2E, WriteMapsEachColumnToItsOwnValue) {
std::string csv =
tsfile_cli_test::unique_temp_path("tsfile_cli_colmap", ".csv");
{
std::ofstream o(csv.c_str());
o << "time,a_bool,b_int,c_long,d_float,e_double,f_str,g_ts,h_date\n"
"10,true,42,9000000000,1.5,3.25,hello,1700000000000,2024-06-15\n";
}
std::string out_path =
tsfile_cli_test::unique_temp_path("tsfile_cli_colmap_out", ".tsfile");
std::ostringstream wout;
std::ostringstream werr;
int wc = tsfile_cli::run_cli(
{"write", "--table", "t1", "--field", "a_bool", "BOOLEAN",
"--field", "b_int", "INT32", "--field", "c_long", "INT64",
"--field", "d_float", "FLOAT", "--field", "e_double", "DOUBLE",
"--field", "f_str", "STRING", "--field", "g_ts", "TIMESTAMP",
"--field", "h_date", "DATE", "-i", csv, "-o",
out_path},
wout, werr);
ASSERT_EQ(wc, 0) << werr.str();
std::ostringstream rout;
std::ostringstream rerr;
ASSERT_EQ(
tsfile_cli::run_cli({"cat", "-f", "ndjson", out_path}, rout, rerr), 0)
<< rerr.str();
const std::string& j = rout.str();
EXPECT_NE(j.find("\"a_bool\":true"), std::string::npos) << j;
EXPECT_NE(j.find("\"b_int\":42"), std::string::npos) << j;
EXPECT_NE(j.find("\"c_long\":\"9000000000\""), std::string::npos) << j;
EXPECT_NE(j.find("\"d_float\":1.5"), std::string::npos) << j;
EXPECT_NE(j.find("\"e_double\":3.25"), std::string::npos) << j;
EXPECT_NE(j.find("\"f_str\":\"hello\""), std::string::npos) << j;
EXPECT_NE(j.find("\"g_ts\":\"1700000000000\""), std::string::npos) << j;
EXPECT_NE(j.find("\"h_date\":\"2024-06-15\""), std::string::npos) << j;
std::remove(csv.c_str());
std::remove(out_path.c_str());
}
// A multi-type import spanning several batches must keep every value in its own
// column after each flush. Tags vary so timestamps may repeat across devices.
TEST(CliE2E, WriteMultiTypeAcrossBatchesRoundTrips) {
std::string csv =
tsfile_cli_test::unique_temp_path("tsfile_cli_mtb", ".csv");
const int kRows = 2500; // > 2 batches of 1024
{
std::ofstream o(csv.c_str());
o << "time,id,n,note\n";
for (int i = 0; i < kRows; ++i) {
o << i << ",dev," << (i * 3) << ",row" << i << "\n";
}
}
std::string out_path =
tsfile_cli_test::unique_temp_path("tsfile_cli_mtb_out", ".tsfile");
std::ostringstream wout;
std::ostringstream werr;
int wc = tsfile_cli::run_cli(
{"write", "--table", "t", "--tag", "id", "STRING", "--field", "n",
"INT64", "--field", "note", "TEXT", "-i", csv, "-o", out_path},
wout, werr);
ASSERT_EQ(wc, 0) << werr.str();
std::ostringstream cout_;
std::ostringstream cerr_;
ASSERT_EQ(
tsfile_cli::run_cli({"count", "-f", "csv", out_path}, cout_, cerr_), 0);
EXPECT_NE(cout_.str().find("table,t,n,FIELD,2500,1,2500,0,0,2499,scan"),
std::string::npos)
<< cout_.str();
// Spot-check a row from the last batch keeps n and note paired correctly.
std::ostringstream rout;
std::ostringstream rerr;
ASSERT_EQ(tsfile_cli::run_cli({"cat", "--start", "2400", "--end", "2400",
"-f", "ndjson", out_path},
rout, rerr),
0)
<< rerr.str();
EXPECT_NE(rout.str().find("\"n\":\"7200\""), std::string::npos)
<< rout.str();
EXPECT_NE(rout.str().find("\"note\":\"row2400\""), std::string::npos)
<< rout.str();
std::remove(csv.c_str());
std::remove(out_path.c_str());
}
// A quoted STRING field containing the delimiter and escaped quotes must
// survive import and re-export unchanged (RFC 4180 round-trip through the
// writer).
TEST(CliE2E, WriteRoundTripsQuotedSpecialChars) {
std::string csv =
tsfile_cli_test::unique_temp_path("tsfile_cli_special", ".csv");
{
std::ofstream o(csv.c_str());
// note = a,b "q" c (comma + embedded quotes)
o << "time,id,note\n0,dev,\"a,b \"\"q\"\" c\"\n";
}
std::string out_path =
tsfile_cli_test::unique_temp_path("tsfile_cli_special_out", ".tsfile");
std::ostringstream wout;
std::ostringstream werr;
int wc = tsfile_cli::run_cli(
{"write", "--table", "t", "--tag", "id", "STRING", "--field", "note",
"STRING", "-i", csv, "-o", out_path},
wout, werr);
ASSERT_EQ(wc, 0) << werr.str();
// JSON escapes the embedded quotes; the comma is preserved verbatim.
std::ostringstream rout;
std::ostringstream rerr;
ASSERT_EQ(
tsfile_cli::run_cli({"cat", "-f", "ndjson", out_path}, rout, rerr), 0)
<< rerr.str();
EXPECT_NE(rout.str().find("\"note\":\"a,b \\\"q\\\" c\""),
std::string::npos)
<< rout.str();
std::remove(csv.c_str());
std::remove(out_path.c_str());
}
TEST(CliE2E, WriteRejectsTimestampOverflow) {
std::string err;
EXPECT_EQ(write_one_value("TIMESTAMP", "99999999999999999999999999", err),
2);
EXPECT_NE(err.find("TIMESTAMP out of range"), std::string::npos) << err;
}
TEST(CliE2E, WriteRejectsNonNumericTimestampColumn) {
std::string err;
EXPECT_EQ(write_one_value("TIMESTAMP", "not-a-number", err), 2);
EXPECT_NE(err.find("bad TIMESTAMP"), std::string::npos) << err;
}
TEST(CliE2E, WriteRejectsImpossibleDate) {
// Syntactically YYYY-MM-DD but not a real calendar date.
std::string err;
EXPECT_EQ(write_one_value("DATE", "2024-13-40", err), 2);
EXPECT_NE(err.find("bad DATE"), std::string::npos) << err;
}
TEST(CliE2E, WriteAcceptsDateBoundary) {
std::string err;
EXPECT_EQ(write_one_value("DATE", "2024-02-29", err), 0)
<< err; // leap day
}
// CSV nulls use unquoted \N; quoted empty strings stay distinct from null.
TEST(CliE2E, WriteDistinguishesCsvNullAndEmptyString) {
std::string csv =
tsfile_cli_test::unique_temp_path("tsfile_cli_null", ".csv");
{
std::ofstream o(csv.c_str());
o << "time,id,n\n0,dev,\\N\n1,\"\",7\n";
}
std::string out_path =
tsfile_cli_test::unique_temp_path("tsfile_cli_null_out", ".tsfile");
std::ostringstream wout;
std::ostringstream werr;
int wc = tsfile_cli::run_cli(
{"write", "--table", "t", "--tag", "id", "STRING", "--field", "n",
"INT64", "-i", csv, "-o", out_path},
wout, werr);
ASSERT_EQ(wc, 0) << werr.str();
std::ostringstream rout;
std::ostringstream rerr;
ASSERT_EQ(
tsfile_cli::run_cli({"cat", "-f", "ndjson", out_path}, rout, rerr), 0)
<< rerr.str();
EXPECT_NE(rout.str().find("\"n\":null"), std::string::npos) << rout.str();
EXPECT_NE(rout.str().find("\"id\":\"\""), std::string::npos) << rout.str();
std::remove(csv.c_str());
std::remove(out_path.c_str());
}