Book Image

Advanced C++

By : Gazihan Alankus, Olena Lizina, Rakesh Mane, Vivek Nagarajan, Brian Price
5 (1)
Book Image

Advanced C++

5 (1)
By: Gazihan Alankus, Olena Lizina, Rakesh Mane, Vivek Nagarajan, Brian Price

Overview of this book

C++ is one of the most widely used programming languages and is applied in a variety of domains, right from gaming to graphical user interface (GUI) programming and even operating systems. If you're looking to expand your career opportunities, mastering the advanced features of C++ is key. The book begins with advanced C++ concepts by helping you decipher the sophisticated C++ type system and understand how various stages of compilation convert source code to object code. You'll then learn how to recognize the tools that need to be used in order to control the flow of execution, capture data, and pass data around. By creating small models, you'll even discover how to use advanced lambdas and captures and express common API design patterns in C++. As you cover later chapters, you'll explore ways to optimize your code by learning about memory alignment, cache access, and the time a program takes to run. The concluding chapter will help you to maximize performance by understanding modern CPU branch prediction and how to make your code cache-friendly. By the end of this book, you'll have developed programming skills that will set you apart from other C++ programmers.
Table of Contents (11 chapters)
7
6. Streams and I/O

Chapter 7 - Everybody Falls, It's How You Get Back Up – Testing and Debugging

Activity 1: Checking the Accuracy of the Functions Using Test Cases and Understanding Test-Driven Development (TDD)

For this activity, we'll develop the functions to parse the RecordFile.txt and CurrencyConversion.txt files and write test cases to check the accuracy of the functions. Follow these steps to implement this activity:

  1. Create a configuration file named parse.conf and write the configurations.
  2. Note that only two variables are of interest here, that is, currencyFile and recordFile. The rest are meant for other environment variables:

    CONFIGURATION_FILE

    currencyFile = ./CurrencyConversion.txt

    recordFile = ./RecordFile.txt

    DatabaseServer = 192.123.41.112

    UserId = sqluser

    Password = sqluser

    RestApiServer = 101.21.231.11

    LogFilePath = /var/project/logs

  3. Create a header file named CommonHeader.h and declare all the utility functions, that is, isAllNumbers(), isDigit(), parseLine(), checkFile(), parseConfig(), parseCurrencyParameters(), fillCurrencyMap(), parseRecordFile(), checkRecord(), displayCurrencyMap(), and displayRecords().

    #ifndef __COMMON_HEADER__H

    #define __COMMON_HEADER__H

    #include<iostream>

    #include<cstring>

    #include<fstream>

    #include<vector>

    #include<string>

    #include<map>

    #include<sstream>

    #include<iterator>

    #include<algorithm>

    #include<iomanip>

    using namespace std;

    // Forward declaration of global variables.

    extern string configFile;

    extern string recordFile;

    extern string currencyFile;

    extern map<string, float> currencyMap;

    struct record;

    extern vector<record>      vecRecord;

    //Structure to hold Record Data .

    struct record{

        int     customerId;

        string  firstName;

        string  lastName;

        int     orderId;

        int     productId;

        int     quantity;

        float   totalPriceRegional;

        string  currency;

        float   totalPriceUsd;

        

        record(vector<string> & in){

            customerId      = atoi(in[0].c_str());

            firstName       = in[1];

            lastName        = in[2];

            orderId         = atoi(in[3].c_str());

            productId       = atoi(in[4].c_str());

            quantity        = atoi(in[5].c_str());

            totalPriceRegional = static_cast<float>(atof(in[6].c_str()));

            currency        = in[7];

            totalPriceUsd   = static_cast<float>(atof(in[8].c_str()));

        }

    };

    // Declaration of Utility Functions..

    string trim (string &);

    bool isAllNumbers(const string &);

    bool isDigit(const string &);

    void parseLine(ifstream &, vector<string> &, char);

    bool checkFile(ifstream &, string &, string, char, string &);

    bool parseConfig();

    bool parseCurrencyParameters( vector<string> &);

    bool fillCurrencyMap();

    bool parseRecordFile();

    bool checkRecord(vector<string> &);

    void displayCurrencyMap();

    ostream& operator<<(ostream &, const record &);

    void displayRecords();

    #endif

  4. Create a file named Util.cpp and define all the utility functions. Write the following code to define the trim() function:

    #include<CommonHeader.h>

    // Utility function to remove spaces and tabs from start of string and end of string..

    string trim (string &str) { // remove space and tab from string.

        string res("");

        if ((str.find(' ') != string::npos) || (str.find(' ') != string::npos)){ // if space or tab found..

            size_t begin, end;

            if ((begin = str.find_first_not_of(" \t")) != string::npos){ // if string is not empty..

                end = str.find_last_not_of(" \t");

                if ( end >= begin )

                    res = str.substr(begin, end - begin + 1);

            }

        }else{

            res = str; // No space or tab found..

        }

        str = res;

        return res;

    }

  5. Write the following code to define the isAllNumbers(), isDigit(), and parseLine() functions:

    // Utility function to check if string contains only digits ( 0-9) and only single '.'

    // eg . 1121.23 , .113, 121. are valid, but 231.14.143 is not valid.

    bool isAllNumbers(const string &str){ // make sure, it only contains digit and only single '.' if any

        return ( all_of(str.begin(), str.end(), [](char c) { return ( isdigit(c) || (c == '.')); })

                 && (count(str.begin(), str.end(), '.') <= 1) );

    }

    //Utility function to check if string contains only digits (0-9)..

    bool isDigit(const string &str){

        return ( all_of(str.begin(), str.end(), [](char c) { return isdigit(c); }));

    }

    // Utility function, where single line of file <infile> is parsed using delimiter.

    // And store the tokens in vector of string.

    void parseLine(ifstream &infile, vector<string> & vec, char delimiter){

        string line, token;

        getline(infile, line);

        istringstream ss(line);

        vec.clear();

        while(getline(ss, token, delimiter)) // break line using delimiter

            vec.push_back(token);  // store tokens in vector of string

    }

  6. Write the following code to define the parseCurrencyParameters() and checkRecord() functions:

    // Utility function to check if vector string of 2 strings contain correct

    // currency and conversion ratio. currency should be 3 characters, conversion ratio

    // should be in decimal number format.

    bool parseCurrencyParameters( vector<string> & vec){

        trim(vec[0]);  trim(vec[1]);

        return ( (!vec[0].empty()) && (vec[0].size() == 3) && (!vec[1].empty()) && (isAllNumbers(vec[1])) );

    }

    // Utility function, to check if vector of string has correct format for records parsed from Record File.

    // CustomerId, OrderId, ProductId, Quantity should be in integer format

    // TotalPrice Regional and USD should be in decimal number format

    // Currecny should be present in map.

    bool checkRecord(vector<string> &split){

        // Trim all string in vector

        for (auto &s : split)

            trim(s);

        

        if ( !(isDigit(split[0]) && isDigit(split[3]) && isDigit(split[4]) && isDigit(split[5])) ){

            cerr << "ERROR: Record with customer id:" << split[0] << " doesnt have right DIGIT parameter" << endl;

            return false;

        }

        if ( !(isAllNumbers(split[6]) && isAllNumbers(split[8])) ){

            cerr << "ERROR: Record with customer id:" << split[0] << " doesnt have right NUMBER parameter" << endl;

            return false;

        }

        if ( currencyMap.find(split[7]) == currencyMap.end() ){

            cerr << "ERROR: Record with customer id :" << split[0] << " has currency :" << split[7] << " not present in map" << endl;

            return false;

        }

        return true;

    }

  7. Write the following code to define the checkFile() function:

    // Function to test initial conditions of file..

    // Check if file is present and has correct header information.

    bool checkFile(ifstream &inFile, string &fileName, string parameter, char delimiter, string &error){

        bool flag = true;

        inFile.open(fileName);

        if ( inFile.fail() ){

            error = "Failed opening " + fileName + " file, with error: " + strerror(errno);

            flag = false;

        }

        if (flag){

            vector<string> split;

            // Parse first line as header and make sure it contains parameter as first token.

            parseLine(inFile, split, delimiter);

            if (split.empty()){

                error = fileName + " is empty";

                flag = false;

            } else if ( split[0].find(parameter) == string::npos ){

                error = "In " + fileName + " file, first line doesnt contain header ";

                flag = false;

            }

        }

        return flag;

    }

  8. Write the following code to define parseConfig() function:

    // Function to parse Config file. Each line will have '<name> = <value> format

    // Store CurrencyConversion file and Record File parameters correctly.

    bool parseConfig() {

        ifstream coffle;

        string error;

        if (!checkFile(confFile, configFile, "CONFIGURATION_FILE", '=', error)){

            cerr << "ERROR: " << error << endl;

            return false;

        }

        bool flag = true;

        vector<string> split;

        while (confFile.good()){

            parseLine(confFile, split, '=');

            if ( split.size() == 2 ){

                string name = trim(split[0]);

                string value = trim(split[1]);

                if ( name == "currencyFile" )

                    currencyFile = value;

                else if ( name == "recordFile")

                    recordFile = value;

            }

        }

        if ( currencyFile.empty() || recordFile.empty() ){

            cerr << "ERROR : currencyfile or recordfile not set correctly." << endl;

            flag = false;

        }

        return flag;

    }

  9. Write the following code to define the fillCurrencyMap() function:

    // Function to parse CurrencyConversion file and store values in Map.

    bool fillCurrencyMap() {

        ifstream currFile;

        string error;

        if (!checkFile(currFile, currencyFile, "Currency", '|', error)){

            cerr << "ERROR: " << error << endl;

            return false;

        }

        bool flag = true;

        vector<string> split;

        while (currFile.good()){

            parseLine(currFile, split, '|');

            if (split.size() == 2){

                if (parseCurrencyParameters(split)){

                    currencyMap[split[0]] = static_cast<float>(atof(split[1].c_str())); // make sure currency is valid.

                } else {

                    cerr << "ERROR: Processing Currency Conversion file for Currency: "<< split[0] << endl;

                    flag = false;

                    break;

                }

            } else if (!split.empty()){

                cerr << "ERROR: Processing Currency Conversion , got incorrect parameters for Currency: " << split[0] << endl;

                flag = false;

                break;

            }

        }

        return flag;

    }

  10. Write the following code to define the parseRecordFile() function:

    // Function to parse Record File ..

    bool parseRecordFile(){

        ifstream recFile;

        string error;

        if (!checkFile(recFile, recordFile, "Customer Id", '|', error)){

            cerr << "ERROR: " << error << endl;

            return false;

        }

        bool flag = true;

        vector<string> split;

        while(recFile.good()){

            parseLine(recFile, split, '|');

            if (split.size() == 9){

                if (checkRecord(split)){

                    vecRecord.push_back(split); //Construct struct record and save it in vector...

                }else{

                    cerr << "ERROR : Parsing Record, for Customer Id: " << split[0] << endl;

                    flag = false;

                    break;

                }

            } else if (!split.empty()){

                cerr << "ERROR: Processing Record, for Customer Id: " << split[0] << endl;

                flag = false;

                break;

            }

        }

        return flag;

    }

  11. Write the following code to define the displayCurrencyMap() function:

    void displayCurrencyMap(){

         

        cout << "Currency MAP :" << endl;

        for (auto p : currencyMap)

            cout << p.first <<"  :  " << p.second << endl;

        cout << endl;

    }

    ostream& operator<<(ostream& os, const record &rec){

        os << rec.customerId <<"|" << rec.firstName << "|" << rec.lastName << "|"

           << rec.orderId << "|" << rec.productId << "|" << rec.quantity << "|"

           << fixed << setprecision(2) << rec.totalPriceRegional << "|" << rec.currency << "|"

           << fixed << setprecision(2) << rec.totalPriceUsd << endl;

        return os;

    }

  12. Write the following code to define the displayRecords() function:

    void displayRecords(){

        cout << " Displaying records with '|' delimiter" << endl;

        for (auto rec : vecRecord){

            cout << rec;

        }

        cout << endl;

    }

  13. Create a file named ParseFiles.cpp and call the parseConfig(), fillCurrencyMap(), and parseRecordFile() functions:

    #include <CommonHeader.h>

    // Global variables ...

    string configFile = "./parse.conf";

    string recordFile;

    string currencyFile;

    map<string, float>  currencyMap;

    vector<record>      vecRecord;

    int main(){

        // Read Config file to set global configuration variables.

        if (!parseConfig()){

            cerr << "Error parsing Config File " << endl;

            return false;

        }

        // Read Currency file and fill map

        if (!fillCurrencyMap()){

            cerr << "Error setting CurrencyConversion Map " << endl;

            return false;

        }

        if (!parseRecordFile()){

            cerr << "Error parsing Records File " << endl;

            return false;

        }

            displayCurrencyMap();

        displayRecords();

        return 0;

    }

  14. Open the compiler. Compile and execute the Util.cpp and ParseFiles.cpp files by writing the following command:

    g++ -c -g -I. -Wall Util.cpp

    g++ -g -I. -Wall Util.o ParseFiles.cpp -o ParseFiles

    The binary files for both will be generated.

    In the following screenshot, you will see that both commands are stored in the build.sh script and executed. After running this script, you will see that the latest Util.o and ParseFiles files have been generated:

    Figure 7.25: New files generated
    Figure 7.25: New files generated
  15. After running the ParseFiles executable, we'll receive the following output:
    Figure 7.26: New files generated
    Figure 7.26: New files generated
  16. Create a file named ParseFileTestCases.cpp and write test cases for the utility functions. Write the following test cases for the trim function:

    #include<gtest/gtest.h>

    #include"../CommonHeader.h"

    using namespace std;

    // Global variables ...

    string configFile = "./parse.conf";

    string recordFile;

    string currencyFile;

    map<string, float>  currencyMap;

    vector<record>      vecRecord;

    void setDefault(){

        configFile = "./parse.conf";

        recordFile.clear();

        currencyFile.clear();

        currencyMap.clear();

        vecRecord.clear();

    }

    // Test Cases for trim function ...

    TEST(trim, empty){

        string str="    ";

        EXPECT_EQ(trim(str), string());

    }

    TEST(trim, start_space){

        string str = "   adas";

        EXPECT_EQ(trim(str), string("adas"));

    }

    TEST(trim, end_space){

        string str = "trip      ";

        EXPECT_EQ(trim(str), string("trip"));

    }

    TEST(trim, string_middle){

        string str = "  hdgf   ";

        EXPECT_EQ(trim(str), string("hdgf"));

    }

    TEST(trim, single_char_start){

        string str = "c  ";

        EXPECT_EQ(trim(str), string("c"));

    }

    TEST(trim, single_char_end){

        string str = "   c";

        EXPECT_EQ(trim(str), string("c"));

    }

    TEST(trim, single_char_middle){

        string str = "      c  ";

        EXPECT_EQ(trim(str), string("c"));

    }

  17. Write the following test cases for the isAllNumbers function:

    // Test Cases for isAllNumbers function..

    TEST(isNumber, alphabets_present){

        string str = "11.qwe13";

        ASSERT_FALSE(isAllNumbers(str));

    }

    TEST(isNumber, special_character_present){

        string str = "34.^%3";

        ASSERT_FALSE(isAllNumbers(str));

    }

    TEST(isNumber, correct_number){

        string str = "54.765";

        ASSERT_TRUE(isAllNumbers(str));

    }

    TEST(isNumber, decimal_begin){

        string str = ".624";

        ASSERT_TRUE(isAllNumbers(str));

    }

    TEST(isNumber, decimal_end){

        string str = "53.";

        ASSERT_TRUE(isAllNumbers(str));

    }

  18. Write the following test cases for the isDigit function:

    // Test Cases for isDigit funtion...

    TEST(isDigit, alphabet_present){

        string str = "527A";

        ASSERT_FALSE(isDigit(str));

    }

    TEST(isDigit, decimal_present){

        string str = "21.55";

        ASSERT_FALSE(isDigit(str));

    }

    TEST(isDigit, correct_digit){

        string str = "9769";

        ASSERT_TRUE(isDigit(str));

    }

  19. Write the following test cases for the parseCurrencyParameters function:

    // Test Cases for parseCurrencyParameters function

    TEST(CurrencyParameters, extra_currency_chararcters){

        vector<string> vec {"ASAA","34.22"};

        ASSERT_FALSE(parseCurrencyParameters(vec));

    }

    TEST(CurrencyParameters, correct_parameters){

        vector<string> vec {"INR","1.44"};

        ASSERT_TRUE(parseCurrencyParameters(vec));

    }

  20. Write the following test cases for the checkFile function:

    //Test Cases for checkFile function...

    TEST(checkFile, no_file_present){

        string fileName = "./NoFile";

        ifstream infile;

        string parameter("nothing");

        char delimit =';';

        string err;

        ASSERT_FALSE(checkFile(infile, fileName, parameter, delimit, err));

    }

    TEST(checkFile, empty_file){

        string fileName = "./emptyFile";

        ifstream infile;

        string parameter("nothing");

        char delimit =';';

        string err;

        ASSERT_FALSE(checkFile(infile, fileName, parameter, delimit, err));

    }

    TEST(checkFile, no_header){

        string fileName = "./noHeaderFile";

        ifstream infile;

        string parameter("header");

        char delimit ='|';

        string err;

        ASSERT_FALSE(checkFile(infile, fileName, parameter, delimit, err));

    }

    TEST(checkFile, incorrect_header){

        string fileName = "./correctHeaderFile";

        ifstream infile;

        string parameter("header");

        char delimit ='|';

        string err;

        ASSERT_FALSE(checkFile(infile, fileName, parameter, delimit, err));

    }

    TEST(checkFile, correct_file){

        string fileName = "./correctHeaderFile";

        ifstream infile;

        string parameter("Currency");

        char delimit ='|';

        string err;

        ASSERT_TRUE(checkFile(infile, fileName, parameter, delimit, err));

    }

    Note

    The NoFile, emptyFile, noHeaderFile, and correctHeaderFile files that were used as input parameters in the preceding functions can be found here: https://github.com/TrainingByPackt/Advanced-CPlusPlus/tree/master/Lesson7/Activity01.

  21. Write the following test cases for the parseConfig function:

    //Test Cases for parseConfig function...

    TEST(parseConfig, missing_currency_file){

        setDefault();

        configFile = "./parseMissingCurrency.conf";

        ASSERT_FALSE(parseConfig());

    }

    TEST(parseConfig, missing_record_file){

        setDefault();

        configFile = "./parseMissingRecord.conf";

        ASSERT_FALSE(parseConfig());

    }

    TEST(parseConfig, correct_config_file){

        setDefault();

        configFile = "./parse.conf";

        ASSERT_TRUE(parseConfig());

    }

    Note

    The parseMissingCurrency.conf, parseMissingRecord.conf, and parse.conf files that were used as input parameters in the preceding functions can be found here: https://github.com/TrainingByPackt/Advanced-CPlusPlus/tree/master/Lesson7/Activity01.

  22. Write the following test cases for the fillCurrencyMap function:

    //Test Cases for fillCurrencyMap function...

    TEST(fillCurrencyMap, wrong_delimiter){

        currencyFile = "./CurrencyWrongDelimiter.txt";

        ASSERT_FALSE(fillCurrencyMap());

    }

    TEST(fillCurrencyMap, extra_column){

        currencyFile = "./CurrencyExtraColumn.txt";

        ASSERT_FALSE(fillCurrencyMap());

    }

    TEST(fillCurrencyMap, correct_file){

        currencyFile = "./CurrencyConversion.txt";

        ASSERT_TRUE(fillCurrencyMap());

    }

    Note

    The CurrencyWrongDelimiter.txt, CurrencyExtraColumn.txt, and CurrencyConversion.txt files that were used as input parameters in the preceding functions can be found here: https://github.com/TrainingByPackt/Advanced-CPlusPlus/tree/master/Lesson7/Activity01.

  23. Write the following test cases for the parseRecordFile function:

    //Test Cases for parseRecordFile function...

    TEST(parseRecordFile, wrong_delimiter){

        recordFile = "./RecordWrongDelimiter.txt";

        ASSERT_FALSE(parseRecordFile());

    }

    TEST(parseRecordFile, extra_column){

        recordFile = "./RecordExtraColumn.txt";

        ASSERT_FALSE(parseRecordFile());

    }

    TEST(parseRecordFile, correct_file){

        recordFile = "./RecordFile.txt";

        ASSERT_TRUE(parseRecordFile());

    }

    The RecordWrongDelimiter.txt, RecordExtraColumn.txt, and RecordFile.txt files that were used as input parameters in the preceding functions can be found here: https://github.com/TrainingByPackt/Advanced-CPlusPlus/tree/master/Lesson7/Activity01.

  24. Open the compiler. Compile and execute the Util.cpp and ParseFileTestCases.cpp files by writing the following commands:

    g++ -c -g -Wall ../Util.cpp -I../

    g++ -c -g -Wall ParseFileTestCases.cpp

    g++ -g -Wall Util.o ParseFileTestCases.o -lgtest -lgtest_main -pthread -o ParseFileTestCases

    The following is a screenshot of this. You will see all the commands stored in Test.make script file. Once executed, it will create the binary program that was meant for unit testing called ParseFileTestCases. You will also notice that a directory has been created in Project called unitTesting. In this directory, all the unit testing-related code is written, and a binary file is created. Also, the dependent library of the project, Util.o, is also created by compiling the project in the Util.cpp file:

    Figure 7.27: Executing all commands present in the script file
  25. Type the following command to run all the test cases:

    ./ParseFileTestCases

    The output on the screen will display the total tests running, that is, 31 from 8 test suites. It will also display the statistics of individual test suites, along with pass/fail results:

Figure 7.28: All tests running properly
Figure 7.28: All tests running properly

Below is the screenshot of the next tests:

Figure 7.29: All tests running properly
Figure 7.29: All tests running properly

Finally, we checked the accuracy of the functions that we developed by parsing two files with the help of our test cases. This will ensure that our project will be running fine when it's integrated with different functions/modules that have test cases.