Browse Source

[3360] Implemented base class for read/write in CSV file.

Marcin Siodelski 11 years ago
parent
commit
c3f63b985f

+ 2 - 1
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

+ 295 - 0
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 <util/csv_file.h>
+#include <fstream>
+#include <sstream>
+
+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<std::string> 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());
+    }
+
+}
+
+}
+}

+ 352 - 0
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 <exceptions/exceptions.h>
+#include <boost/lexical_cast.hpp>
+#include <boost/shared_ptr.hpp>
+#include <fstream>
+#include <ostream>
+#include <string>
+#include <vector>
+
+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<typename T>
+    void writeAt(const size_t at, const T value) {
+        checkIndex(at);
+        try {
+            values_[at] = boost::lexical_cast<std::string>(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<std::string> 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<std::fstream> fs_;
+
+    /// @brief Holds CSV file columns.
+    std::vector<std::string> cols_;
+};
+
+} // namespace isc::util
+} // namespace isc
+
+#endif // CSV_FILE_H

+ 3 - 0
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

+ 284 - 0
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 <config.h>
+#include <util/csv_file.h>
+#include <boost/scoped_ptr.hpp>
+#include <gtest/gtest.h>
+#include <fstream>
+#include <sstream>
+#include <string>
+
+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<char>(fs)),
+                         std::istreambuf_iterator<char>());
+    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<CSVFile> 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<CSVFile> 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<CSVFile> 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