2023-02-24

Qbs tutorial part 2/5: Tests

Tests


In the last part we looked at the overal structure of the project as well as the main project file OctoMY.qbs and how it referred to sub-projects. In this part we will go into the sub-project file test/tests.qbs in detail. But first our file tree as a reference:

.
├── OctoMY.qbs
├── src
│   ├── agent
│   │   ├── AgentMain.cpp
│   │   ├── AgentMain.hpp
│   │   ├── app.qbs
│   │   └── README.md
│   ├── apps.qbs
│   ├── combined
│   │   └── lib.qbs
│   ├── libs
│   │   ├── libagent
│   │   │   ├── agent
│   │   │   │   ├── Agent.cpp
│   │   │   │   ├── Agent.hpp
│   │   │   │   └── AgentWindow.ui
│   │   │   ├── lib.qbs
│   │   │   └── README.md
│   ├── libs.qbs
├── test
│   ├── common_test
│   │   ├── Common_test.hpp
│   │   ├── CommsTester.cpp
│   │   ├── CommsTester.hpp
│   │   ├── mock
│   │   │   ├── MockCourier.cpp
│   │   │   └── MockCourier.hpp
│   │   ├── resources
│   │   │   ├── icons
│   │   │   │   ├── profile.svg
│   │   │   │   ├── stress.svg
│   │   │   │   └── test.svg
│   │   │   └── test_resources.qrc
│   │   ├── Utility_test.cpp
│   │   └── Utility_test.hpp
│   ├── testLogHandler
│   │   ├── TestLogHandler.cpp
│   │   ├── TestLogHandler.hpp
│   │   └── test.qbs
│   ├── testTryToggle
│   │   ├── TestTryToggle.cpp
│   │   ├── TestTryToggle.hpp
│   │   └── test.qbs
│   └── tests.qbs
└── integration
   └── qbs
     └── imports
            ├── OctoMYApp.qbs
            ├── OctoMYLibProbe.qbs
            ├── OctoMYAutoLib.qbs
            ├── OctoMYTestProbe.qbs
            ├── OctoMYAutoTest.qbs
            ├── OctoMYFiles.qbs
            ├── OctoMYQtDepends.qbs
            └── utils.js


Looking at test/tests.qbs it looks like this:


import qbs.FileInfo

Project{
    name: "Tests"
    property string srcDir: FileInfo.cleanPath(FileInfo.joinPaths(project.sourceDirectory, "src")) + FileInfo.pathSeparator()
    property string projectDir: FileInfo.cleanPath(project.sourceDirectory) + FileInfo.pathSeparator()
    property string testDir: FileInfo.cleanPath(FileInfo.joinPaths(projectDir, "test")) + FileInfo.pathSeparator()
    property string commonTestDir: testDir + "common_test" + FileInfo.pathSeparator()
    property string commonTestLib: commonTestDir + "lib.qbs"
    
    // Enumerate OctoMY test projects
    OctoMYTestProbe {
        id: octomyTests
        searchPath: testDir
    }
    references: octomyTests.testFiles.concat([commonTestDir])
}


As you can clearly see, this is also a project. Since it is referred to by the top-level project it is in fact a sub-project, and you can see that the name it will display in QtCreator is "Tests".

Further you can see that it has some notable properties:
  • srcDir
  • projectDir
  • testDir
  • commonTestDir
  • commonTestLib
These are set using JavaScript expressions that use the Qbs FileInfo service. Qbs has many services or libraries with functions that help us with all kinds of things ranging from file and string manipulation to getting information about the platform.

Next we see a new item called OctoMYTestProbe. You might have put two together already and realized where this was defined. In our main project file we used a property called qbsSearchPaths to ask Qbs to look for our user defined re-usable components. In that folder there was an item called OctoMYTestProbe.qbs that Qbs now conveniently has loaded and made available to us.

NOTE: components are only made available in files referred to by the main project for technical reasons. This is one of many reasons why it is a good idea to separate your Qbs project into multiple files, for all but the smalest of projects.

Qbs has a concept of Probes. The job of a probe is to look for things or perform expensive calculations. Probes are executed before the build starts and Qbs has some clever caching mechanisms that ensure they are only executed once. If you are doing anything intense, be it search through folders, calculate hashes or whatever else, Probes are your best friend. Probes also promote code re-use.

Our OctoMYTestProbe.qbs probe looks like this:

import qbs.File
import qbs.FileInfo

Probe{
    // Parameters
    property string name: "OctoMYTestProbe"
    property string searchPath: FileInfo.cleanPath(FileInfo.joinPaths(project.sourceDirectory, "test")) + FileInfo.pathSeparator()
    property string testFileName: "test.qbs"
    property string testDefinePrefix: "OC_USE_TEST_"
    property string commonTestDir:  FileInfo.cleanPath(FileInfo.joinPaths(searchPath, "common_test")) + FileInfo.pathSeparator()
    // Outputs
    property stringList testNames: []
    property stringList testFolders: []
    property stringList testFiles: []
    property stringList testDefines: []
    configure: {
        found=false;
        var raw = File.directoryEntries(searchPath, File.Dirs | File.NoDot | File.NoDotDot);
        testNames = (raw || []).filter(function(dir){
            if(testFileName){
                return dir.startsWith("test") && File.exists(FileInfo.joinPaths(searchPath, dir, testFileName));
            }
            else{
                return dir.startsWith("test");
            }
        });
        testFolders = testNames.map(function(testName){
            return FileInfo.joinPaths(searchPath, testName);
        })
        testFiles = testFolders.map(function(testFolder){
            return FileInfo.joinPaths(testFolder, testFileName);
        })
        testDefines = testNames.map(function(testName){
            return testDefinePrefix + testName.toUpperCase();
        })
        found = testNames.length > 0;
        console.info("Probing returned for '" + name+"':");
        console.info(" + found="+found);
        console.info(" + searchPath="+JSON.stringify(searchPath, null, "\t"));
        console.info(" + testNames="+JSON.stringify(testNames, null, "\t"));
        console.info(" + testFolders="+JSON.stringify(testFolders, null, "\t"));
        console.info(" + testFiles="+JSON.stringify(testFiles, null, "\t"));
    }
}

As you can see, the probe uses the File service to look for directories beginning with "test" that contains a file called "test.qbs".

It sets the special property found to true if the number of folders found is more than zero.

It also logs a bunch of stuff to the console.

Qbs Supports printing of debug strings to the console using console.info() which can be very useful while debugging.

The probe will be ran and produce some useful properties for us:
  • testNames - a list of the names of tests found in our project
  • testFolders - the folders for each of our tests
  • testFiles - the test.qbs file for each test
  • testDefines - some defines that refer to our test names
Now when we go back to test/tests.qbs and have a closer look, we will see this:

...
    OctoMYTestProbe {
        id: octomyTests
        searchPath: testDir
    }
    references: octomyTests.testFiles.concat([commonTestDir])
...

As you can tell, we are making an instance of the OctoMYTestProbe called octomyTests, and we pass the calcualted property testFiles, which is a list of qbs files, one for each test, to the references. In other words, we are telling Qbs to read in all these test project qbs files as part of the Test project. In QtCreator you will see that it genereates a full list of tests in the project tree.

So why did we go to all this trouble instead of just naming the tests manually in a list?

Well, now we never have to change this project file again. Whenever we create a new test, it will magically appear thanks to our clever Probe🎉.


This concludes the second part in a series on using Qbs to our benefit and joy in a non-trivial project, namely OctoMY™

In the next part we will look at src/libs.qbs and how it relates to the main project file.

Illustrations borrowed from the amazing collection at https://ericjoyner.com/

No comments:

Post a Comment