// Copyright (C) 2014-2021 Internet Systems Consortium, Inc. ("ISC") // // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at http://mozilla.org/MPL/2.0/. #include #include #include #include #include #include #include namespace { using namespace isc::util; // This test exercises escaping and unescaping of characters. TEST(CSVRowTest, escapeUnescape) { std::string orig(",FO^O\\,B?,AR,"); // We'll escape commas, question marks, and carets. std::string escaped = CSVRow::escapeCharacters(orig, ",?^"); EXPECT_EQ ("ˏO^O\\ˋ?ˊR,", escaped); // Now make sure we can unescape it correctly. std::string unescaped = CSVRow::unescapeCharacters(escaped); EXPECT_EQ (orig, unescaped); // Make sure that an incident occurrence of just the escape tag // is left intact. orig = ("noscape"); escaped = CSVRow::escapeCharacters(orig, ","); unescaped = CSVRow::unescapeCharacters(orig); EXPECT_EQ (orig, unescaped); // Make sure that an incidental occurrence of a valid // escape tag sequence left intact. orig = ("noˎscape"); escaped = CSVRow::escapeCharacters(orig, ","); unescaped = CSVRow::unescapeCharacters(escaped); EXPECT_EQ (orig, unescaped); } // This test checks that the single data row is parsed. TEST(CSVRow, parse) { CSVRow row0("foo,bar,foo-bar"); ASSERT_EQ(3, row0.getValuesCount()); EXPECT_EQ("foo", row0.readAt(0)); EXPECT_EQ("bar", row0.readAt(1)); EXPECT_EQ("foo-bar", row0.readAt(2)); row0.parse("bar,,foo-bar"); ASSERT_EQ(3, row0.getValuesCount()); EXPECT_EQ("bar", row0.readAt(0)); EXPECT_TRUE(row0.readAt(1).empty()); EXPECT_EQ("foo-bar", row0.readAt(2)); row0.parse("bar,foo,-bar"); ASSERT_EQ(2, row0.getValuesCount()); EXPECT_EQ("bar", row0.readAt(0)); // Read the second column as-is and escaped EXPECT_EQ("foo,-bar", row0.readAt(1)); EXPECT_EQ("foo,-bar", row0.readAtEscaped(1)); CSVRow row1("foo-bar|foo|bar|", '|'); ASSERT_EQ(4, row1.getValuesCount()); EXPECT_EQ("foo-bar", row1.readAt(0)); EXPECT_EQ("foo", row1.readAt(1)); EXPECT_EQ("bar", row1.readAt(2)); EXPECT_TRUE(row1.readAt(3).empty()); row1.parse(""); ASSERT_EQ(1, row1.getValuesCount()); EXPECT_TRUE(row1.readAt(0).empty()); } // Verifies that empty columns are handled correctly. TEST(CSVRow, emptyColumns) { // Should get four columns, all blank except column the second one. CSVRow row(",one,,"); ASSERT_EQ(4, row.getValuesCount()); EXPECT_EQ("", row.readAt(0)); EXPECT_EQ("one", row.readAt(1)); EXPECT_EQ("", row.readAt(2)); EXPECT_EQ("", row.readAt(3)); } // Verifies that empty columns are handled correctly. TEST(CSVRow, oneColumn) { // Should get one column CSVRow row("zero"); ASSERT_EQ(1, row.getValuesCount()); EXPECT_EQ("zero", row.readAt(0)); } // This test checks that the text representation of the CSV row // is created correctly. TEST(CSVRow, render) { CSVRow row0(3); row0.writeAt(0, "foo"); row0.writeAt(1, "foo-bar"); row0.writeAt(2, "bar"); std::string text; ASSERT_NO_THROW(text = row0.render()); EXPECT_EQ(text, "foo,foo-bar,bar"); CSVRow row1(4, ';'); row1.writeAt(0, "foo"); row1.writeAt(2, "bar"); row1.writeAt(3, 10); ASSERT_NO_THROW(text = row1.render()); EXPECT_EQ(text, "foo;;bar;10"); CSVRow row2(0); ASSERT_NO_THROW(text = row2.render()); EXPECT_TRUE(text.empty()); } // This test checks that the data values can be set for the CSV row. TEST(CSVRow, writeAt) { CSVRow row(4); row.writeAt(0, 10); row.writeAt(1, "foo"); row.writeAt(2, "bar"); row.writeAtEscaped(3, "bar,one,two"); EXPECT_EQ("10", row.readAt(0)); EXPECT_EQ("foo", row.readAt(1)); EXPECT_EQ("bar", row.readAt(2)); // Read third column as-is and unescaped EXPECT_EQ("bar,one,two", row.readAt(3)); EXPECT_EQ("bar,one,two", row.readAtEscaped(3)); EXPECT_THROW(row.writeAt(4, 20), CSVFileError); EXPECT_THROW(row.writeAt(4, "foo"), CSVFileError); } // Checks whether writeAt() and append() can be mixed together. TEST(CSVRow, append) { CSVRow row(3); EXPECT_EQ(3, row.getValuesCount()); row.writeAt(0, "alpha"); ASSERT_NO_THROW(row.append("delta")); EXPECT_EQ(4, row.getValuesCount()); row.writeAt(1, "beta"); row.writeAt(2, "gamma"); ASSERT_NO_THROW(row.append("epsilon")); EXPECT_EQ(5, row.getValuesCount()); std::string text; ASSERT_NO_THROW(text = row.render()); EXPECT_EQ("alpha,beta,gamma,delta,epsilon", text); } // This test checks that a row can be trimmed of // a given number of elements TEST(CSVRow, trim) { CSVRow row("zero,one,two,three,four"); ASSERT_EQ(5, row.getValuesCount()); EXPECT_EQ("zero", row.readAt(0)); EXPECT_EQ("one", row.readAt(1)); EXPECT_EQ("two", row.readAt(2)); EXPECT_EQ("three", row.readAt(3)); EXPECT_EQ("four", row.readAt(4)); ASSERT_THROW(row.trim(10), CSVFileError); // Verify that we can erase just one ASSERT_NO_THROW(row.trim(1)); ASSERT_EQ(4, row.getValuesCount()); EXPECT_EQ("zero", row.readAt(0)); EXPECT_EQ("one", row.readAt(1)); EXPECT_EQ("two", row.readAt(2)); EXPECT_EQ("three", row.readAt(3)); // Verify we can trim more than one ASSERT_NO_THROW(row.trim(2)); ASSERT_EQ(2, row.getValuesCount()); EXPECT_EQ("zero", row.readAt(0)); EXPECT_EQ("one", row.readAt(1)); } /// @brief Test fixture class for testing operations on CSV file. /// /// It implements basic operations on files, such as reading writing /// file removal and checking presence of the file. This is used by /// unit tests to verify correctness of the file created by the /// CSVFile class. class CSVFileTest : public ::testing::Test { public: /// @brief Constructor. /// /// Sets the path to the CSV file used throughout the tests. /// The name of the file is test.csv and it is located in the /// current build folder. /// /// It also deletes any dangling files after previous tests. CSVFileTest(); /// @brief Destructor. /// /// Deletes the test CSV file if any. virtual ~CSVFileTest(); /// @brief Prepends the absolute path to the file specified /// as an argument. /// /// @param filename Name of the file. /// @return Absolute path to the test file. static std::string absolutePath(const std::string& filename); /// @brief Check if test file exists on disk. bool exists() const; /// @brief Reads whole CSV file. /// /// @return Contents of the file. std::string readFile() const; /// @brief Removes existing file (if any). int removeFile() const; /// @brief Creates file with contents. /// /// @param contents Contents of the file. void writeFile(const std::string& contents) const; /// @brief Absolute path to the file used in the tests. std::string testfile_; }; CSVFileTest::CSVFileTest() : testfile_(absolutePath("test.csv")) { static_cast(removeFile()); } CSVFileTest::~CSVFileTest() { static_cast(removeFile()); } std::string CSVFileTest::absolutePath(const std::string& filename) { std::ostringstream s; s << TEST_DATA_BUILDDIR << "/" << filename; return (s.str()); } bool CSVFileTest::exists() const { std::ifstream fs(testfile_.c_str()); bool ok = fs.good(); fs.close(); return (ok); } std::string CSVFileTest::readFile() const { std::ifstream fs(testfile_.c_str()); if (!fs.is_open()) { return (""); } std::string contents((std::istreambuf_iterator(fs)), std::istreambuf_iterator()); fs.close(); return (contents); } int CSVFileTest::removeFile() const { return (remove(testfile_.c_str())); } void CSVFileTest::writeFile(const std::string& contents) const { std::ofstream fs(testfile_.c_str(), std::ofstream::out); if (fs.is_open()) { fs << contents; fs.close(); } } // This test checks that the function which is used to add columns of the // CSV file works as expected. TEST_F(CSVFileTest, addColumn) { boost::scoped_ptr csv(new CSVFile(testfile_)); // Add two columns. ASSERT_NO_THROW(csv->addColumn("animal")); ASSERT_NO_THROW(csv->addColumn("color")); // Make sure we can't add duplicates. EXPECT_THROW(csv->addColumn("animal"), CSVFileError); EXPECT_THROW(csv->addColumn("color"), CSVFileError); // But we should still be able to add unique columns. EXPECT_NO_THROW(csv->addColumn("age")); EXPECT_NO_THROW(csv->addColumn("comments")); // Assert that the file is opened, because the rest of the test relies // on this. ASSERT_NO_THROW(csv->recreate()); ASSERT_TRUE(exists()); // Make sure we can't add columns (even unique) when the file is open. ASSERT_THROW(csv->addColumn("zoo"), CSVFileError); // Close the file. ASSERT_NO_THROW(csv->close()); // And check that now it is possible to add the column. EXPECT_NO_THROW(csv->addColumn("zoo")); } // This test checks that the appropriate file name is initialized. TEST_F(CSVFileTest, getFilename) { CSVFile csv(testfile_); EXPECT_EQ(testfile_, csv.getFilename()); } // This test checks that the file can be opened, its whole content is // parsed correctly and data may be appended. It also checks that empty // row is returned when EOF is reached. TEST_F(CSVFileTest, openReadAllWrite) { // Create a new CSV file that contains a header and two data rows. writeFile("animal,age,color\n" "cat,10,white\n" "lion,15,yellow\n"); // Open this file and check that the header is parsed. boost::scoped_ptr csv(new CSVFile(testfile_)); ASSERT_NO_THROW(csv->open()); ASSERT_EQ(3, csv->getColumnCount()); EXPECT_EQ("animal", csv->getColumnName(0)); EXPECT_EQ("age", csv->getColumnName(1)); EXPECT_EQ("color", csv->getColumnName(2)); // Read first row. CSVRow row; ASSERT_TRUE(csv->next(row)); ASSERT_EQ(3, row.getValuesCount()); EXPECT_EQ("cat", row.readAt(0)); EXPECT_EQ("10", row.readAt(1)); EXPECT_EQ("white", row.readAt(2)); // Read second row. ASSERT_TRUE(csv->next(row)); ASSERT_EQ(3, row.getValuesCount()); EXPECT_EQ("lion", row.readAt(0)); EXPECT_EQ("15", row.readAt(1)); EXPECT_EQ("yellow", row.readAt(2)); // There is no 3rd row, so the empty one should be returned. ASSERT_TRUE(csv->next(row)); EXPECT_EQ(CSVFile::EMPTY_ROW(), row); // It should be fine to read again, but again empty row should be returned. ASSERT_TRUE(csv->next(row)); EXPECT_EQ(CSVFile::EMPTY_ROW(), row); // Now, let's try to append something to this file. CSVRow row_write(3); row_write.writeAt(0, "dog"); row_write.writeAt(1, 2); row_write.writeAt(2, "blue"); ASSERT_NO_THROW(csv->append(row_write)); // Close the file. ASSERT_NO_THROW(csv->flush()); csv->close(); // Check the file contents are correct. EXPECT_EQ("animal,age,color\n" "cat,10,white\n" "lion,15,yellow\n" "dog,2,blue\n", readFile()); // Any attempt to read from the file or write to it should now fail. EXPECT_FALSE(csv->next(row)); EXPECT_THROW(csv->append(row_write), CSVFileError); CSVRow row_write2(3); row_write2.writeAt(0, "bird"); row_write2.writeAt(1, 3); row_write2.writeAt(2, "purple"); // Reopen the file, seek to the end of file so as we can append // some more data. ASSERT_NO_THROW(csv->open(true)); // The file pointer should be at the end of file, so an attempt // to read should result in an empty row. ASSERT_TRUE(csv->next(row)); EXPECT_EQ(CSVFile::EMPTY_ROW(), row); // We should be able to append new data. ASSERT_NO_THROW(csv->append(row_write2)); ASSERT_NO_THROW(csv->flush()); csv->close(); // Check that new data has been appended. EXPECT_EQ("animal,age,color\n" "cat,10,white\n" "lion,15,yellow\n" "dog,2,blue\n" "bird,3,purple\n", readFile()); } // This test checks that contents may be appended to a file which hasn't // been fully parsed/read. TEST_F(CSVFileTest, openReadPartialWrite) { // Create a CSV file with two rows in it. writeFile("animal,age,color\n" "cat,10,white\n" "lion,15,yellow\n"); // Open this file. boost::scoped_ptr csv(new CSVFile(testfile_)); ASSERT_NO_THROW(csv->open()); // Read the first row. CSVRow row0(0); ASSERT_NO_THROW(csv->next(row0)); ASSERT_EQ(3, row0.getValuesCount()); EXPECT_EQ("cat", row0.readAt(0)); EXPECT_EQ("10", row0.readAt(1)); EXPECT_EQ("white", row0.readAt(2)); // There is still second row to be read. But, it should be possible to // skip reading it and append new row to the end of file. CSVRow row_write(3); row_write.writeAt(0, "dog"); row_write.writeAt(1, 2); row_write.writeAt(2, "blue"); ASSERT_NO_THROW(csv->append(row_write)); // At this point, the file pointer is at the end of file, so reading // should return empty row. CSVRow row1(0); ASSERT_NO_THROW(csv->next(row1)); EXPECT_EQ(CSVFile::EMPTY_ROW(), row1); // Close the file. ASSERT_NO_THROW(csv->flush()); csv->close(); // Check that there are two initial lines and one new there. EXPECT_EQ("animal,age,color\n" "cat,10,white\n" "lion,15,yellow\n" "dog,2,blue\n", readFile()); } // This test checks that the new CSV file is created and header // is written to it. It also checks that data rows can be // appended to it. TEST_F(CSVFileTest, recreate) { boost::scoped_ptr csv(new CSVFile(testfile_)); csv->addColumn("animal"); csv->addColumn("color"); csv->addColumn("age"); csv->addColumn("comments"); ASSERT_NO_THROW(csv->recreate()); ASSERT_TRUE(exists()); CSVRow row0(4); row0.writeAt(0, "dog"); row0.writeAt(1, "grey"); row0.writeAt(2, 3); row0.writeAt(3, "nice one"); ASSERT_NO_THROW(csv->append(row0)); CSVRow row1(4); row1.writeAt(0, "cat"); row1.writeAt(1, "black"); row1.writeAt(2, 2); ASSERT_NO_THROW(csv->append(row1)); ASSERT_NO_THROW(csv->flush()); csv->close(); EXPECT_EQ("animal,color,age,comments\n" "dog,grey,3,nice one\n" "cat,black,2,\n", readFile()); } // This test checks that the error is reported when the size of the row being // read doesn't match the number of columns of the CSV file. TEST_F(CSVFileTest, validate) { // Create CSV file with 2 invalid rows in it: one too long, one too short. // Apart from that, there are two valid columns that should be read // successfully. writeFile("animal,age,color\n" "cat,10,white\n" "lion,15,yellow,black\n" "dog,3,green\n" "elephant,11\n"); boost::scoped_ptr csv(new CSVFile(testfile_)); ASSERT_NO_THROW(csv->open()); // First row is correct. CSVRow row0; ASSERT_TRUE(csv->next(row0)); EXPECT_EQ("cat", row0.readAt(0)); EXPECT_EQ("10", row0.readAt(1)); EXPECT_EQ("white", row0.readAt(2)); EXPECT_EQ("success", csv->getReadMsg()); // This row is too long. CSVRow row1; EXPECT_FALSE(csv->next(row1)); EXPECT_NE("success", csv->getReadMsg()); // This row is correct. CSVRow row2; ASSERT_TRUE(csv->next(row2)); EXPECT_EQ("dog", row2.readAt(0)); EXPECT_EQ("3", row2.readAt(1)); EXPECT_EQ("green", row2.readAt(2)); EXPECT_EQ("success", csv->getReadMsg()); // This row is too short. CSVRow row3; EXPECT_FALSE(csv->next(row3)); EXPECT_NE("success", csv->getReadMsg()); } // Test test checks that exception is thrown when the header of the CSV file // parsed, doesn't match the columns specified. TEST_F(CSVFileTest, validateHeader) { // Create CSV file with 3 columns. writeFile("animal,age,color\n" "cat,10,white\n" "lion,15,yellow,black\n"); // Invalid order of columns. boost::scoped_ptr csv(new CSVFile(testfile_)); csv->addColumn("color"); csv->addColumn("animal"); csv->addColumn("age"); EXPECT_THROW(csv->open(), CSVFileError); // Too many columns. csv.reset(new CSVFile(testfile_)); csv->addColumn("animal"); csv->addColumn("age"); csv->addColumn("color"); csv->addColumn("notes"); EXPECT_THROW(csv->open(), CSVFileError); // Too few columns. csv.reset(new CSVFile(testfile_)); csv->addColumn("animal"); csv->addColumn("age"); EXPECT_THROW(csv->open(), CSVFileError); } // This test checks that the exists method of the CSVFile class properly // checks that the file exists. TEST_F(CSVFileTest, exists) { // Create a new CSV file that contains a header and two data rows. writeFile("animal,age,color\n" "cat,10,white\n" "lion,15,yellow\n"); boost::scoped_ptr csv(new CSVFile(testfile_)); // The CSVFile class should return true even if the file hasn't been // opened. EXPECT_TRUE(csv->exists()); // Now open the file and make sure it still returns true. ASSERT_NO_THROW(csv->open()); EXPECT_TRUE(csv->exists()); // Close the file and remove it. csv->close(); EXPECT_EQ(0, removeFile()); // The file should not exist. EXPECT_FALSE(csv->exists()); } // Check that a single header without a trailing blank line can be parsed. TEST_F(CSVFileTest, parseHeaderWithoutTrailingBlankLine) { // Create a new CSV file that only contains a header without a new line. writeFile("animal,age,color"); // Open this file and check that the header is parsed. CSVFile csv(testfile_); ASSERT_NO_THROW(csv.open()); ASSERT_EQ(3, csv.getColumnCount()); EXPECT_EQ("animal", csv.getColumnName(0)); EXPECT_EQ("age", csv.getColumnName(1)); EXPECT_EQ("color", csv.getColumnName(2)); // Attempt to read the next row which doesn't exist. CSVRow row; ASSERT_TRUE(csv.next(row)); EXPECT_EQ(CSVFile::EMPTY_ROW(), row); // Close the file. csv.close(); } // Check that content without a trailing blank line can be parsed. TEST_F(CSVFileTest, parseContentWithoutTrailingBlankLine) { // Now create a new CSV file that contains header plus data, but the last // line is missing a new line. writeFile("animal,age,color\n" "cat,4,white\n" "lion,8,yellow"); // Open this file and check that the header is parsed. CSVFile csv(testfile_); ASSERT_NO_THROW(csv.open()); ASSERT_EQ(3, csv.getColumnCount()); EXPECT_EQ("animal", csv.getColumnName(0)); EXPECT_EQ("age", csv.getColumnName(1)); EXPECT_EQ("color", csv.getColumnName(2)); // Check the first data row. CSVRow row; ASSERT_TRUE(csv.next(row)); EXPECT_EQ("cat", row.readAt(0)); EXPECT_EQ("4", row.readAt(1)); EXPECT_EQ("white", row.readAt(2)); EXPECT_EQ("success", csv.getReadMsg()); // Check the second data row. ASSERT_TRUE(csv.next(row)); EXPECT_EQ("lion", row.readAt(0)); EXPECT_EQ("8", row.readAt(1)); EXPECT_EQ("yellow", row.readAt(2)); EXPECT_EQ("success", csv.getReadMsg()); // Attempt to read the next row which doesn't exist. ASSERT_TRUE(csv.next(row)); EXPECT_EQ(CSVFile::EMPTY_ROW(), row); // Close the file. csv.close(); } // Check that blank lines are skipped when reading from a file. TEST_F(CSVFileTest, parseContentWithBlankLines) { for (char const* const& content : { // Single intermediary blank line "animal,age,color\n" "cat,4,white\n" "\n" "lion,8,yellow\n", // Blank lines all over "\n" "\n" "animal,age,color\n" "\n" "\n" "cat,4,white\n" "\n" "\n" "lion,8,yellow\n" "\n" "\n", }) { // Create a new CSV file. writeFile(content); // Open this file and check that the header is parsed. CSVFile csv(testfile_); ASSERT_NO_THROW(csv.open()); ASSERT_EQ(3, csv.getColumnCount()); EXPECT_EQ("animal", csv.getColumnName(0)); EXPECT_EQ("age", csv.getColumnName(1)); EXPECT_EQ("color", csv.getColumnName(2)); // Check the first data row. CSVRow row; ASSERT_TRUE(csv.next(row)); EXPECT_EQ("cat", row.readAt(0)); EXPECT_EQ("4", row.readAt(1)); EXPECT_EQ("white", row.readAt(2)); EXPECT_EQ("success", csv.getReadMsg()); // Check the second non-blank data row. ASSERT_TRUE(csv.next(row)); EXPECT_EQ("lion", row.readAt(0)); EXPECT_EQ("8", row.readAt(1)); EXPECT_EQ("yellow", row.readAt(2)); EXPECT_EQ("success", csv.getReadMsg()); // Attempt to read the next row which doesn't exist. ASSERT_TRUE(csv.next(row)); EXPECT_EQ(CSVFile::EMPTY_ROW(), row); // Close the file. csv.close(); } } } // end of anonymous namespace