* Creating Tests To run tests with justbuild, we do /not/ have a dedicated ~test~ subcommand. Instead, we consider tests being a specific action that generates a test report. Consequently, we use the ~build~ subcommand to build the test report, and thereby run the test action. Test actions, however, are slightly different from normal actions in that we don't want the build of the test report to be aborted if a test action fails (but still, we want only successfully actions taken from cache). Rules defining targets containing such special actions have to identify themselves as /tainted/ by specifying a string explaining why such special actions are justified; in our case, the string is ~"test"~. Besides the implicit marking by using a tainted rule, those tainting strings can also be explicitly assigned by the user in the definition of a target, e.g., to mark test data. Any target has to be tainted with (at least) all the strings any of its dependencies is tainted with. In this way, it is ensured that no test target will end up in a production build. For the remainder of this section, we expect to have the project files available resulting from successfully completing the tutorial section on /Building Hello World/. We will demonstrate how to write a test binary for the ~greet~ library and a shell test for the ~helloworld~ binary. ** Creating a C++ test binary First, we will create a C++ test binary for testing the correct functionality of the ~greet~ library. Therefore, we need to provide a C++ source file that performs the actual testing and returns non-~0~ on failure. For simplicity reasons, we do not use a testing framework for this tutorial. A simple test that captures standard output and verifies it with the expected output should be provided in the file ~greet/greet.test.cpp~: #+BEGIN_SRC cpp #include #include #include #include #include "greet/greet.hpp" template auto capture_stdout(std::function const& func) -> std::string { int fd[2]; if (pipe(fd) < 0) return {}; int fd_stdout = dup(fileno(stdout)); fflush(stdout); dup2(fd[1], fileno(stdout)); func(); fflush(stdout); std::string buf(kMaxBufSize, '\0'); auto n = read(fd[0], &(*buf.begin()), kMaxBufSize); close(fd[0]); close(fd[1]); dup2(fd_stdout, fileno(stdout)); return buf.substr(0, n); } auto test_greet(std::string const& name) -> bool { auto expect = std::string{"Hello "} + name + "!\n"; auto result = capture_stdout([&name] { greet(name); }); std::cout << "greet output: " << result << std::endl; return result == expect; } int main() { return test_greet("World") && test_greet("Universe") ? 0 : 1; } #+END_SRC Next, a new test target needs to be created in module ~greet~. This target uses the rule ~["@", "rules", "CC/test", "test"]~ and needs to depend on the ~"greet"~ target. To create the test target, add the following to ~greet/TARGETS~: #+BEGIN_SRC js { "greet": { /* ... unchanged ... */ } , "test": { "type": ["@", "rules", "CC/test", "test"] , "name": ["test_greet"] , "srcs": ["greet.test.cpp"] , "private-deps": ["greet"] } } #+END_SRC Before we can run the test, a proper default module for ~CC/test~ must be provided. By specifying the appropriate target in this module the default test runner can be overwritten by a different test runner fom the rule's workspace root. However, in our case, we want to use the default runner and therefore it is sufficient to create an empty module: Create ~tutorial-defaults/CC/test/TARGETS~: #+BEGIN_SRC sh $ mkdir -p tutorial-defaults/CC/test $ echo '{}' > tutorial-defaults/CC/test/TARGETS #+END_SRC Now we can run the test (i.e., build the test result): #+BEGIN_SRC sh $ just-mr build greet test INFO: Requested target is [["@","tutorial","greet","test"],{}] INFO: Analysed target [["@","tutorial","greet","test"],{}] INFO: Export targets found: 0 cached, 0 uncached, 0 not eligible for caching INFO: Target tainted ["test"]. INFO: Discovered 5 actions, 3 trees, 0 blobs INFO: Building [["@","tutorial","greet","test"],{}]. INFO: Processed 5 actions, 2 cache hits. INFO: Artifacts built, logical paths are: result [359456b7b6a1f91dc435e483cc2b1fc4e8981bf0:8:f] stderr [e69de29bb2d1d6434b8b29ae775ad8c2e48c5391:0:f] stdout [21ac68aa0bceffd6d5ad49ba17df80f44f7f3735:59:f] time-start [c119fd0e22e79555f2446410fd90de4c091039d9:11:f] time-stop [c119fd0e22e79555f2446410fd90de4c091039d9:11:f] (1 runfiles omitted.) INFO: Target tainted ["test"]. $ #+END_SRC Note that the target is correctly reported as tainted with ~"test"~. It will produce 3 additional actions for compiling, linking and running the test binary. The result of the test target are 5 artifacts: ~result~ (containing ~UNKNOWN~, ~PASS~, or ~FAIL~), ~stderr~, ~stdout~, ~time-start~, and ~time-stop~, and a single runfile (omitted in the output above), which is a tree artifact with the name ~test_greet~ that contains all of the above artifacts. The test was run successfully as otherwise all reported artifacts would have been reported as ~FAILED~ in the output, and justbuild would have returned the exit code ~2~. To immediately print the standard output produced by the test binary on the command line, the ~-P~ option can be used. Argument to this option is the name of the artifact that should be printed on the command line, in our case ~stdout~: #+BEGIN_SRC sh $ just-mr build greet test --log-limit 1 -P stdout greet output: Hello World! greet output: Hello Universe! $ #+END_SRC Note that ~--log-limit 1~ was just added to omit justbuild's ~INFO:~ prints. Our test binary does not have any useful options for directly interacting with it. When working with test frameworks, it sometimes can be desirable to get hold of the test binary itself for manual interaction. The running of the test binary is the last action associated with the test and the test binary is, of course, one of its inputs. #+BEGIN_SRC sh $ just-mr analyse --request-action-input -1 greet test INFO: Requested target is [["@","tutorial","greet","test"],{}] INFO: Request is input of action #-1 INFO: Result of input of action #-1 of target [["@","tutorial","greet","test"],{}]: { "artifacts": { "runner.sh": {"data":{"path":"CC/test/test_runner.sh","repository":"just-rules"},"type":"LOCAL"}, "test": {"data":{"id":"70769663e63241b9a30cb32d03b377374813ebd9","path":"test_greet"},"type":"ACTION"} }, "provides": { "cmd": [ "sh", "./runner.sh" ], "env": { }, "may_fail": "CC test test_greet failed", "output": [ "result", "stderr", "stdout", "time-start", "time-stop" ], "output_dirs": [ ] }, "runfiles": { } } INFO: Target tainted ["test"]. $ #+END_SRC The provided data also shows us the precise description of the action for which we request the input. This allows us to manually rerun the action. Or we can simply interact with the test binary manually after installing the inputs to this action. Requesting the inputs of an action can also be useful when debugging a build failure. #+BEGIN_SRC sh $ just-mr install -o work --request-action-input -1 greet test INFO: Requested target is [["@","tutorial","greet","test"],{}] INFO: Request is input of action #-1 INFO: Analysed target [["@","tutorial","greet","test"],{}] INFO: Export targets found: 0 cached, 0 uncached, 1 not eligible for caching INFO: Target tainted ["test"]. INFO: Discovered 8 actions, 5 trees, 0 blobs INFO: Building input of action #-1 of [["@","tutorial","greet","test"],{}]. INFO: Processed 7 actions, 7 cache hits. INFO: Artifacts can be found in: /tmp/tutorial/work/runner.sh [52568676f1efba1bec099cdd325a54a415a1474f:686:f] /tmp/tutorial/work/test [e9fb451860442c37d6d84c01ce1c698597b88000:139088:x] INFO: Target tainted ["test"]. $ cd work/ $ ./test greet output: Hello World! greet output: Hello Universe! $ echo $? 0 $ cd .. $ rm -rf work #+END_SRC ** Creating a shell test Similarly, to create a shell test for testing the ~helloworld~ binary, a test script must be provided: ~test.sh~: #+BEGIN_SRC sh set -e [ "$(./helloworld)" = "Hello Universe!" ] #+END_SRC The test target for this shell tests uses the rule ~["@", "rules", "shell/test", "script"]~ and must depend on the ~"helloworld"~ target. To create the test target, add the following to the top-level ~TARGETS~ file: #+BEGIN_SRC js { "helloworld": { /* ... unchanged ... */ } , "test": { "type": ["@", "rules", "shell/test", "script"] , "name": ["test_helloworld"] , "test": ["test.sh"] , "deps": ["helloworld"] } } #+END_SRC Similar to the binary tests, also for shell tests we need to provide at least an empty module for the test rule defaults: Create ~tutorial-defaults/shell/test/TARGETS~: #+BEGIN_SRC sh $ mkdir -p tutorial-defaults/shell/test $ echo '{}' > tutorial-defaults/shell/test/TARGETS #+END_SRC Now we can run the shell test (i.e., build the test result): #+BEGIN_SRC sh $ just-mr build test INFO: Requested target is [["@","tutorial","","test"],{}] INFO: Analysed target [["@","tutorial","","test"],{}] INFO: Export targets found: 0 cached, 0 uncached, 0 not eligible for caching INFO: Target tainted ["test"]. INFO: Discovered 5 actions, 4 trees, 0 blobs INFO: Building [["@","tutorial","","test"],{}]. INFO: Processed 5 actions, 4 cache hits. INFO: Artifacts built, logical paths are: result [7ef22e9a431ad0272713b71fdc8794016c8ef12f:5:f] stderr [e69de29bb2d1d6434b8b29ae775ad8c2e48c5391:0:f] stdout [e69de29bb2d1d6434b8b29ae775ad8c2e48c5391:0:f] time-start [9b4a96cc3b929d2909f74395d0317e93a59e621f:11:f] time-stop [9b4a96cc3b929d2909f74395d0317e93a59e621f:11:f] (1 runfiles omitted.) INFO: Target tainted ["test"]. $ #+END_SRC The result is also similar, containing also the 5 artifacts and a single runfile (omitted in the output above), which is a tree artifact with the name ~test_helloworld~ that contains all of the above artifacts. ** Creating a compound test target As most people probably do not want to call every test target by hand, it is desirable to compound test target that triggers the build of multiple test reports. To do so, an ~"install"~ target can be used. The field ~"deps"~ of an install target is a list of targets for which the runfiles are collected. As for the tests the runfiles happen to be tree artifacts named the same way as the test and containing all test results, this is precisely what we need. Furthermore, as the dependent test targets are tainted by ~"test"~, also the compound test target must be tainted by the same string. To create the compound test target combining the two tests above (the test ~"test"~ from the current module and ~["greet", "test"]~, i.e., the target ~"test"~ from the module ~"greet"~) add the following to the top-level ~TARGETS~ file: #+BEGIN_SRC js { "helloworld": { /* ... unchanged ... */ } , "test": { /* ... unchanged ... */ } , "all_tests": { "type": "install" , "tainted": ["test"] , "deps": [ "test" , ["greet", "test"] ] } } #+END_SRC Now we can run all tests at once by just building the compound test target ~"all_tests"~: #+BEGIN_SRC sh $ just-mr build all_tests INFO: Requested target is [["@","tutorial","","all_tests"],{}] INFO: Analysed target [["@","tutorial","","all_tests"],{}] INFO: Export targets found: 0 cached, 0 uncached, 0 not eligible for caching INFO: Target tainted ["test"]. INFO: Discovered 8 actions, 5 trees, 0 blobs INFO: Building [["@","tutorial","","all_tests"],{}]. INFO: Processed 8 actions, 8 cache hits. INFO: Artifacts built, logical paths are: test_greet [7116c231e3a6da3d23b0549340d75d36a0a0c4ef:285:t] test_helloworld [c5a0d9fb4ba586d88dc5cddadedb6ddb670e95c4:283:t] INFO: Target tainted ["test"]. $ #+END_SRC As a result it reports the runfiles (result directories) of both tests as artifacts. Both tests ran successfully as none of those artifacts in this output above are tagged as ~FAILED~.