Wednesday, February 28, 2018

redesigning the constructChildPath();

inpuPath a former member of class FileBot. I decided to removed this member variable in the recent design for constructChildPath(). There are 5 test cases for this design specification - capturing files, 2 of the test case were failed, which is test case 4 and 5.
  1. one file is captured without sub folder.
  2. two files are captured without sub folder.
  3. one files are captured when there is a sub folder.
  4. one files are captured in directory under test and one file in sub folder.
  5. zero file are captured in directory under test and one file in sub folder.
The root cause for this failure was due to the misused of inputPath as reference point in constructChildPath().
void FileBot::constructChildPath()
{
    vector<path> pathList;
 
    // where am I now?
    BOOST_LOG_TRIVIAL(info) << "current path: " << inputPath << inputPath.filename() << endl;
 
    // capture the paths/files list under the source
    copy(filesystem::directory_iterator(inputPath), filesystem::directory_iterator(), back_inserter(pathList));

    ...
    ...
}
For this purpose, I decide to remove this member variable to make constructChildPath() more robust in processing file directory. To do this, the first make over is to build a method to construct the path to feed directory_iterator().
string FileBot::constructPathAddress(FileNode *node)
{
    if( root == nullptr )
        return "";

    string filePath = "";

    if( node->getParentNode() != nullptr )
        filePath = constructPathAddress(node->getParentNode());

    return filePath + node->getName().string() + string(1, boost::filesystem::path::preferred_separator);
}
Unlike the previous version, the constructChildPath() has now been transformed into a recursive function. Take note on the inputPath is getting the outcome from constructPathAddress() and then feed into directory_iterator().
void FileBot::constructChildPath(FileNode *currentRootNode)
{
    vector<path> pathList;

    // construct path address
    string inputPath = constructPathAddress(currentRootNode);

    // capture the paths/files list under the source
    copy(filesystem::directory_iterator(inputPath), filesystem::directory_iterator(), back_inserter(pathList));

    std::sort(pathList.begin(), pathList.end());

    // scan through path and those files sit in that path
    // scan for files in sub directory if there is any sub directory in that path
    for(vector<path>::const_iterator it(pathList.begin()); it != pathList.end(); ++it) {
        string file = FileHelper::convertPathToString((*it));

        if( is_directory(file) ) {
            FileNode *node = new FileNode();
            node->setName(file.substr(file.find_last_of(boost::filesystem::path::preferred_separator) + 1, file.length()));
            node->setType('d');
            node->setParentNode(currentRootNode);

            currentRootNode->sibling.push_back(*node);

            BOOST_LOG_TRIVIAL(info) << "subFolderName : " << node->getName().string() << " : " << node;

            constructChildPath(node);
        }
        else {
            FileNode *node = new FileNode();
            node->setName(file.substr(file.find_last_of(boost::filesystem::path::preferred_separator) + 1, file.length()));
            node->setType('f');
            node->setParentNode(currentRootNode);

            currentRootNode->sibling.push_back(*node);
        }
    }
}
Last is to reassign the task of constructor to validatePath(). I just felt that this task doesn't suitable to put into the constructor since it only use by constructParentPath().
path FileBot::validatePath(boost::filesystem::path filePath)
{
    // remove trailing file separator if found any
    char lastChar = filePath.generic_string().at(filePath.generic_string().length() - 1);
    if( lastChar == '/' )
        filePath = filePath.generic_string().substr(0, filePath.generic_string().length() - 1);

    return filePath;
}
Then in constructParentPath() just call this method to update the input value again.
FileNode* FileBot::constructParentPath(boost::filesystem::path inputPath)
{
    inputPath = validatePath(inputPath);

    // bail out if the path doesn't exists
    if( !exists(inputPath.generic_string()) )
        return nullptr;

    ...
    ...
}

Sunday, February 25, 2018

Grouping test cases with test suite

Sometimes I just want to skip those unit test cases that have already been pass and focus those test cases on my current development. And then re-run again the whole unit test case again when I have finished the task to ensure no flaw happened to the other unit test cases. Sometimes it is so annoying to run all unit test cases that are not relevant to the task I'm currently working on. I found out one trick that could allow me to control which unit test code should execute and which should stop.

The code snippet below is the ordinary declaration for my unit test code, this declaration form will be executed whenever the code were run.
BOOST_AUTO_TEST_CASE(TL_1)
{
   ...
}
Then later I added some spice to tell the framework whether the unit test case should be executed or not. Following code snippet is the trick I have done for this purpose.
BOOST_AUTO_TEST_CASE(TL_1, *boost::unit_test::enable_if<false>())
{
   ...
}
In my case, this would be a great help. I don't need to wait for all unit test cases are finished in order to see the result. Some more there is only one or two test cases I can really focus on for the same tasks at the same time. Now, as my unit test code are expanding, I need more flexibility and the ability to group relevant test cases together in one source file, and then I can decide which test code to run. Here is the code snippet for the solution.
BOOST_AUTO_TEST_SUITE(testlab, *boost::unit_test::label("testlab"))

BOOST_AUTO_TEST_CASE(TL_1, *boost::unit_test::enable_if<false>())
{
   ...
   ...
}

BOOST_AUTO_TEST_SUITE_END()
With this code, the test case are grouped under the test suite named testlab. When the code is run, all test cases under this suite will be execute. But there is one flaw in this code, the enable_if() is not working. The test will continue to execute even though it is false. Not only that, neither disabled() and enabled() are working. Luckily precondition() comes to a rescue. The code snippet below tells the framework to continue to execute the test case.
struct skipTest
{
    bool skip;
    skipTest(bool s) : skip(s){}

    boost::test_tools::assertion_result operator()(boost::unit_test::test_unit_id)
    {
        if( skip == false )
            return true;
        else
            return false;
    }
};

BOOST_AUTO_TEST_CASE(TL_1, *boost::unit_test::precondition(skipTest(false)))
{
   ...
   ...
}
As of now, the test suite will help me to control which group of test case to execute and precondition() will allow me to choose which test case should skip whenever I want during the development.

Friday, February 16, 2018

Improving the piece in constructParentPath() and constructChildPath()

In the existing FileBot class design, I have an inputPath class member variable.
class FileBot
{
public:
   FileBot(boost::filesystem::path filePath);

   int constructParentPath();
   void constructChildPath();

   ...
   ...

private:
   boost::filesystem::path inputPath;
};
The purpose of this variable is to keep track of the starting point where the files should begin to search with. It is so important that nothing is missed during the initialization, otherwise nothing will get from the search. Thus I'm doing the initialization through the constructor.
FileBot::FileBot(boost::filesystem::path filePath)
{
    // remove trailing file separator if found any
    char lastChar = filePath.generic_string().at(filePath.generic_string().length() - 1);
    if( lastChar == '/' )
        inputPath = filePath.generic_string().substr(0, filePath.generic_string().length() - 1);
    else
        inputPath = filePath;
}
In the client code, the programmer must instantiate the class through the following way. Indirectly the variable will get initialized as well.
FileBot fb("/home");

// or

FileBot *fb = new FileBot("/home");
And then this variable was also incorporated in other methods such as loading path into memory. There are 2 scenarios on loading path into memory; the first scenario is to load the parent path into memory. Say for example when the given input value is /home/user, then only the particular home and user were constructed into memory. Any other files located under the /home directory will be ignored. This is the piece of this construct:
int FileBot::constructParentPath()
{
    // bail out if the path doesn't exists
    if( !exists(inputPath.generic_string()) )
        return 0;

    // construct the parent path first
    string path = inputPath.generic_string();
    FileNode *parent = NULL;

    typedef split_iterator<string::iterator> string_split_iterator;
    for( string_split_iterator it = make_split_iterator(path, first_finder("/", is_equal()));
         it != string_split_iterator();
         ++it)
    {
        FileNode *node = new FileNode();
        node->setType('d');
        node->setName(copy_range<string>(*it));

        cout << "value inserted: [" << copy_range<string>(*it) << "]" << endl;

        if( parent == NULL ) {
            fileList.push_back(*node);
        }
        else
            parent->sibling.push_back(*node);

        parent = node;

        cout << "parent: " << parent;
        cout << " parent value: " << parent->getName();
        cout << " sibling size: " << parent->sibling.size() << endl;
    }

    // keep the current path in memory
    root = parent;

    cout << "***** debug : root node " << root << " : " << root->getName() << endl;

    return 1;
}

The second scenario would be loading the child path into memory. This time it will load all the files under the directory and expand to load the files in any other subdirectories (if any). Continuing from the previous example, /home/user, this section will load the files under the user directory, including the underlying sub directory. The construct for this is as follows:
void FileBot::constructChildPath()
{
    vector<path> pathList;

    // where am I now?
    BOOST_LOG_TRIVIAL(info) << "current path: " << inputPath << inputPath.filename() << endl;

    // capture the paths/files list under the source
    copy(filesystem::directory_iterator(inputPath), filesystem::directory_iterator(), back_inserter(pathList));

    std::sort(pathList.begin(), pathList.end());

    // scan through path and those files sit in that path
    // scan for files in sub directory if there is any sub directory in that path
    for(vector<path>::const_iterator it(pathList.begin()); it != pathList.end(); ++it) {
        string file = FileHelper::convertPathToString((*it));

        // extract the file name from the path
        file = file.substr(file.find_last_of(boost::filesystem::path::preferred_separator) + 1, file.length());

        if( is_directory(file) ) {
            cout << file << " is a directory." << endl;

            FileNode *node = new FileNode();
            node->setName(inputPath.filename());
            node->setType('d');

            root->sibling.push_back(*node);
        }
        else {
            FileNode *node = new FileNode();
            node->setName(file);
            node->setType('f');

            root->sibling.push_back(*node);
        }
    }
}
To ensure my code is always implemented correctly, the following test case must return a successful result:
  • load 1 level parent path.
  • load 2 level parent path.
  • load 3 level parent path.
  • file separator appear at the end of a path.
  • test on one file is captured without sub folder.
  • test on two files are captured without sub folder.
  • test on one files are captured when there is a sub folder.