From 040eee1aa49b49df4698d83a05af57c220127fd1 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Sat, 4 May 2024 13:36:04 +0200 Subject: Adding upstream version 2.2.0. Signed-off-by: Daniel Baumann --- src/lib/util/tests/csv_file_unittest.cc | 700 ++++++++++++++++++++++++++++++++ 1 file changed, 700 insertions(+) create mode 100644 src/lib/util/tests/csv_file_unittest.cc (limited to 'src/lib/util/tests/csv_file_unittest.cc') diff --git a/src/lib/util/tests/csv_file_unittest.cc b/src/lib/util/tests/csv_file_unittest.cc new file mode 100644 index 0000000..f55ec55 --- /dev/null +++ b/src/lib/util/tests/csv_file_unittest.cc @@ -0,0 +1,700 @@ +// 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 -- cgit v1.2.3