summaryrefslogtreecommitdiffstats
path: root/src/lib/util/tests/versioned_csv_file_unittest.cc
diff options
context:
space:
mode:
Diffstat (limited to 'src/lib/util/tests/versioned_csv_file_unittest.cc')
-rw-r--r--src/lib/util/tests/versioned_csv_file_unittest.cc501
1 files changed, 501 insertions, 0 deletions
diff --git a/src/lib/util/tests/versioned_csv_file_unittest.cc b/src/lib/util/tests/versioned_csv_file_unittest.cc
new file mode 100644
index 0000000..36a1f91
--- /dev/null
+++ b/src/lib/util/tests/versioned_csv_file_unittest.cc
@@ -0,0 +1,501 @@
+// Copyright (C) 2015-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 <config.h>
+#include <util/versioned_csv_file.h>
+#include <boost/scoped_ptr.hpp>
+#include <gtest/gtest.h>
+#include <fstream>
+#include <sstream>
+#include <string>
+
+#include <boost/algorithm/string/classification.hpp>
+#include <boost/algorithm/string/constants.hpp>
+#include <boost/algorithm/string/split.hpp>
+
+namespace {
+
+using namespace isc::util;
+
+/// @brief Test fixture class for testing operations on VersionedCSVFile.
+///
+/// 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 VersionedCSVFileTest : 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.
+ VersionedCSVFileTest();
+
+ /// @brief Destructor.
+ ///
+ /// Deletes the test CSV file if any.
+ virtual ~VersionedCSVFileTest();
+
+ /// @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_;
+
+};
+
+VersionedCSVFileTest::VersionedCSVFileTest()
+ : testfile_(absolutePath("test.csv")) {
+ static_cast<void>(removeFile());
+}
+
+VersionedCSVFileTest::~VersionedCSVFileTest() {
+ static_cast<void>(removeFile());
+}
+
+std::string
+VersionedCSVFileTest::absolutePath(const std::string& filename) {
+ std::ostringstream s;
+ s << TEST_DATA_BUILDDIR << "/" << filename;
+ return (s.str());
+}
+
+bool
+VersionedCSVFileTest::exists() const {
+ std::ifstream fs(testfile_.c_str());
+ bool ok = fs.good();
+ fs.close();
+ return (ok);
+}
+
+std::string
+VersionedCSVFileTest::readFile() const {
+ std::ifstream fs(testfile_.c_str());
+ if (!fs.is_open()) {
+ return ("");
+ }
+ std::string contents((std::istreambuf_iterator<char>(fs)),
+ std::istreambuf_iterator<char>());
+ fs.close();
+ return (contents);
+}
+
+int
+VersionedCSVFileTest::removeFile() const {
+ return (remove(testfile_.c_str()));
+}
+
+void
+VersionedCSVFileTest::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(VersionedCSVFileTest, addColumn) {
+ boost::scoped_ptr<VersionedCSVFile> csv(new VersionedCSVFile(testfile_));
+
+ // Verify that we're not allowed to open it without the schema
+ ASSERT_THROW(csv->open(), VersionedCSVFileError);
+
+ // Add two columns.
+ ASSERT_NO_THROW(csv->addColumn("animal", "1.0", ""));
+ ASSERT_NO_THROW(csv->addColumn("color", "2.0", "blue"));
+
+ // Make sure we can't add duplicates.
+ EXPECT_THROW(csv->addColumn("animal", "1.0", ""), CSVFileError);
+ EXPECT_THROW(csv->addColumn("color", "2.0", "blue"), CSVFileError);
+
+ // But we should still be able to add unique columns.
+ EXPECT_NO_THROW(csv->addColumn("age", "3.0", "21"));
+
+ // Assert that the file is opened, because the rest of the test relies
+ // on this.
+ ASSERT_NO_THROW(csv->recreate());
+ ASSERT_TRUE(exists());
+
+ // We should have 3 defined columns
+ // Input Header should match defined columns on new files
+ // Valid columns should match defined columns on new files
+ // Minimum valid columns wasn't set. (Remember it's optional)
+ EXPECT_EQ(3, csv->getColumnCount());
+ EXPECT_EQ(3, csv->getInputHeaderCount());
+ EXPECT_EQ(3, csv->getValidColumnCount());
+ EXPECT_EQ(0, csv->getMinimumValidColumns());
+
+ // Schema versions for new files should always match
+ EXPECT_EQ("3.0", csv->getInputSchemaVersion());
+ EXPECT_EQ("3.0", csv->getSchemaVersion());
+
+ // Input Schema State should be current for new files
+ EXPECT_EQ(VersionedCSVFile::CURRENT, csv->getInputSchemaState());
+ EXPECT_FALSE(csv->needsConversion());
+
+ // Make sure we can't add columns (even unique) when the file is open.
+ ASSERT_THROW(csv->addColumn("zoo", "3.0", ""), 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", "3.0", ""));
+}
+
+// Verifies that a current schema version file loads correctly.
+TEST_F(VersionedCSVFileTest, currentSchemaTest) {
+
+ // Create our versioned file, with three columns
+ boost::scoped_ptr<VersionedCSVFile> csv(new VersionedCSVFile(testfile_));
+ ASSERT_NO_THROW(csv->addColumn("animal", "2.0", ""));
+ ASSERT_NO_THROW(csv->addColumn("color", "2.0", "grey"));
+ ASSERT_NO_THROW(csv->addColumn("age", "2.0", "0"));
+
+ // Write a file compliant with the current schema version.
+ writeFile("animal,color,age\n"
+ "cat,black,2\n"
+ "lion,yellow,17\n"
+ "dog,brown,5\n");
+
+ // Header should pass validation and allow the open to succeed.
+ ASSERT_NO_THROW(csv->open());
+
+ // For schema current file We should have:
+ // 3 defined columns
+ // 3 columns total found in the header
+ // 3 valid columns found in the header
+ // Minimum valid columns wasn't set. (Remember it's optional)
+ EXPECT_EQ(3, csv->getColumnCount());
+ EXPECT_EQ(3, csv->getInputHeaderCount());
+ EXPECT_EQ(3, csv->getValidColumnCount());
+ EXPECT_EQ(0, csv->getMinimumValidColumns());
+
+ // Input schema and current schema should both be 2.0
+ EXPECT_EQ("2.0", csv->getInputSchemaVersion());
+ EXPECT_EQ("2.0", csv->getSchemaVersion());
+
+ // Input Schema State should be CURRENT
+ EXPECT_EQ(VersionedCSVFile::CURRENT, csv->getInputSchemaState());
+ EXPECT_FALSE(csv->needsConversion());
+
+ // First row is correct.
+ CSVRow row;
+ ASSERT_TRUE(csv->next(row));
+ EXPECT_EQ("cat", row.readAt(0));
+ EXPECT_EQ("black", row.readAt(1));
+ EXPECT_EQ("2", row.readAt(2));
+
+ // Second row is correct.
+ ASSERT_TRUE(csv->next(row));
+ EXPECT_EQ("lion", row.readAt(0));
+ EXPECT_EQ("yellow", row.readAt(1));
+ EXPECT_EQ("17", row.readAt(2));
+
+ // Third row is correct.
+ ASSERT_TRUE(csv->next(row));
+ EXPECT_EQ("dog", row.readAt(0));
+ EXPECT_EQ("brown", row.readAt(1));
+ EXPECT_EQ("5", row.readAt(2));
+}
+
+
+// Verifies the basic ability to upgrade valid files.
+// It starts with a version 1.0 file and updates
+// it through two schema evolutions.
+TEST_F(VersionedCSVFileTest, upgradeOlderVersions) {
+
+ // Create version 1.0 schema CSV file
+ writeFile("animal\n"
+ "cat\n"
+ "lion\n"
+ "dog\n");
+
+ // Create our versioned file, with two columns, one for each
+ // schema version
+ boost::scoped_ptr<VersionedCSVFile> csv(new VersionedCSVFile(testfile_));
+ ASSERT_NO_THROW(csv->addColumn("animal", "1.0", ""));
+ ASSERT_NO_THROW(csv->addColumn("color", "2.0", "blue"));
+
+ // Header should pass validation and allow the open to succeed.
+ ASSERT_NO_THROW(csv->open());
+
+ // We should have:
+ // 2 defined columns
+ // 1 column found in the header
+ // 1 valid column in the header
+ // Minimum valid columns wasn't set. (Remember it's optional)
+ EXPECT_EQ(2, csv->getColumnCount());
+ EXPECT_EQ(1, csv->getInputHeaderCount());
+ EXPECT_EQ(1, csv->getValidColumnCount());
+ EXPECT_EQ(0, csv->getMinimumValidColumns());
+
+ // Input schema should be 1.0, while our current schema should be 2.0
+ EXPECT_EQ("1.0", csv->getInputSchemaVersion());
+ EXPECT_EQ("2.0", csv->getSchemaVersion());
+
+ // Input Schema State should be NEEDS_UPGRADE
+ EXPECT_EQ(VersionedCSVFile::NEEDS_UPGRADE, csv->getInputSchemaState());
+ EXPECT_TRUE(csv->needsConversion());
+
+ // First row is correct.
+ CSVRow row;
+ ASSERT_TRUE(csv->next(row));
+ EXPECT_EQ("cat", row.readAt(0));
+ EXPECT_EQ("blue", row.readAt(1));
+
+ // Second row is correct.
+ ASSERT_TRUE(csv->next(row));
+ EXPECT_EQ("lion", row.readAt(0));
+ EXPECT_EQ("blue", row.readAt(1));
+
+ // Third row is correct.
+ ASSERT_TRUE(csv->next(row));
+ EXPECT_EQ("dog", row.readAt(0));
+ EXPECT_EQ("blue", row.readAt(1));
+
+ // Now, let's try to append something to this file.
+ CSVRow row_write(2);
+ row_write.writeAt(0, "bird");
+ row_write.writeAt(1, "yellow");
+ ASSERT_NO_THROW(csv->append(row_write));
+
+ // Close the file
+ ASSERT_NO_THROW(csv->flush());
+ ASSERT_NO_THROW(csv->close());
+
+
+ // Check the file contents are correct.
+ EXPECT_EQ("animal\n"
+ "cat\n"
+ "lion\n"
+ "dog\n"
+ "bird,yellow\n",
+ readFile());
+
+ // Create a third schema by adding a column
+ ASSERT_NO_THROW(csv->addColumn("age", "3.0", "21"));
+ ASSERT_EQ(3, csv->getColumnCount());
+
+ // Header should pass validation and allow the open to succeed
+ ASSERT_NO_THROW(csv->open());
+
+ // We should have:
+ // 3 defined columns
+ // 1 column found in the header
+ // 1 valid column in the header
+ // Minimum valid columns wasn't set. (Remember it's optional)
+ EXPECT_EQ(3, csv->getColumnCount());
+ EXPECT_EQ(1, csv->getInputHeaderCount());
+ EXPECT_EQ(1, csv->getValidColumnCount());
+ EXPECT_EQ(0, csv->getMinimumValidColumns());
+
+ // Make sure schema versions are accurate
+ EXPECT_EQ("1.0", csv->getInputSchemaVersion());
+ EXPECT_EQ("3.0", csv->getSchemaVersion());
+
+ // Input Schema State should be NEEDS_UPGRADE
+ EXPECT_EQ(VersionedCSVFile::NEEDS_UPGRADE, csv->getInputSchemaState());
+ EXPECT_TRUE(csv->needsConversion());
+
+ // First row is correct.
+ ASSERT_TRUE(csv->next(row));
+ EXPECT_EQ("cat", row.readAt(0));
+ EXPECT_EQ("blue", row.readAt(1));
+ EXPECT_EQ("21", row.readAt(2));
+
+ // Second row is correct.
+ ASSERT_TRUE(csv->next(row));
+ EXPECT_EQ("lion", row.readAt(0));
+ EXPECT_EQ("blue", row.readAt(1));
+ EXPECT_EQ("21", row.readAt(2));
+
+ // Third row is correct.
+ ASSERT_TRUE(csv->next(row));
+ EXPECT_EQ("dog", row.readAt(0));
+ EXPECT_EQ("blue", row.readAt(1));
+ EXPECT_EQ("21", row.readAt(2));
+
+ // Fourth row is correct.
+ ASSERT_TRUE(csv->next(row));
+ EXPECT_EQ("bird", row.readAt(0));
+ EXPECT_EQ("yellow", row.readAt(1));
+ EXPECT_EQ("21", row.readAt(2));
+}
+
+TEST_F(VersionedCSVFileTest, minimumValidColumn) {
+ // Create version 1.0 schema CSV file
+ writeFile("animal\n"
+ "cat\n"
+ "lion\n"
+ "dog\n");
+
+ // Create our versioned file, with three columns, one for each
+ // schema version
+ boost::scoped_ptr<VersionedCSVFile> csv(new VersionedCSVFile(testfile_));
+ ASSERT_NO_THROW(csv->addColumn("animal", "1.0", ""));
+ ASSERT_NO_THROW(csv->addColumn("color", "2.0", "blue"));
+ ASSERT_NO_THROW(csv->addColumn("age", "3.0", "21"));
+
+ // Verify we can't set minimum columns with a non-existent column
+ EXPECT_THROW(csv->setMinimumValidColumns("bogus"), VersionedCSVFileError);
+
+ // Set the minimum number of columns to "color"
+ csv->setMinimumValidColumns("color");
+ EXPECT_EQ(2, csv->getMinimumValidColumns());
+
+ // Header validation should fail, too few columns
+ ASSERT_THROW(csv->open(), CSVFileError);
+
+ // Set the minimum number of columns to 1. File should parse now.
+ csv->setMinimumValidColumns("animal");
+ EXPECT_EQ(1, csv->getMinimumValidColumns());
+ ASSERT_NO_THROW(csv->open());
+
+ // First row is correct.
+ CSVRow row;
+ ASSERT_TRUE(csv->next(row));
+ EXPECT_EQ("cat", row.readAt(0));
+ EXPECT_EQ("blue", row.readAt(1));
+ EXPECT_EQ("21", row.readAt(2));
+
+ ASSERT_TRUE(csv->next(row));
+ EXPECT_EQ("lion", row.readAt(0));
+ EXPECT_EQ("blue", row.readAt(1));
+ EXPECT_EQ("21", row.readAt(2));
+
+ ASSERT_TRUE(csv->next(row));
+ EXPECT_EQ("dog", row.readAt(0));
+ EXPECT_EQ("blue", row.readAt(1));
+ EXPECT_EQ("21", row.readAt(2));
+}
+
+TEST_F(VersionedCSVFileTest, invalidHeaderColumn) {
+
+ // Create our version 2.0 schema file
+ boost::scoped_ptr<VersionedCSVFile> csv(new VersionedCSVFile(testfile_));
+ ASSERT_NO_THROW(csv->addColumn("animal", "1.0", ""));
+ ASSERT_NO_THROW(csv->addColumn("color", "2.0", "blue"));
+
+ // Create a file with the correct number of columns but a wrong column name
+ writeFile("animal,colour\n"
+ "cat,red\n"
+ "lion,green\n");
+
+ // Header validation should fail, we have an invalid column
+ ASSERT_THROW(csv->open(), CSVFileError);
+}
+
+TEST_F(VersionedCSVFileTest, downGrading) {
+ // Create our version 2.0 schema file
+ boost::scoped_ptr<VersionedCSVFile> csv(new VersionedCSVFile(testfile_));
+ ASSERT_NO_THROW(csv->addColumn("animal", "1.0", ""));
+ ASSERT_NO_THROW(csv->addColumn("color", "2.0", "blue"));
+
+ // Create schema 2.0 file PLUS an extra column
+ writeFile("animal,color,age\n"
+ "cat,red,5\n"
+ "lion,green,8\n");
+
+ // Header should validate and file should open.
+ ASSERT_NO_THROW(csv->open());
+
+ // We should have:
+ // 2 defined columns
+ // 3 columns found in the header
+ // 2 valid columns in the header
+ // Minimum valid columns wasn't set. (Remember it's optional)
+ EXPECT_EQ(2, csv->getColumnCount());
+ EXPECT_EQ(3, csv->getInputHeaderCount());
+ EXPECT_EQ(2, csv->getValidColumnCount());
+ EXPECT_EQ(0, csv->getMinimumValidColumns());
+
+ // Input schema and current schema should both be 2.0
+ EXPECT_EQ("2.0", csv->getInputSchemaVersion());
+ EXPECT_EQ("2.0", csv->getSchemaVersion());
+
+ // Input Schema State should be NEEDS_DOWNGRADE
+ EXPECT_EQ(VersionedCSVFile::NEEDS_DOWNGRADE, csv->getInputSchemaState());
+ EXPECT_TRUE(csv->needsConversion());
+
+ // First row is correct.
+ CSVRow row;
+ EXPECT_TRUE(csv->next(row));
+ EXPECT_EQ("cat", row.readAt(0));
+ EXPECT_EQ("red", row.readAt(1));
+
+ // No data beyond the second column
+ EXPECT_THROW(row.readAt(2), CSVFileError);
+
+ // Second row is correct.
+ ASSERT_TRUE(csv->next(row));
+ EXPECT_EQ("lion", row.readAt(0));
+ EXPECT_EQ("green", row.readAt(1));
+
+ // No data beyond the second column
+ EXPECT_THROW(row.readAt(2), CSVFileError);
+}
+
+
+TEST_F(VersionedCSVFileTest, rowChecking) {
+ // Create version 2.0 schema CSV file with a
+ // - valid header
+ // - row 0 has too many values
+ // - row 1 is valid
+ // - row 3 is too few values
+ writeFile("animal,color\n"
+ "cat,red,bogus_row_value\n"
+ "lion,green\n"
+ "too_few\n");
+
+ // Create our versioned file, with two columns, one for each
+ // schema version
+ boost::scoped_ptr<VersionedCSVFile> csv(new VersionedCSVFile(testfile_));
+ csv->addColumn("animal", "1.0", "");
+ csv->addColumn("color", "2.0", "blue");
+
+ // Header validation should pass, so we can open
+ ASSERT_NO_THROW(csv->open());
+
+ CSVRow row;
+ // First row has too many
+ EXPECT_FALSE(csv->next(row));
+
+ // Second row is valid
+ ASSERT_TRUE(csv->next(row));
+ EXPECT_EQ("lion", row.readAt(0));
+ EXPECT_EQ("green", row.readAt(1));
+
+ // Third row has too few
+ EXPECT_FALSE(csv->next(row));
+}
+
+} // end of anonymous namespace