@@ -849,7 +849,8 @@ TEST(CliE2E, WriteImportsQuotedFieldWithEmbeddedNewline) {
849849 std::string csv_path =
850850 tsfile_cli_test::unique_temp_path (" tsfile_cli_nl_in" , " .csv" );
851851 {
852- std::ofstream o (csv_path.c_str ());
852+ // Keep the fixture's LF bytes unchanged on Windows as well.
853+ std::ofstream o (csv_path.c_str (), std::ios::binary);
853854 // The note field on the first row spans two physical lines inside
854855 // quotes; it must import as a single row, not be split into two.
855856 o << " time,id1,note\n 0,dev,\" line one\n line two\"\n 1,dev,plain\n " ;
@@ -1985,3 +1986,90 @@ TEST(CliE2E, WriteCatCsvWritePreservesLeadingBackslashes) {
19851986 std::remove (exported.c_str ());
19861987 std::remove (second.c_str ());
19871988}
1989+
1990+ TEST (CliE2E, WriteCatAndExportCsvPreserveQuotedLineEndings) {
1991+ const std::string input =
1992+ tsfile_cli_test::unique_temp_path (" tsfile_cli_crlf_input" , " .csv" );
1993+ const std::string first =
1994+ tsfile_cli_test::unique_temp_path (" tsfile_cli_crlf_first" , " .tsfile" );
1995+ const std::string expected_csv =
1996+ " time,site,message,note\n "
1997+ " 0,\" s\r\n 1\" ,\" first\r\n second\" ,\"\"\" quoted\"\"\r\n\r\n last\"\n "
1998+ " 1,\" s\r\n 1\" ,\" lf\n only\" ,\" mixed\r\n and\n more\"\n "
1999+ " 2,\" s\r\n 1\" ,\" carriage\r return\" ,\"\r\n\"\n "
2000+ " 3,\" s\r\n 1\" ,\" ends\r\" ,\" line\r\n end\"\n " ;
2001+ {
2002+ std::ofstream csv (input.c_str (), std::ios::binary);
2003+ ASSERT_TRUE (csv.good ());
2004+ // CRLF record boundaries, embedded CRLF/LF/CR, and no final newline.
2005+ csv << " time,site,message,note\r\n "
2006+ " 0,\" s\r\n 1\" ,\" first\r\n second\" ,"
2007+ " \"\"\" quoted\"\"\r\n\r\n last\"\r\n "
2008+ " 1,\" s\r\n 1\" ,\" lf\n only\" ,\" mixed\r\n and\n more\"\r\n "
2009+ " 2,\" s\r\n 1\" ,\" carriage\r return\" ,\"\r\n\"\r\n "
2010+ " 3,\" s\r\n 1\" ,\" ends\r\" ,\" line\r\n end\" " ;
2011+ }
2012+ const auto write = [&](const std::string& source,
2013+ const std::string& target) {
2014+ std::ostringstream out, err;
2015+ const int code = tsfile_cli::run_cli (
2016+ {" write" , " --table" , " t" , " --tag" , " site" , " STRING" , " --field" ,
2017+ " message" , " STRING" , " --field" , " note" , " TEXT" , " -i" , source, " -o" ,
2018+ target},
2019+ out, err);
2020+ EXPECT_EQ (code, 0 ) << err.str ();
2021+ return code;
2022+ };
2023+ const auto read = [&](const std::string& path, const std::string& format) {
2024+ std::ostringstream out, err;
2025+ EXPECT_EQ (tsfile_cli::run_cli ({" cat" , " -t" , " t" , " -f" , format, path},
2026+ out, err),
2027+ 0 )
2028+ << err.str ();
2029+ return out.str ();
2030+ };
2031+ const std::string expected_ndjson =
2032+ R"json( {"time":"0","site":"s\r\n1","message":"first\r\nsecond","note":"\"quoted\"\r\n\r\nlast"})json"
2033+ " \n "
2034+ R"json( {"time":"1","site":"s\r\n1","message":"lf\nonly","note":"mixed\r\nand\nmore"})json"
2035+ " \n "
2036+ R"json( {"time":"2","site":"s\r\n1","message":"carriage\rreturn","note":"\r\n"})json"
2037+ " \n "
2038+ R"json( {"time":"3","site":"s\r\n1","message":"ends\r","note":"line\r\nend"})json"
2039+ " \n " ;
2040+ ASSERT_EQ (write (input, first), 0 );
2041+ EXPECT_EQ (read (first, " ndjson" ), expected_ndjson);
2042+
2043+ for (const std::string command : {" cat" , " export" }) {
2044+ SCOPED_TRACE (command);
2045+ const std::string exported = tsfile_cli_test::unique_temp_path (
2046+ " tsfile_cli_crlf_" + command, " .csv" );
2047+ const std::string second = tsfile_cli_test::unique_temp_path (
2048+ " tsfile_cli_crlf_" + command, " .tsfile" );
2049+ if (command == " cat" ) {
2050+ const std::string csv = read (first, " csv" );
2051+ EXPECT_EQ (csv, expected_csv);
2052+ std::ofstream file (exported.c_str (), std::ios::binary);
2053+ ASSERT_TRUE (file.good ());
2054+ file << csv;
2055+ } else {
2056+ std::ostringstream out, err;
2057+ ASSERT_EQ (tsfile_cli::run_cli ({" export" , " -t" , " t" , " --type" , " csv" ,
2058+ " -o" , exported, first},
2059+ out, err),
2060+ 0 )
2061+ << err.str ();
2062+ std::ifstream file (exported.c_str (), std::ios::binary);
2063+ ASSERT_TRUE (file.good ());
2064+ std::ostringstream bytes;
2065+ bytes << file.rdbuf ();
2066+ EXPECT_EQ (bytes.str (), expected_csv);
2067+ }
2068+ ASSERT_EQ (write (exported, second), 0 );
2069+ EXPECT_EQ (read (second, " ndjson" ), expected_ndjson);
2070+ std::remove (exported.c_str ());
2071+ std::remove (second.c_str ());
2072+ }
2073+ std::remove (input.c_str ());
2074+ std::remove (first.c_str ());
2075+ }
0 commit comments