From: Marcin Siodelski Date: Thu, 13 Mar 2014 17:41:08 +0000 (+0100) Subject: [3360] Implemented base class for read/write in CSV file. X-Git-Tag: bind10-1.2.0rc1-release~14^2~19 X-Git-Url: http://git.ipfire.org/gitweb.cgi?a=commitdiff_plain;h=c3f63b985fde7d458d0624960ff3ecb17269709b;p=thirdparty%2Fkea.git [3360] Implemented base class for read/write in CSV file. --- diff --git a/src/lib/util/Makefile.am b/src/lib/util/Makefile.am index 7d3781eb9e..e3ba39fe67 100644 --- a/src/lib/util/Makefile.am +++ b/src/lib/util/Makefile.am @@ -19,7 +19,8 @@ AM_CXXFLAGS += $(BOOST_MAPPED_FILE_CXXFLAG) endif lib_LTLIBRARIES = libb10-util.la -libb10_util_la_SOURCES = filename.h filename.cc +libb10_util_la_SOURCES = csv_file.h csv_file.cc +libb10_util_la_SOURCES += filename.h filename.cc libb10_util_la_SOURCES += locks.h lru_list.h libb10_util_la_SOURCES += strutil.h strutil.cc libb10_util_la_SOURCES += buffer.h io_utilities.h diff --git a/src/lib/util/csv_file.cc b/src/lib/util/csv_file.cc new file mode 100644 index 0000000000..10ce9e5cde --- /dev/null +++ b/src/lib/util/csv_file.cc @@ -0,0 +1,295 @@ +// Copyright (C) 2014 Internet Systems Consortium, Inc. ("ISC") +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE +// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +#include +#include +#include + +namespace isc { +namespace util { + +CSVRow::CSVRow(const size_t cols, const char separator) + : separator_(separator), values_(cols) { +} + +CSVRow::CSVRow(const std::string& text, const char separator) + : separator_(separator) { + // Parsing is exception safe, so this will not throw. + parse(text.c_str()); +} + +void +CSVRow::parse(const char* line) { + std::string s(line); + // The 'pos' value holds the current position in the parsed stream. + // Normally, it points to the position of one of the the separator + // characters following the parsed value. For the first value, it + // has to be set to -1. + int pos = -1; + // Position of the first character of the currently parsed value. + size_t start_pos; + // Flag which indicates whether parsing should end because last value + // has been just parsed. + bool leave = false; + // Temporary container which holds parsed values. On successful + // parsing completion, the contents of this container are moved + // to the container holding values for the row. + std::vector values; + + do { + // Set the position of the currently parsed value. + start_pos = pos + 1; + // Find the first separator, following the character at + // start_pos. + pos = s.find(separator_, start_pos); + // The last value is not followed by a separator, so if + // we reached the end of line, take reminder of the string + // and make it a value. + if (pos == std::string::npos) { + pos = s.length(); + // Finish parsing as we already parsed the last value. + leave = true; + } + // Store the parsed value. + values.push_back(s.substr(start_pos, pos - start_pos)); + } while (!leave); + + // Assign new values. + std::swap(values, values_); +} + +std::string +CSVRow::readAt(const size_t at) const { + checkIndex(at); + return (values_[at]); +} + +std::string +CSVRow::render() const { + std::ostringstream s; + for (int i = 0; i < values_.size(); ++i) { + // Do not put separator before the first value. + if (i > 0) { + s << separator_; + } + s << values_[i]; + } + return (s.str()); +} + +void +CSVRow::writeAt(const size_t at, const char* value) { + checkIndex(at); + values_[at] = value; +} + +void +CSVRow::writeAt(const size_t at, const std::string& value) { + writeAt(at, value.c_str()); +} + +bool +CSVRow::operator==(const CSVRow& other) const { + return (render() == other.render()); +} + +bool +CSVRow::operator!=(const CSVRow& other) const { + return (render() != other.render()); +} + +std::ostream& operator<<(std::ostream& os, const CSVRow& row) { + os << row.render(); + return (os); +} + +void +CSVRow::checkIndex(const size_t at) const { + if (at >= values_.size()) { + isc_throw(CSVFileError, "value index '" << at << "' of the CSV row" + " is out of bounds; maximal index is '" + << (values_.size() - 1) << "'"); + } +} + +CSVFile::CSVFile(const std::string& filename) + : primary_separator_(','), filename_(filename), fs_() { +} + +CSVFile::~CSVFile() { + close(); +} + +void +CSVFile::close() { + // It is allowed to close multiple times. If file has been already closed, + // this is no-op. + if (fs_) { + fs_->close(); + fs_.reset(); + } +} + +void +CSVFile::addColumn(const std::string& col_name) { + if (getColumnIndex(col_name) >= 0) { + isc_throw(CSVFileError, "attempt to add duplicate column '" + << col_name << "'"); + } + cols_.push_back(col_name); +} + +void +CSVFile::append(const CSVRow& row) const { + if (!fs_) { + isc_throw(CSVFileError, "unable to write a row to the CSV file '" + << filename_ << "', which is closed"); + } + + if (row.getValuesCount() != getColumnCount()) { + isc_throw(CSVFileError, "number of values in the CSV row '" + << row.getValuesCount() << "' doesn't match the number of" + " columns in the CSV file '" << getColumnCount() << "'"); + } + + fs_->seekp(0, std::ios_base::end); + fs_->seekg(0, std::ios_base::end); + fs_->clear(); + + std::string text = row.render(); + *fs_ << text << std::endl; + if (!fs_->good()) { + fs_->clear(); + isc_throw(CSVFileError, "failed to write CSV row '" + << text << "' to the file '" << filename_ << "'"); + } +} + +std::ifstream::pos_type +CSVFile::size() const { + std::ifstream fs(filename_.c_str()); + bool ok = fs.good(); + // If something goes wrong, including that the file doesn't exist, + // return 0. + if (!ok) { + fs.close(); + return (0); + } + // Seek to the end of file and see where we are. This is a size of + // the file. + fs.seekg(0, std::ifstream::end); + std::ifstream::pos_type pos = fs.tellg(); + fs.close(); + return (pos); +} + +int +CSVFile::getColumnIndex(const std::string& col_name) const { + for (int i = 0; i < cols_.size(); ++i) { + if (cols_[i] == col_name) { + return (i); + } + } + return (-1); +} + +std::string +CSVFile::getColumnName(const size_t col_index) const { + if (col_index > cols_.size()) { + isc_throw(isc::OutOfRange, "column index " << col_index << " in the " + " CSV file '" << filename_ << "' is out of range; the CSV" + " file has only " << cols_.size() << " columns "); + } + return (cols_[col_index]); +} + +void +CSVFile::next(CSVRow& row) { + // Get exactly one line of the file. + std::string line; + std::getline(*fs_, line); + // If we got empty line because we reached the end of file + // return an empty row. + if (line.empty() && fs_->eof()) { + row = EMPTY_ROW(); + return; + } + // If we read anything, parse it. + row.parse(line.c_str()); +} + +void +CSVFile::open() { + // If file doesn't exist or is empty, we have to create our own file. + if (size() == 0) { + recreate(); + + } else { + // Try to open existing file, holding some data. + fs_.reset(new std::fstream(filename_.c_str())); + // The file may fail to open. For example, because of insufficient + // persmissions. Although the file is not open we should call close + // to reset our internal pointer. + if (!fs_->is_open()) { + close(); + isc_throw(CSVFileError, "unable to open '" << filename_ << "'"); + } + // Make sure we are on the beginning of the file, so as we can parse + // the header. + fs_->seekg(0); + + // Get the header. + std::string line; + std::getline(*fs_, line); + CSVRow header(line.c_str(), primary_separator_); + // Initialize columns. + for (size_t i = 0; i < header.getValuesCount(); ++i) { + addColumn(header.readAt(i)); + } + } +} + +void +CSVFile::recreate() { + // There is no sense to create a file is we don't specify columns for it. + if (getColumnCount() == 0) { + close(); + isc_throw(CSVFileError, "no columns defined for the newly" + " created CSV file '" << filename_ << "'"); + } + + // Close any danglining files. + close(); + fs_.reset(new std::fstream(filename_.c_str(), std::fstream::out)); + if (!fs_->is_open()) { + close(); + isc_throw(CSVFileError, "unable to open '" << filename_ << "'"); + } + // Opened successfuly. Write a header to it. + try { + CSVRow header(getColumnCount()); + for (int i = 0; i < getColumnCount(); ++i) { + header.writeAt(i, getColumnName(i)); + } + *fs_ << header << std::endl; + + } catch (const std::exception& ex) { + close(); + isc_throw(CSVFileError, ex.what()); + } + +} + +} +} diff --git a/src/lib/util/csv_file.h b/src/lib/util/csv_file.h new file mode 100644 index 0000000000..52267f89b1 --- /dev/null +++ b/src/lib/util/csv_file.h @@ -0,0 +1,352 @@ +// Copyright (C) 2014 Internet Systems Consortium, Inc. ("ISC") +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE +// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +#ifndef CSV_FILE_H +#define CSV_FILE_H + +#include +#include +#include +#include +#include +#include +#include + +namespace isc { +namespace util { + +/// @brief Exception thrown when an error occurs during CSV file processing. +class CSVFileError : public Exception { +public: + CSVFileError(const char* file, size_t line, const char* what) : + isc::Exception(file, line, what) { }; +}; + +/// @brief Represents a single row of the CSV file. +/// +/// The object of this type can create the string holding a collection of the +/// comma separated values, representing a row of the CSV file. It allows to +/// select ANY character as a separator for the values. The default separator +/// is a comma sign. +/// +/// The @c CSVRow object can be constructed in two different ways. The first +/// option is that the caller creates an object holding empty values +/// and then adds values one by one. Note that it is possible to either add +/// a string or a number. The number is stringified to the appropriate text +/// representation. When all the values are added, the text representation of +/// the row can be obtained by calling @c CSVRow::render function or output +/// stream operator. +/// +/// The @c CSVRow object can be also constructed by parsing the row of the +/// CSV file. In this case, the separator has to be known in advance and +/// passed to the class constructor. Constructor will call the @c CSVRow::parse +/// function internally to tokenize the CSV row and create collection of the +/// values. The class accessors can be then used to retrieve individual +/// values. +/// +/// This class is meant to be used by the @c CSVFile class to manipulate +/// the individual rows of the CSV file. +class CSVRow { +public: + + /// @brief Constructor, creates the raw to be used for output. + /// + /// Creates CSV row with empty values. The values should be + /// later set using the @c CSVRow::writeAt functions. When the + /// @c CSVRow::render is called, the text representation of the + /// row will be created using a separator character specified + /// as an argument of this constructor. + /// + /// This constructor is exception-free. + /// + /// @param cols Number of values in the row. + /// @param separator Character being a separator between values in the + /// text representation of the row. + CSVRow(const size_t cols, const char separator = ','); + + /// @brief Constructor, parses a single row of the CSV file. + /// + /// This constructor should be used to parse a single row of the CSV + /// file. The separator being used for the particular row needs to + /// be known in advance and specified as an argument of the constructor + /// if other than default separator is used in the row being parsed. + /// An example string to be parsed by this function looks as follows: + /// "foo,bar,foo-bar". + /// + /// This constructor is exception-free. + /// + /// @param text Text representation of the CSV row. + /// @param separator Character being used as a separator in a parsed file. + CSVRow(const std::string& text, const char separator = ','); + + /// @brief Returns number of values in a CSV row. + size_t getValuesCount() const { + return (values_.size()); + } + + /// @brief Parse the CSV file row. + /// + /// This function parses a string containing CSV values and assigns them + /// to the @c values_ private container. These values can be retrieved + /// from the container by calling @c CSVRow::readAt function. + /// + /// This function is exception-free. + /// + /// @param line String holding a row of comma separated values. + void parse(const char* line); + + /// @brief Retrieves a value from the internal container. + /// + /// @param at Index of the value in the container. The values are indexed + /// from 0, where 0 corresponds to the left-most value in the CSV file row. + /// + /// @return Value at specified index in the text form. + /// + /// @throw CSVFileError if the index is out of range. The number of elements + /// being held by the container can be obtained using + /// @c CSVRow::getValuesCount. + std::string readAt(const size_t at) const; + + /// @brief Creates a text representation of the CSV file row. + /// + /// This function iterates over all values currently held in the internal + /// @c values_ container and appends them into a string. The values are + /// separated using a separator character specified in the constructor. + /// + /// This function is exception free. + /// + /// @return Text representation of the CSV file row. + std::string render() const; + + /// @brief Replaces the value at specified index. + /// + /// This function is used to set values to be rendered using + /// @c CSVRow::render function. + /// + /// @param at Index of the value to be replaced. + /// @param value Value to be written given as string. + /// + /// @throw CSVFileError if index is out of range. + void writeAt(const size_t at, const char* value); + + /// @brief Replaces the value at specified index. + /// + /// This function is used to set values to be rendered using + /// @c CSVRow::render function. + /// + /// @param at Index of the value to be replaced. + /// @param value Value to be written given as string. + /// + /// @throw CSVFileError if index is out of range. + void writeAt(const size_t at, const std::string& value); + + /// @brief Replaces the value at specified index. + /// + /// This function is used to set values to be rendered using + /// @c CSVRow::render function. + /// + /// @param at Index of the value to be replaced. + /// @param value Value to be written - typically a number. + /// @tparam T Type of the value being written. + /// + /// @throw CSVFileError if index is out of range. + template + void writeAt(const size_t at, const T value) { + checkIndex(at); + try { + values_[at] = boost::lexical_cast(value); + } catch (const boost::bad_lexical_cast& ex) { + isc_throw(CSVFileError, "unable to stringify the value to be" + " written in the CSV file row at position '" + << at << "'"); + } + } + + /// @brief Equality operator. + /// + /// Two CSV rows are equal when their string representation is equal. This + /// includes the order of fields, separator etc. + /// + /// @param other Object to compare to. + bool operator==(const CSVRow& other) const; + + /// @brief Unequality operator. + /// + /// Two CSV rows are unequal when their string representation is unequal. + /// This includes the order of fields, separator etc. + /// + /// @param other Object to compare to. + bool operator!=(const CSVRow& other) const; + +private: + + /// @brief Check if the specified index of the value is in range. + /// + /// This function is used interally by other functions. + /// + /// @param at Value index. + /// @throw CSVFileError if specified index is not in range. + void checkIndex(const size_t at) const; + + /// @brief Separator character specifed in the constructor. + char separator_; + + /// @brief Internal container holding values that belong to the row. + std::vector values_; +}; + +/// @brief Overrides standard output stream operator for @c CSVRow object. +/// +/// The resulting string of characters is the same as the one returned by +/// @c CSVRow::render function. +/// +/// @param os Output stream. +/// @param row Object representing a CSV file row. +std::ostream& operator<<(std::ostream& os, const CSVRow& row); + +/// @brief Provides input/output access to CSV files. +/// +/// This class provides basic methods to access (parse) and create CSV files. +/// The file is identified by its name qualified with the absolute path. +/// The name of the file is passed to the constructor. Constructor doesn't +/// open/create a file, but simply records a file name specified by a caller. +/// +/// There are two functions that can be used to open a file: +/// - @c open - opens an existing file; if the file doesn't exist it creates it, +/// - @c recreate - removes existing file and creates a new one. +/// +/// When the file is opened its header file is parsed and column names are +/// idenetified. At this point it is already possible to get the list of the +/// column names using appropriate accessors. The data rows are not parsed +/// at this time. The row parsing is triggered by calling @c next function. +/// The result of parsing a row is stored in the @c CSVRow object passed as +/// a parameter. +/// +/// When the new file is created (when @c recreate is called), the CSV header is +/// immediately written into it. The header consists of the column names +/// specified with the @c addColumn function. The subsequent rows are written +/// into this file by calling @c append. +class CSVFile { +public: + + /// @brief Constructor. + /// + /// @param filename CSV file name. + CSVFile(const std::string& filename); + + /// @brief Destructor + virtual ~CSVFile(); + + /// @brief Adds new column name. + /// + /// This column adds a new column but doesn't write it to the file yet. + /// The name of the column will be placed in the CSV header when new file + /// is created by calling @c recreate or @c open function. + /// + /// @param col_name Name of the column. + /// + /// @throw CSVFileError if a column with the specified name exists. + void addColumn(const std::string& col_name); + + /// @brief Writes the CSV row into the file. + /// + /// @param Object representing a CSV file row. + /// + /// @throw CSVFileError When error occured during IO operation or if the + /// size of the row doesn't match the number of columns. + void append(const CSVRow& row) const; + + /// @brief Closes the CSV file. + void close(); + + /// @brief Returns the number of columns in the file. + size_t getColumnCount() const { + return (cols_.size()); + } + + /// @brief Returns the index of the column having specified name. + /// + /// This function is exception safe. + /// + /// @param col_name Name of the column. + /// @return Index of the column or negative value if the column doesn't + /// exist. + int getColumnIndex(const std::string& col_name) const; + + /// @brief Returns the name of the column. + /// + /// @param col_index Index of the column. + /// + /// @return Name of the column. + /// @throw CSVFileError if the specified index is out of range. + std::string getColumnName(const size_t col_index) const; + + /// @brief Reads next row from CSV file. + /// + /// This function will return the @c CSVRow object representing a + /// parsed row if parsing is successful. If the end of file has been + /// reached, the empty row is returned (a row containing no values). + /// + /// @param [out] row Object receiving the parsed CSV file. + void next(CSVRow& row); + + /// @brief Opens existing file or creates a new one. + /// + /// This function will try to open existing file if this file has size + /// greater than 0. If the file doesn't exist or has size of 0, the + /// file is recreated. If the existing file has been opened, the header + /// is parsed and column names are initialized in the @c CSVFile object. + /// The data pointer in the file is set to the beginning of the first + /// row. In order to retrieve the row contents the @c next function should + /// be called. + /// + /// @throw CSVFileError when IO operation fails. + void open(); + + /// @brief Creates a new CSV file. + /// + /// The file creation will fail if there are no columns specified. + /// Otherwise, this function will write the header to the file. + /// In order to write rows to opened file, the @c append function + /// should be called. + void recreate(); + + /// @brief Represents empty row. + static CSVRow EMPTY_ROW() { + static CSVRow row(0); + return (row); + } + +private: + + /// @brief Returns size of the CSV file. + std::ifstream::pos_type size() const; + + /// @brief Separator used by CSV file. + char primary_separator_; + + /// @brief CSV file name. + std::string filename_; + + /// @brief Holds a pointer to the file stream. + boost::shared_ptr fs_; + + /// @brief Holds CSV file columns. + std::vector cols_; +}; + +} // namespace isc::util +} // namespace isc + +#endif // CSV_FILE_H diff --git a/src/lib/util/tests/Makefile.am b/src/lib/util/tests/Makefile.am index d8f3d305f1..9e18510353 100644 --- a/src/lib/util/tests/Makefile.am +++ b/src/lib/util/tests/Makefile.am @@ -14,6 +14,8 @@ AM_LDFLAGS = -static endif CLEANFILES = *.gcno *.gcda +# CSV files are created by unit tests for CSVFile class. +CLEANFILES += *.csv TESTS_ENVIRONMENT = \ $(LIBTOOL) --mode=execute $(VALGRIND_COMMAND) @@ -25,6 +27,7 @@ run_unittests_SOURCES = run_unittests.cc run_unittests_SOURCES += base32hex_unittest.cc run_unittests_SOURCES += base64_unittest.cc run_unittests_SOURCES += buffer_unittest.cc +run_unittests_SOURCES += csv_file_unittest.cc run_unittests_SOURCES += fd_share_tests.cc run_unittests_SOURCES += fd_tests.cc run_unittests_SOURCES += filename_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 0000000000..b22ecaa5a5 --- /dev/null +++ b/src/lib/util/tests/csv_file_unittest.cc @@ -0,0 +1,284 @@ +// Copyright (C) 2014 Internet Systems Consortium, Inc. ("ISC") +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE +// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using namespace isc::util; + +// 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)); + + 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()); +} + +// 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(3); + row.writeAt(0, 10); + row.writeAt(1, "foo"); + row.writeAt(2, "bar"); + + EXPECT_EQ("10", row.readAt(0)); + EXPECT_EQ("foo", row.readAt(1)); + EXPECT_EQ("bar", row.readAt(2)); + + EXPECT_THROW(row.writeAt(3, 20), CSVFileError); + EXPECT_THROW(row.writeAt(3, "foo"), CSVFileError); +} + +/// @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). + void removeFile() const; + + /// @brief Creates file with contents. + /// + /// @param contents Contents of the file. + void writeFile(const std::string& contents) const; + + std::string testfile_; + +}; + +CSVFileTest::CSVFileTest() + : testfile_(absolutePath("test.csv")) { + removeFile(); +} + +CSVFileTest::~CSVFileTest() { + 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); +} + +void +CSVFileTest::removeFile() const { + 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 file can be opened and its content +// parsed correctly. It also checks that empty row is returned +// when EOF is reached. +TEST_F(CSVFileTest, open) { + writeFile("animal,age,color\n" + "cat,10,white\n" + "lion,15,yellow\n"); + 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)); + + CSVRow row(0); + ASSERT_NO_THROW(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)); + + ASSERT_NO_THROW(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)); + + ASSERT_NO_THROW(csv->next(row)); + EXPECT_EQ(CSVFile::EMPTY_ROW(), row); +} + +// This test checks that a file can be used both for reading +// and writing. When content is appended to the end of file, +// an attempt to read results in empty row returned. +TEST_F(CSVFileTest, openReadWrite) { + writeFile("animal,age,color\n" + "cat,10,white\n" + "lion,15,yellow\n"); + boost::scoped_ptr csv(new CSVFile(testfile_)); + ASSERT_NO_THROW(csv->open()); + + 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)); + + 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)); + + CSVRow row1(0); + ASSERT_NO_THROW(csv->next(row1)); + EXPECT_EQ(CSVFile::EMPTY_ROW(), row1); +} + +// 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)); + + EXPECT_EQ("animal,color,age,comments\n" + "dog,grey,3,nice one\n" + "cat,black,2,\n", + readFile()); +} + + +} // end of anonymous namespace