// 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 #include #include #include #include #include #include #include #include #include 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(removeFile()); } VersionedCSVFileTest::~VersionedCSVFileTest() { static_cast(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(fs)), std::istreambuf_iterator()); 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 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 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 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 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 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 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 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