diff --git a/contrib/kyua/AUTHORS b/contrib/kyua/AUTHORS index ac0998fb937c..26bfcd3471d3 100644 --- a/contrib/kyua/AUTHORS +++ b/contrib/kyua/AUTHORS @@ -1,11 +1,12 @@ # This is the official list of Kyua authors for copyright purposes. # # This file is distinct from the CONTRIBUTORS files; see the latter for # an explanation. # # Names are sorted alphabetically and should be added to this file as: # # * Name # * Organization * Google Inc. +* Igor Ostapenko diff --git a/contrib/kyua/cli/cmd_config_test.cpp b/contrib/kyua/cli/cmd_config_test.cpp index f084f99bb90a..a5f6930ba027 100644 --- a/contrib/kyua/cli/cmd_config_test.cpp +++ b/contrib/kyua/cli/cmd_config_test.cpp @@ -1,144 +1,146 @@ // Copyright 2011 The Kyua Authors. // All rights reserved. // // Redistribution and use in source and binary forms, with or without // modification, are permitted provided that the following conditions are // met: // // * Redistributions of source code must retain the above copyright // notice, this list of conditions and the following disclaimer. // * Redistributions in binary form must reproduce the above copyright // notice, this list of conditions and the following disclaimer in the // documentation and/or other materials provided with the distribution. // * Neither the name of Google Inc. nor the names of its contributors // may be used to endorse or promote products derived from this software // without specific prior written permission. // // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS // "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT // LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR // A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT // OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT // LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #include "cli/cmd_config.hpp" #include #include #include "cli/common.ipp" #include "engine/config.hpp" #include "utils/cmdline/globals.hpp" #include "utils/cmdline/parser.hpp" #include "utils/cmdline/ui_mock.hpp" #include "utils/config/tree.ipp" #include "utils/optional.ipp" namespace cmdline = utils::cmdline; namespace config = utils::config; using cli::cmd_config; using utils::none; namespace { /// Instantiates a fake user configuration for testing purposes. /// /// The user configuration is populated with a collection of test-suite /// properties and some hardcoded values for the generic configuration options. /// /// \return A new user configuration object. static config::tree fake_config(void) { config::tree user_config = engine::default_config(); user_config.set_string("architecture", "the-architecture"); + user_config.set_string("execenvs", "the-env"); user_config.set_string("parallelism", "128"); user_config.set_string("platform", "the-platform"); //user_config.set_string("unprivileged_user", ""); user_config.set_string("test_suites.foo.bar", "first"); user_config.set_string("test_suites.foo.baz", "second"); return user_config; } } // anonymous namespace ATF_TEST_CASE_WITHOUT_HEAD(all); ATF_TEST_CASE_BODY(all) { cmdline::args_vector args; args.push_back("config"); cmd_config cmd; cmdline::ui_mock ui; ATF_REQUIRE_EQ(EXIT_SUCCESS, cmd.main(&ui, args, fake_config())); - ATF_REQUIRE_EQ(5, ui.out_log().size()); + ATF_REQUIRE_EQ(6, ui.out_log().size()); ATF_REQUIRE_EQ("architecture = the-architecture", ui.out_log()[0]); - ATF_REQUIRE_EQ("parallelism = 128", ui.out_log()[1]); - ATF_REQUIRE_EQ("platform = the-platform", ui.out_log()[2]); - ATF_REQUIRE_EQ("test_suites.foo.bar = first", ui.out_log()[3]); - ATF_REQUIRE_EQ("test_suites.foo.baz = second", ui.out_log()[4]); + ATF_REQUIRE_EQ("execenvs = the-env", ui.out_log()[1]); + ATF_REQUIRE_EQ("parallelism = 128", ui.out_log()[2]); + ATF_REQUIRE_EQ("platform = the-platform", ui.out_log()[3]); + ATF_REQUIRE_EQ("test_suites.foo.bar = first", ui.out_log()[4]); + ATF_REQUIRE_EQ("test_suites.foo.baz = second", ui.out_log()[5]); ATF_REQUIRE(ui.err_log().empty()); } ATF_TEST_CASE_WITHOUT_HEAD(some__ok); ATF_TEST_CASE_BODY(some__ok) { cmdline::args_vector args; args.push_back("config"); args.push_back("platform"); args.push_back("test_suites.foo.baz"); cmd_config cmd; cmdline::ui_mock ui; ATF_REQUIRE_EQ(EXIT_SUCCESS, cmd.main(&ui, args, fake_config())); ATF_REQUIRE_EQ(2, ui.out_log().size()); ATF_REQUIRE_EQ("platform = the-platform", ui.out_log()[0]); ATF_REQUIRE_EQ("test_suites.foo.baz = second", ui.out_log()[1]); ATF_REQUIRE(ui.err_log().empty()); } ATF_TEST_CASE_WITHOUT_HEAD(some__fail); ATF_TEST_CASE_BODY(some__fail) { cmdline::args_vector args; args.push_back("config"); args.push_back("platform"); args.push_back("unknown"); args.push_back("test_suites.foo.baz"); cmdline::init("progname"); cmd_config cmd; cmdline::ui_mock ui; ATF_REQUIRE_EQ(EXIT_FAILURE, cmd.main(&ui, args, fake_config())); ATF_REQUIRE_EQ(2, ui.out_log().size()); ATF_REQUIRE_EQ("platform = the-platform", ui.out_log()[0]); ATF_REQUIRE_EQ("test_suites.foo.baz = second", ui.out_log()[1]); ATF_REQUIRE_EQ(1, ui.err_log().size()); ATF_REQUIRE(atf::utils::grep_string("unknown.*not defined", ui.err_log()[0])); } ATF_INIT_TEST_CASES(tcs) { ATF_ADD_TEST_CASE(tcs, all); ATF_ADD_TEST_CASE(tcs, some__ok); ATF_ADD_TEST_CASE(tcs, some__fail); } diff --git a/contrib/kyua/doc/kyua.conf.5.in b/contrib/kyua/doc/kyua.conf.5.in index 05a9499b48c4..7188bb8888c3 100644 --- a/contrib/kyua/doc/kyua.conf.5.in +++ b/contrib/kyua/doc/kyua.conf.5.in @@ -1,141 +1,150 @@ -.\" Copyright 2012 The Kyua Authors. +.\" Copyright 2012-2024 The Kyua Authors. .\" All rights reserved. .\" .\" Redistribution and use in source and binary forms, with or without .\" modification, are permitted provided that the following conditions are .\" met: .\" .\" * Redistributions of source code must retain the above copyright .\" notice, this list of conditions and the following disclaimer. .\" * Redistributions in binary form must reproduce the above copyright .\" notice, this list of conditions and the following disclaimer in the .\" documentation and/or other materials provided with the distribution. .\" * Neither the name of Google Inc. nor the names of its contributors .\" may be used to endorse or promote products derived from this software .\" without specific prior written permission. .\" .\" THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS .\" "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT .\" LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR .\" A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT .\" OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, .\" SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT .\" LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, .\" DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY .\" THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT .\" (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE .\" OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -.Dd February 20, 2015 +.Dd March 22, 2024 .Dt KYUA.CONF 5 .Os .Sh NAME .Nm kyua.conf .Nd Configuration file for the kyua tool .Sh SYNOPSIS .Fn syntax "int version" .Pp Variables: .Va architecture , +.Va execenvs , .Va platform , .Va test_suites , .Va unprivileged_user . .Sh DESCRIPTION The configuration of Kyua is a simple collection of key/value pairs called configuration variables. There are configuration variables that have a special meaning to the runtime engine implemented by .Xr kyua 1 , and there are variables that only have meaning in the context of particular test suites. .Pp Configuration files are Lua scripts. In their most basic form, their whole purpose is to assign values to variables, but the user has the freedom to implement any logic he desires to compute such values. .Ss File versioning Every .Nm file starts with a call to .Fn syntax "int version" . This call determines the specific schema used by the file so that future backwards-incompatible modifications to the file can be introduced. .Pp Any new .Nm file should set .Fa version to .Sq 2 . .Ss Runtime configuration variables The following variables are internally recognized by .Xr kyua 1 : .Bl -tag -width XX -offset indent .It Va architecture Name of the system architecture (aka processor type). +.It Va execenvs +Whitespace-separated list of execution environment names. +.Pp +Only tests which require one of the given execution environments will be run. +.Pp +See +.Xr kyuafile 5 +for the list of possible execution environments. .It Va parallelism Maximum number of test cases to execute concurrently. .It Va platform Name of the system platform (aka machine type). .It Va unprivileged_user Name or UID of the unprivileged user. .Pp If set, the given user must exist in the system and his privileges will be used to run test cases that need regular privileges when .Xr kyua 1 is executed as root. .El .Ss Test-suite configuration variables Each test suite is able to recognize arbitrary configuration variables, and their type and meaning is specific to the test suite. Because the existence and naming of these variables depends on every test suite, this manual page cannot detail them; please refer to the documentation of the test suite you are working with for more details on this topic. .Pp Test-suite specific configuration variables are defined inside the .Va test_suites dictionary. The general syntax is: .Bd -literal -offset indent test_suites.. = .Ed .Pp where .Va test_suite_name is the name of the test suite, .Va variable_name is the name of the variable to set, and .Va value is a value. The value can be a string, an integer or a boolean. .Sh FILES .Bl -tag -width XX .It __EGDIR__/kyua.conf Sample configuration file. .El .Sh EXAMPLES The following .Nm shows a simple configuration file that overrides a bunch of the built-in .Xr kyua 1 configuration variables: .Bd -literal -offset indent syntax(2) architecture = 'x86_64' platform = 'amd64' .Ed .Pp The following is a more complex example that introduces the definition of per-test suite configuration variables: .Bd -literal -offset indent syntax(2) -- Assign built-in variables. unprivileged_user = '_tests' -- Assign test-suite variables. All of these must be strings. test_suites.NetBSD.file_systems = 'ffs ext2fs' test_suites.X11.graphics_driver = 'vesa' .Ed .Sh SEE ALSO .Xr kyua 1 diff --git a/contrib/kyua/doc/kyuafile.5.in b/contrib/kyua/doc/kyuafile.5.in index 06cb2dbc42a8..a667f5dc2816 100644 --- a/contrib/kyua/doc/kyuafile.5.in +++ b/contrib/kyua/doc/kyuafile.5.in @@ -1,407 +1,506 @@ -.\" Copyright 2012 The Kyua Authors. +.\" Copyright 2012-2024 The Kyua Authors. .\" All rights reserved. .\" .\" Redistribution and use in source and binary forms, with or without .\" modification, are permitted provided that the following conditions are .\" met: .\" .\" * Redistributions of source code must retain the above copyright .\" notice, this list of conditions and the following disclaimer. .\" * Redistributions in binary form must reproduce the above copyright .\" notice, this list of conditions and the following disclaimer in the .\" documentation and/or other materials provided with the distribution. .\" * Neither the name of Google Inc. nor the names of its contributors .\" may be used to endorse or promote products derived from this software .\" without specific prior written permission. .\" .\" THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS .\" "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT .\" LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR .\" A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT .\" OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, .\" SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT .\" LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, .\" DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY .\" THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT .\" (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE .\" OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -.Dd July 3, 2015 +.Dd March 23, 2024 .Dt KYUAFILE 5 .Os .Sh NAME .Nm Kyuafile .Nd Test suite description files .Sh SYNOPSIS .Fn atf_test_program "string name" "[string metadata]" .Fn current_kyuafile .Fn fs.basename "string path" .Fn fs.dirname "string path" .Fn fs.exists "string path" .Fn fs.files "string path" .Fn fs.is_absolute "string path" .Fn fs.join "string path" "string path" .Fn include "string path" .Fn plain_test_program "string name" "[string metadata]" .Fn syntax "int version" .Fn tap_test_program "string name" "[string metadata]" .Fn test_suite "string name" .Sh DESCRIPTION A test suite is a collection of test programs and is represented by a hierarchical layout of test binaries on the file system. Any subtree of the file system can represent a test suite, provided that it includes one or more .Nm Ns s , which are the test suite definition files. .Pp A .Nm is a Lua script whose purpose is to describe the structure of the test suite it belongs to. To do so, the script has access to a collection of special functions provided by .Xr kyua 1 as described in .Sx Helper functions . .Ss File versioning Every .Nm file starts with a call to .Fn syntax "int version" . This call determines the specific schema used by the file so that future backwards-incompatible modifications to the file can be introduced. .Pp Any new .Nm file should set .Fa version to .Sq 2 . .Ss Test suite definition If the .Nm registers any test programs, the .Nm must define the name of the test suite the test programs belong to by using the .Fn test_suite function at the very beginning of the file. .Pp The test suite name provided in the .Fn test_suite call tells .Xr kyua 1 which set of configuration variables from .Xr kyua.conf 5 to pass to the test programs at run time. .Ss Test program registration A .Nm can register test programs by means of a variety of .Fn *_test_program functions, all of which take the name of a test program and a set of optional metadata properties that describe such test program. .Pp The test programs to be registered must live in the current directory; in other words, the various .Fn *_test_program calls cannot reference test programs in other directories. The rationale for this is to force all .Nm files to be self-contained, and to simplify their internal representation. .Pp .Em ATF test programs are those that use the .Xr atf 7 libraries. They can be registered with the .Fn atf_test_program table constructor. This function takes the .Fa name of the test program and a collection of optional metadata settings for all the test cases in the test program. Any metadata properties defined by the test cases themselves override the metadata values defined here. .Pp .Em Plain test programs are those that return 0 on success and non-0 on failure; in general, most test programs (even those that use fancy unit-testing libraries) behave this way and thus also qualify as plain test programs. They can be registered with the .Fn plain_test_program table constructor. This function takes the .Fa name of the test program, an optional .Fa test_suite name that overrides the global test suite name, and a collection of optional metadata settings for the test program. .Pp .Em TAP test programs are those that implement the Test Anything Protocol. They can be registered with the .Fn tap_test_program table constructor. This function takes the .Fa name of the test program and a collection of optional metadata settings for the test program. .Pp The following metadata properties can be passed to any test program definition: .Bl -tag -width XX -offset indent .It Va allowed_architectures Whitespace-separated list of machine architecture names allowed by the test. If empty or not defined, the test is allowed to run on any machine architecture. .It Va allowed_platforms Whitespace-separated list of machine platform names allowed by the test. If empty or not defined, the test is allowed to run on any machine platform. .It Va custom.NAME Custom variable defined by the test where .Sq NAME denotes the name of the variable. These variables are useful to tag your tests with information specific to your project. The values of such variables are propagated all the way from the tests to the results files and later to any generated reports. .Pp Note that if the name happens to have dashes or any other special characters in it, you will have to use a special Lua syntax to define the property. Refer to the .Sx EXAMPLES section below for clarification. .It Va description Textual description of the test. +.It Va execenv +The name of the execution environment to be used for running the test. +If empty or not defined, the +.Sq host +execution environment is meant. +The possible values are: +.Bl -tag -width xUnnnnnnn +.It host +The default environment which runs the test as a usual child process. +.It jail +The +.Fx +.Xr jail 8 +environment. +It creates a temporary jail to run the test and its optional cleanup logic +within. +.Pp +This feature requires +.Xr kyua 1 +to be running with superuser privileges. +.Pp +The difference between +.Va security.jail.children.max +and +.Va security.jail.children.cur +sysctl of the jail +.Xr kyua 1 +is running within must have a value high enough for the jail based tests +planned to be run. +For instance, the value 1 should be enough for a sequential run of simple +tests. +Otherwise, such aspects as parallel test execution and sub-jails spawned +by specific test cases should be considered. +.Pp +The formula of a temporary jail name is +.Sq kyua ++ +.Va test program path ++ +.Sq _ ++ +.Va test case name . +All non-alphanumeric characters are replaced with +.Sq _ . +.Sq kyua_usr_tests_sys_netpfil_pf_pass_block_v4 +is an example for /usr/tests/sys/netpfil/pf/pass_block:v4 test case. +.El +.It Va execenv_jail_params +Additional test-specific whitespace-separated parameters of +.Fx +.Xr jail 8 +to create a temporary jail within which the test is run. +It makes sense only if execenv is set to +.Sq jail . +.sp +.Xr kyua 1 +implicitly passes +.Sq children.max +parameter to +.Xr jail 8 +for a temporary jail with the maximum possible value according to +the jail +.Xr kyua 1 +itself is running within. +It allows tests to easily spawn their own sub-jails without additional +configuration. +It can be overridden via +.Va execenv_jail_params +if needed. .It Va is_exclusive If true, indicates that this test program cannot be executed along any other programs at the same time. Test programs that affect global system state, such as those that modify the value of a .Xr sysctl 8 setting, must set themselves as exclusive to prevent failures due to race conditions. Defaults to false. .It Va required_configs Whitespace-separated list of configuration variables that the test requires to be defined before it can run. .It Va required_disk_space Amount of available disk space that the test needs to run successfully. .It Va required_files Whitespace-separated list of paths that the test requires to exist before it can run. .It Va required_memory Amount of physical memory that the test needs to run successfully. .It Va required_programs Whitespace-separated list of basenames or absolute paths pointing to executable binaries that the test requires to exist before it can run. .It Va required_user If empty, the test has no restrictions on the calling user for it to run. If set to .Sq unprivileged , the test needs to not run as root. If set to .Sq root , the test must run as root. .It Va timeout Amount of seconds that the test is allowed to execute before being killed. .El .Ss Recursion To reference test programs in another subdirectory, a different .Nm must be created in that directory and it must be included into the original .Nm by means of the .Fn include function. .Pp .Fn include may only be called with a relative path and with at most one directory component. This is by design: Kyua uses the file system structure as the layout of the test suite definition. Therefore, each subdirectory in a test suite must include its own .Nm and each .Nm can only descend into the .Nm Ns s of immediate subdirectories. .Pp If you need to source a .Nm located in disjoint parts of your file system namespace, you will have to create a .Sq shadow tree using symbolic links and possibly helper .Nm Ns s to plug the various subdirectories together. See the .Sx EXAMPLES section below for details. .Pp Note that each file is processed in its own Lua environment: there is no mechanism to pass state from one file to the other. The reason for this is that there is no such thing as a .Dq top-level .Nm in a test suite: the user has to be able to run the test suite from any directory in a given hierarchy, and this execution must not depend on files that live in parent directories. .Ss Top-level Kyuafile Every system has a top directory into which test suites get installed. The default is .Pa __TESTSDIR__ . Within this directory live test suites, each of which is in an independent subdirectory. Each subdirectory can be provided separately by independent third-party packages. .Pp Kyua allows running all the installed test suites at once in order to provide comprehensive cross-component reports. In order to do this, there is a special file in the top directory that knows how to inspect the subdirectories in search for other Kyuafiles and include them. .Pp The .Sx FILES section includes more details on where this file lives. .Ss Helper functions The .Sq base , .Sq string , and .Sq table Lua modules are fully available in the context of a .Nm . .Pp The following extra functions are provided by Kyua: .Bl -tag -width XX -offset indent .It Ft string Fn current_kyuafile Returns the absolute path to the current .Nm . .It Ft string Fn fs.basename "string path" Returns the last component of the given path. .It Ft string Fn fs.dirname "string path" Returns the given path without its last component or a dot if the path has a single component. .It Ft bool Fn fs.exists "string path" Checks if the given path exists. If the path is not absolute, it is relative to the directory containing the .Nm in which the call to this function occurs. .It Ft iterator Fn fs.files "string path" Opens a directory for scanning of its entries. The returned iterator yields an entry on each call, and the entry is simply the filename. If the path is not absolute, it is relative to the directory containing the .Nm in which the call to this function occurs. .It Ft is_absolute Fn fs.is_absolute "string path" Returns true if the given path is absolute; false otherwise. .It Ft join Fn fs.join "string path" "string path" Concatenates the two paths. The second path cannot be absolute. .El .Sh FILES .Bl -tag -width XX .It Pa __TESTSDIR__/Kyuafile . Top-level .Nm for the current system. .It Pa __EGDIR__/Kyuafile.top . Sample file to serve as a top-level .Nm . .El .Sh EXAMPLES The following .Nm is the simplest you can define. It provides a test suite definition and registers a couple of different test programs using different interfaces: .Bd -literal -offset indent syntax(2) test_suite('first') atf_test_program{name='integration_test'} plain_test_program{name='legacy_test'} .Ed .Pp The following example is a bit more elaborate. It introduces some metadata properties to the test program definitions and recurses into a couple of subdirectories: .Bd -literal -offset indent syntax(2) test_suite('second') plain_test_program{name='legacy_test', allowed_architectures='amd64 i386', required_files='/bin/ls', timeout=30} tap_test_program{name='privileged_test', required_user='root'} include('module-1/Kyuafile') include('module-2/Kyuafile') .Ed .Pp The syntax to define custom properties may be not obvious if their names have any characters that make the property name not be a valid Lua identifier. Dashes are just one example. To set such properties, do something like this: .Bd -literal -offset indent syntax(2) test_suite('FreeBSD') plain_test_program{name='the_test', ['custom.FreeBSD-Bug-Id']='category/12345'} .Ed +.Ss FreeBSD jail execution environment +The following example configures the test to be run within a temporary jail +with +.Xr vnet 9 +support and the permission to create raw sockets: +.Bd -literal -offset indent +syntax(2) + +test_suite('FreeBSD') + +atf_test_program{name='network_test', + execenv='jail', + execenv_jail_params='vnet allow.raw_sockets', + required_user='root'} +.Ed +.Pp +A test case itself may have no requirements in superuser privileges, +but required_user='root' metadata property reminds that the jail execution +environment requires +.Xr kyua 1 +being running with root privileges, and the test is skipped otherwise with +the respective message. The combination of +.Va execenv +set to +.Sq jail +and +.Va required_user +set to +.Sq unprivileged +does not work respectively. .Ss Connecting disjoint test suites Now suppose you had various test suites on your file system and you would like to connect them together so that they could be executed and treated as a single unit. The test suites we would like to connect live under .Pa /usr/tests , .Pa /usr/local/tests and .Pa ~/local/tests . .Pp We cannot create a .Nm that references these because the .Fn include directive does not support absolute paths. Instead, what we can do is create a shadow tree using symbolic links: .Bd -literal -offset indent $ mkdir ~/everything $ ln -s /usr/tests ~/everything/system-tests $ ln -s /usr/local/tests ~/everything/local-tests $ ln -s ~/local/tests ~/everything/home-tests .Ed .Pp And then we create an .Pa ~/everything/Kyuafile file to drive the execution of the integrated test suite: .Bd -literal -offset indent syntax(2) test_suite('test-all-the-things') include('system-tests/Kyuafile') include('local-tests/Kyuafile') include('home-tests/Kyuafile') .Ed .Pp Or, simply, you could reuse the sample top-level .Nm to avoid having to manually craft the list of directories into which to recurse: .Bd -literal -offset indent $ cp __EGDIR__/Kyuafile.top ~/everything/Kyuafile .Ed .Sh SEE ALSO .Xr kyua 1 diff --git a/contrib/kyua/drivers/report_junit_test.cpp b/contrib/kyua/drivers/report_junit_test.cpp index 462dca72f9be..0f009c6befd3 100644 --- a/contrib/kyua/drivers/report_junit_test.cpp +++ b/contrib/kyua/drivers/report_junit_test.cpp @@ -1,415 +1,423 @@ // Copyright 2014 The Kyua Authors. // All rights reserved. // // Redistribution and use in source and binary forms, with or without // modification, are permitted provided that the following conditions are // met: // // * Redistributions of source code must retain the above copyright // notice, this list of conditions and the following disclaimer. // * Redistributions in binary form must reproduce the above copyright // notice, this list of conditions and the following disclaimer in the // documentation and/or other materials provided with the distribution. // * Neither the name of Google Inc. nor the names of its contributors // may be used to endorse or promote products derived from this software // without specific prior written permission. // // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS // "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT // LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR // A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT // OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT // LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #include "drivers/report_junit.hpp" #include #include #include #include "drivers/scan_results.hpp" #include "engine/filters.hpp" #include "model/context.hpp" #include "model/metadata.hpp" #include "model/test_case.hpp" #include "model/test_program.hpp" #include "model/test_result.hpp" #include "store/write_backend.hpp" #include "store/write_transaction.hpp" #include "utils/datetime.hpp" #include "utils/format/macros.hpp" #include "utils/fs/path.hpp" #include "utils/optional.ipp" #include "utils/units.hpp" namespace datetime = utils::datetime; namespace fs = utils::fs; namespace units = utils::units; using utils::none; namespace { /// Formatted metadata for a test case with defaults. static const char* const default_metadata = "allowed_architectures is empty\n" "allowed_platforms is empty\n" "description is empty\n" + "execenv is empty\n" + "execenv_jail_params is empty\n" "has_cleanup = false\n" "is_exclusive = false\n" "required_configs is empty\n" "required_disk_space = 0\n" "required_files is empty\n" "required_memory = 0\n" "required_programs is empty\n" "required_user is empty\n" "timeout = 300\n"; /// Formatted metadata for a test case constructed with the "with_metadata" flag /// set to true in add_tests. static const char* const overriden_metadata = "allowed_architectures is empty\n" "allowed_platforms is empty\n" "description = Textual description\n" + "execenv is empty\n" + "execenv_jail_params is empty\n" "has_cleanup = false\n" "is_exclusive = false\n" "required_configs is empty\n" "required_disk_space = 0\n" "required_files is empty\n" "required_memory = 0\n" "required_programs is empty\n" "required_user is empty\n" "timeout = 5678\n"; /// Populates the context of the given database. /// /// \param tx Transaction to use for the writes to the database. /// \param env_vars Number of environment variables to add to the context. static void add_context(store::write_transaction& tx, const std::size_t env_vars) { std::map< std::string, std::string > env; for (std::size_t i = 0; i < env_vars; i++) env[F("VAR%s") % i] = F("Value %s") % i; const model::context context(fs::path("/root"), env); (void)tx.put_context(context); } /// Adds a new test program with various test cases to the given database. /// /// \param tx Transaction to use for the writes to the database. /// \param prog Test program name. /// \param results Collection of results for the added test cases. The size of /// this vector indicates the number of tests in the test program. /// \param with_metadata Whether to add metadata overrides to the test cases. /// \param with_output Whether to add stdout/stderr messages to the test cases. static void add_tests(store::write_transaction& tx, const char* prog, const std::vector< model::test_result >& results, const bool with_metadata, const bool with_output) { model::test_program_builder test_program_builder( "plain", fs::path(prog), fs::path("/root"), "suite"); for (std::size_t j = 0; j < results.size(); j++) { model::metadata_builder builder; if (with_metadata) { builder.set_description("Textual description"); builder.set_timeout(datetime::delta(5678, 0)); } test_program_builder.add_test_case(F("t%s") % j, builder.build()); } const model::test_program test_program = test_program_builder.build(); const int64_t tp_id = tx.put_test_program(test_program); for (std::size_t j = 0; j < results.size(); j++) { const int64_t tc_id = tx.put_test_case(test_program, F("t%s") % j, tp_id); const datetime::timestamp start = datetime::timestamp::from_microseconds(0); const datetime::timestamp end = datetime::timestamp::from_microseconds(j * 1000000 + 500000); tx.put_result(results[j], tc_id, start, end); if (with_output) { atf::utils::create_file("fake-out", F("stdout file %s") % j); tx.put_test_case_file("__STDOUT__", fs::path("fake-out"), tc_id); atf::utils::create_file("fake-err", F("stderr file %s") % j); tx.put_test_case_file("__STDERR__", fs::path("fake-err"), tc_id); } } } } // anonymous namespace ATF_TEST_CASE_WITHOUT_HEAD(junit_classname); ATF_TEST_CASE_BODY(junit_classname) { const model::test_program test_program = model::test_program_builder( "plain", fs::path("dir1/dir2/program"), fs::path("/root"), "suite") .build(); ATF_REQUIRE_EQ("dir1.dir2.program", drivers::junit_classname(test_program)); } ATF_TEST_CASE_WITHOUT_HEAD(junit_duration); ATF_TEST_CASE_BODY(junit_duration) { ATF_REQUIRE_EQ("0.457", drivers::junit_duration(datetime::delta(0, 456700))); ATF_REQUIRE_EQ("3.120", drivers::junit_duration(datetime::delta(3, 120000))); ATF_REQUIRE_EQ("5.000", drivers::junit_duration(datetime::delta(5, 0))); } ATF_TEST_CASE_WITHOUT_HEAD(junit_metadata__defaults); ATF_TEST_CASE_BODY(junit_metadata__defaults) { const model::metadata metadata = model::metadata_builder().build(); const std::string expected = std::string() + drivers::junit_metadata_header + default_metadata; ATF_REQUIRE_EQ(expected, drivers::junit_metadata(metadata)); } ATF_TEST_CASE_WITHOUT_HEAD(junit_metadata__overrides); ATF_TEST_CASE_BODY(junit_metadata__overrides) { const model::metadata metadata = model::metadata_builder() .add_allowed_architecture("arch1") .add_allowed_platform("platform1") .set_description("This is a test") + .set_execenv("jail") + .set_execenv_jail_params("vnet") .set_has_cleanup(true) .set_is_exclusive(true) .add_required_config("config1") .set_required_disk_space(units::bytes(456)) .add_required_file(fs::path("file1")) .set_required_memory(units::bytes(123)) .add_required_program(fs::path("prog1")) .set_required_user("root") .set_timeout(datetime::delta(10, 0)) .build(); const std::string expected = std::string() + drivers::junit_metadata_header + "allowed_architectures = arch1\n" + "allowed_platforms = platform1\n" + "description = This is a test\n" + + "execenv = jail\n" + + "execenv_jail_params = vnet\n" + "has_cleanup = true\n" + "is_exclusive = true\n" + "required_configs = config1\n" + "required_disk_space = 456\n" + "required_files = file1\n" + "required_memory = 123\n" + "required_programs = prog1\n" + "required_user = root\n" + "timeout = 10\n"; ATF_REQUIRE_EQ(expected, drivers::junit_metadata(metadata)); } ATF_TEST_CASE_WITHOUT_HEAD(junit_timing); ATF_TEST_CASE_BODY(junit_timing) { const std::string expected = std::string() + drivers::junit_timing_header + "Start time: 2015-06-12T01:02:35.123456Z\n" "End time: 2016-07-13T18:47:10.000001Z\n" "Duration: 34364674.877s\n"; const datetime::timestamp start_time = datetime::timestamp::from_values(2015, 6, 12, 1, 2, 35, 123456); const datetime::timestamp end_time = datetime::timestamp::from_values(2016, 7, 13, 18, 47, 10, 1); ATF_REQUIRE_EQ(expected, drivers::junit_timing(start_time, end_time)); } ATF_TEST_CASE_WITHOUT_HEAD(report_junit_hooks__minimal); ATF_TEST_CASE_BODY(report_junit_hooks__minimal) { store::write_backend backend = store::write_backend::open_rw( fs::path("test.db")); store::write_transaction tx = backend.start_write(); add_context(tx, 0); tx.commit(); backend.close(); std::ostringstream output; drivers::report_junit_hooks hooks(output); drivers::scan_results::drive(fs::path("test.db"), std::set< engine::test_filter >(), hooks); const char* expected = "\n" "\n" "\n" "\n" "\n" "\n"; ATF_REQUIRE_EQ(expected, output.str()); } ATF_TEST_CASE_WITHOUT_HEAD(report_junit_hooks__some_tests); ATF_TEST_CASE_BODY(report_junit_hooks__some_tests) { std::vector< model::test_result > results1; results1.push_back(model::test_result( model::test_result_broken, "Broken")); results1.push_back(model::test_result( model::test_result_expected_failure, "XFail")); results1.push_back(model::test_result( model::test_result_failed, "Failed")); std::vector< model::test_result > results2; results2.push_back(model::test_result( model::test_result_passed)); results2.push_back(model::test_result( model::test_result_skipped, "Skipped")); store::write_backend backend = store::write_backend::open_rw( fs::path("test.db")); store::write_transaction tx = backend.start_write(); add_context(tx, 2); add_tests(tx, "dir/prog-1", results1, false, false); add_tests(tx, "dir/sub/prog-2", results2, true, true); tx.commit(); backend.close(); std::ostringstream output; drivers::report_junit_hooks hooks(output); drivers::scan_results::drive(fs::path("test.db"), std::set< engine::test_filter >(), hooks); const std::string expected = std::string() + "\n" "\n" "\n" "\n" "\n" "\n" "\n" "\n" "\n" "" + drivers::junit_metadata_header + default_metadata + drivers::junit_timing_header + "Start time: 1970-01-01T00:00:00.000000Z\n" "End time: 1970-01-01T00:00:00.500000Z\n" "Duration: 0.500s\n" + drivers::junit_stderr_header + "<EMPTY>\n" "\n" "\n" "\n" "" "Expected failure result details\n" "-------------------------------\n" "\n" "XFail\n" "\n" + drivers::junit_metadata_header + default_metadata + drivers::junit_timing_header + "Start time: 1970-01-01T00:00:00.000000Z\n" "End time: 1970-01-01T00:00:01.500000Z\n" "Duration: 1.500s\n" + drivers::junit_stderr_header + "<EMPTY>\n" "\n" "\n" "\n" "\n" "" + drivers::junit_metadata_header + default_metadata + drivers::junit_timing_header + "Start time: 1970-01-01T00:00:00.000000Z\n" "End time: 1970-01-01T00:00:02.500000Z\n" "Duration: 2.500s\n" + drivers::junit_stderr_header + "<EMPTY>\n" "\n" "\n" "\n" "stdout file 0\n" "" + drivers::junit_metadata_header + overriden_metadata + drivers::junit_timing_header + "Start time: 1970-01-01T00:00:00.000000Z\n" "End time: 1970-01-01T00:00:00.500000Z\n" "Duration: 0.500s\n" + drivers::junit_stderr_header + "stderr file 0\n" "\n" "\n" "\n" "stdout file 1\n" "" "Skipped result details\n" "----------------------\n" "\n" "Skipped\n" "\n" + drivers::junit_metadata_header + overriden_metadata + drivers::junit_timing_header + "Start time: 1970-01-01T00:00:00.000000Z\n" "End time: 1970-01-01T00:00:01.500000Z\n" "Duration: 1.500s\n" + drivers::junit_stderr_header + "stderr file 1\n" "\n" "\n"; ATF_REQUIRE_EQ(expected, output.str()); } ATF_INIT_TEST_CASES(tcs) { ATF_ADD_TEST_CASE(tcs, junit_classname); ATF_ADD_TEST_CASE(tcs, junit_duration); ATF_ADD_TEST_CASE(tcs, junit_metadata__defaults); ATF_ADD_TEST_CASE(tcs, junit_metadata__overrides); ATF_ADD_TEST_CASE(tcs, junit_timing); ATF_ADD_TEST_CASE(tcs, report_junit_hooks__minimal); ATF_ADD_TEST_CASE(tcs, report_junit_hooks__some_tests); } diff --git a/contrib/kyua/engine/atf.cpp b/contrib/kyua/engine/atf.cpp index eb63be20b0e7..f6746dd2f29f 100644 --- a/contrib/kyua/engine/atf.cpp +++ b/contrib/kyua/engine/atf.cpp @@ -1,242 +1,249 @@ // Copyright 2014 The Kyua Authors. // All rights reserved. // // Redistribution and use in source and binary forms, with or without // modification, are permitted provided that the following conditions are // met: // // * Redistributions of source code must retain the above copyright // notice, this list of conditions and the following disclaimer. // * Redistributions in binary form must reproduce the above copyright // notice, this list of conditions and the following disclaimer in the // documentation and/or other materials provided with the distribution. // * Neither the name of Google Inc. nor the names of its contributors // may be used to endorse or promote products derived from this software // without specific prior written permission. // // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS // "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT // LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR // A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT // OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT // LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #include "engine/atf.hpp" extern "C" { #include } #include #include #include #include "engine/atf_list.hpp" #include "engine/atf_result.hpp" #include "engine/exceptions.hpp" +#include "engine/execenv/execenv.hpp" #include "model/test_case.hpp" #include "model/test_program.hpp" #include "model/test_result.hpp" #include "utils/defs.hpp" #include "utils/env.hpp" #include "utils/format/macros.hpp" #include "utils/fs/path.hpp" #include "utils/logging/macros.hpp" #include "utils/optional.ipp" #include "utils/process/exceptions.hpp" #include "utils/process/operations.hpp" #include "utils/process/status.hpp" #include "utils/stream.hpp" namespace config = utils::config; +namespace execenv = engine::execenv; namespace fs = utils::fs; namespace process = utils::process; using utils::optional; namespace { /// Basename of the file containing the result written by the ATF test case. static const char* result_name = "result.atf"; /// Magic numbers returned by exec_list when exec(2) fails. enum list_exit_code { exit_eacces = 90, exit_enoent, exit_enoexec, }; } // anonymous namespace /// Executes a test program's list operation. /// /// This method is intended to be called within a subprocess and is expected /// to terminate execution either by exec(2)ing the test program or by /// exiting with a failure. /// /// \param test_program The test program to execute. /// \param vars User-provided variables to pass to the test program. void engine::atf_interface::exec_list(const model::test_program& test_program, const config::properties_map& vars) const { utils::setenv("__RUNNING_INSIDE_ATF_RUN", "internal-yes-value"); process::args_vector args; for (config::properties_map::const_iterator iter = vars.begin(); iter != vars.end(); ++iter) { args.push_back(F("-v%s=%s") % (*iter).first % (*iter).second); } args.push_back("-l"); try { process::exec_unsafe(test_program.absolute_path(), args); } catch (const process::system_error& e) { if (e.original_errno() == EACCES) ::_exit(exit_eacces); else if (e.original_errno() == ENOENT) ::_exit(exit_enoent); else if (e.original_errno() == ENOEXEC) ::_exit(exit_enoexec); throw; } } /// Computes the test cases list of a test program. /// /// \param status The termination status of the subprocess used to execute /// the exec_test() method or none if the test timed out. /// \param stdout_path Path to the file containing the stdout of the test. /// \param stderr_path Path to the file containing the stderr of the test. /// /// \return A list of test cases. /// /// \throw error If there is a problem parsing the test case list. model::test_cases_map engine::atf_interface::parse_list(const optional< process::status >& status, const fs::path& stdout_path, const fs::path& stderr_path) const { const std::string stderr_contents = utils::read_file(stderr_path); if (!stderr_contents.empty()) LW("Test case list wrote to stderr: " + stderr_contents); if (!status) throw engine::error("Test case list timed out"); if (status.get().exited()) { const int exitstatus = status.get().exitstatus(); if (exitstatus == EXIT_SUCCESS) { // Nothing to do; fall through. } else if (exitstatus == exit_eacces) { throw engine::error("Permission denied to run test program"); } else if (exitstatus == exit_enoent) { throw engine::error("Cannot find test program"); } else if (exitstatus == exit_enoexec) { throw engine::error("Invalid test program format"); } else { throw engine::error("Test program did not exit cleanly"); } } else { throw engine::error("Test program received signal"); } std::ifstream input(stdout_path.c_str()); if (!input) throw engine::load_error(stdout_path, "Cannot open file for read"); const model::test_cases_map test_cases = parse_atf_list(input); if (!stderr_contents.empty()) throw engine::error("Test case list wrote to stderr"); return test_cases; } /// Executes a test case of the test program. /// /// This method is intended to be called within a subprocess and is expected /// to terminate execution either by exec(2)ing the test program or by /// exiting with a failure. /// /// \param test_program The test program to execute. /// \param test_case_name Name of the test case to invoke. /// \param vars User-provided variables to pass to the test program. /// \param control_directory Directory where the interface may place control /// files. void engine::atf_interface::exec_test(const model::test_program& test_program, const std::string& test_case_name, const config::properties_map& vars, const fs::path& control_directory) const { utils::setenv("__RUNNING_INSIDE_ATF_RUN", "internal-yes-value"); process::args_vector args; for (config::properties_map::const_iterator iter = vars.begin(); iter != vars.end(); ++iter) { args.push_back(F("-v%s=%s") % (*iter).first % (*iter).second); } args.push_back(F("-r%s") % (control_directory / result_name)); args.push_back(test_case_name); - process::exec(test_program.absolute_path(), args); + + auto e = execenv::get(test_program, test_case_name); + e->init(); + e->exec(args); } /// Executes a test cleanup routine of the test program. /// /// This method is intended to be called within a subprocess and is expected /// to terminate execution either by exec(2)ing the test program or by /// exiting with a failure. /// /// \param test_program The test program to execute. /// \param test_case_name Name of the test case to invoke. /// \param vars User-provided variables to pass to the test program. void engine::atf_interface::exec_cleanup( const model::test_program& test_program, const std::string& test_case_name, const config::properties_map& vars, const fs::path& /* control_directory */) const { utils::setenv("__RUNNING_INSIDE_ATF_RUN", "internal-yes-value"); process::args_vector args; for (config::properties_map::const_iterator iter = vars.begin(); iter != vars.end(); ++iter) { args.push_back(F("-v%s=%s") % (*iter).first % (*iter).second); } args.push_back(F("%s:cleanup") % test_case_name); - process::exec(test_program.absolute_path(), args); + + auto e = execenv::get(test_program, test_case_name); + e->exec(args); } /// Computes the result of a test case based on its termination status. /// /// \param status The termination status of the subprocess used to execute /// the exec_test() method or none if the test timed out. /// \param control_directory Directory where the interface may have placed /// control files. /// /// \return A test result. model::test_result engine::atf_interface::compute_result( const optional< process::status >& status, const fs::path& control_directory, const fs::path& /* stdout_path */, const fs::path& /* stderr_path */) const { return calculate_atf_result(status, control_directory / result_name); } diff --git a/contrib/kyua/engine/atf_list.cpp b/contrib/kyua/engine/atf_list.cpp index a16b889c74f0..c9c2fed70175 100644 --- a/contrib/kyua/engine/atf_list.cpp +++ b/contrib/kyua/engine/atf_list.cpp @@ -1,196 +1,200 @@ // Copyright 2015 The Kyua Authors. // All rights reserved. // // Redistribution and use in source and binary forms, with or without // modification, are permitted provided that the following conditions are // met: // // * Redistributions of source code must retain the above copyright // notice, this list of conditions and the following disclaimer. // * Redistributions in binary form must reproduce the above copyright // notice, this list of conditions and the following disclaimer in the // documentation and/or other materials provided with the distribution. // * Neither the name of Google Inc. nor the names of its contributors // may be used to endorse or promote products derived from this software // without specific prior written permission. // // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS // "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT // LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR // A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT // OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT // LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #include "engine/atf_list.hpp" #include #include #include #include "engine/exceptions.hpp" #include "model/metadata.hpp" #include "model/test_case.hpp" #include "utils/config/exceptions.hpp" #include "utils/format/macros.hpp" namespace config = utils::config; namespace fs = utils::fs; namespace { /// Splits a property line of the form "name: word1 [... wordN]". /// /// \param line The line to parse. /// /// \return A (property_name, property_value) pair. /// /// \throw format_error If the value of line is invalid. static std::pair< std::string, std::string > split_prop_line(const std::string& line) { const std::string::size_type pos = line.find(": "); if (pos == std::string::npos) throw engine::format_error("Invalid property line; expecting line of " "the form 'name: value'"); return std::make_pair(line.substr(0, pos), line.substr(pos + 2)); } /// Parses a set of consecutive property lines. /// /// Processing stops when an empty line or the end of file is reached. None of /// these conditions indicate errors. /// /// \param input The stream to read the lines from. /// /// \return The parsed property lines. /// /// throw format_error If the input stream has an invalid format. static model::properties_map parse_properties(std::istream& input) { model::properties_map properties; std::string line; while (std::getline(input, line).good() && !line.empty()) { const std::pair< std::string, std::string > property = split_prop_line( line); if (properties.find(property.first) != properties.end()) throw engine::format_error("Duplicate value for property " + property.first); properties.insert(property); } return properties; } } // anonymous namespace /// Parses the metadata of an ATF test case. /// /// \param props The properties (name/value string pairs) as provided by the /// ATF test program. /// /// \return A parsed metadata object. /// /// \throw engine::format_error If the syntax of any of the properties is /// invalid. model::metadata engine::parse_atf_metadata(const model::properties_map& props) { model::metadata_builder mdbuilder; try { for (model::properties_map::const_iterator iter = props.begin(); iter != props.end(); iter++) { const std::string& name = (*iter).first; const std::string& value = (*iter).second; if (name == "descr") { mdbuilder.set_string("description", value); } else if (name == "has.cleanup") { mdbuilder.set_string("has_cleanup", value); } else if (name == "require.arch") { mdbuilder.set_string("allowed_architectures", value); + } else if (name == "execenv") { + mdbuilder.set_string("execenv", value); + } else if (name == "execenv.jail.params") { + mdbuilder.set_string("execenv_jail_params", value); } else if (name == "require.config") { mdbuilder.set_string("required_configs", value); } else if (name == "require.files") { mdbuilder.set_string("required_files", value); } else if (name == "require.machine") { mdbuilder.set_string("allowed_platforms", value); } else if (name == "require.memory") { mdbuilder.set_string("required_memory", value); } else if (name == "require.progs") { mdbuilder.set_string("required_programs", value); } else if (name == "require.user") { mdbuilder.set_string("required_user", value); } else if (name == "timeout") { mdbuilder.set_string("timeout", value); } else if (name.length() > 2 && name.substr(0, 2) == "X-") { mdbuilder.add_custom(name.substr(2), value); } else { throw engine::format_error(F("Unknown test case metadata " "property '%s'") % name); } } } catch (const config::error& e) { throw engine::format_error(e.what()); } return mdbuilder.build(); } /// Parses the ATF list of test cases from an open stream. /// /// \param input The stream to read from. /// /// \return The collection of parsed test cases. /// /// \throw format_error If there is any problem in the input data. model::test_cases_map engine::parse_atf_list(std::istream& input) { std::string line; std::getline(input, line); if (line != "Content-Type: application/X-atf-tp; version=\"1\"" || !input.good()) throw format_error(F("Invalid header for test case list; expecting " "Content-Type for application/X-atf-tp version 1, " "got '%s'") % line); std::getline(input, line); if (!line.empty() || !input.good()) throw format_error(F("Invalid header for test case list; expecting " "a blank line, got '%s'") % line); model::test_cases_map_builder test_cases_builder; while (std::getline(input, line).good()) { const std::pair< std::string, std::string > ident = split_prop_line( line); if (ident.first != "ident" or ident.second.empty()) throw format_error("Invalid test case definition; must be " "preceeded by the identifier"); const model::properties_map props = parse_properties(input); test_cases_builder.add(ident.second, parse_atf_metadata(props)); } const model::test_cases_map test_cases = test_cases_builder.build(); if (test_cases.empty()) { // The scheduler interface also checks for the presence of at least one // test case. However, because the atf format itself requires one test // case to be always present, we check for this condition here as well. throw format_error("No test cases"); } return test_cases; } diff --git a/contrib/kyua/engine/config.cpp b/contrib/kyua/engine/config.cpp index 3f162a94fbb5..a7c418e3164c 100644 --- a/contrib/kyua/engine/config.cpp +++ b/contrib/kyua/engine/config.cpp @@ -1,254 +1,272 @@ // Copyright 2010 The Kyua Authors. // All rights reserved. // // Redistribution and use in source and binary forms, with or without // modification, are permitted provided that the following conditions are // met: // // * Redistributions of source code must retain the above copyright // notice, this list of conditions and the following disclaimer. // * Redistributions in binary form must reproduce the above copyright // notice, this list of conditions and the following disclaimer in the // documentation and/or other materials provided with the distribution. // * Neither the name of Google Inc. nor the names of its contributors // may be used to endorse or promote products derived from this software // without specific prior written permission. // // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS // "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT // LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR // A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT // OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT // LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #include "engine/config.hpp" #if defined(HAVE_CONFIG_H) # include "config.h" #endif #include #include "engine/exceptions.hpp" +#include "engine/execenv/execenv.hpp" #include "utils/config/exceptions.hpp" #include "utils/config/parser.hpp" #include "utils/config/tree.ipp" #include "utils/passwd.hpp" #include "utils/text/exceptions.hpp" #include "utils/text/operations.ipp" namespace config = utils::config; +namespace execenv = engine::execenv; namespace fs = utils::fs; namespace passwd = utils::passwd; namespace text = utils::text; namespace { /// Defines the schema of a configuration tree. /// /// \param [in,out] tree The tree to populate. The tree should be empty on /// entry to prevent collisions with the keys defined in here. static void init_tree(config::tree& tree) { tree.define< config::string_node >("architecture"); + tree.define< config::strings_set_node >("execenvs"); tree.define< config::positive_int_node >("parallelism"); tree.define< config::string_node >("platform"); tree.define< engine::user_node >("unprivileged_user"); tree.define_dynamic("test_suites"); } /// Fills in a configuration tree with default values. /// /// \param [in,out] tree The tree to populate. init_tree() must have been /// called on it beforehand. static void set_defaults(config::tree& tree) { tree.set< config::string_node >("architecture", KYUA_ARCHITECTURE); + + std::set< std::string > supported; + for (auto em : execenv::execenvs()) + if (em->is_supported()) + supported.insert(em->name()); + supported.insert(execenv::default_execenv_name); + tree.set< config::strings_set_node >("execenvs", supported); + // TODO(jmmv): Automatically derive this from the number of CPUs in the // machine and forcibly set to a value greater than 1. Still testing // the new parallel implementation as of 2015-02-27 though. tree.set< config::positive_int_node >("parallelism", 1); tree.set< config::string_node >("platform", KYUA_PLATFORM); } /// Configuration parser specialization for Kyua configuration files. class config_parser : public config::parser { /// Initializes the configuration tree. /// /// This is a callback executed when the configuration script invokes the /// syntax() method. We populate the configuration tree from here with the /// schema version requested by the file. /// /// \param [in,out] tree The tree to populate. /// \param syntax_version The version of the file format as specified in the /// configuration file. /// /// \throw config::syntax_error If the syntax_format/syntax_version /// combination is not supported. void setup(config::tree& tree, const int syntax_version) { if (syntax_version < 1 || syntax_version > 2) throw config::syntax_error(F("Unsupported config version %s") % syntax_version); init_tree(tree); set_defaults(tree); } public: /// Initializes the parser. /// /// \param [out] tree_ The tree in which the results of the parsing will be /// stored when parse() is called. Should be empty on entry. Because /// we grab a reference to this object, the tree must remain valid for /// the existence of the parser object. explicit config_parser(config::tree& tree_) : config::parser(tree_) { } }; } // anonymous namespace /// Copies the node. /// /// \return A dynamically-allocated node. config::detail::base_node* engine::user_node::deep_copy(void) const { std::auto_ptr< user_node > new_node(new user_node()); new_node->_value = _value; return new_node.release(); } /// Pushes the node's value onto the Lua stack. /// /// \param state The Lua state onto which to push the value. void engine::user_node::push_lua(lutok::state& state) const { state.push_string(value().name); } /// Sets the value of the node from an entry in the Lua stack. /// /// \param state The Lua state from which to get the value. /// \param value_index The stack index in which the value resides. /// /// \throw value_error If the value in state(value_index) cannot be /// processed by this node. void engine::user_node::set_lua(lutok::state& state, const int value_index) { if (state.is_number(value_index)) { config::typed_leaf_node< passwd::user >::set( passwd::find_user_by_uid(state.to_integer(-1))); } else if (state.is_string(value_index)) { config::typed_leaf_node< passwd::user >::set( passwd::find_user_by_name(state.to_string(-1))); } else throw config::value_error("Invalid user identifier"); } /// Sets the value of the node from a raw string representation. /// /// \param raw_value The value to set the node to. /// /// \throw value_error If the value is invalid. void engine::user_node::set_string(const std::string& raw_value) { try { config::typed_leaf_node< passwd::user >::set( passwd::find_user_by_name(raw_value)); } catch (const std::runtime_error& e) { int uid; try { uid = text::to_type< int >(raw_value); } catch (const text::value_error& e2) { throw error(F("Cannot find user with name '%s'") % raw_value); } try { config::typed_leaf_node< passwd::user >::set( passwd::find_user_by_uid(uid)); } catch (const std::runtime_error& e2) { throw error(F("Cannot find user with UID %s") % uid); } } } /// Converts the contents of the node to a string. /// /// \pre The node must have a value. /// /// \return A string representation of the value held by the node. std::string engine::user_node::to_string(void) const { return config::typed_leaf_node< passwd::user >::value().name; } /// Constructs a config with the built-in settings. /// /// \return A default test suite configuration. config::tree engine::default_config(void) { config::tree tree(false); init_tree(tree); set_defaults(tree); return tree; } /// Constructs a config with the built-in settings. /// /// \return An empty test suite configuration. config::tree engine::empty_config(void) { config::tree tree(false); init_tree(tree); + + // Tests of Kyua itself tend to use an empty config, i.e. default + // execution environment is used. Let's allow it. + std::set< std::string > supported; + supported.insert(engine::execenv::default_execenv_name); + tree.set< config::strings_set_node >("execenvs", supported); + return tree; } /// Parses a test suite configuration file. /// /// \param file The file to parse. /// /// \return High-level representation of the configuration file. /// /// \throw load_error If there is any problem loading the file. This includes /// file access errors and syntax errors. config::tree engine::load_config(const utils::fs::path& file) { config::tree tree(false); try { config_parser(tree).parse(file); } catch (const config::error& e) { throw load_error(file, e.what()); } return tree; } diff --git a/contrib/kyua/main.cpp b/contrib/kyua/engine/execenv/execenv.cpp similarity index 58% copy from contrib/kyua/main.cpp copy to contrib/kyua/engine/execenv/execenv.cpp index 4344248f89db..b043bcda52cb 100644 --- a/contrib/kyua/main.cpp +++ b/contrib/kyua/engine/execenv/execenv.cpp @@ -1,50 +1,74 @@ -// Copyright 2010 The Kyua Authors. +// Copyright 2023 The Kyua Authors. // All rights reserved. // // Redistribution and use in source and binary forms, with or without // modification, are permitted provided that the following conditions are // met: // // * Redistributions of source code must retain the above copyright // notice, this list of conditions and the following disclaimer. // * Redistributions in binary form must reproduce the above copyright // notice, this list of conditions and the following disclaimer in the // documentation and/or other materials provided with the distribution. // * Neither the name of Google Inc. nor the names of its contributors // may be used to endorse or promote products derived from this software // without specific prior written permission. // // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS // "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT // LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR // A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT // OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT // LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -#include "cli/main.hpp" +#include "engine/execenv/execenv.hpp" +#include "engine/execenv/execenv_host.hpp" -/// Program entry point. -/// -/// The whole purpose of this extremely-simple function is to delegate execution -/// to an internal module that does not contain a proper ::main() function. -/// This is to allow unit-testing of the internal code. -/// -/// \param argc The number of arguments passed on the command line. -/// \param argv NULL-terminated array containing the command line arguments. -/// -/// \return 0 on success, some other integer on error. +namespace execenv = engine::execenv; + +using utils::none; + + +const char* execenv::default_execenv_name = "host"; + + +/// List of registered execution environments, except default host one. /// -/// \throw std::exception This throws any uncaught exception. Such exceptions -/// are bugs, but we let them propagate so that the runtime will abort and -/// dump core. -int -main(const int argc, const char* const* const argv) +/// Use register_execenv() to add an entry to this global list. +static std::vector< std::shared_ptr< execenv::manager > > + execenv_managers; + + +void +execenv::register_execenv(const std::shared_ptr< execenv::manager > manager) { - return cli::main(argc, argv); + execenv_managers.push_back(manager); +} + + +const std::vector< std::shared_ptr< execenv::manager> > +execenv::execenvs() +{ + return execenv_managers; +} + + +std::unique_ptr< execenv::interface > +execenv::get(const model::test_program& test_program, + const std::string& test_case_name) +{ + for (auto m : execenv_managers) { + auto e = m->probe(test_program, test_case_name); + if (e != nullptr) + return e; + } + + return std::unique_ptr< execenv::interface >( + new execenv::execenv_host(test_program, test_case_name)); } diff --git a/contrib/kyua/engine/execenv/execenv.hpp b/contrib/kyua/engine/execenv/execenv.hpp new file mode 100644 index 000000000000..e667ff205d85 --- /dev/null +++ b/contrib/kyua/engine/execenv/execenv.hpp @@ -0,0 +1,149 @@ +// Copyright 2023 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +/// \file engine/execenv/execenv.hpp +/// Execution environment subsystem interface. + +#if !defined(ENGINE_EXECENV_EXECENV_HPP) +#define ENGINE_EXECENV_EXECENV_HPP + +#include "model/test_program.hpp" +#include "utils/optional.ipp" +#include "utils/process/operations_fwd.hpp" + +using utils::process::args_vector; +using utils::optional; + +namespace engine { +namespace execenv { + + +extern const char* default_execenv_name; + + +/// Abstract interface of an execution environment. +class interface { +protected: + const model::test_program& _test_program; + const std::string& _test_case_name; + +public: + /// Constructor. + /// + /// \param program The test program. + /// \param test_case_name Name of the test case. + interface(const model::test_program& test_program, + const std::string& test_case_name) : + _test_program(test_program), + _test_case_name(test_case_name) + {} + + /// Destructor. + virtual ~interface() {} + + /// Initializes execution environment. + /// + /// It's expected to be called inside a fork which runs + /// scheduler::interface::exec_test(), so we can fail a test fast if its + /// execution environment setup fails, and test execution could use the + /// configured proc environment, if expected. + virtual void init() const = 0; + + /// Cleanups or removes execution environment. + /// + /// It's expected to be called inside a fork for execenv cleanup. + virtual void cleanup() const = 0; + + /// Executes a test within the execution environment. + /// + /// It's expected to be called inside a fork which runs + /// scheduler::interface::exec_test() or exec_cleanup(). + /// + /// \param args The arguments to pass to the binary. + virtual void exec(const args_vector& args) const UTILS_NORETURN = 0; +}; + + +/// Abstract interface of an execution environment manager. +class manager { +public: + /// Destructor. + virtual ~manager() {} + + /// Returns name of an execution environment. + virtual const std::string& name() const = 0; + + /// Returns whether this execution environment is actually supported. + /// + /// It can be compile time and/or runtime check. + virtual bool is_supported() const = 0; + + /// Returns execution environment for a test. + /// + /// It checks if the given test is designed for this execution environment. + /// + /// \param program The test program. + /// \param test_case_name Name of the test case. + /// + /// \return An execenv object if the test conforms, or none. + virtual std::unique_ptr< interface > probe( + const model::test_program& test_program, + const std::string& test_case_name) const = 0; + + // TODO: execenv related extra metadata could be provided by a manager + // not to know how exactly and where it should be added to the kyua +}; + + +/// Registers an execution environment. +/// +/// \param manager Execution environment manager. +void register_execenv(const std::shared_ptr< manager > manager); + + +/// Returns list of registered execenv managers, except default host one. +/// +/// \return A vector of pointers to execenv managers. +const std::vector< std::shared_ptr< manager> > execenvs(); + + +/// Returns execution environment for a test case. +/// +/// \param program The test program. +/// \param test_case_name Name of the test case. +/// +/// \return An execution environment of a test. +std::unique_ptr< execenv::interface > get( + const model::test_program& test_program, + const std::string& test_case_name); + + +} // namespace execenv +} // namespace engine + +#endif // !defined(ENGINE_EXECENV_EXECENV_HPP) diff --git a/contrib/kyua/main.cpp b/contrib/kyua/engine/execenv/execenv_host.cpp similarity index 66% copy from contrib/kyua/main.cpp copy to contrib/kyua/engine/execenv/execenv_host.cpp index 4344248f89db..4e37fca3e7d3 100644 --- a/contrib/kyua/main.cpp +++ b/contrib/kyua/engine/execenv/execenv_host.cpp @@ -1,50 +1,52 @@ -// Copyright 2010 The Kyua Authors. +// Copyright 2024 The Kyua Authors. // All rights reserved. // // Redistribution and use in source and binary forms, with or without // modification, are permitted provided that the following conditions are // met: // // * Redistributions of source code must retain the above copyright // notice, this list of conditions and the following disclaimer. // * Redistributions in binary form must reproduce the above copyright // notice, this list of conditions and the following disclaimer in the // documentation and/or other materials provided with the distribution. // * Neither the name of Google Inc. nor the names of its contributors // may be used to endorse or promote products derived from this software // without specific prior written permission. // // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS // "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT // LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR // A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT // OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT // LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -#include "cli/main.hpp" +#include "engine/execenv/execenv_host.hpp" +#include "utils/fs/path.hpp" +#include "utils/process/operations.hpp" -/// Program entry point. -/// -/// The whole purpose of this extremely-simple function is to delegate execution -/// to an internal module that does not contain a proper ::main() function. -/// This is to allow unit-testing of the internal code. -/// -/// \param argc The number of arguments passed on the command line. -/// \param argv NULL-terminated array containing the command line arguments. -/// -/// \return 0 on success, some other integer on error. -/// -/// \throw std::exception This throws any uncaught exception. Such exceptions -/// are bugs, but we let them propagate so that the runtime will abort and -/// dump core. -int -main(const int argc, const char* const* const argv) +void +execenv::execenv_host::init() const { - return cli::main(argc, argv); + // nothing to do +} + + +void +execenv::execenv_host::cleanup() const +{ + // nothing to do +} + + +void +execenv::execenv_host::exec(const args_vector& args) const +{ + utils::process::exec(_test_program.absolute_path(), args); } diff --git a/contrib/kyua/main.cpp b/contrib/kyua/engine/execenv/execenv_host.hpp similarity index 64% copy from contrib/kyua/main.cpp copy to contrib/kyua/engine/execenv/execenv_host.hpp index 4344248f89db..2742366cfd6f 100644 --- a/contrib/kyua/main.cpp +++ b/contrib/kyua/engine/execenv/execenv_host.hpp @@ -1,50 +1,63 @@ -// Copyright 2010 The Kyua Authors. +// Copyright 2024 The Kyua Authors. // All rights reserved. // // Redistribution and use in source and binary forms, with or without // modification, are permitted provided that the following conditions are // met: // // * Redistributions of source code must retain the above copyright // notice, this list of conditions and the following disclaimer. // * Redistributions in binary form must reproduce the above copyright // notice, this list of conditions and the following disclaimer in the // documentation and/or other materials provided with the distribution. // * Neither the name of Google Inc. nor the names of its contributors // may be used to endorse or promote products derived from this software // without specific prior written permission. // // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS // "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT // LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR // A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT // OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT // LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -#include "cli/main.hpp" - - -/// Program entry point. -/// -/// The whole purpose of this extremely-simple function is to delegate execution -/// to an internal module that does not contain a proper ::main() function. -/// This is to allow unit-testing of the internal code. -/// -/// \param argc The number of arguments passed on the command line. -/// \param argv NULL-terminated array containing the command line arguments. -/// -/// \return 0 on success, some other integer on error. -/// -/// \throw std::exception This throws any uncaught exception. Such exceptions -/// are bugs, but we let them propagate so that the runtime will abort and -/// dump core. -int -main(const int argc, const char* const* const argv) -{ - return cli::main(argc, argv); -} +/// \file engine/execenv/execenv_host.hpp +/// Default execution environment. + +#if !defined(ENGINE_EXECENV_EXECENV_HOST_HPP) +#define ENGINE_EXECENV_EXECENV_HOST_HPP + +#include "engine/execenv/execenv.hpp" + +#include "utils/process/operations_fwd.hpp" + +namespace execenv = engine::execenv; + +using utils::process::args_vector; + +namespace engine { +namespace execenv { + + +class execenv_host : public execenv::interface { +public: + execenv_host(const model::test_program& test_program, + const std::string& test_case_name) : + execenv::interface(test_program, test_case_name) + {} + + void init() const; + void cleanup() const; + void exec(const args_vector& args) const UTILS_NORETURN; +}; + + +} // namespace execenv +} // namespace engine + +#endif // !defined(ENGINE_EXECENV_EXECENV_HOST_HPP) diff --git a/contrib/kyua/engine/plain.cpp b/contrib/kyua/engine/plain.cpp index 8346e50bbecf..9a2c63f8b663 100644 --- a/contrib/kyua/engine/plain.cpp +++ b/contrib/kyua/engine/plain.cpp @@ -1,143 +1,148 @@ // Copyright 2014 The Kyua Authors. // All rights reserved. // // Redistribution and use in source and binary forms, with or without // modification, are permitted provided that the following conditions are // met: // // * Redistributions of source code must retain the above copyright // notice, this list of conditions and the following disclaimer. // * Redistributions in binary form must reproduce the above copyright // notice, this list of conditions and the following disclaimer in the // documentation and/or other materials provided with the distribution. // * Neither the name of Google Inc. nor the names of its contributors // may be used to endorse or promote products derived from this software // without specific prior written permission. // // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS // "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT // LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR // A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT // OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT // LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #include "engine/plain.hpp" extern "C" { #include } #include +#include "engine/execenv/execenv.hpp" #include "model/test_case.hpp" #include "model/test_program.hpp" #include "model/test_result.hpp" #include "utils/defs.hpp" #include "utils/env.hpp" #include "utils/format/macros.hpp" #include "utils/fs/path.hpp" #include "utils/optional.ipp" #include "utils/process/operations.hpp" #include "utils/process/status.hpp" #include "utils/sanity.hpp" namespace config = utils::config; +namespace execenv = engine::execenv; namespace fs = utils::fs; namespace process = utils::process; using utils::optional; /// Executes a test program's list operation. /// /// This method is intended to be called within a subprocess and is expected /// to terminate execution either by exec(2)ing the test program or by /// exiting with a failure. void engine::plain_interface::exec_list( const model::test_program& /* test_program */, const config::properties_map& /* vars */) const { ::_exit(EXIT_SUCCESS); } /// Computes the test cases list of a test program. /// /// \return A list of test cases. model::test_cases_map engine::plain_interface::parse_list( const optional< process::status >& /* status */, const fs::path& /* stdout_path */, const fs::path& /* stderr_path */) const { return model::test_cases_map_builder().add("main").build(); } /// Executes a test case of the test program. /// /// This method is intended to be called within a subprocess and is expected /// to terminate execution either by exec(2)ing the test program or by /// exiting with a failure. /// /// \param test_program The test program to execute. /// \param test_case_name Name of the test case to invoke. /// \param vars User-provided variables to pass to the test program. void engine::plain_interface::exec_test( const model::test_program& test_program, const std::string& test_case_name, const config::properties_map& vars, const fs::path& /* control_directory */) const { PRE(test_case_name == "main"); for (config::properties_map::const_iterator iter = vars.begin(); iter != vars.end(); ++iter) { utils::setenv(F("TEST_ENV_%s") % (*iter).first, (*iter).second); } process::args_vector args; - process::exec(test_program.absolute_path(), args); + + auto e = execenv::get(test_program, test_case_name); + e->init(); + e->exec(args); } /// Computes the result of a test case based on its termination status. /// /// \param status The termination status of the subprocess used to execute /// the exec_test() method or none if the test timed out. /// /// \return A test result. model::test_result engine::plain_interface::compute_result( const optional< process::status >& status, const fs::path& /* control_directory */, const fs::path& /* stdout_path */, const fs::path& /* stderr_path */) const { if (!status) { return model::test_result(model::test_result_broken, "Test case timed out"); } if (status.get().exited()) { const int exitstatus = status.get().exitstatus(); if (exitstatus == EXIT_SUCCESS) { return model::test_result(model::test_result_passed); } else { return model::test_result( model::test_result_failed, F("Returned non-success exit status %s") % exitstatus); } } else { return model::test_result( model::test_result_broken, F("Received signal %s") % status.get().termsig()); } } diff --git a/contrib/kyua/engine/requirements.cpp b/contrib/kyua/engine/requirements.cpp index a7b0a90d97db..a6a4cae7511c 100644 --- a/contrib/kyua/engine/requirements.cpp +++ b/contrib/kyua/engine/requirements.cpp @@ -1,293 +1,326 @@ // Copyright 2012 The Kyua Authors. // All rights reserved. // // Redistribution and use in source and binary forms, with or without // modification, are permitted provided that the following conditions are // met: // // * Redistributions of source code must retain the above copyright // notice, this list of conditions and the following disclaimer. // * Redistributions in binary form must reproduce the above copyright // notice, this list of conditions and the following disclaimer in the // documentation and/or other materials provided with the distribution. // * Neither the name of Google Inc. nor the names of its contributors // may be used to endorse or promote products derived from this software // without specific prior written permission. // // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS // "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT // LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR // A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT // OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT // LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #include "engine/requirements.hpp" +#include "engine/execenv/execenv.hpp" #include "model/metadata.hpp" #include "model/types.hpp" #include "utils/config/nodes.ipp" #include "utils/config/tree.ipp" #include "utils/format/macros.hpp" #include "utils/fs/operations.hpp" #include "utils/fs/path.hpp" #include "utils/memory.hpp" #include "utils/passwd.hpp" #include "utils/sanity.hpp" #include "utils/units.hpp" namespace config = utils::config; namespace fs = utils::fs; namespace passwd = utils::passwd; namespace units = utils::units; namespace { /// Checks if all required configuration variables are present. /// /// \param required_configs Set of required variable names. /// \param user_config Runtime user configuration. /// \param test_suite_name Name of the test suite the test belongs to. /// /// \return Empty if all variables are present or an error message otherwise. static std::string check_required_configs(const model::strings_set& required_configs, const config::tree& user_config, const std::string& test_suite_name) { for (model::strings_set::const_iterator iter = required_configs.begin(); iter != required_configs.end(); iter++) { std::string property; // TODO(jmmv): All this rewrite logic belongs in the ATF interface. if ((*iter) == "unprivileged-user" || (*iter) == "unprivileged_user") property = "unprivileged_user"; else property = F("test_suites.%s.%s") % test_suite_name % (*iter); if (!user_config.is_set(property)) return F("Required configuration property '%s' not defined") % (*iter); } return ""; } /// Checks if the allowed architectures match the current architecture. /// /// \param allowed_architectures Set of allowed architectures. /// \param user_config Runtime user configuration. /// /// \return Empty if the current architecture is in the list or an error /// message otherwise. static std::string check_allowed_architectures(const model::strings_set& allowed_architectures, const config::tree& user_config) { if (!allowed_architectures.empty()) { const std::string architecture = user_config.lookup< config::string_node >("architecture"); if (allowed_architectures.find(architecture) == allowed_architectures.end()) return F("Current architecture '%s' not supported") % architecture; } return ""; } +/// Checks if test's execenv matches the user configuration. +/// +/// \param execenv Execution environment name a test is designed for. +/// \param user_config Runtime user configuration. +/// +/// \return Empty if the execenv is in the list or an error message otherwise. +static std::string +check_execenv(const std::string& execenv, const config::tree& user_config) +{ + std::string name = execenv; + if (name.empty()) + name = engine::execenv::default_execenv_name; // if test claims nothing + + std::set< std::string > execenvs; + try { + execenvs = user_config.lookup< config::strings_set_node >("execenvs"); + } catch (const config::unknown_key_error&) { + // okay, user config does not define it, empty set then + } + + if (execenvs.find(name) == execenvs.end()) + return F("'%s' execenv is not supported or not allowed by " + "the runtime user configuration") % name; + + return ""; +} + + /// Checks if the allowed platforms match the current architecture. /// /// \param allowed_platforms Set of allowed platforms. /// \param user_config Runtime user configuration. /// /// \return Empty if the current platform is in the list or an error message /// otherwise. static std::string check_allowed_platforms(const model::strings_set& allowed_platforms, const config::tree& user_config) { if (!allowed_platforms.empty()) { const std::string platform = user_config.lookup< config::string_node >("platform"); if (allowed_platforms.find(platform) == allowed_platforms.end()) return F("Current platform '%s' not supported") % platform; } return ""; } /// Checks if the current user matches the required user. /// /// \param required_user Name of the required user category. /// \param user_config Runtime user configuration. /// /// \return Empty if the current user fits the required user characteristics or /// an error message otherwise. static std::string check_required_user(const std::string& required_user, const config::tree& user_config) { if (!required_user.empty()) { const passwd::user user = passwd::current_user(); if (required_user == "root") { if (!user.is_root()) return "Requires root privileges"; } else if (required_user == "unprivileged") { if (user.is_root()) if (!user_config.is_set("unprivileged_user")) return "Requires an unprivileged user but the " "unprivileged-user configuration variable is not " "defined"; } else UNREACHABLE_MSG("Value of require.user not properly validated"); } return ""; } /// Checks if all required files exist. /// /// \param required_files Set of paths. /// /// \return Empty if the required files all exist or an error message otherwise. static std::string check_required_files(const model::paths_set& required_files) { for (model::paths_set::const_iterator iter = required_files.begin(); iter != required_files.end(); iter++) { INV((*iter).is_absolute()); if (!fs::exists(*iter)) return F("Required file '%s' not found") % *iter; } return ""; } /// Checks if all required programs exist. /// /// \param required_programs Set of paths. /// /// \return Empty if the required programs all exist or an error message /// otherwise. static std::string check_required_programs(const model::paths_set& required_programs) { for (model::paths_set::const_iterator iter = required_programs.begin(); iter != required_programs.end(); iter++) { if ((*iter).is_absolute()) { if (!fs::exists(*iter)) return F("Required program '%s' not found") % *iter; } else { if (!fs::find_in_path((*iter).c_str())) return F("Required program '%s' not found in PATH") % *iter; } } return ""; } /// Checks if the current system has the specified amount of memory. /// /// \param required_memory Amount of required physical memory, or zero if not /// applicable. /// /// \return Empty if the current system has the required amount of memory or an /// error message otherwise. static std::string check_required_memory(const units::bytes& required_memory) { if (required_memory > 0) { const units::bytes physical_memory = utils::physical_memory(); if (physical_memory > 0 && physical_memory < required_memory) return F("Requires %s bytes of physical memory but only %s " "available") % required_memory.format() % physical_memory.format(); } return ""; } /// Checks if the work directory's file system has enough free disk space. /// /// \param required_disk_space Amount of required free disk space, or zero if /// not applicable. /// \param work_directory Path to where the test case will be run. /// /// \return Empty if the file system where the work directory is hosted has /// enough free disk space or an error message otherwise. static std::string check_required_disk_space(const units::bytes& required_disk_space, const fs::path& work_directory) { if (required_disk_space > 0) { const units::bytes free_disk_space = fs::free_disk_space( work_directory); if (free_disk_space < required_disk_space) return F("Requires %s bytes of free disk space but only %s " "available") % required_disk_space.format() % free_disk_space.format(); } return ""; } } // anonymous namespace /// Checks if all the requirements specified by the test case are met. /// /// \param md The test metadata. /// \param cfg The engine configuration. /// \param test_suite Name of the test suite the test belongs to. /// \param work_directory Path to where the test case will be run. /// /// \return A string describing the reason for skipping the test, or empty if /// the test should be executed. std::string engine::check_reqs(const model::metadata& md, const config::tree& cfg, const std::string& test_suite, const fs::path& work_directory) { std::string reason; reason = check_required_configs(md.required_configs(), cfg, test_suite); if (!reason.empty()) return reason; reason = check_allowed_architectures(md.allowed_architectures(), cfg); if (!reason.empty()) return reason; + reason = check_execenv(md.execenv(), cfg); + if (!reason.empty()) + return reason; + reason = check_allowed_platforms(md.allowed_platforms(), cfg); if (!reason.empty()) return reason; reason = check_required_user(md.required_user(), cfg); if (!reason.empty()) return reason; reason = check_required_files(md.required_files()); if (!reason.empty()) return reason; reason = check_required_programs(md.required_programs()); if (!reason.empty()) return reason; reason = check_required_memory(md.required_memory()); if (!reason.empty()) return reason; reason = check_required_disk_space(md.required_disk_space(), work_directory); if (!reason.empty()) return reason; INV(reason.empty()); return reason; } diff --git a/contrib/kyua/engine/scheduler.cpp b/contrib/kyua/engine/scheduler.cpp index e7b51d23acca..e75091a40e38 100644 --- a/contrib/kyua/engine/scheduler.cpp +++ b/contrib/kyua/engine/scheduler.cpp @@ -1,1373 +1,1639 @@ // Copyright 2014 The Kyua Authors. // All rights reserved. // // Redistribution and use in source and binary forms, with or without // modification, are permitted provided that the following conditions are // met: // // * Redistributions of source code must retain the above copyright // notice, this list of conditions and the following disclaimer. // * Redistributions in binary form must reproduce the above copyright // notice, this list of conditions and the following disclaimer in the // documentation and/or other materials provided with the distribution. // * Neither the name of Google Inc. nor the names of its contributors // may be used to endorse or promote products derived from this software // without specific prior written permission. // // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS // "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT // LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR // A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT // OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT // LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #include "engine/scheduler.hpp" extern "C" { #include } #include #include #include #include #include #include "engine/config.hpp" #include "engine/exceptions.hpp" +#include "engine/execenv/execenv.hpp" #include "engine/requirements.hpp" #include "model/context.hpp" #include "model/metadata.hpp" #include "model/test_case.hpp" #include "model/test_program.hpp" #include "model/test_result.hpp" #include "utils/config/tree.ipp" #include "utils/datetime.hpp" #include "utils/defs.hpp" #include "utils/env.hpp" #include "utils/format/macros.hpp" #include "utils/fs/directory.hpp" #include "utils/fs/exceptions.hpp" #include "utils/fs/operations.hpp" #include "utils/fs/path.hpp" #include "utils/logging/macros.hpp" #include "utils/noncopyable.hpp" #include "utils/optional.ipp" #include "utils/passwd.hpp" #include "utils/process/executor.ipp" #include "utils/process/status.hpp" #include "utils/sanity.hpp" #include "utils/stacktrace.hpp" #include "utils/stream.hpp" #include "utils/text/operations.ipp" namespace config = utils::config; namespace datetime = utils::datetime; +namespace execenv = engine::execenv; namespace executor = utils::process::executor; namespace fs = utils::fs; namespace logging = utils::logging; namespace passwd = utils::passwd; namespace process = utils::process; namespace scheduler = engine::scheduler; namespace text = utils::text; using utils::none; using utils::optional; /// Timeout for the test case cleanup operation. /// /// TODO(jmmv): This is here only for testing purposes. Maybe we should expose /// this setting as part of the user_config. datetime::delta scheduler::cleanup_timeout(60, 0); +/// Timeout for the test case execenv cleanup operation. +datetime::delta scheduler::execenv_cleanup_timeout(60, 0); + + /// Timeout for the test case listing operation. /// /// TODO(jmmv): This is here only for testing purposes. Maybe we should expose /// this setting as part of the user_config. datetime::delta scheduler::list_timeout(300, 0); namespace { /// Magic exit status to indicate that the test case was probably skipped. /// /// The test case was only skipped if and only if we return this exit code and /// we find the skipped_cookie file on disk. static const int exit_skipped = 84; /// Text file containing the skip reason for the test case. /// /// This will only be present within unique_work_directory if the test case /// exited with the exit_skipped code. However, there is no guarantee that the /// file is there (say if the test really decided to exit with code exit_skipped /// on its own). static const char* skipped_cookie = "skipped.txt"; /// Mapping of interface names to interface definitions. typedef std::map< std::string, std::shared_ptr< scheduler::interface > > interfaces_map; /// Mapping of interface names to interface definitions. /// /// Use register_interface() to add an entry to this global table. static interfaces_map interfaces; /// Scans the contents of a directory and appends the file listing to a file. /// /// \param dir_path The directory to scan. /// \param output_file The file to which to append the listing. /// /// \throw engine::error If there are problems listing the files. static void append_files_listing(const fs::path& dir_path, const fs::path& output_file) { std::ofstream output(output_file.c_str(), std::ios::app); if (!output) throw engine::error(F("Failed to open output file %s for append") % output_file); try { std::set < std::string > names; const fs::directory dir(dir_path); for (fs::directory::const_iterator iter = dir.begin(); iter != dir.end(); ++iter) { if (iter->name != "." && iter->name != "..") names.insert(iter->name); } if (!names.empty()) { output << "Files left in work directory after failure: " << text::join(names, ", ") << '\n'; } } catch (const fs::error& e) { throw engine::error(F("Cannot append files listing to %s: %s") % output_file % e.what()); } } /// Maintenance data held while a test is being executed. /// /// This data structure exists from the moment when a test is executed via /// scheduler::spawn_test() or scheduler::impl::spawn_cleanup() to when it is /// cleaned up with result_handle::cleanup(). /// /// This is a base data type intended to be extended for the test and cleanup /// cases so that each contains only the relevant data. struct exec_data : utils::noncopyable { /// Test program data for this test case. const model::test_program_ptr test_program; /// Name of the test case. const std::string test_case_name; /// Constructor. /// /// \param test_program_ Test program data for this test case. /// \param test_case_name_ Name of the test case. exec_data(const model::test_program_ptr test_program_, const std::string& test_case_name_) : test_program(test_program_), test_case_name(test_case_name_) { } /// Destructor. virtual ~exec_data(void) { } }; /// Maintenance data held while a test is being executed. struct test_exec_data : public exec_data { /// Test program-specific execution interface. const std::shared_ptr< scheduler::interface > interface; /// User configuration passed to the execution of the test. We need this /// here to recover it later when chaining the execution of a cleanup /// routine (if any). const config::tree user_config; /// Whether this test case still needs to have its cleanup routine executed. /// /// This is set externally when the cleanup routine is actually invoked to /// denote that no further attempts shall be made at cleaning this up. bool needs_cleanup; + /// Whether this test case still needs to have its execenv cleanup executed. + /// + /// This is set externally when the cleanup routine is actually invoked to + /// denote that no further attempts shall be made at cleaning this up. + bool needs_execenv_cleanup; + + /// Original PID of the test case subprocess. + /// + /// This is used for the cleanup upon termination by a signal, to reap the + /// leftovers and form missing exit_handle. + pid_t pid; + /// The exit_handle for this test once it has completed. /// /// This is set externally when the test case has finished, as we need this /// information to invoke the followup cleanup routine in the right context, /// as indicated by needs_cleanup. optional< executor::exit_handle > exit_handle; /// Constructor. /// /// \param test_program_ Test program data for this test case. /// \param test_case_name_ Name of the test case. /// \param interface_ Test program-specific execution interface. /// \param user_config_ User configuration passed to the test. test_exec_data(const model::test_program_ptr test_program_, const std::string& test_case_name_, const std::shared_ptr< scheduler::interface > interface_, - const config::tree& user_config_) : + const config::tree& user_config_, + const pid_t pid_) : exec_data(test_program_, test_case_name_), - interface(interface_), user_config(user_config_) + interface(interface_), user_config(user_config_), pid(pid_) { const model::test_case& test_case = test_program->find(test_case_name); needs_cleanup = test_case.get_metadata().has_cleanup(); + needs_execenv_cleanup = test_case.get_metadata().has_execenv(); } }; /// Maintenance data held while a test cleanup routine is being executed. /// /// Instances of this object are related to a previous test_exec_data, as /// cleanup routines can only exist once the test has been run. struct cleanup_exec_data : public exec_data { /// The exit handle of the test. This is necessary so that we can return /// the correct exit_handle to the user of the scheduler. executor::exit_handle body_exit_handle; /// The final result of the test's body. This is necessary to compute the /// right return value for a test with a cleanup routine: the body result is /// respected if it is a "bad" result; else the result of the cleanup /// routine is used if it has failed. model::test_result body_result; /// Constructor. /// /// \param test_program_ Test program data for this test case. /// \param test_case_name_ Name of the test case. /// \param body_exit_handle_ If not none, exit handle of the body /// corresponding to the cleanup routine represented by this exec_data. /// \param body_result_ If not none, result of the body corresponding to the /// cleanup routine represented by this exec_data. cleanup_exec_data(const model::test_program_ptr test_program_, const std::string& test_case_name_, const executor::exit_handle& body_exit_handle_, const model::test_result& body_result_) : exec_data(test_program_, test_case_name_), body_exit_handle(body_exit_handle_), body_result(body_result_) { } }; +/// Maintenance data held while a test execenv cleanup is being executed. +/// +/// Instances of this object are related to a previous test_exec_data, as +/// cleanup routines can only exist once the test has been run. +struct execenv_exec_data : public exec_data { + /// The exit handle of the test. This is necessary so that we can return + /// the correct exit_handle to the user of the scheduler. + executor::exit_handle body_exit_handle; + + /// The final result of the test's body. This is necessary to compute the + /// right return value for a test with a cleanup routine: the body result is + /// respected if it is a "bad" result; else the result of the cleanup + /// routine is used if it has failed. + model::test_result body_result; + + /// Constructor. + /// + /// \param test_program_ Test program data for this test case. + /// \param test_case_name_ Name of the test case. + /// \param body_exit_handle_ If not none, exit handle of the body + /// corresponding to the cleanup routine represented by this exec_data. + /// \param body_result_ If not none, result of the body corresponding to the + /// cleanup routine represented by this exec_data. + execenv_exec_data(const model::test_program_ptr test_program_, + const std::string& test_case_name_, + const executor::exit_handle& body_exit_handle_, + const model::test_result& body_result_) : + exec_data(test_program_, test_case_name_), + body_exit_handle(body_exit_handle_), body_result(body_result_) + { + } +}; + + /// Shared pointer to exec_data. /// /// We require this because we want exec_data to not be copyable, and thus we /// cannot just store it in the map without move constructors. typedef std::shared_ptr< exec_data > exec_data_ptr; /// Mapping of active PIDs to their maintenance data. typedef std::map< int, exec_data_ptr > exec_data_map; /// Enforces a test program to hold an absolute path. /// /// TODO(jmmv): This function (which is a pretty ugly hack) exists because we /// want the interface hooks to receive a test_program as their argument. /// However, those hooks run after the test program has been isolated, which /// means that the current directory has changed since when the test_program /// objects were created. This causes the absolute_path() method of /// test_program to return bogus values if the internal representation of their /// path is relative. We should fix somehow: maybe making the fs module grab /// its "current_path" view at program startup time; or maybe by grabbing the /// current path at test_program creation time; or maybe something else. /// /// \param program The test program to modify. /// /// \return A new test program whose internal paths are absolute. static model::test_program force_absolute_paths(const model::test_program program) { const std::string& relative = program.relative_path().str(); const std::string absolute = program.absolute_path().str(); const std::string root = absolute.substr( 0, absolute.length() - relative.length()); return model::test_program( program.interface_name(), program.relative_path(), fs::path(root), program.test_suite_name(), program.get_metadata(), program.test_cases()); } /// Functor to list the test cases of a test program. class list_test_cases { /// Interface of the test program to execute. std::shared_ptr< scheduler::interface > _interface; /// Test program to execute. const model::test_program _test_program; /// User-provided configuration variables. const config::tree& _user_config; public: /// Constructor. /// /// \param interface Interface of the test program to execute. /// \param test_program Test program to execute. /// \param user_config User-provided configuration variables. list_test_cases( const std::shared_ptr< scheduler::interface > interface, const model::test_program* test_program, const config::tree& user_config) : _interface(interface), _test_program(force_absolute_paths(*test_program)), _user_config(user_config) { } /// Body of the subprocess. void operator()(const fs::path& /* control_directory */) { const config::properties_map vars = scheduler::generate_config( _user_config, _test_program.test_suite_name()); _interface->exec_list(_test_program, vars); } }; /// Functor to execute a test program in a child process. class run_test_program { /// Interface of the test program to execute. std::shared_ptr< scheduler::interface > _interface; /// Test program to execute. const model::test_program _test_program; /// Name of the test case to execute. const std::string& _test_case_name; /// User-provided configuration variables. const config::tree& _user_config; /// Verifies if the test case needs to be skipped or not. /// /// We could very well run this on the scheduler parent process before /// issuing the fork. However, doing this here in the child process is /// better for two reasons: first, it allows us to continue using the simple /// spawn/wait abstraction of the scheduler; and, second, we parallelize the /// requirements checks among tests. /// /// \post If the test's preconditions are not met, the caller process is /// terminated with a special exit code and a "skipped cookie" is written to /// the disk with the reason for the failure. /// /// \param skipped_cookie_path File to create with the skip reason details /// if this test is skipped. void do_requirements_check(const fs::path& skipped_cookie_path) { const model::test_case& test_case = _test_program.find( _test_case_name); const std::string skip_reason = engine::check_reqs( test_case.get_metadata(), _user_config, _test_program.test_suite_name(), fs::current_path()); if (skip_reason.empty()) return; std::ofstream output(skipped_cookie_path.c_str()); if (!output) { std::perror((F("Failed to open %s for write") % skipped_cookie_path).str().c_str()); std::abort(); } output << skip_reason; output.close(); // Abruptly terminate the process. We don't want to run any destructors // inherited from the parent process by mistake, which could, for // example, delete our own control files! ::_exit(exit_skipped); } public: /// Constructor. /// /// \param interface Interface of the test program to execute. /// \param test_program Test program to execute. /// \param test_case_name Name of the test case to execute. /// \param user_config User-provided configuration variables. run_test_program( const std::shared_ptr< scheduler::interface > interface, const model::test_program_ptr test_program, const std::string& test_case_name, const config::tree& user_config) : _interface(interface), _test_program(force_absolute_paths(*test_program)), _test_case_name(test_case_name), _user_config(user_config) { } /// Body of the subprocess. /// /// \param control_directory The testcase directory where files will be /// read from. void operator()(const fs::path& control_directory) { const model::test_case& test_case = _test_program.find( _test_case_name); if (test_case.fake_result()) ::_exit(EXIT_SUCCESS); do_requirements_check(control_directory / skipped_cookie); const config::properties_map vars = scheduler::generate_config( _user_config, _test_program.test_suite_name()); _interface->exec_test(_test_program, _test_case_name, vars, control_directory); } }; /// Functor to execute a test program in a child process. class run_test_cleanup { /// Interface of the test program to execute. std::shared_ptr< scheduler::interface > _interface; /// Test program to execute. const model::test_program _test_program; /// Name of the test case to execute. const std::string& _test_case_name; /// User-provided configuration variables. const config::tree& _user_config; public: /// Constructor. /// /// \param interface Interface of the test program to execute. /// \param test_program Test program to execute. /// \param test_case_name Name of the test case to execute. /// \param user_config User-provided configuration variables. run_test_cleanup( const std::shared_ptr< scheduler::interface > interface, const model::test_program_ptr test_program, const std::string& test_case_name, const config::tree& user_config) : _interface(interface), _test_program(force_absolute_paths(*test_program)), _test_case_name(test_case_name), _user_config(user_config) { } /// Body of the subprocess. /// /// \param control_directory The testcase directory where cleanup will be /// run from. void operator()(const fs::path& control_directory) { const config::properties_map vars = scheduler::generate_config( _user_config, _test_program.test_suite_name()); _interface->exec_cleanup(_test_program, _test_case_name, vars, control_directory); } }; +/// Functor to execute a test execenv cleanup in a child process. +class run_execenv_cleanup { + /// Test program to execute. + const model::test_program _test_program; + + /// Name of the test case to execute. + const std::string& _test_case_name; + +public: + /// Constructor. + /// + /// \param test_program Test program to execute. + /// \param test_case_name Name of the test case to execute. + run_execenv_cleanup( + const model::test_program_ptr test_program, + const std::string& test_case_name) : + _test_program(force_absolute_paths(*test_program)), + _test_case_name(test_case_name) + { + } + + /// Body of the subprocess. + /// + /// \param control_directory The testcase directory where cleanup will be + /// run from. + void + operator()(const fs::path& /* control_directory */) + { + auto e = execenv::get(_test_program, _test_case_name); + e->cleanup(); + } +}; + + /// Obtains the right scheduler interface for a given test program. /// /// \param name The name of the interface of the test program. /// /// \return An scheduler interface. std::shared_ptr< scheduler::interface > find_interface(const std::string& name) { const interfaces_map::const_iterator iter = interfaces.find(name); PRE(interfaces.find(name) != interfaces.end()); return (*iter).second; } } // anonymous namespace void scheduler::interface::exec_cleanup( const model::test_program& /* test_program */, const std::string& /* test_case_name */, const config::properties_map& /* vars */, const utils::fs::path& /* control_directory */) const { // Most test interfaces do not support standalone cleanup routines so // provide a default implementation that does nothing. UNREACHABLE_MSG("exec_cleanup not implemented for an interface that " "supports standalone cleanup routines"); } /// Internal implementation of a lazy_test_program. struct engine::scheduler::lazy_test_program::impl : utils::noncopyable { /// Whether the test cases list has been yet loaded or not. bool _loaded; /// User configuration to pass to the test program list operation. config::tree _user_config; /// Scheduler context to use to load test cases. scheduler::scheduler_handle& _scheduler_handle; /// Constructor. /// /// \param user_config_ User configuration to pass to the test program list /// operation. /// \param scheduler_handle_ Scheduler context to use when loading test /// cases. impl(const config::tree& user_config_, scheduler::scheduler_handle& scheduler_handle_) : _loaded(false), _user_config(user_config_), _scheduler_handle(scheduler_handle_) { } }; /// Constructs a new test program. /// /// \param interface_name_ Name of the test program interface. /// \param binary_ The name of the test program binary relative to root_. /// \param root_ The root of the test suite containing the test program. /// \param test_suite_name_ The name of the test suite this program belongs to. /// \param md_ Metadata of the test program. /// \param user_config_ User configuration to pass to the scheduler. /// \param scheduler_handle_ Scheduler context to use to load test cases. scheduler::lazy_test_program::lazy_test_program( const std::string& interface_name_, const fs::path& binary_, const fs::path& root_, const std::string& test_suite_name_, const model::metadata& md_, const config::tree& user_config_, scheduler::scheduler_handle& scheduler_handle_) : test_program(interface_name_, binary_, root_, test_suite_name_, md_, model::test_cases_map()), _pimpl(new impl(user_config_, scheduler_handle_)) { } /// Gets or loads the list of test cases from the test program. /// /// \return The list of test cases provided by the test program. const model::test_cases_map& scheduler::lazy_test_program::test_cases(void) const { _pimpl->_scheduler_handle.check_interrupt(); if (!_pimpl->_loaded) { const model::test_cases_map tcs = _pimpl->_scheduler_handle.list_tests( this, _pimpl->_user_config); // Due to the restrictions on when set_test_cases() may be called (as a // way to lazily initialize the test cases list before it is ever // returned), this cast is valid. const_cast< scheduler::lazy_test_program* >(this)->set_test_cases(tcs); _pimpl->_loaded = true; _pimpl->_scheduler_handle.check_interrupt(); } INV(_pimpl->_loaded); return test_program::test_cases(); } /// Internal implementation for the result_handle class. struct engine::scheduler::result_handle::bimpl : utils::noncopyable { /// Generic executor exit handle for this result handle. executor::exit_handle generic; /// Mutable pointer to the corresponding scheduler state. /// /// This object references a member of the scheduler_handle that yielded /// this result_handle instance. We need this direct access to clean up /// after ourselves when the result is destroyed. exec_data_map& all_exec_data; /// Constructor. /// /// \param generic_ Generic executor exit handle for this result handle. /// \param [in,out] all_exec_data_ Global object keeping track of all active /// executions for an scheduler. This is a pointer to a member of the /// scheduler_handle object. bimpl(const executor::exit_handle generic_, exec_data_map& all_exec_data_) : generic(generic_), all_exec_data(all_exec_data_) { } /// Destructor. ~bimpl(void) { LD(F("Removing %s from all_exec_data") % generic.original_pid()); all_exec_data.erase(generic.original_pid()); } }; /// Constructor. /// /// \param pbimpl Constructed internal implementation. scheduler::result_handle::result_handle(std::shared_ptr< bimpl > pbimpl) : _pbimpl(pbimpl) { } /// Destructor. scheduler::result_handle::~result_handle(void) { } /// Cleans up the test case results. /// /// This function should be called explicitly as it provides the means to /// control any exceptions raised during cleanup. Do not rely on the destructor /// to clean things up. /// /// \throw engine::error If the cleanup fails, especially due to the inability /// to remove the work directory. void scheduler::result_handle::cleanup(void) { _pbimpl->generic.cleanup(); } /// Returns the original PID corresponding to this result. /// /// \return An exec_handle. int scheduler::result_handle::original_pid(void) const { return _pbimpl->generic.original_pid(); } /// Returns the timestamp of when spawn_test was called. /// /// \return A timestamp. const datetime::timestamp& scheduler::result_handle::start_time(void) const { return _pbimpl->generic.start_time(); } /// Returns the timestamp of when wait_any_test returned this object. /// /// \return A timestamp. const datetime::timestamp& scheduler::result_handle::end_time(void) const { return _pbimpl->generic.end_time(); } /// Returns the path to the test-specific work directory. /// /// This is guaranteed to be clear of files created by the scheduler. /// /// \return The path to a directory that exists until cleanup() is called. fs::path scheduler::result_handle::work_directory(void) const { return _pbimpl->generic.work_directory(); } /// Returns the path to the test's stdout file. /// /// \return The path to a file that exists until cleanup() is called. const fs::path& scheduler::result_handle::stdout_file(void) const { return _pbimpl->generic.stdout_file(); } /// Returns the path to the test's stderr file. /// /// \return The path to a file that exists until cleanup() is called. const fs::path& scheduler::result_handle::stderr_file(void) const { return _pbimpl->generic.stderr_file(); } /// Internal implementation for the test_result_handle class. struct engine::scheduler::test_result_handle::impl : utils::noncopyable { /// Test program data for this test case. model::test_program_ptr test_program; /// Name of the test case. std::string test_case_name; /// The actual result of the test execution. const model::test_result test_result; /// Constructor. /// /// \param test_program_ Test program data for this test case. /// \param test_case_name_ Name of the test case. /// \param test_result_ The actual result of the test execution. impl(const model::test_program_ptr test_program_, const std::string& test_case_name_, const model::test_result& test_result_) : test_program(test_program_), test_case_name(test_case_name_), test_result(test_result_) { } }; /// Constructor. /// /// \param pbimpl Constructed internal implementation for the base object. /// \param pimpl Constructed internal implementation. scheduler::test_result_handle::test_result_handle( std::shared_ptr< bimpl > pbimpl, std::shared_ptr< impl > pimpl) : result_handle(pbimpl), _pimpl(pimpl) { } /// Destructor. scheduler::test_result_handle::~test_result_handle(void) { } /// Returns the test program that yielded this result. /// /// \return A test program. const model::test_program_ptr scheduler::test_result_handle::test_program(void) const { return _pimpl->test_program; } /// Returns the name of the test case that yielded this result. /// /// \return A test case name const std::string& scheduler::test_result_handle::test_case_name(void) const { return _pimpl->test_case_name; } /// Returns the actual result of the test execution. /// /// \return A test result. const model::test_result& scheduler::test_result_handle::test_result(void) const { return _pimpl->test_result; } /// Internal implementation for the scheduler_handle. struct engine::scheduler::scheduler_handle::impl : utils::noncopyable { /// Generic executor instance encapsulated by this one. executor::executor_handle generic; /// Mapping of exec handles to the data required at run time. exec_data_map all_exec_data; /// Collection of test_exec_data objects. typedef std::vector< const test_exec_data* > test_exec_data_vector; /// Constructor. impl(void) : generic(executor::setup()) { } /// Destructor. /// /// This runs any pending cleanup routines, which should only happen if the /// scheduler is abruptly terminated (aka if a signal is received). ~impl(void) { const test_exec_data_vector tests_data = tests_needing_cleanup(); for (test_exec_data_vector::const_iterator iter = tests_data.begin(); iter != tests_data.end(); ++iter) { const test_exec_data* test_data = *iter; try { sync_cleanup(test_data); } catch (const std::runtime_error& e) { LW(F("Failed to run cleanup routine for %s:%s on abrupt " "termination") % test_data->test_program->relative_path() % test_data->test_case_name); } } + + const test_exec_data_vector td = tests_needing_execenv_cleanup(); + + for (test_exec_data_vector::const_iterator iter = td.begin(); + iter != td.end(); ++iter) { + const test_exec_data* test_data = *iter; + + try { + sync_execenv_cleanup(test_data); + } catch (const std::runtime_error& e) { + LW(F("Failed to run execenv cleanup routine for %s:%s on abrupt " + "termination") + % test_data->test_program->relative_path() + % test_data->test_case_name); + } + } } /// Finds any pending exec_datas that correspond to tests needing cleanup. /// /// \return The collection of test_exec_data objects that have their /// needs_cleanup property set to true. test_exec_data_vector tests_needing_cleanup(void) { test_exec_data_vector tests_data; for (exec_data_map::const_iterator iter = all_exec_data.begin(); iter != all_exec_data.end(); ++iter) { const exec_data_ptr data = (*iter).second; try { test_exec_data* test_data = &dynamic_cast< test_exec_data& >( *data.get()); if (test_data->needs_cleanup) { tests_data.push_back(test_data); test_data->needs_cleanup = false; + if (!test_data->exit_handle) + test_data->exit_handle = generic.reap(test_data->pid); } } catch (const std::bad_cast& e) { // Do nothing for cleanup_exec_data objects. } } return tests_data; } + /// Finds any pending exec_datas that correspond to tests needing execenv + /// cleanup. + /// + /// \return The collection of test_exec_data objects that have their + /// specific execenv property set. + test_exec_data_vector + tests_needing_execenv_cleanup(void) + { + test_exec_data_vector tests_data; + + for (exec_data_map::const_iterator iter = all_exec_data.begin(); + iter != all_exec_data.end(); ++iter) { + const exec_data_ptr data = (*iter).second; + + try { + test_exec_data* test_data = &dynamic_cast< test_exec_data& >( + *data.get()); + if (test_data->needs_execenv_cleanup) { + tests_data.push_back(test_data); + test_data->needs_execenv_cleanup = false; + if (!test_data->exit_handle) + test_data->exit_handle = generic.reap(test_data->pid); + } + } catch (const std::bad_cast& e) { + // Do nothing for other objects. + } + } + + return tests_data; + } + /// Cleans up a single test case synchronously. /// /// \param test_data The data of the previously executed test case to be /// cleaned up. void sync_cleanup(const test_exec_data* test_data) { // The message in this result should never be seen by the user, but use // something reasonable just in case it leaks and we need to pinpoint // the call site. model::test_result result(model::test_result_broken, "Test case died abruptly"); const executor::exec_handle cleanup_handle = spawn_cleanup( test_data->test_program, test_data->test_case_name, test_data->user_config, test_data->exit_handle.get(), result); generic.wait(cleanup_handle); } /// Forks and executes a test case cleanup routine asynchronously. /// /// \param test_program The container test program. /// \param test_case_name The name of the test case to run. /// \param user_config User-provided configuration variables. /// \param body_handle The exit handle of the test case's corresponding /// body. The cleanup will be executed in the same context. /// \param body_result The result of the test case's corresponding body. /// /// \return A handle for the background operation. Used to match the result /// of the execution returned by wait_any() with this invocation. executor::exec_handle spawn_cleanup(const model::test_program_ptr test_program, const std::string& test_case_name, const config::tree& user_config, const executor::exit_handle& body_handle, const model::test_result& body_result) { generic.check_interrupt(); const std::shared_ptr< scheduler::interface > interface = find_interface(test_program->interface_name()); LI(F("Spawning %s:%s (cleanup)") % test_program->absolute_path() % test_case_name); const executor::exec_handle handle = generic.spawn_followup( run_test_cleanup(interface, test_program, test_case_name, user_config), body_handle, cleanup_timeout); const exec_data_ptr data(new cleanup_exec_data( test_program, test_case_name, body_handle, body_result)); LD(F("Inserting %s into all_exec_data (cleanup)") % handle.pid()); INV_MSG(all_exec_data.find(handle.pid()) == all_exec_data.end(), F("PID %s already in all_exec_data; not properly cleaned " "up or reused too fast") % handle.pid());; all_exec_data.insert(exec_data_map::value_type(handle.pid(), data)); return handle; } + + /// Cleans up a single test case execenv synchronously. + /// + /// \param test_data The data of the previously executed test case to be + /// cleaned up. + void + sync_execenv_cleanup(const test_exec_data* test_data) + { + // The message in this result should never be seen by the user, but use + // something reasonable just in case it leaks and we need to pinpoint + // the call site. + model::test_result result(model::test_result_broken, + "Test case died abruptly"); + + const executor::exec_handle cleanup_handle = spawn_execenv_cleanup( + test_data->test_program, test_data->test_case_name, + test_data->exit_handle.get(), result); + generic.wait(cleanup_handle); + } + + /// Forks and executes a test case execenv cleanup asynchronously. + /// + /// \param test_program The container test program. + /// \param test_case_name The name of the test case to run. + /// \param body_handle The exit handle of the test case's corresponding + /// body. The cleanup will be executed in the same context. + /// \param body_result The result of the test case's corresponding body. + /// + /// \return A handle for the background operation. Used to match the result + /// of the execution returned by wait_any() with this invocation. + executor::exec_handle + spawn_execenv_cleanup(const model::test_program_ptr test_program, + const std::string& test_case_name, + const executor::exit_handle& body_handle, + const model::test_result& body_result) + { + generic.check_interrupt(); + + LI(F("Spawning %s:%s (execenv cleanup)") + % test_program->absolute_path() % test_case_name); + + const executor::exec_handle handle = generic.spawn_followup( + run_execenv_cleanup(test_program, test_case_name), + body_handle, execenv_cleanup_timeout); + + const exec_data_ptr data(new execenv_exec_data( + test_program, test_case_name, body_handle, body_result)); + LD(F("Inserting %s into all_exec_data (execenv cleanup)") % handle.pid()); + INV_MSG(all_exec_data.find(handle.pid()) == all_exec_data.end(), + F("PID %s already in all_exec_data; not properly cleaned " + "up or reused too fast") % handle.pid());; + all_exec_data.insert(exec_data_map::value_type(handle.pid(), data)); + + return handle; + } }; /// Constructor. scheduler::scheduler_handle::scheduler_handle(void) : _pimpl(new impl()) { } /// Destructor. scheduler::scheduler_handle::~scheduler_handle(void) { } /// Queries the path to the root of the work directory for all tests. /// /// \return A path. const fs::path& scheduler::scheduler_handle::root_work_directory(void) const { return _pimpl->generic.root_work_directory(); } /// Cleans up the scheduler state. /// /// This function should be called explicitly as it provides the means to /// control any exceptions raised during cleanup. Do not rely on the destructor /// to clean things up. /// /// \throw engine::error If there are problems cleaning up the scheduler. void scheduler::scheduler_handle::cleanup(void) { _pimpl->generic.cleanup(); } /// Checks if the given interface name is valid. /// /// \param name The name of the interface to validate. /// /// \throw engine::error If the given interface is not supported. void scheduler::ensure_valid_interface(const std::string& name) { if (interfaces.find(name) == interfaces.end()) throw engine::error(F("Unsupported test interface '%s'") % name); } /// Registers a new interface. /// /// \param name The name of the interface. Must not have yet been registered. /// \param spec Interface specification. void scheduler::register_interface(const std::string& name, const std::shared_ptr< interface > spec) { PRE(interfaces.find(name) == interfaces.end()); interfaces.insert(interfaces_map::value_type(name, spec)); } /// Returns the names of all registered interfaces. /// /// \return A collection of interface names. std::set< std::string > scheduler::registered_interface_names(void) { std::set< std::string > names; for (interfaces_map::const_iterator iter = interfaces.begin(); iter != interfaces.end(); ++iter) { names.insert((*iter).first); } return names; } /// Initializes the scheduler. /// /// \pre This function can only be called if there is no other scheduler_handle /// object alive. /// /// \return A handle to the operations of the scheduler. scheduler::scheduler_handle scheduler::setup(void) { return scheduler_handle(); } /// Retrieves the list of test cases from a test program. /// /// This operation is currently synchronous. /// /// This operation should never throw. Any errors during the processing of the /// test case list are subsumed into a single test case in the return value that /// represents the failed retrieval. /// /// \param test_program The test program from which to obtain the list of test /// cases. /// \param user_config User-provided configuration variables. /// /// \return The list of test cases. model::test_cases_map scheduler::scheduler_handle::list_tests( const model::test_program* test_program, const config::tree& user_config) { _pimpl->generic.check_interrupt(); const std::shared_ptr< scheduler::interface > interface = find_interface( test_program->interface_name()); try { const executor::exec_handle exec_handle = _pimpl->generic.spawn( list_test_cases(interface, test_program, user_config), list_timeout, none); executor::exit_handle exit_handle = _pimpl->generic.wait(exec_handle); const model::test_cases_map test_cases = interface->parse_list( exit_handle.status(), exit_handle.stdout_file(), exit_handle.stderr_file()); exit_handle.cleanup(); if (test_cases.empty()) throw std::runtime_error("Empty test cases list"); return test_cases; } catch (const std::runtime_error& e) { // TODO(jmmv): This is a very ugly workaround for the fact that we // cannot report failures at the test-program level. LW(F("Failed to load test cases list: %s") % e.what()); model::test_cases_map fake_test_cases; fake_test_cases.insert(model::test_cases_map::value_type( "__test_cases_list__", model::test_case( "__test_cases_list__", "Represents the correct processing of the test cases list", model::test_result(model::test_result_broken, e.what())))); return fake_test_cases; } } /// Forks and executes a test case asynchronously. /// /// Note that the caller needn't know if the test has a cleanup routine or not. /// If there indeed is a cleanup routine, we trigger it at wait_any() time. /// /// \param test_program The container test program. /// \param test_case_name The name of the test case to run. /// \param user_config User-provided configuration variables. /// /// \return A handle for the background operation. Used to match the result of /// the execution returned by wait_any() with this invocation. scheduler::exec_handle scheduler::scheduler_handle::spawn_test( const model::test_program_ptr test_program, const std::string& test_case_name, const config::tree& user_config) { _pimpl->generic.check_interrupt(); const std::shared_ptr< scheduler::interface > interface = find_interface( test_program->interface_name()); LI(F("Spawning %s:%s") % test_program->absolute_path() % test_case_name); const model::test_case& test_case = test_program->find(test_case_name); optional< passwd::user > unprivileged_user; if (user_config.is_set("unprivileged_user") && test_case.get_metadata().required_user() == "unprivileged") { unprivileged_user = user_config.lookup< engine::user_node >( "unprivileged_user"); } const executor::exec_handle handle = _pimpl->generic.spawn( run_test_program(interface, test_program, test_case_name, user_config), test_case.get_metadata().timeout(), unprivileged_user); const exec_data_ptr data(new test_exec_data( - test_program, test_case_name, interface, user_config)); + test_program, test_case_name, interface, user_config, handle.pid())); LD(F("Inserting %s into all_exec_data") % handle.pid()); INV_MSG( _pimpl->all_exec_data.find(handle.pid()) == _pimpl->all_exec_data.end(), F("PID %s already in all_exec_data; not cleaned up or reused too fast") % handle.pid());; _pimpl->all_exec_data.insert(exec_data_map::value_type(handle.pid(), data)); return handle.pid(); } /// Waits for completion of any forked test case. /// /// Note that if the terminated test case has a cleanup routine, this function /// is the one in charge of spawning the cleanup routine asynchronously. /// /// \return The result of the execution of a subprocess. This is a dynamically /// allocated object because the scheduler can spawn subprocesses of various /// types and, at wait time, we don't know upfront what we are going to get. scheduler::result_handle_ptr scheduler::scheduler_handle::wait_any(void) { _pimpl->generic.check_interrupt(); executor::exit_handle handle = _pimpl->generic.wait_any(); const exec_data_map::iterator iter = _pimpl->all_exec_data.find( handle.original_pid()); exec_data_ptr data = (*iter).second; utils::dump_stacktrace_if_available(data->test_program->absolute_path(), _pimpl->generic, handle); optional< model::test_result > result; + + // test itself try { test_exec_data* test_data = &dynamic_cast< test_exec_data& >( *data.get()); LD(F("Got %s from all_exec_data") % handle.original_pid()); test_data->exit_handle = handle; const model::test_case& test_case = test_data->test_program->find( test_data->test_case_name); result = test_case.fake_result(); if (!result && handle.status() && handle.status().get().exited() && handle.status().get().exitstatus() == exit_skipped) { // If the test's process terminated with our magic "exit_skipped" // status, there are two cases to handle. The first is the case // where the "skipped cookie" exists, in which case we never got to // actually invoke the test program; if that's the case, handle it // here. The second case is where the test case actually decided to // exit with the "exit_skipped" status; in that case, just fall back // to the regular status handling. const fs::path skipped_cookie_path = handle.control_directory() / skipped_cookie; std::ifstream input(skipped_cookie_path.c_str()); if (input) { result = model::test_result(model::test_result_skipped, utils::read_stream(input)); input.close(); // If we determined that the test needs to be skipped, we do not // want to run the cleanup routine because doing so could result // in errors. However, we still want to run the cleanup routine // if the test's body reports a skip (because actions could have // already been taken). test_data->needs_cleanup = false; + test_data->needs_execenv_cleanup = false; } } if (!result) { result = test_data->interface->compute_result( handle.status(), handle.control_directory(), handle.stdout_file(), handle.stderr_file()); } INV(result); if (!result.get().good()) { append_files_listing(handle.work_directory(), handle.stderr_file()); } if (test_data->needs_cleanup) { INV(test_case.get_metadata().has_cleanup()); // The test body has completed and we have processed it. If there // is a cleanup routine, trigger it now and wait for any other test // completion. The caller never knows about cleanup routines. _pimpl->spawn_cleanup(test_data->test_program, test_data->test_case_name, test_data->user_config, handle, result.get()); - test_data->needs_cleanup = false; // TODO(jmmv): Chaining this call is ugly. We'd be better off by // looping over terminated processes until we got a result suitable // for user consumption. For the time being this is good enough and // not a problem because the call chain won't get big: the majority // of test cases do not have cleanup routines. return wait_any(); } + + if (test_data->needs_execenv_cleanup) { + INV(test_case.get_metadata().has_execenv()); + _pimpl->spawn_execenv_cleanup(test_data->test_program, + test_data->test_case_name, + handle, result.get()); + test_data->needs_execenv_cleanup = false; + return wait_any(); + } } catch (const std::bad_cast& e) { + // ok, let's check for another type + } + + // test cleanup + try { const cleanup_exec_data* cleanup_data = &dynamic_cast< const cleanup_exec_data& >(*data.get()); LD(F("Got %s from all_exec_data (cleanup)") % handle.original_pid()); // Handle the completion of cleanup subprocesses internally: the caller // is not aware that these exist so, when we return, we must return the // data for the original test that triggered this routine. For example, // because the caller wants to see the exact same exec_handle that was // returned by spawn_test. const model::test_result& body_result = cleanup_data->body_result; if (body_result.good()) { if (!handle.status()) { result = model::test_result(model::test_result_broken, "Test case cleanup timed out"); } else { if (!handle.status().get().exited() || handle.status().get().exitstatus() != EXIT_SUCCESS) { result = model::test_result( model::test_result_broken, "Test case cleanup did not terminate successfully"); } else { result = body_result; } } } else { result = body_result; } // Untrack the cleanup process. This must be done explicitly because we // do not create a result_handle object for the cleanup, and that is the // one in charge of doing so in the regular (non-cleanup) case. LD(F("Removing %s from all_exec_data (cleanup) in favor of %s") % handle.original_pid() % cleanup_data->body_exit_handle.original_pid()); _pimpl->all_exec_data.erase(handle.original_pid()); handle = cleanup_data->body_exit_handle; + + const exec_data_map::iterator it = _pimpl->all_exec_data.find( + handle.original_pid()); + if (it != _pimpl->all_exec_data.end()) { + exec_data_ptr d = (*it).second; + test_exec_data* test_data = &dynamic_cast< test_exec_data& >( + *d.get()); + const model::test_case& test_case = + cleanup_data->test_program->find(cleanup_data->test_case_name); + test_data->needs_cleanup = false; + + if (test_data->needs_execenv_cleanup) { + INV(test_case.get_metadata().has_execenv()); + _pimpl->spawn_execenv_cleanup(cleanup_data->test_program, + cleanup_data->test_case_name, + handle, result.get()); + test_data->needs_execenv_cleanup = false; + return wait_any(); + } + } + } catch (const std::bad_cast& e) { + // ok, let's check for another type } + + // execenv cleanup + try { + const execenv_exec_data* execenv_data = + &dynamic_cast< const execenv_exec_data& >(*data.get()); + LD(F("Got %s from all_exec_data (execenv cleanup)") % handle.original_pid()); + + const model::test_result& body_result = execenv_data->body_result; + if (body_result.good()) { + if (!handle.status()) { + result = model::test_result(model::test_result_broken, + "Test case execenv cleanup timed out"); + } else { + if (!handle.status().get().exited() || + handle.status().get().exitstatus() != EXIT_SUCCESS) { + result = model::test_result( + model::test_result_broken, + "Test case execenv cleanup did not terminate successfully"); // ? + } else { + result = body_result; + } + } + } else { + result = body_result; + } + + LD(F("Removing %s from all_exec_data (execenv cleanup) in favor of %s") + % handle.original_pid() + % execenv_data->body_exit_handle.original_pid()); + _pimpl->all_exec_data.erase(handle.original_pid()); + + handle = execenv_data->body_exit_handle; + } catch (const std::bad_cast& e) { + // ok, it was one of the types above + } + INV(result); std::shared_ptr< result_handle::bimpl > result_handle_bimpl( new result_handle::bimpl(handle, _pimpl->all_exec_data)); std::shared_ptr< test_result_handle::impl > test_result_handle_impl( new test_result_handle::impl( data->test_program, data->test_case_name, result.get())); return result_handle_ptr(new test_result_handle(result_handle_bimpl, test_result_handle_impl)); } /// Forks and executes a test case synchronously for debugging. /// /// \pre No other processes should be in execution by the scheduler. /// /// \param test_program The container test program. /// \param test_case_name The name of the test case to run. /// \param user_config User-provided configuration variables. /// \param stdout_target File to which to write the stdout of the test case. /// \param stderr_target File to which to write the stderr of the test case. /// /// \return The result of the execution of the test. scheduler::result_handle_ptr scheduler::scheduler_handle::debug_test( const model::test_program_ptr test_program, const std::string& test_case_name, const config::tree& user_config, const fs::path& stdout_target, const fs::path& stderr_target) { const exec_handle exec_handle = spawn_test( test_program, test_case_name, user_config); result_handle_ptr result_handle = wait_any(); // TODO(jmmv): We need to do this while the subprocess is alive. This is // important for debugging purposes, as we should see the contents of stdout // or stderr as they come in. // // Unfortunately, we cannot do so. We cannot just read and block from a // file, waiting for further output to appear... as this only works on pipes // or sockets. We need a better interface for this whole thing. { std::auto_ptr< std::ostream > output = utils::open_ostream( stdout_target); *output << utils::read_file(result_handle->stdout_file()); } { std::auto_ptr< std::ostream > output = utils::open_ostream( stderr_target); *output << utils::read_file(result_handle->stderr_file()); } INV(result_handle->original_pid() == exec_handle); return result_handle; } /// Checks if an interrupt has fired. /// /// Calls to this function should be sprinkled in strategic places through the /// code protected by an interrupts_handler object. /// /// This is just a wrapper over signals::check_interrupt() to avoid leaking this /// dependency to the caller. /// /// \throw signals::interrupted_error If there has been an interrupt. void scheduler::scheduler_handle::check_interrupt(void) const { _pimpl->generic.check_interrupt(); } /// Queries the current execution context. /// /// \return The queried context. model::context scheduler::current_context(void) { return model::context(fs::current_path(), utils::getallenv()); } /// Generates the set of configuration variables for a test program. /// /// \param user_config The configuration variables provided by the user. /// \param test_suite The name of the test suite. /// /// \return The mapping of configuration variables for the test program. config::properties_map scheduler::generate_config(const config::tree& user_config, const std::string& test_suite) { config::properties_map props; try { props = user_config.all_properties(F("test_suites.%s") % test_suite, true); } catch (const config::unknown_key_error& unused_error) { // Ignore: not all test suites have entries in the configuration. } // TODO(jmmv): This is a hack that exists for the ATF interface only, so it // should be moved there. if (user_config.is_set("unprivileged_user")) { const passwd::user& user = user_config.lookup< engine::user_node >("unprivileged_user"); props["unprivileged-user"] = user.name; } return props; } diff --git a/contrib/kyua/engine/scheduler.hpp b/contrib/kyua/engine/scheduler.hpp index 24ff0b5a26fc..ee01c83b4991 100644 --- a/contrib/kyua/engine/scheduler.hpp +++ b/contrib/kyua/engine/scheduler.hpp @@ -1,282 +1,283 @@ // Copyright 2014 The Kyua Authors. // All rights reserved. // // Redistribution and use in source and binary forms, with or without // modification, are permitted provided that the following conditions are // met: // // * Redistributions of source code must retain the above copyright // notice, this list of conditions and the following disclaimer. // * Redistributions in binary form must reproduce the above copyright // notice, this list of conditions and the following disclaimer in the // documentation and/or other materials provided with the distribution. // * Neither the name of Google Inc. nor the names of its contributors // may be used to endorse or promote products derived from this software // without specific prior written permission. // // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS // "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT // LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR // A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT // OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT // LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. /// \file engine/scheduler.hpp /// Multiprogrammed executor of test related operations. /// /// The scheduler's public interface exposes test cases as "black boxes". The /// handling of cleanup routines is completely hidden from the caller and /// happens in two cases: first, once a test case completes; and, second, in the /// case of abrupt termination due to the reception of a signal. /// /// Hiding cleanup routines from the caller is an attempt to keep the logic of /// execution and results handling in a single place. Otherwise, the various /// drivers (say run_tests and debug_test) would need to replicate the handling /// of this logic, which is tricky in itself (particularly due to signal /// handling) and would lead to inconsistencies. /// /// Handling cleanup routines in the manner described above is *incredibly /// complicated* (insane, actually) as you will see from the code. The /// complexity will bite us in the future (today is 2015-06-26). Switching to a /// threads-based implementation would probably simplify the code flow /// significantly and allow parallelization of the test case listings in a /// reasonable manner, though it depends on whether we can get clean handling of /// signals and on whether we could use C++11's std::thread. (Is this a to-do? /// Maybe. Maybe not.) /// /// See the documentation in utils/process/executor.hpp for details on /// the expected workflow of these classes. #if !defined(ENGINE_SCHEDULER_HPP) #define ENGINE_SCHEDULER_HPP #include "engine/scheduler_fwd.hpp" #include #include #include #include "model/context_fwd.hpp" #include "model/metadata_fwd.hpp" #include "model/test_case_fwd.hpp" #include "model/test_program.hpp" #include "model/test_result_fwd.hpp" #include "utils/config/tree_fwd.hpp" #include "utils/datetime_fwd.hpp" #include "utils/defs.hpp" #include "utils/fs/path_fwd.hpp" #include "utils/optional.hpp" #include "utils/process/executor_fwd.hpp" #include "utils/process/status_fwd.hpp" namespace engine { namespace scheduler { /// Abstract interface of a test program scheduler interface. /// /// This interface defines the test program-specific operations that need to be /// invoked at different points during the execution of a given test case. The /// scheduler internally instantiates one of these for every test case. class interface { public: /// Destructor. virtual ~interface() {} /// Executes a test program's list operation. /// /// This method is intended to be called within a subprocess and is expected /// to terminate execution either by exec(2)ing the test program or by /// exiting with a failure. /// /// \param test_program The test program to execute. /// \param vars User-provided variables to pass to the test program. virtual void exec_list(const model::test_program& test_program, const utils::config::properties_map& vars) const UTILS_NORETURN = 0; /// Computes the test cases list of a test program. /// /// \param status The termination status of the subprocess used to execute /// the exec_test() method or none if the test timed out. /// \param stdout_path Path to the file containing the stdout of the test. /// \param stderr_path Path to the file containing the stderr of the test. /// /// \return A list of test cases. virtual model::test_cases_map parse_list( const utils::optional< utils::process::status >& status, const utils::fs::path& stdout_path, const utils::fs::path& stderr_path) const = 0; /// Executes a test case of the test program. /// /// This method is intended to be called within a subprocess and is expected /// to terminate execution either by exec(2)ing the test program or by /// exiting with a failure. /// /// \param test_program The test program to execute. /// \param test_case_name Name of the test case to invoke. /// \param vars User-provided variables to pass to the test program. /// \param control_directory Directory where the interface may place control /// files. virtual void exec_test(const model::test_program& test_program, const std::string& test_case_name, const utils::config::properties_map& vars, const utils::fs::path& control_directory) const UTILS_NORETURN = 0; /// Executes a test cleanup routine of the test program. /// /// This method is intended to be called within a subprocess and is expected /// to terminate execution either by exec(2)ing the test program or by /// exiting with a failure. /// /// \param test_program The test program to execute. /// \param test_case_name Name of the test case to invoke. /// \param vars User-provided variables to pass to the test program. /// \param control_directory Directory where the interface may place control /// files. virtual void exec_cleanup(const model::test_program& test_program, const std::string& test_case_name, const utils::config::properties_map& vars, const utils::fs::path& control_directory) const UTILS_NORETURN; /// Computes the result of a test case based on its termination status. /// /// \param status The termination status of the subprocess used to execute /// the exec_test() method or none if the test timed out. /// \param control_directory Directory where the interface may have placed /// control files. /// \param stdout_path Path to the file containing the stdout of the test. /// \param stderr_path Path to the file containing the stderr of the test. /// /// \return A test result. virtual model::test_result compute_result( const utils::optional< utils::process::status >& status, const utils::fs::path& control_directory, const utils::fs::path& stdout_path, const utils::fs::path& stderr_path) const = 0; }; /// Implementation of a test program with lazy loading of test cases. class lazy_test_program : public model::test_program { struct impl; /// Pointer to the shared internal implementation. std::shared_ptr< impl > _pimpl; public: lazy_test_program(const std::string&, const utils::fs::path&, const utils::fs::path&, const std::string&, const model::metadata&, const utils::config::tree&, scheduler_handle&); const model::test_cases_map& test_cases(void) const; }; /// Base type containing the results of the execution of a subprocess. class result_handle { protected: struct bimpl; private: /// Pointer to internal implementation of the base type. std::shared_ptr< bimpl > _pbimpl; protected: friend class scheduler_handle; result_handle(std::shared_ptr< bimpl >); public: virtual ~result_handle(void) = 0; void cleanup(void); int original_pid(void) const; const utils::datetime::timestamp& start_time() const; const utils::datetime::timestamp& end_time() const; utils::fs::path work_directory(void) const; const utils::fs::path& stdout_file(void) const; const utils::fs::path& stderr_file(void) const; }; /// Container for all test termination data and accessor to cleanup operations. class test_result_handle : public result_handle { struct impl; /// Pointer to internal implementation. std::shared_ptr< impl > _pimpl; friend class scheduler_handle; test_result_handle(std::shared_ptr< bimpl >, std::shared_ptr< impl >); public: ~test_result_handle(void); const model::test_program_ptr test_program(void) const; const std::string& test_case_name(void) const; const model::test_result& test_result(void) const; }; /// Stateful interface to the multiprogrammed execution of tests. class scheduler_handle { struct impl; /// Pointer to internal implementation. std::shared_ptr< impl > _pimpl; friend scheduler_handle setup(void); scheduler_handle(void); public: ~scheduler_handle(void); const utils::fs::path& root_work_directory(void) const; void cleanup(void); model::test_cases_map list_tests(const model::test_program*, const utils::config::tree&); exec_handle spawn_test(const model::test_program_ptr, const std::string&, const utils::config::tree&); result_handle_ptr wait_any(void); result_handle_ptr debug_test(const model::test_program_ptr, const std::string&, const utils::config::tree&, const utils::fs::path&, const utils::fs::path&); void check_interrupt(void) const; }; extern utils::datetime::delta cleanup_timeout; +extern utils::datetime::delta execenv_cleanup_timeout; extern utils::datetime::delta list_timeout; void ensure_valid_interface(const std::string&); void register_interface(const std::string&, const std::shared_ptr< interface >); std::set< std::string > registered_interface_names(void); scheduler_handle setup(void); model::context current_context(void); utils::config::properties_map generate_config(const utils::config::tree&, const std::string&); } // namespace scheduler } // namespace engine #endif // !defined(ENGINE_SCHEDULER_HPP) diff --git a/contrib/kyua/engine/tap.cpp b/contrib/kyua/engine/tap.cpp index 85e23857f5b7..ed35ba40433f 100644 --- a/contrib/kyua/engine/tap.cpp +++ b/contrib/kyua/engine/tap.cpp @@ -1,191 +1,196 @@ // Copyright 2015 The Kyua Authors. // All rights reserved. // // Redistribution and use in source and binary forms, with or without // modification, are permitted provided that the following conditions are // met: // // * Redistributions of source code must retain the above copyright // notice, this list of conditions and the following disclaimer. // * Redistributions in binary form must reproduce the above copyright // notice, this list of conditions and the following disclaimer in the // documentation and/or other materials provided with the distribution. // * Neither the name of Google Inc. nor the names of its contributors // may be used to endorse or promote products derived from this software // without specific prior written permission. // // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS // "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT // LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR // A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT // OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT // LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #include "engine/tap.hpp" extern "C" { #include } #include #include "engine/exceptions.hpp" +#include "engine/execenv/execenv.hpp" #include "engine/tap_parser.hpp" #include "model/test_case.hpp" #include "model/test_program.hpp" #include "model/test_result.hpp" #include "utils/defs.hpp" #include "utils/env.hpp" #include "utils/format/macros.hpp" #include "utils/optional.ipp" #include "utils/process/operations.hpp" #include "utils/process/status.hpp" #include "utils/sanity.hpp" namespace config = utils::config; +namespace execenv = engine::execenv; namespace fs = utils::fs; namespace process = utils::process; using utils::optional; namespace { /// Computes the result of a TAP test program termination. /// /// Timeouts and bad TAP data must be handled by the caller. Here we assume /// that we have been able to successfully parse the TAP output. /// /// \param summary Parsed TAP data for the test program. /// \param status Exit status of the test program. /// /// \return A test result. static model::test_result tap_to_result(const engine::tap_summary& summary, const process::status& status) { if (summary.bailed_out()) { return model::test_result(model::test_result_failed, "Bailed out"); } if (summary.plan() == engine::all_skipped_plan) { return model::test_result(model::test_result_skipped, summary.all_skipped_reason()); } if (summary.not_ok_count() == 0) { if (status.exitstatus() == EXIT_SUCCESS) { return model::test_result(model::test_result_passed); } else { return model::test_result( model::test_result_broken, F("Dubious test program: reported all tests as passed " "but returned exit code %s") % status.exitstatus()); } } else { const std::size_t total = summary.ok_count() + summary.not_ok_count(); return model::test_result(model::test_result_failed, F("%s of %s tests failed") % summary.not_ok_count() % total); } } } // anonymous namespace /// Executes a test program's list operation. /// /// This method is intended to be called within a subprocess and is expected /// to terminate execution either by exec(2)ing the test program or by /// exiting with a failure. void engine::tap_interface::exec_list( const model::test_program& /* test_program */, const config::properties_map& /* vars */) const { ::_exit(EXIT_SUCCESS); } /// Computes the test cases list of a test program. /// /// \return A list of test cases. model::test_cases_map engine::tap_interface::parse_list( const optional< process::status >& /* status */, const fs::path& /* stdout_path */, const fs::path& /* stderr_path */) const { return model::test_cases_map_builder().add("main").build(); } /// Executes a test case of the test program. /// /// This method is intended to be called within a subprocess and is expected /// to terminate execution either by exec(2)ing the test program or by /// exiting with a failure. /// /// \param test_program The test program to execute. /// \param test_case_name Name of the test case to invoke. /// \param vars User-provided variables to pass to the test program. void engine::tap_interface::exec_test( const model::test_program& test_program, const std::string& test_case_name, const config::properties_map& vars, const fs::path& /* control_directory */) const { PRE(test_case_name == "main"); for (config::properties_map::const_iterator iter = vars.begin(); iter != vars.end(); ++iter) { utils::setenv(F("TEST_ENV_%s") % (*iter).first, (*iter).second); } process::args_vector args; - process::exec(test_program.absolute_path(), args); + + auto e = execenv::get(test_program, test_case_name); + e->init(); + e->exec(args); } /// Computes the result of a test case based on its termination status. /// /// \param status The termination status of the subprocess used to execute /// the exec_test() method or none if the test timed out. /// \param stdout_path Path to the file containing the stdout of the test. /// /// \return A test result. model::test_result engine::tap_interface::compute_result( const optional< process::status >& status, const fs::path& /* control_directory */, const fs::path& stdout_path, const fs::path& /* stderr_path */) const { if (!status) { return model::test_result(model::test_result_broken, "Test case timed out"); } else { if (status.get().signaled()) { return model::test_result( model::test_result_broken, F("Received signal %s") % status.get().termsig()); } else { try { const tap_summary summary = parse_tap_output(stdout_path); return tap_to_result(summary, status.get()); } catch (const load_error& e) { return model::test_result( model::test_result_broken, F("TAP test program yielded invalid data: %s") % e.what()); } } } } diff --git a/contrib/kyua/examples/kyua.conf b/contrib/kyua/examples/kyua.conf index 83418a320dc4..d7e2ef60e50a 100644 --- a/contrib/kyua/examples/kyua.conf +++ b/contrib/kyua/examples/kyua.conf @@ -1,69 +1,72 @@ -- Copyright 2011 The Kyua Authors. -- All rights reserved. -- -- Redistribution and use in source and binary forms, with or without -- modification, are permitted provided that the following conditions are -- met: -- -- * Redistributions of source code must retain the above copyright -- notice, this list of conditions and the following disclaimer. -- * Redistributions in binary form must reproduce the above copyright -- notice, this list of conditions and the following disclaimer in the -- documentation and/or other materials provided with the distribution. -- * Neither the name of Google Inc. nor the names of its contributors -- may be used to endorse or promote products derived from this software -- without specific prior written permission. -- -- THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -- "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -- LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR -- A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT -- OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -- SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -- LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -- DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY -- THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -- (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -- OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -- Example file for the configuration of Kyua. -- -- All the values shown here do not reflect the default values that Kyua -- is using on this installation: these are just fictitious settings that -- may or may not work. -- -- To write your own configuration file, it is recommended that you start -- from a blank file and then define only those settings that you want to -- override. If you want to use this file as a template, you will have -- to comment out all the settings first to prevent any side-effects. -- The file must start by declaring the name and version of its format. syntax(2) -- Name of the system architecture (aka processor type). architecture = "x86_64" +-- List of execution environments. +execenvs = "host jail" + -- Maximum number of jobs (such as test case runs) to execute concurrently. parallelism = 16 -- Name of the system platform (aka machine type). platform = "amd64" -- The name or UID of the unprivileged user. -- -- If set, this user must exist in the system and his privileges will be -- used to run test cases that need regular privileges when Kyua is -- executed as root. unprivileged_user = "nobody" -- Set actual configuration properties for the test suite named 'kyua'. test_suites.kyua.run_coredump_tests = "false" -- Set fictitious configuration properties for the test suite named 'FreeBSD'. test_suites.FreeBSD.iterations = "1000" test_suites.FreeBSD.run_old_tests = "false" -- Set fictitious configuration properties for the test suite named 'NetBSD'. test_suites.NetBSD.file_systems = "ffs lfs ext2fs" test_suites.NetBSD.iterations = "100" test_suites.NetBSD.run_broken_tests = "true" diff --git a/contrib/kyua/integration/cmd_config_test.sh b/contrib/kyua/integration/cmd_config_test.sh index ed457e5c4b37..02e7654571e7 100755 --- a/contrib/kyua/integration/cmd_config_test.sh +++ b/contrib/kyua/integration/cmd_config_test.sh @@ -1,355 +1,357 @@ # Copyright 2011 The Kyua Authors. # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are # met: # # * Redistributions of source code must retain the above copyright # notice, this list of conditions and the following disclaimer. # * Redistributions in binary form must reproduce the above copyright # notice, this list of conditions and the following disclaimer in the # documentation and/or other materials provided with the distribution. # * Neither the name of Google Inc. nor the names of its contributors # may be used to endorse or promote products derived from this software # without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. utils_test_case defaults defaults_body() { atf_check -s exit:0 \ -o match:'^architecture = ' \ -o match:'^platform = ' \ kyua config } utils_test_case all all_body() { mkdir "${HOME}/.kyua" cat >"${HOME}/.kyua/kyua.conf" <expout <"${HOME}/.kyua/kyua.conf" <expout <"${HOME}/.kyua/kyua.conf" <experr <"${HOME}/.kyua/kyua.conf" <expout <"${HOME}/.kyua/kyua.conf" <expout <experr <kyua.conf <kyua.conf <.kyua/kyua.conf <kyua.conf <kyua.conf <experr <"${HOME}/.kyua/kyua.conf" <config <experr <experr <Kyuafile <"${dbfile_name}" rm stdout # Ensure the results of 'report-junit' come from the database. rm Kyuafile simple_all_pass } # Removes the contents of a properties tag from stdout. strip_properties='awk " BEGIN { skip = 0; } /<\/properties>/ { print \"\"; skip = 0; next; } // { print \"\"; print \"CONTENTS STRIPPED BY TEST\"; skip = 1; next; } { if (!skip) print; }"' utils_test_case default_behavior__ok default_behavior__ok_body() { utils_install_times_wrapper run_tests "mock1 this should not be seen mock1 new line" unused_dbfile_name cat >expout < CONTENTS STRIPPED BY TEST This is the stdout of pass Test case metadata ------------------ allowed_architectures is empty allowed_platforms is empty description is empty +execenv is empty +execenv_jail_params is empty has_cleanup = false is_exclusive = false required_configs is empty required_disk_space = 0 required_files is empty required_memory = 0 required_programs is empty required_user is empty timeout = 300 Timing information ------------------ Start time: YYYY-MM-DDTHH:MM:SS.ssssssZ End time: YYYY-MM-DDTHH:MM:SS.ssssssZ Duration: S.UUUs Original stderr --------------- This is the stderr of pass This is the stdout of skip Skipped result details ---------------------- The reason for skipping is this Test case metadata ------------------ allowed_architectures is empty allowed_platforms is empty description is empty +execenv is empty +execenv_jail_params is empty has_cleanup = false is_exclusive = false required_configs is empty required_disk_space = 0 required_files is empty required_memory = 0 required_programs is empty required_user is empty timeout = 300 Timing information ------------------ Start time: YYYY-MM-DDTHH:MM:SS.ssssssZ End time: YYYY-MM-DDTHH:MM:SS.ssssssZ Duration: S.UUUs Original stderr --------------- This is the stderr of skip EOF atf_check -s exit:0 -o file:expout -e empty -x "kyua report-junit" \ "| ${strip_properties}" } utils_test_case default_behavior__no_store default_behavior__no_store_body() { echo 'kyua: E: No previous results file found for test suite' \ "$(utils_test_suite_id)." >experr atf_check -s exit:2 -o empty -e file:experr kyua report-junit } utils_test_case results_file__explicit results_file__explicit_body() { run_tests "mock1" dbfile_name1 run_tests "mock2" dbfile_name2 atf_check -s exit:0 -o match:"MOCK.*mock1" -o not-match:"MOCK.*mock2" \ -e empty kyua report-junit --results-file="$(cat dbfile_name1)" atf_check -s exit:0 -o not-match:"MOCK.*mock1" -o match:"MOCK.*mock2" \ -e empty kyua report-junit --results-file="$(cat dbfile_name2)" } utils_test_case results_file__not_found results_file__not_found_body() { atf_check -s exit:2 -o empty -e match:"kyua: E: No previous results.*foo" \ kyua report-junit --results-file=foo } utils_test_case output__explicit output__explicit_body() { run_tests unused_mock unused_dbfile_name cat >report < CONTENTS STRIPPED BY TEST This is the stdout of pass Test case metadata ------------------ allowed_architectures is empty allowed_platforms is empty description is empty +execenv is empty +execenv_jail_params is empty has_cleanup = false is_exclusive = false required_configs is empty required_disk_space = 0 required_files is empty required_memory = 0 required_programs is empty required_user is empty timeout = 300 Timing information ------------------ Start time: YYYY-MM-DDTHH:MM:SS.ssssssZ End time: YYYY-MM-DDTHH:MM:SS.ssssssZ Duration: S.UUUs Original stderr --------------- This is the stderr of pass This is the stdout of skip Skipped result details ---------------------- The reason for skipping is this Test case metadata ------------------ allowed_architectures is empty allowed_platforms is empty description is empty +execenv is empty +execenv_jail_params is empty has_cleanup = false is_exclusive = false required_configs is empty required_disk_space = 0 required_files is empty required_memory = 0 required_programs is empty required_user is empty timeout = 300 Timing information ------------------ Start time: YYYY-MM-DDTHH:MM:SS.ssssssZ End time: YYYY-MM-DDTHH:MM:SS.ssssssZ Duration: S.UUUs Original stderr --------------- This is the stderr of skip EOF atf_check -s exit:0 -o file:report -e empty -x kyua report-junit \ --output=/dev/stdout "| ${strip_properties} | ${utils_strip_times}" atf_check -s exit:0 -o empty -e save:stderr kyua report-junit \ --output=/dev/stderr atf_check -s exit:0 -o file:report -x cat stderr \ "| ${strip_properties} | ${utils_strip_times}" atf_check -s exit:0 -o empty -e empty kyua report-junit \ --output=my-file atf_check -s exit:0 -o file:report -x cat my-file \ "| ${strip_properties} | ${utils_strip_times}" } atf_init_test_cases() { atf_add_test_case default_behavior__ok atf_add_test_case default_behavior__no_store atf_add_test_case results_file__explicit atf_add_test_case results_file__not_found atf_add_test_case output__explicit } diff --git a/contrib/kyua/integration/cmd_report_test.sh b/contrib/kyua/integration/cmd_report_test.sh index 18a5db386dfd..8b2b97f9cb4a 100755 --- a/contrib/kyua/integration/cmd_report_test.sh +++ b/contrib/kyua/integration/cmd_report_test.sh @@ -1,381 +1,383 @@ # Copyright 2011 The Kyua Authors. # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are # met: # # * Redistributions of source code must retain the above copyright # notice, this list of conditions and the following disclaimer. # * Redistributions in binary form must reproduce the above copyright # notice, this list of conditions and the following disclaimer in the # documentation and/or other materials provided with the distribution. # * Neither the name of Google Inc. nor the names of its contributors # may be used to endorse or promote products derived from this software # without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. # Executes a mock test suite to generate data in the database. # # \param mock_env The value to store in a MOCK variable in the environment. # Use this to be able to differentiate executions by inspecting the # context of the output. # \param dbfile_name File to which to write the path to the generated database # file. run_tests() { local mock_env="${1}"; shift local dbfile_name="${1}"; shift cat >Kyuafile <"${dbfile_name}" rm stdout # Ensure the results of 'report' come from the database. rm Kyuafile simple_all_pass } utils_test_case default_behavior__ok default_behavior__ok_body() { utils_install_times_wrapper run_tests "mock1" dbfile_name1 cat >expout < Skipped tests simple_all_pass:skip -> skipped: The reason for skipping is this [S.UUUs] ===> Summary Results read from $(cat dbfile_name1) Test cases: 2 total, 1 skipped, 0 expected failures, 0 broken, 0 failed Total time: S.UUUs EOF atf_check -s exit:0 -o file:expout -e empty kyua report run_tests "mock2" dbfile_name2 cat >expout < Skipped tests simple_all_pass:skip -> skipped: The reason for skipping is this [S.UUUs] ===> Summary Results read from $(cat dbfile_name2) Test cases: 2 total, 1 skipped, 0 expected failures, 0 broken, 0 failed Total time: S.UUUs EOF atf_check -s exit:0 -o file:expout -e empty kyua report } utils_test_case default_behavior__no_store default_behavior__no_store_body() { echo 'kyua: E: No previous results file found for test suite' \ "$(utils_test_suite_id)." >experr atf_check -s exit:2 -o empty -e file:experr kyua report } utils_test_case results_file__explicit results_file__explicit_body() { run_tests "mock1" dbfile_name1 run_tests "mock2" dbfile_name2 atf_check -s exit:0 -o match:"MOCK=mock1" -o not-match:"MOCK=mock2" \ -e empty kyua report --results-file="$(cat dbfile_name1)" \ --verbose atf_check -s exit:0 -o not-match:"MOCK=mock1" -o match:"MOCK=mock2" \ -e empty kyua report --results-file="$(cat dbfile_name2)" \ --verbose } utils_test_case results_file__not_found results_file__not_found_body() { atf_check -s exit:2 -o empty -e match:"kyua: E: No previous results.*foo" \ kyua report --results-file=foo } utils_test_case output__explicit output__explicit_body() { run_tests unused_mock dbfile_name cat >report < Skipped tests simple_all_pass:skip -> skipped: The reason for skipping is this [S.UUUs] ===> Summary Results read from $(cat dbfile_name) Test cases: 2 total, 1 skipped, 0 expected failures, 0 broken, 0 failed Total time: S.UUUs EOF atf_check -s exit:0 -o file:report -e empty -x kyua report \ --output=/dev/stdout "| ${utils_strip_times_but_not_ids}" atf_check -s exit:0 -o empty -e save:stderr kyua report \ --output=/dev/stderr atf_check -s exit:0 -o file:report -x cat stderr \ "| ${utils_strip_times_but_not_ids}" atf_check -s exit:0 -o empty -e empty kyua report \ --output=my-file atf_check -s exit:0 -o file:report -x cat my-file \ "| ${utils_strip_times_but_not_ids}" } utils_test_case filter__ok filter__ok_body() { utils_install_times_wrapper run_tests "mock1" dbfile_name1 cat >expout < Skipped tests simple_all_pass:skip -> skipped: The reason for skipping is this [S.UUUs] ===> Summary Results read from $(cat dbfile_name1) Test cases: 1 total, 1 skipped, 0 expected failures, 0 broken, 0 failed Total time: S.UUUs EOF atf_check -s exit:0 -o file:expout -e empty kyua report \ simple_all_pass:skip } utils_test_case filter__ok_passed_excluded_by_default filter__ok_passed_excluded_by_default_body() { utils_install_times_wrapper run_tests "mock1" dbfile_name1 # Passed results are excluded by default so they are not displayed even if # requested with a test case filter. This might be somewhat confusing... cat >expout < Summary Results read from $(cat dbfile_name1) Test cases: 1 total, 0 skipped, 0 expected failures, 0 broken, 0 failed Total time: S.UUUs EOF atf_check -s exit:0 -o file:expout -e empty kyua report \ simple_all_pass:pass cat >expout < Passed tests simple_all_pass:pass -> passed [S.UUUs] ===> Summary Results read from $(cat dbfile_name1) Test cases: 1 total, 0 skipped, 0 expected failures, 0 broken, 0 failed Total time: S.UUUs EOF atf_check -s exit:0 -o file:expout -e empty kyua report \ --results-filter= simple_all_pass:pass } utils_test_case filter__no_match filter__no_match_body() { utils_install_times_wrapper run_tests "mock1" dbfile_name1 cat >expout < Skipped tests simple_all_pass:skip -> skipped: The reason for skipping is this [S.UUUs] ===> Summary Results read from $(cat dbfile_name1) Test cases: 1 total, 1 skipped, 0 expected failures, 0 broken, 0 failed Total time: S.UUUs EOF cat >experr <expout < Execution context Current directory: ${real_cwd} Environment variables: EOF # $_ is a bash variable. To keep our tests stable, we override its value # below to match the hardcoded value in run_tests. env \ HOME="${real_cwd}" \ MOCK="mock1 has multiple lines and terminates here" \ _='fake-value' \ "$(atf_get_srcdir)/helpers/dump_env" ' ' ' ' >>expout cat >>expout < simple_all_pass:skip Result: skipped: The reason for skipping is this Start time: YYYY-MM-DDTHH:MM:SS.ssssssZ End time: YYYY-MM-DDTHH:MM:SS.ssssssZ Duration: S.UUUs Metadata: allowed_architectures is empty allowed_platforms is empty description is empty + execenv is empty + execenv_jail_params is empty has_cleanup = false is_exclusive = false required_configs is empty required_disk_space = 0 required_files is empty required_memory = 0 required_programs is empty required_user is empty timeout = 300 Standard output: This is the stdout of skip Standard error: This is the stderr of skip ===> Skipped tests simple_all_pass:skip -> skipped: The reason for skipping is this [S.UUUs] ===> Summary Results read from $(cat dbfile_name) Test cases: 2 total, 1 skipped, 0 expected failures, 0 broken, 0 failed Start time: YYYY-MM-DDTHH:MM:SS.ssssssZ End time: YYYY-MM-DDTHH:MM:SS.ssssssZ Total time: S.UUUs EOF atf_check -s exit:0 -o file:expout -e empty -x kyua report --verbose \ "| ${utils_strip_times_but_not_ids}" } utils_test_case results_filter__empty results_filter__empty_body() { utils_install_times_wrapper run_tests "mock1" dbfile_name1 cat >expout < Passed tests simple_all_pass:pass -> passed [S.UUUs] ===> Skipped tests simple_all_pass:skip -> skipped: The reason for skipping is this [S.UUUs] ===> Summary Results read from $(cat dbfile_name1) Test cases: 2 total, 1 skipped, 0 expected failures, 0 broken, 0 failed Total time: S.UUUs EOF atf_check -s exit:0 -o file:expout -e empty kyua report --results-filter= } utils_test_case results_filter__one results_filter__one_body() { utils_install_times_wrapper run_tests "mock1" dbfile_name1 cat >expout < Passed tests simple_all_pass:pass -> passed [S.UUUs] ===> Summary Results read from $(cat dbfile_name1) Test cases: 2 total, 1 skipped, 0 expected failures, 0 broken, 0 failed Total time: S.UUUs EOF atf_check -s exit:0 -o file:expout -e empty kyua report \ --results-filter=passed } utils_test_case results_filter__multiple_all_match results_filter__multiple_all_match_body() { utils_install_times_wrapper run_tests "mock1" dbfile_name1 cat >expout < Skipped tests simple_all_pass:skip -> skipped: The reason for skipping is this [S.UUUs] ===> Passed tests simple_all_pass:pass -> passed [S.UUUs] ===> Summary Results read from $(cat dbfile_name1) Test cases: 2 total, 1 skipped, 0 expected failures, 0 broken, 0 failed Total time: S.UUUs EOF atf_check -s exit:0 -o file:expout -e empty kyua report \ --results-filter=skipped,passed } utils_test_case results_filter__multiple_some_match results_filter__multiple_some_match_body() { utils_install_times_wrapper run_tests "mock1" dbfile_name1 cat >expout < Skipped tests simple_all_pass:skip -> skipped: The reason for skipping is this [S.UUUs] ===> Summary Results read from $(cat dbfile_name1) Test cases: 2 total, 1 skipped, 0 expected failures, 0 broken, 0 failed Total time: S.UUUs EOF atf_check -s exit:0 -o file:expout -e empty kyua report \ --results-filter=skipped,xfail,broken,failed } atf_init_test_cases() { atf_add_test_case default_behavior__ok atf_add_test_case default_behavior__no_store atf_add_test_case results_file__explicit atf_add_test_case results_file__not_found atf_add_test_case filter__ok atf_add_test_case filter__ok_passed_excluded_by_default atf_add_test_case filter__no_match atf_add_test_case verbose atf_add_test_case output__explicit atf_add_test_case results_filter__empty atf_add_test_case results_filter__one atf_add_test_case results_filter__multiple_all_match atf_add_test_case results_filter__multiple_some_match } diff --git a/contrib/kyua/main.cpp b/contrib/kyua/main.cpp index 4344248f89db..54ef87476820 100644 --- a/contrib/kyua/main.cpp +++ b/contrib/kyua/main.cpp @@ -1,50 +1,53 @@ // Copyright 2010 The Kyua Authors. // All rights reserved. // // Redistribution and use in source and binary forms, with or without // modification, are permitted provided that the following conditions are // met: // // * Redistributions of source code must retain the above copyright // notice, this list of conditions and the following disclaimer. // * Redistributions in binary form must reproduce the above copyright // notice, this list of conditions and the following disclaimer in the // documentation and/or other materials provided with the distribution. // * Neither the name of Google Inc. nor the names of its contributors // may be used to endorse or promote products derived from this software // without specific prior written permission. // // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS // "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT // LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR // A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT // OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT // LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #include "cli/main.hpp" +#include "os/freebsd/main.hpp" /// Program entry point. /// /// The whole purpose of this extremely-simple function is to delegate execution /// to an internal module that does not contain a proper ::main() function. /// This is to allow unit-testing of the internal code. /// /// \param argc The number of arguments passed on the command line. /// \param argv NULL-terminated array containing the command line arguments. /// /// \return 0 on success, some other integer on error. /// /// \throw std::exception This throws any uncaught exception. Such exceptions /// are bugs, but we let them propagate so that the runtime will abort and /// dump core. int main(const int argc, const char* const* const argv) { + freebsd::main(argc, argv); + return cli::main(argc, argv); } diff --git a/contrib/kyua/model/metadata.cpp b/contrib/kyua/model/metadata.cpp index d27e3237dcf2..26b7f7322c6e 100644 --- a/contrib/kyua/model/metadata.cpp +++ b/contrib/kyua/model/metadata.cpp @@ -1,1068 +1,1144 @@ // Copyright 2012 The Kyua Authors. // All rights reserved. // // Redistribution and use in source and binary forms, with or without // modification, are permitted provided that the following conditions are // met: // // * Redistributions of source code must retain the above copyright // notice, this list of conditions and the following disclaimer. // * Redistributions in binary form must reproduce the above copyright // notice, this list of conditions and the following disclaimer in the // documentation and/or other materials provided with the distribution. // * Neither the name of Google Inc. nor the names of its contributors // may be used to endorse or promote products derived from this software // without specific prior written permission. // // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS // "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT // LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR // A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT // OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT // LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #include "model/metadata.hpp" #include +#include "engine/execenv/execenv.hpp" #include "model/exceptions.hpp" #include "model/types.hpp" #include "utils/config/exceptions.hpp" #include "utils/config/nodes.ipp" #include "utils/config/tree.ipp" #include "utils/datetime.hpp" #include "utils/defs.hpp" #include "utils/format/macros.hpp" #include "utils/fs/exceptions.hpp" #include "utils/fs/path.hpp" #include "utils/noncopyable.hpp" #include "utils/optional.ipp" #include "utils/sanity.hpp" #include "utils/text/exceptions.hpp" #include "utils/text/operations.hpp" #include "utils/units.hpp" namespace config = utils::config; namespace datetime = utils::datetime; namespace fs = utils::fs; namespace text = utils::text; namespace units = utils::units; using utils::optional; namespace { /// Global instance of defaults. /// /// This exists so that the getters in metadata can return references instead /// of object copies. Use get_defaults() to query. static optional< config::tree > defaults; /// A leaf node that holds a bytes quantity. class bytes_node : public config::native_leaf_node< units::bytes > { public: /// Copies the node. /// /// \return A dynamically-allocated node. virtual base_node* deep_copy(void) const { std::auto_ptr< bytes_node > new_node(new bytes_node()); new_node->_value = _value; return new_node.release(); } /// Pushes the node's value onto the Lua stack. void push_lua(lutok::state& /* state */) const { UNREACHABLE; } /// Sets the value of the node from an entry in the Lua stack. void set_lua(lutok::state& /* state */, const int /* index */) { UNREACHABLE; } }; /// A leaf node that holds a time delta. class delta_node : public config::typed_leaf_node< datetime::delta > { public: /// Copies the node. /// /// \return A dynamically-allocated node. virtual base_node* deep_copy(void) const { std::auto_ptr< delta_node > new_node(new delta_node()); new_node->_value = _value; return new_node.release(); } /// Sets the value of the node from a raw string representation. /// /// \param raw_value The value to set the node to. /// /// \throw value_error If the value is invalid. void set_string(const std::string& raw_value) { unsigned int seconds; try { seconds = text::to_type< unsigned int >(raw_value); } catch (const text::error& e) { throw config::value_error(F("Invalid time delta %s") % raw_value); } set(datetime::delta(seconds, 0)); } /// Converts the contents of the node to a string. /// /// \pre The node must have a value. /// /// \return A string representation of the value held by the node. std::string to_string(void) const { return F("%s") % value().seconds; } /// Pushes the node's value onto the Lua stack. void push_lua(lutok::state& /* state */) const { UNREACHABLE; } /// Sets the value of the node from an entry in the Lua stack. void set_lua(lutok::state& /* state */, const int /* index */) { UNREACHABLE; } }; /// A leaf node that holds a "required user" property. /// /// This node is just a string, but it provides validation of the only allowed /// values. class user_node : public config::string_node { /// Copies the node. /// /// \return A dynamically-allocated node. virtual base_node* deep_copy(void) const { std::auto_ptr< user_node > new_node(new user_node()); new_node->_value = _value; return new_node.release(); } /// Checks a given user textual representation for validity. /// /// \param user The value to validate. /// /// \throw config::value_error If the value is not valid. void validate(const value_type& user) const { if (!user.empty() && user != "root" && user != "unprivileged") throw config::value_error("Invalid required user value"); } }; /// A leaf node that holds a set of paths. /// /// This node type is used to represent the value of the required files and /// required programs, for example, and these do not allow relative paths. We /// check this here. class paths_set_node : public config::base_set_node< fs::path > { /// Copies the node. /// /// \return A dynamically-allocated node. virtual base_node* deep_copy(void) const { std::auto_ptr< paths_set_node > new_node(new paths_set_node()); new_node->_value = _value; return new_node.release(); } /// Converts a single path to the native type. /// /// \param raw_value The value to parse. /// /// \return The parsed value. /// /// \throw config::value_error If the value is invalid. fs::path parse_one(const std::string& raw_value) const { try { return fs::path(raw_value); } catch (const fs::error& e) { throw config::value_error(e.what()); } } /// Checks a collection of paths for validity. /// /// \param paths The value to validate. /// /// \throw config::value_error If the value is not valid. void validate(const value_type& paths) const { for (value_type::const_iterator iter = paths.begin(); iter != paths.end(); ++iter) { const fs::path& path = *iter; if (!path.is_absolute() && path.ncomponents() > 1) throw config::value_error(F("Relative path '%s' not allowed") % *iter); } } }; /// Initializes a tree to hold test case requirements. /// /// \param [in,out] tree The tree to initialize. static void init_tree(config::tree& tree) { tree.define< config::strings_set_node >("allowed_architectures"); tree.define< config::strings_set_node >("allowed_platforms"); tree.define_dynamic("custom"); tree.define< config::string_node >("description"); + tree.define< config::string_node >("execenv"); + tree.define< config::string_node >("execenv_jail_params"); tree.define< config::bool_node >("has_cleanup"); tree.define< config::bool_node >("is_exclusive"); tree.define< config::strings_set_node >("required_configs"); tree.define< bytes_node >("required_disk_space"); tree.define< paths_set_node >("required_files"); tree.define< bytes_node >("required_memory"); tree.define< paths_set_node >("required_programs"); tree.define< user_node >("required_user"); tree.define< delta_node >("timeout"); } /// Sets default values on a tree object. /// /// \param [in,out] tree The tree to configure. static void set_defaults(config::tree& tree) { tree.set< config::strings_set_node >("allowed_architectures", model::strings_set()); tree.set< config::strings_set_node >("allowed_platforms", model::strings_set()); tree.set< config::string_node >("description", ""); + tree.set< config::string_node >("execenv", ""); + tree.set< config::string_node >("execenv_jail_params", ""); tree.set< config::bool_node >("has_cleanup", false); tree.set< config::bool_node >("is_exclusive", false); tree.set< config::strings_set_node >("required_configs", model::strings_set()); tree.set< bytes_node >("required_disk_space", units::bytes(0)); tree.set< paths_set_node >("required_files", model::paths_set()); tree.set< bytes_node >("required_memory", units::bytes(0)); tree.set< paths_set_node >("required_programs", model::paths_set()); tree.set< user_node >("required_user", ""); // TODO(jmmv): We shouldn't be setting a default timeout like this. See // Issue 5 for details. tree.set< delta_node >("timeout", datetime::delta(300, 0)); } /// Queries the global defaults tree object with lazy initialization. /// /// \return A metadata tree. This object is statically allocated so it is /// acceptable to obtain references to it and its members. const config::tree& get_defaults(void) { if (!defaults) { config::tree props; init_tree(props); set_defaults(props); defaults = props; } return defaults.get(); } /// Looks up a value in a tree with error rewriting. /// /// \tparam NodeType The type of the node. /// \param tree The tree in which to insert the value. /// \param key The key to set. /// /// \return A read-write reference to the value in the node. /// /// \throw model::error If the key is not known or if the value is not valid. template< class NodeType > typename NodeType::value_type& lookup_rw(config::tree& tree, const std::string& key) { try { return tree.lookup_rw< NodeType >(key); } catch (const config::unknown_key_error& e) { throw model::error(F("Unknown metadata property %s") % key); } catch (const config::value_error& e) { throw model::error(F("Invalid value for metadata property %s: %s") % key % e.what()); } } /// Sets a value in a tree with error rewriting. /// /// \tparam NodeType The type of the node. /// \param tree The tree in which to insert the value. /// \param key The key to set. /// \param value The value to set the node to. /// /// \throw model::error If the key is not known or if the value is not valid. template< class NodeType > void set(config::tree& tree, const std::string& key, const typename NodeType::value_type& value) { try { tree.set< NodeType >(key, value); } catch (const config::unknown_key_error& e) { throw model::error(F("Unknown metadata property %s") % key); } catch (const config::value_error& e) { throw model::error(F("Invalid value for metadata property %s: %s") % key % e.what()); } } } // anonymous namespace /// Internal implementation of the metadata class. struct model::metadata::impl : utils::noncopyable { /// Metadata properties. config::tree props; /// Constructor. /// /// \param props_ Metadata properties of the test. impl(const utils::config::tree& props_) : props(props_) { } /// Equality comparator. /// /// \param other The other object to compare this one to. /// /// \return True if this object and other are equal; false otherwise. bool operator==(const impl& other) const { return (get_defaults().combine(props) == get_defaults().combine(other.props)); } }; /// Constructor. /// /// \param props Metadata properties of the test. model::metadata::metadata(const utils::config::tree& props) : _pimpl(new impl(props)) { } /// Destructor. model::metadata::~metadata(void) { } /// Applies a set of overrides to this metadata object. /// /// \param overrides The overrides to apply. Any values explicitly set in this /// other object will override any possible values set in this object. /// /// \return A new metadata object with the combination. model::metadata model::metadata::apply_overrides(const metadata& overrides) const { return metadata(_pimpl->props.combine(overrides._pimpl->props)); } /// Returns the architectures allowed by the test. /// /// \return Set of architectures, or empty if this does not apply. const model::strings_set& model::metadata::allowed_architectures(void) const { if (_pimpl->props.is_set("allowed_architectures")) { return _pimpl->props.lookup< config::strings_set_node >( "allowed_architectures"); } else { return get_defaults().lookup< config::strings_set_node >( "allowed_architectures"); } } /// Returns the platforms allowed by the test. /// /// \return Set of platforms, or empty if this does not apply. const model::strings_set& model::metadata::allowed_platforms(void) const { if (_pimpl->props.is_set("allowed_platforms")) { return _pimpl->props.lookup< config::strings_set_node >( "allowed_platforms"); } else { return get_defaults().lookup< config::strings_set_node >( "allowed_platforms"); } } /// Returns all the user-defined metadata properties. /// /// \return A key/value map of properties. model::properties_map model::metadata::custom(void) const { return _pimpl->props.all_properties("custom", true); } /// Returns the description of the test. /// /// \return Textual description; may be empty. const std::string& model::metadata::description(void) const { if (_pimpl->props.is_set("description")) { return _pimpl->props.lookup< config::string_node >("description"); } else { return get_defaults().lookup< config::string_node >("description"); } } +/// Returns execution environment name. +/// +/// \return Name of configured execution environment. +const std::string& +model::metadata::execenv(void) const +{ + if (_pimpl->props.is_set("execenv")) { + return _pimpl->props.lookup< config::string_node >("execenv"); + } else { + return get_defaults().lookup< config::string_node >("execenv"); + } +} + + +/// Returns execenv jail(8) parameters string to run a test with. +/// +/// \return String of jail parameters. +const std::string& +model::metadata::execenv_jail_params(void) const +{ + if (_pimpl->props.is_set("execenv_jail_params")) { + return _pimpl->props.lookup< config::string_node >( + "execenv_jail_params"); + } else { + return get_defaults().lookup< config::string_node >( + "execenv_jail_params"); + } +} + + /// Returns whether the test has a cleanup part or not. /// /// \return True if there is a cleanup part; false otherwise. bool model::metadata::has_cleanup(void) const { if (_pimpl->props.is_set("has_cleanup")) { return _pimpl->props.lookup< config::bool_node >("has_cleanup"); } else { return get_defaults().lookup< config::bool_node >("has_cleanup"); } } +/// Returns whether the test has a specific execenv apart from default one. +/// +/// \return True if there is a non-host execenv configured; false otherwise. +bool +model::metadata::has_execenv(void) const +{ + const std::string& name = execenv(); + return !name.empty() && name != engine::execenv::default_execenv_name; +} + + /// Returns whether the test is exclusive or not. /// /// \return True if the test has to be run on its own, not concurrently with any /// other tests; false otherwise. bool model::metadata::is_exclusive(void) const { if (_pimpl->props.is_set("is_exclusive")) { return _pimpl->props.lookup< config::bool_node >("is_exclusive"); } else { return get_defaults().lookup< config::bool_node >("is_exclusive"); } } /// Returns the list of configuration variables needed by the test. /// /// \return Set of configuration variables. const model::strings_set& model::metadata::required_configs(void) const { if (_pimpl->props.is_set("required_configs")) { return _pimpl->props.lookup< config::strings_set_node >( "required_configs"); } else { return get_defaults().lookup< config::strings_set_node >( "required_configs"); } } /// Returns the amount of free disk space required by the test. /// /// \return Number of bytes, or 0 if this does not apply. const units::bytes& model::metadata::required_disk_space(void) const { if (_pimpl->props.is_set("required_disk_space")) { return _pimpl->props.lookup< bytes_node >("required_disk_space"); } else { return get_defaults().lookup< bytes_node >("required_disk_space"); } } /// Returns the list of files needed by the test. /// /// \return Set of paths. const model::paths_set& model::metadata::required_files(void) const { if (_pimpl->props.is_set("required_files")) { return _pimpl->props.lookup< paths_set_node >("required_files"); } else { return get_defaults().lookup< paths_set_node >("required_files"); } } /// Returns the amount of memory required by the test. /// /// \return Number of bytes, or 0 if this does not apply. const units::bytes& model::metadata::required_memory(void) const { if (_pimpl->props.is_set("required_memory")) { return _pimpl->props.lookup< bytes_node >("required_memory"); } else { return get_defaults().lookup< bytes_node >("required_memory"); } } /// Returns the list of programs needed by the test. /// /// \return Set of paths. const model::paths_set& model::metadata::required_programs(void) const { if (_pimpl->props.is_set("required_programs")) { return _pimpl->props.lookup< paths_set_node >("required_programs"); } else { return get_defaults().lookup< paths_set_node >("required_programs"); } } /// Returns the user required by the test. /// /// \return One of unprivileged, root or empty. const std::string& model::metadata::required_user(void) const { if (_pimpl->props.is_set("required_user")) { return _pimpl->props.lookup< user_node >("required_user"); } else { return get_defaults().lookup< user_node >("required_user"); } } /// Returns the timeout of the test. /// /// \return A time delta; should be compared to default_timeout to see if it has /// been overriden. const datetime::delta& model::metadata::timeout(void) const { if (_pimpl->props.is_set("timeout")) { return _pimpl->props.lookup< delta_node >("timeout"); } else { return get_defaults().lookup< delta_node >("timeout"); } } /// Externalizes the metadata to a set of key/value textual pairs. /// /// \return A key/value representation of the metadata. model::properties_map model::metadata::to_properties(void) const { const config::tree fully_specified = get_defaults().combine(_pimpl->props); return fully_specified.all_properties(); } /// Equality comparator. /// /// \param other The other object to compare this one to. /// /// \return True if this object and other are equal; false otherwise. bool model::metadata::operator==(const metadata& other) const { return _pimpl == other._pimpl || *_pimpl == *other._pimpl; } /// Inequality comparator. /// /// \param other The other object to compare this one to. /// /// \return True if this object and other are different; false otherwise. bool model::metadata::operator!=(const metadata& other) const { return !(*this == other); } /// Injects the object into a stream. /// /// \param output The stream into which to inject the object. /// \param object The object to format. /// /// \return The output stream. std::ostream& model::operator<<(std::ostream& output, const metadata& object) { output << "metadata{"; bool first = true; const model::properties_map props = object.to_properties(); for (model::properties_map::const_iterator iter = props.begin(); iter != props.end(); ++iter) { if (!first) output << ", "; output << F("%s=%s") % (*iter).first % text::quote((*iter).second, '\''); first = false; } output << "}"; return output; } /// Internal implementation of the metadata_builder class. struct model::metadata_builder::impl : utils::noncopyable { /// Collection of requirements. config::tree props; /// Whether we have created a metadata object or not. bool built; /// Constructor. impl(void) : built(false) { init_tree(props); } /// Constructor. /// /// \param base The base model to construct a copy from. impl(const model::metadata& base) : props(base._pimpl->props.deep_copy()), built(false) { } }; /// Constructor. model::metadata_builder::metadata_builder(void) : _pimpl(new impl()) { } /// Constructor. model::metadata_builder::metadata_builder(const model::metadata& base) : _pimpl(new impl(base)) { } /// Destructor. model::metadata_builder::~metadata_builder(void) { } /// Accumulates an additional allowed architecture. /// /// \param arch The architecture. /// /// \return A reference to this builder. /// /// \throw model::error If the value is invalid. model::metadata_builder& model::metadata_builder::add_allowed_architecture(const std::string& arch) { if (!_pimpl->props.is_set("allowed_architectures")) { _pimpl->props.set< config::strings_set_node >( "allowed_architectures", get_defaults().lookup< config::strings_set_node >( "allowed_architectures")); } lookup_rw< config::strings_set_node >( _pimpl->props, "allowed_architectures").insert(arch); return *this; } /// Accumulates an additional allowed platform. /// /// \param platform The platform. /// /// \return A reference to this builder. /// /// \throw model::error If the value is invalid. model::metadata_builder& model::metadata_builder::add_allowed_platform(const std::string& platform) { if (!_pimpl->props.is_set("allowed_platforms")) { _pimpl->props.set< config::strings_set_node >( "allowed_platforms", get_defaults().lookup< config::strings_set_node >( "allowed_platforms")); } lookup_rw< config::strings_set_node >( _pimpl->props, "allowed_platforms").insert(platform); return *this; } /// Accumulates a single user-defined property. /// /// \param key Name of the property to define. /// \param value Value of the property. /// /// \return A reference to this builder. /// /// \throw model::error If the value is invalid. model::metadata_builder& model::metadata_builder::add_custom(const std::string& key, const std::string& value) { _pimpl->props.set_string(F("custom.%s") % key, value); return *this; } /// Accumulates an additional required configuration variable. /// /// \param var The name of the configuration variable. /// /// \return A reference to this builder. /// /// \throw model::error If the value is invalid. model::metadata_builder& model::metadata_builder::add_required_config(const std::string& var) { if (!_pimpl->props.is_set("required_configs")) { _pimpl->props.set< config::strings_set_node >( "required_configs", get_defaults().lookup< config::strings_set_node >( "required_configs")); } lookup_rw< config::strings_set_node >( _pimpl->props, "required_configs").insert(var); return *this; } /// Accumulates an additional required file. /// /// \param path The path to the file. /// /// \return A reference to this builder. /// /// \throw model::error If the value is invalid. model::metadata_builder& model::metadata_builder::add_required_file(const fs::path& path) { if (!_pimpl->props.is_set("required_files")) { _pimpl->props.set< paths_set_node >( "required_files", get_defaults().lookup< paths_set_node >("required_files")); } lookup_rw< paths_set_node >(_pimpl->props, "required_files").insert(path); return *this; } /// Accumulates an additional required program. /// /// \param path The path to the program. /// /// \return A reference to this builder. /// /// \throw model::error If the value is invalid. model::metadata_builder& model::metadata_builder::add_required_program(const fs::path& path) { if (!_pimpl->props.is_set("required_programs")) { _pimpl->props.set< paths_set_node >( "required_programs", get_defaults().lookup< paths_set_node >("required_programs")); } lookup_rw< paths_set_node >(_pimpl->props, "required_programs").insert(path); return *this; } /// Sets the architectures allowed by the test. /// /// \param as Set of architectures. /// /// \return A reference to this builder. /// /// \throw model::error If the value is invalid. model::metadata_builder& model::metadata_builder::set_allowed_architectures( const model::strings_set& as) { set< config::strings_set_node >(_pimpl->props, "allowed_architectures", as); return *this; } /// Sets the platforms allowed by the test. /// /// \return ps Set of platforms. /// /// \return A reference to this builder. /// /// \throw model::error If the value is invalid. model::metadata_builder& model::metadata_builder::set_allowed_platforms(const model::strings_set& ps) { set< config::strings_set_node >(_pimpl->props, "allowed_platforms", ps); return *this; } /// Sets the user-defined properties. /// /// \param props The custom properties to set. /// /// \return A reference to this builder. /// /// \throw model::error If the value is invalid. model::metadata_builder& model::metadata_builder::set_custom(const model::properties_map& props) { for (model::properties_map::const_iterator iter = props.begin(); iter != props.end(); ++iter) _pimpl->props.set_string(F("custom.%s") % (*iter).first, (*iter).second); return *this; } /// Sets the description of the test. /// /// \param description Textual description of the test. /// /// \return A reference to this builder. /// /// \throw model::error If the value is invalid. model::metadata_builder& model::metadata_builder::set_description(const std::string& description) { set< config::string_node >(_pimpl->props, "description", description); return *this; } +/// Sets execution environment name. +/// +/// \param name Execution environment name. +/// +/// \return A reference to this builder. +/// +/// \throw model::error If the value is invalid. +model::metadata_builder& +model::metadata_builder::set_execenv(const std::string& name) +{ + set< config::string_node >(_pimpl->props, "execenv", name); + return *this; +} + + +/// Sets execenv jail(8) parameters string to run the test with. +/// +/// \param params String of jail parameters. +/// +/// \return A reference to this builder. +/// +/// \throw model::error If the value is invalid. +model::metadata_builder& +model::metadata_builder::set_execenv_jail_params(const std::string& params) +{ + set< config::string_node >(_pimpl->props, "execenv_jail_params", params); + return *this; +} + + /// Sets whether the test has a cleanup part or not. /// /// \param cleanup True if the test has a cleanup part; false otherwise. /// /// \return A reference to this builder. /// /// \throw model::error If the value is invalid. model::metadata_builder& model::metadata_builder::set_has_cleanup(const bool cleanup) { set< config::bool_node >(_pimpl->props, "has_cleanup", cleanup); return *this; } /// Sets whether the test is exclusive or not. /// /// \param exclusive True if the test is exclusive; false otherwise. /// /// \return A reference to this builder. /// /// \throw model::error If the value is invalid. model::metadata_builder& model::metadata_builder::set_is_exclusive(const bool exclusive) { set< config::bool_node >(_pimpl->props, "is_exclusive", exclusive); return *this; } /// Sets the list of configuration variables needed by the test. /// /// \param vars Set of configuration variables. /// /// \return A reference to this builder. /// /// \throw model::error If the value is invalid. model::metadata_builder& model::metadata_builder::set_required_configs(const model::strings_set& vars) { set< config::strings_set_node >(_pimpl->props, "required_configs", vars); return *this; } /// Sets the amount of free disk space required by the test. /// /// \param bytes Number of bytes. /// /// \return A reference to this builder. /// /// \throw model::error If the value is invalid. model::metadata_builder& model::metadata_builder::set_required_disk_space(const units::bytes& bytes) { set< bytes_node >(_pimpl->props, "required_disk_space", bytes); return *this; } /// Sets the list of files needed by the test. /// /// \param files Set of paths. /// /// \return A reference to this builder. /// /// \throw model::error If the value is invalid. model::metadata_builder& model::metadata_builder::set_required_files(const model::paths_set& files) { set< paths_set_node >(_pimpl->props, "required_files", files); return *this; } /// Sets the amount of memory required by the test. /// /// \param bytes Number of bytes. /// /// \return A reference to this builder. /// /// \throw model::error If the value is invalid. model::metadata_builder& model::metadata_builder::set_required_memory(const units::bytes& bytes) { set< bytes_node >(_pimpl->props, "required_memory", bytes); return *this; } /// Sets the list of programs needed by the test. /// /// \param progs Set of paths. /// /// \return A reference to this builder. /// /// \throw model::error If the value is invalid. model::metadata_builder& model::metadata_builder::set_required_programs(const model::paths_set& progs) { set< paths_set_node >(_pimpl->props, "required_programs", progs); return *this; } /// Sets the user required by the test. /// /// \param user One of unprivileged, root or empty. /// /// \return A reference to this builder. /// /// \throw model::error If the value is invalid. model::metadata_builder& model::metadata_builder::set_required_user(const std::string& user) { set< user_node >(_pimpl->props, "required_user", user); return *this; } /// Sets a metadata property by name from its textual representation. /// /// \param key The property to set. /// \param value The value to set the property to. /// /// \return A reference to this builder. /// /// \throw model::error If the value is invalid or the key does not exist. model::metadata_builder& model::metadata_builder::set_string(const std::string& key, const std::string& value) { try { _pimpl->props.set_string(key, value); } catch (const config::unknown_key_error& e) { throw model::format_error(F("Unknown metadata property %s") % key); } catch (const config::value_error& e) { throw model::format_error( F("Invalid value for metadata property %s: %s") % key % e.what()); } return *this; } /// Sets the timeout of the test. /// /// \param timeout The timeout to set. /// /// \return A reference to this builder. /// /// \throw model::error If the value is invalid. model::metadata_builder& model::metadata_builder::set_timeout(const datetime::delta& timeout) { set< delta_node >(_pimpl->props, "timeout", timeout); return *this; } /// Creates a new metadata object. /// /// \pre This has not yet been called. We only support calling this function /// once due to the way the internal tree works: we pass around references, not /// deep copies, so if we allowed a second build, we'd encourage reusing the /// same builder to construct different metadata objects, and this could have /// unintended consequences. /// /// \return The constructed metadata object. model::metadata model::metadata_builder::build(void) const { PRE(!_pimpl->built); _pimpl->built = true; return metadata(_pimpl->props); } diff --git a/contrib/kyua/model/metadata.hpp b/contrib/kyua/model/metadata.hpp index c7dd4519f122..83bc5348774a 100644 --- a/contrib/kyua/model/metadata.hpp +++ b/contrib/kyua/model/metadata.hpp @@ -1,130 +1,135 @@ // Copyright 2012 The Kyua Authors. // All rights reserved. // // Redistribution and use in source and binary forms, with or without // modification, are permitted provided that the following conditions are // met: // // * Redistributions of source code must retain the above copyright // notice, this list of conditions and the following disclaimer. // * Redistributions in binary form must reproduce the above copyright // notice, this list of conditions and the following disclaimer in the // documentation and/or other materials provided with the distribution. // * Neither the name of Google Inc. nor the names of its contributors // may be used to endorse or promote products derived from this software // without specific prior written permission. // // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS // "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT // LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR // A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT // OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT // LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. /// \file model/metadata.hpp /// Definition of the "test metadata" concept. #if !defined(MODEL_METADATA_HPP) #define MODEL_METADATA_HPP #include "model/metadata_fwd.hpp" #include #include #include #include "model/types.hpp" #include "utils/config/tree_fwd.hpp" #include "utils/datetime_fwd.hpp" #include "utils/fs/path_fwd.hpp" #include "utils/noncopyable.hpp" #include "utils/units_fwd.hpp" namespace model { /// Collection of metadata properties of a test. class metadata { struct impl; /// Pointer to the shared internal implementation. std::shared_ptr< impl > _pimpl; friend class metadata_builder; public: metadata(const utils::config::tree&); ~metadata(void); metadata apply_overrides(const metadata&) const; const strings_set& allowed_architectures(void) const; const strings_set& allowed_platforms(void) const; model::properties_map custom(void) const; const std::string& description(void) const; + const std::string& execenv(void) const; + const std::string& execenv_jail_params(void) const; bool has_cleanup(void) const; + bool has_execenv(void) const; bool is_exclusive(void) const; const strings_set& required_configs(void) const; const utils::units::bytes& required_disk_space(void) const; const paths_set& required_files(void) const; const utils::units::bytes& required_memory(void) const; const paths_set& required_programs(void) const; const std::string& required_user(void) const; const utils::datetime::delta& timeout(void) const; model::properties_map to_properties(void) const; bool operator==(const metadata&) const; bool operator!=(const metadata&) const; }; std::ostream& operator<<(std::ostream&, const metadata&); /// Builder for a metadata object. class metadata_builder : utils::noncopyable { struct impl; /// Pointer to the shared internal implementation. std::auto_ptr< impl > _pimpl; public: metadata_builder(void); explicit metadata_builder(const metadata&); ~metadata_builder(void); metadata_builder& add_allowed_architecture(const std::string&); metadata_builder& add_allowed_platform(const std::string&); metadata_builder& add_custom(const std::string&, const std::string&); metadata_builder& add_required_config(const std::string&); metadata_builder& add_required_file(const utils::fs::path&); metadata_builder& add_required_program(const utils::fs::path&); metadata_builder& set_allowed_architectures(const strings_set&); metadata_builder& set_allowed_platforms(const strings_set&); metadata_builder& set_custom(const model::properties_map&); metadata_builder& set_description(const std::string&); + metadata_builder& set_execenv(const std::string&); + metadata_builder& set_execenv_jail_params(const std::string&); metadata_builder& set_has_cleanup(const bool); metadata_builder& set_is_exclusive(const bool); metadata_builder& set_required_configs(const strings_set&); metadata_builder& set_required_disk_space(const utils::units::bytes&); metadata_builder& set_required_files(const paths_set&); metadata_builder& set_required_memory(const utils::units::bytes&); metadata_builder& set_required_programs(const paths_set&); metadata_builder& set_required_user(const std::string&); metadata_builder& set_string(const std::string&, const std::string&); metadata_builder& set_timeout(const utils::datetime::delta&); metadata build(void) const; }; } // namespace model #endif // !defined(MODEL_METADATA_HPP) diff --git a/contrib/kyua/model/metadata_test.cpp b/contrib/kyua/model/metadata_test.cpp index 7b22653ec1a2..b4c3dff5b029 100644 --- a/contrib/kyua/model/metadata_test.cpp +++ b/contrib/kyua/model/metadata_test.cpp @@ -1,461 +1,465 @@ // Copyright 2012 The Kyua Authors. // All rights reserved. // // Redistribution and use in source and binary forms, with or without // modification, are permitted provided that the following conditions are // met: // // * Redistributions of source code must retain the above copyright // notice, this list of conditions and the following disclaimer. // * Redistributions in binary form must reproduce the above copyright // notice, this list of conditions and the following disclaimer in the // documentation and/or other materials provided with the distribution. // * Neither the name of Google Inc. nor the names of its contributors // may be used to endorse or promote products derived from this software // without specific prior written permission. // // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS // "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT // LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR // A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT // OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT // LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #include "model/metadata.hpp" #include #include #include "model/types.hpp" #include "utils/datetime.hpp" #include "utils/format/containers.ipp" #include "utils/fs/path.hpp" #include "utils/units.hpp" namespace datetime = utils::datetime; namespace fs = utils::fs; namespace units = utils::units; ATF_TEST_CASE_WITHOUT_HEAD(defaults); ATF_TEST_CASE_BODY(defaults) { const model::metadata md = model::metadata_builder().build(); ATF_REQUIRE(md.allowed_architectures().empty()); ATF_REQUIRE(md.allowed_platforms().empty()); ATF_REQUIRE(md.allowed_platforms().empty()); ATF_REQUIRE(md.custom().empty()); ATF_REQUIRE(md.description().empty()); ATF_REQUIRE(!md.has_cleanup()); ATF_REQUIRE(!md.is_exclusive()); ATF_REQUIRE(md.required_configs().empty()); ATF_REQUIRE_EQ(units::bytes(0), md.required_disk_space()); ATF_REQUIRE(md.required_files().empty()); ATF_REQUIRE_EQ(units::bytes(0), md.required_memory()); ATF_REQUIRE(md.required_programs().empty()); ATF_REQUIRE(md.required_user().empty()); ATF_REQUIRE(datetime::delta(300, 0) == md.timeout()); } ATF_TEST_CASE_WITHOUT_HEAD(add); ATF_TEST_CASE_BODY(add) { model::strings_set architectures; architectures.insert("1-architecture"); architectures.insert("2-architecture"); model::strings_set platforms; platforms.insert("1-platform"); platforms.insert("2-platform"); model::properties_map custom; custom["1-custom"] = "first"; custom["2-custom"] = "second"; model::strings_set configs; configs.insert("1-config"); configs.insert("2-config"); model::paths_set files; files.insert(fs::path("1-file")); files.insert(fs::path("2-file")); model::paths_set programs; programs.insert(fs::path("1-program")); programs.insert(fs::path("2-program")); const model::metadata md = model::metadata_builder() .add_allowed_architecture("1-architecture") .add_allowed_platform("1-platform") .add_custom("1-custom", "first") .add_custom("2-custom", "second") .add_required_config("1-config") .add_required_file(fs::path("1-file")) .add_required_program(fs::path("1-program")) .add_allowed_architecture("2-architecture") .add_allowed_platform("2-platform") .add_required_config("2-config") .add_required_file(fs::path("2-file")) .add_required_program(fs::path("2-program")) .build(); ATF_REQUIRE(architectures == md.allowed_architectures()); ATF_REQUIRE(platforms == md.allowed_platforms()); ATF_REQUIRE(custom == md.custom()); ATF_REQUIRE(configs == md.required_configs()); ATF_REQUIRE(files == md.required_files()); ATF_REQUIRE(programs == md.required_programs()); } ATF_TEST_CASE_WITHOUT_HEAD(copy); ATF_TEST_CASE_BODY(copy) { const model::metadata md1 = model::metadata_builder() .add_allowed_architecture("1-architecture") .add_allowed_platform("1-platform") .build(); const model::metadata md2 = model::metadata_builder(md1) .add_allowed_architecture("2-architecture") .build(); ATF_REQUIRE_EQ(1, md1.allowed_architectures().size()); ATF_REQUIRE_EQ(2, md2.allowed_architectures().size()); ATF_REQUIRE_EQ(1, md1.allowed_platforms().size()); ATF_REQUIRE_EQ(1, md2.allowed_platforms().size()); } ATF_TEST_CASE_WITHOUT_HEAD(apply_overrides); ATF_TEST_CASE_BODY(apply_overrides) { const model::metadata md1 = model::metadata_builder() .add_allowed_architecture("1-architecture") .add_allowed_platform("1-platform") .set_description("Explicit description") .build(); const model::metadata md2 = model::metadata_builder() .add_allowed_architecture("2-architecture") .set_description("") .set_timeout(datetime::delta(500, 0)) .build(); const model::metadata merge_1_2 = model::metadata_builder() .add_allowed_architecture("2-architecture") .add_allowed_platform("1-platform") .set_description("") .set_timeout(datetime::delta(500, 0)) .build(); ATF_REQUIRE_EQ(merge_1_2, md1.apply_overrides(md2)); const model::metadata merge_2_1 = model::metadata_builder() .add_allowed_architecture("1-architecture") .add_allowed_platform("1-platform") .set_description("Explicit description") .set_timeout(datetime::delta(500, 0)) .build(); ATF_REQUIRE_EQ(merge_2_1, md2.apply_overrides(md1)); } ATF_TEST_CASE_WITHOUT_HEAD(override_all_with_setters); ATF_TEST_CASE_BODY(override_all_with_setters) { model::strings_set architectures; architectures.insert("the-architecture"); model::strings_set platforms; platforms.insert("the-platforms"); model::properties_map custom; custom["first"] = "hello"; custom["second"] = "bye"; const std::string description = "Some long text"; model::strings_set configs; configs.insert("the-configs"); model::paths_set files; files.insert(fs::path("the-files")); const units::bytes disk_space(6789); const units::bytes memory(12345); model::paths_set programs; programs.insert(fs::path("the-programs")); const std::string user = "root"; const datetime::delta timeout(123, 0); const model::metadata md = model::metadata_builder() .set_allowed_architectures(architectures) .set_allowed_platforms(platforms) .set_custom(custom) .set_description(description) .set_has_cleanup(true) .set_is_exclusive(true) .set_required_configs(configs) .set_required_disk_space(disk_space) .set_required_files(files) .set_required_memory(memory) .set_required_programs(programs) .set_required_user(user) .set_timeout(timeout) .build(); ATF_REQUIRE(architectures == md.allowed_architectures()); ATF_REQUIRE(platforms == md.allowed_platforms()); ATF_REQUIRE(custom == md.custom()); ATF_REQUIRE_EQ(description, md.description()); ATF_REQUIRE(md.has_cleanup()); ATF_REQUIRE(md.is_exclusive()); ATF_REQUIRE(configs == md.required_configs()); ATF_REQUIRE_EQ(disk_space, md.required_disk_space()); ATF_REQUIRE(files == md.required_files()); ATF_REQUIRE_EQ(memory, md.required_memory()); ATF_REQUIRE(programs == md.required_programs()); ATF_REQUIRE_EQ(user, md.required_user()); ATF_REQUIRE(timeout == md.timeout()); } ATF_TEST_CASE_WITHOUT_HEAD(override_all_with_set_string); ATF_TEST_CASE_BODY(override_all_with_set_string) { model::strings_set architectures; architectures.insert("a1"); architectures.insert("a2"); model::strings_set platforms; platforms.insert("p1"); platforms.insert("p2"); model::properties_map custom; custom["user-defined"] = "the-value"; const std::string description = "Another long text"; model::strings_set configs; configs.insert("config-var"); model::paths_set files; files.insert(fs::path("plain")); files.insert(fs::path("/absolute/path")); const units::bytes disk_space( static_cast< uint64_t >(16) * 1024 * 1024 * 1024); const units::bytes memory(1024 * 1024); model::paths_set programs; programs.insert(fs::path("program")); programs.insert(fs::path("/absolute/prog")); const std::string user = "unprivileged"; const datetime::delta timeout(45, 0); const model::metadata md = model::metadata_builder() .set_string("allowed_architectures", "a1 a2") .set_string("allowed_platforms", "p1 p2") .set_string("custom.user-defined", "the-value") .set_string("description", "Another long text") .set_string("has_cleanup", "true") .set_string("is_exclusive", "true") .set_string("required_configs", "config-var") .set_string("required_disk_space", "16G") .set_string("required_files", "plain /absolute/path") .set_string("required_memory", "1M") .set_string("required_programs", "program /absolute/prog") .set_string("required_user", "unprivileged") .set_string("timeout", "45") .build(); ATF_REQUIRE(architectures == md.allowed_architectures()); ATF_REQUIRE(platforms == md.allowed_platforms()); ATF_REQUIRE(custom == md.custom()); ATF_REQUIRE_EQ(description, md.description()); ATF_REQUIRE(md.has_cleanup()); ATF_REQUIRE(md.is_exclusive()); ATF_REQUIRE(configs == md.required_configs()); ATF_REQUIRE_EQ(disk_space, md.required_disk_space()); ATF_REQUIRE(files == md.required_files()); ATF_REQUIRE_EQ(memory, md.required_memory()); ATF_REQUIRE(programs == md.required_programs()); ATF_REQUIRE_EQ(user, md.required_user()); ATF_REQUIRE(timeout == md.timeout()); } ATF_TEST_CASE_WITHOUT_HEAD(to_properties); ATF_TEST_CASE_BODY(to_properties) { const model::metadata md = model::metadata_builder() .add_allowed_architecture("abc") .add_required_file(fs::path("foo")) .add_required_file(fs::path("bar")) .set_required_memory(units::bytes(1024)) .add_custom("foo", "bar") .build(); model::properties_map props; props["allowed_architectures"] = "abc"; props["allowed_platforms"] = ""; props["custom.foo"] = "bar"; props["description"] = ""; + props["execenv"] = ""; + props["execenv_jail_params"] = ""; props["has_cleanup"] = "false"; props["is_exclusive"] = "false"; props["required_configs"] = ""; props["required_disk_space"] = "0"; props["required_files"] = "bar foo"; props["required_memory"] = "1.00K"; props["required_programs"] = ""; props["required_user"] = ""; props["timeout"] = "300"; ATF_REQUIRE_EQ(props, md.to_properties()); } ATF_TEST_CASE_WITHOUT_HEAD(operators_eq_and_ne__empty); ATF_TEST_CASE_BODY(operators_eq_and_ne__empty) { const model::metadata md1 = model::metadata_builder().build(); const model::metadata md2 = model::metadata_builder().build(); ATF_REQUIRE( md1 == md2); ATF_REQUIRE(!(md1 != md2)); } ATF_TEST_CASE_WITHOUT_HEAD(operators_eq_and_ne__copy); ATF_TEST_CASE_BODY(operators_eq_and_ne__copy) { const model::metadata md1 = model::metadata_builder() .add_custom("foo", "bar") .build(); const model::metadata md2 = md1; ATF_REQUIRE( md1 == md2); ATF_REQUIRE(!(md1 != md2)); } ATF_TEST_CASE_WITHOUT_HEAD(operators_eq_and_ne__equal); ATF_TEST_CASE_BODY(operators_eq_and_ne__equal) { const model::metadata md1 = model::metadata_builder() .add_allowed_architecture("a") .add_allowed_architecture("b") .add_custom("foo", "bar") .build(); const model::metadata md2 = model::metadata_builder() .add_allowed_architecture("b") .add_allowed_architecture("a") .add_custom("foo", "bar") .build(); ATF_REQUIRE( md1 == md2); ATF_REQUIRE(!(md1 != md2)); } ATF_TEST_CASE_WITHOUT_HEAD(operators_eq_and_ne__equal_overriden_defaults); ATF_TEST_CASE_BODY(operators_eq_and_ne__equal_overriden_defaults) { const model::metadata defaults = model::metadata_builder().build(); const model::metadata md1 = model::metadata_builder() .add_allowed_architecture("a") .build(); const model::metadata md2 = model::metadata_builder() .add_allowed_architecture("a") .set_timeout(defaults.timeout()) .build(); ATF_REQUIRE( md1 == md2); ATF_REQUIRE(!(md1 != md2)); } ATF_TEST_CASE_WITHOUT_HEAD(operators_eq_and_ne__different); ATF_TEST_CASE_BODY(operators_eq_and_ne__different) { const model::metadata md1 = model::metadata_builder() .add_custom("foo", "bar") .build(); const model::metadata md2 = model::metadata_builder() .add_custom("foo", "bar") .add_custom("baz", "foo bar") .build(); ATF_REQUIRE(!(md1 == md2)); ATF_REQUIRE( md1 != md2); } ATF_TEST_CASE_WITHOUT_HEAD(output__defaults); ATF_TEST_CASE_BODY(output__defaults) { std::ostringstream str; str << model::metadata_builder().build(); ATF_REQUIRE_EQ("metadata{allowed_architectures='', allowed_platforms='', " - "description='', has_cleanup='false', is_exclusive='false', " + "description='', execenv='', execenv_jail_params='', " + "has_cleanup='false', is_exclusive='false', " "required_configs='', " "required_disk_space='0', required_files='', " "required_memory='0', " "required_programs='', required_user='', timeout='300'}", str.str()); } ATF_TEST_CASE_WITHOUT_HEAD(output__some_values); ATF_TEST_CASE_BODY(output__some_values) { std::ostringstream str; str << model::metadata_builder() .add_allowed_architecture("abc") .add_required_file(fs::path("foo")) .add_required_file(fs::path("bar")) .set_is_exclusive(true) .set_required_memory(units::bytes(1024)) .build(); ATF_REQUIRE_EQ( "metadata{allowed_architectures='abc', allowed_platforms='', " - "description='', has_cleanup='false', is_exclusive='true', " + "description='', execenv='', execenv_jail_params='', " + "has_cleanup='false', is_exclusive='true', " "required_configs='', " "required_disk_space='0', required_files='bar foo', " "required_memory='1.00K', " "required_programs='', required_user='', timeout='300'}", str.str()); } ATF_INIT_TEST_CASES(tcs) { ATF_ADD_TEST_CASE(tcs, defaults); ATF_ADD_TEST_CASE(tcs, add); ATF_ADD_TEST_CASE(tcs, copy); ATF_ADD_TEST_CASE(tcs, apply_overrides); ATF_ADD_TEST_CASE(tcs, override_all_with_setters); ATF_ADD_TEST_CASE(tcs, override_all_with_set_string); ATF_ADD_TEST_CASE(tcs, to_properties); ATF_ADD_TEST_CASE(tcs, operators_eq_and_ne__empty); ATF_ADD_TEST_CASE(tcs, operators_eq_and_ne__copy); ATF_ADD_TEST_CASE(tcs, operators_eq_and_ne__equal); ATF_ADD_TEST_CASE(tcs, operators_eq_and_ne__equal_overriden_defaults); ATF_ADD_TEST_CASE(tcs, operators_eq_and_ne__different); ATF_ADD_TEST_CASE(tcs, output__defaults); ATF_ADD_TEST_CASE(tcs, output__some_values); // TODO(jmmv): Add tests for error conditions (invalid keys and invalid // values). } diff --git a/contrib/kyua/model/test_case_test.cpp b/contrib/kyua/model/test_case_test.cpp index 1a55de0fab42..1e2597d1501e 100644 --- a/contrib/kyua/model/test_case_test.cpp +++ b/contrib/kyua/model/test_case_test.cpp @@ -1,263 +1,264 @@ // Copyright 2010 The Kyua Authors. // All rights reserved. // // Redistribution and use in source and binary forms, with or without // modification, are permitted provided that the following conditions are // met: // // * Redistributions of source code must retain the above copyright // notice, this list of conditions and the following disclaimer. // * Redistributions in binary form must reproduce the above copyright // notice, this list of conditions and the following disclaimer in the // documentation and/or other materials provided with the distribution. // * Neither the name of Google Inc. nor the names of its contributors // may be used to endorse or promote products derived from this software // without specific prior written permission. // // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS // "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT // LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR // A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT // OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT // LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #include "model/test_case.hpp" #include #include #include "model/metadata.hpp" #include "model/test_result.hpp" #include "utils/datetime.hpp" #include "utils/format/containers.ipp" #include "utils/fs/path.hpp" #include "utils/optional.ipp" namespace datetime = utils::datetime; namespace fs = utils::fs; ATF_TEST_CASE_WITHOUT_HEAD(test_case__ctor_and_getters) ATF_TEST_CASE_BODY(test_case__ctor_and_getters) { const model::metadata md = model::metadata_builder() .add_custom("first", "value") .build(); const model::test_case test_case("foo", md); ATF_REQUIRE_EQ("foo", test_case.name()); ATF_REQUIRE_EQ(md, test_case.get_metadata()); ATF_REQUIRE_EQ(md, test_case.get_raw_metadata()); } ATF_TEST_CASE_WITHOUT_HEAD(test_case__fake_result) ATF_TEST_CASE_BODY(test_case__fake_result) { const model::test_result result(model::test_result_skipped, "Some reason"); const model::test_case test_case("__foo__", "Some description", result); ATF_REQUIRE_EQ("__foo__", test_case.name()); ATF_REQUIRE_EQ(result, test_case.fake_result().get()); const model::metadata exp_metadata = model::metadata_builder() .set_description("Some description") .build(); ATF_REQUIRE_EQ(exp_metadata, test_case.get_metadata()); ATF_REQUIRE_EQ(exp_metadata, test_case.get_raw_metadata()); } ATF_TEST_CASE_WITHOUT_HEAD(test_case__apply_metadata_overrides__real_test_case) ATF_TEST_CASE_BODY(test_case__apply_metadata_overrides__real_test_case) { const model::metadata overrides = model::metadata_builder() .add_required_config("the-variable") .set_description("The test case") .build(); const model::test_case base_test_case("foo", overrides); const model::metadata defaults = model::metadata_builder() .set_description("Default description") .set_timeout(datetime::delta(10, 0)) .build(); const model::test_case test_case = base_test_case.apply_metadata_defaults( &defaults); const model::metadata expected = model::metadata_builder() .add_required_config("the-variable") .set_description("The test case") .set_timeout(datetime::delta(10, 0)) .build(); ATF_REQUIRE_EQ(expected, test_case.get_metadata()); ATF_REQUIRE_EQ(overrides, test_case.get_raw_metadata()); // Ensure the original (although immutable) test case was not touched. ATF_REQUIRE_EQ(overrides, base_test_case.get_metadata()); } ATF_TEST_CASE_WITHOUT_HEAD(test_case__apply_metadata_overrides__fake_test_case) ATF_TEST_CASE_BODY(test_case__apply_metadata_overrides__fake_test_case) { const model::test_result result(model::test_result_skipped, "Irrelevant"); const model::test_case base_test_case("__foo__", "Fake test", result); const model::metadata overrides = model::metadata_builder() .set_description("Fake test") .build(); const model::metadata defaults = model::metadata_builder() .add_allowed_platform("some-value") .set_description("Default description") .build(); const model::test_case test_case = base_test_case.apply_metadata_defaults( &defaults); const model::metadata expected = model::metadata_builder() .add_allowed_platform("some-value") .set_description("Fake test") .build(); ATF_REQUIRE_EQ(expected, test_case.get_metadata()); ATF_REQUIRE_EQ(overrides, test_case.get_raw_metadata()); } ATF_TEST_CASE_WITHOUT_HEAD(test_case__operators_eq_and_ne__copy); ATF_TEST_CASE_BODY(test_case__operators_eq_and_ne__copy) { const model::test_case tc1("name", model::metadata_builder().build()); const model::test_case tc2 = tc1; ATF_REQUIRE( tc1 == tc2); ATF_REQUIRE(!(tc1 != tc2)); } ATF_TEST_CASE_WITHOUT_HEAD(test_case__operators_eq_and_ne__not_copy); ATF_TEST_CASE_BODY(test_case__operators_eq_and_ne__not_copy) { const std::string base_name("name"); const model::metadata base_metadata = model::metadata_builder() .add_custom("foo", "bar") .build(); const model::test_case base_tc(base_name, base_metadata); // Construct with all same values. { const model::test_case other_tc(base_name, base_metadata); ATF_REQUIRE( base_tc == other_tc); ATF_REQUIRE(!(base_tc != other_tc)); } // Construct with all same values but different metadata objects. { const model::metadata other_metadata = model::metadata_builder() .add_custom("foo", "bar") .set_timeout(base_metadata.timeout()) .build(); const model::test_case other_tc(base_name, other_metadata); ATF_REQUIRE( base_tc == other_tc); ATF_REQUIRE(!(base_tc != other_tc)); } // Different name. { const model::test_case other_tc("other", base_metadata); ATF_REQUIRE(!(base_tc == other_tc)); ATF_REQUIRE( base_tc != other_tc); } // Different metadata. { const model::test_case other_tc(base_name, model::metadata_builder().build()); ATF_REQUIRE(!(base_tc == other_tc)); ATF_REQUIRE( base_tc != other_tc); } } ATF_TEST_CASE_WITHOUT_HEAD(test_case__output); ATF_TEST_CASE_BODY(test_case__output) { const model::test_case tc1( "the-name", model::metadata_builder() .add_allowed_platform("foo").add_custom("bar", "baz").build()); std::ostringstream str; str << tc1; ATF_REQUIRE_EQ( "test_case{name='the-name', " "metadata=metadata{allowed_architectures='', allowed_platforms='foo', " - "custom.bar='baz', description='', has_cleanup='false', " + "custom.bar='baz', description='', execenv='', execenv_jail_params='', " + "has_cleanup='false', " "is_exclusive='false', " "required_configs='', required_disk_space='0', required_files='', " "required_memory='0', " "required_programs='', required_user='', timeout='300'}}", str.str()); } ATF_TEST_CASE_WITHOUT_HEAD(test_cases_map__builder); ATF_TEST_CASE_BODY(test_cases_map__builder) { model::test_cases_map_builder builder; model::test_cases_map exp_test_cases; ATF_REQUIRE_EQ(exp_test_cases, builder.build()); builder.add("default-metadata"); { const model::test_case tc1("default-metadata", model::metadata_builder().build()); exp_test_cases.insert( model::test_cases_map::value_type(tc1.name(), tc1)); } ATF_REQUIRE_EQ(exp_test_cases, builder.build()); builder.add("with-metadata", model::metadata_builder().set_description("text").build()); { const model::test_case tc1("with-metadata", model::metadata_builder() .set_description("text").build()); exp_test_cases.insert( model::test_cases_map::value_type(tc1.name(), tc1)); } ATF_REQUIRE_EQ(exp_test_cases, builder.build()); const model::test_case tc1("fully_built", model::metadata_builder() .set_description("something else").build()); builder.add(tc1); exp_test_cases.insert(model::test_cases_map::value_type(tc1.name(), tc1)); ATF_REQUIRE_EQ(exp_test_cases, builder.build()); } ATF_INIT_TEST_CASES(tcs) { ATF_ADD_TEST_CASE(tcs, test_case__ctor_and_getters); ATF_ADD_TEST_CASE(tcs, test_case__fake_result); ATF_ADD_TEST_CASE(tcs, test_case__apply_metadata_overrides__real_test_case); ATF_ADD_TEST_CASE(tcs, test_case__apply_metadata_overrides__fake_test_case); ATF_ADD_TEST_CASE(tcs, test_case__operators_eq_and_ne__copy); ATF_ADD_TEST_CASE(tcs, test_case__operators_eq_and_ne__not_copy); ATF_ADD_TEST_CASE(tcs, test_case__output); ATF_ADD_TEST_CASE(tcs, test_cases_map__builder); } diff --git a/contrib/kyua/model/test_program_test.cpp b/contrib/kyua/model/test_program_test.cpp index f9a8f7e59da3..ddfbc430387c 100644 --- a/contrib/kyua/model/test_program_test.cpp +++ b/contrib/kyua/model/test_program_test.cpp @@ -1,711 +1,714 @@ // Copyright 2010 The Kyua Authors. // All rights reserved. // // Redistribution and use in source and binary forms, with or without // modification, are permitted provided that the following conditions are // met: // // * Redistributions of source code must retain the above copyright // notice, this list of conditions and the following disclaimer. // * Redistributions in binary form must reproduce the above copyright // notice, this list of conditions and the following disclaimer in the // documentation and/or other materials provided with the distribution. // * Neither the name of Google Inc. nor the names of its contributors // may be used to endorse or promote products derived from this software // without specific prior written permission. // // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS // "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT // LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR // A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT // OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT // LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #include "model/test_program.hpp" extern "C" { #include #include } #include #include #include #include "model/exceptions.hpp" #include "model/metadata.hpp" #include "model/test_case.hpp" #include "model/test_result.hpp" #include "utils/env.hpp" #include "utils/format/containers.ipp" #include "utils/format/macros.hpp" #include "utils/fs/operations.hpp" #include "utils/fs/path.hpp" #include "utils/optional.ipp" namespace fs = utils::fs; namespace { /// Test program that sets its test cases lazily. /// /// This test class exists to test the behavior of a test_program object when /// the class is extended to offer lazy loading of test cases. We simulate such /// lazy loading here by storing the list of test cases aside at construction /// time and later setting it lazily the first time test_cases() is called. class lazy_test_program : public model::test_program { /// Whether set_test_cases() has yet been called or not. mutable bool _set_test_cases_called; /// The list of test cases for this test program. /// /// Only use this in the call to set_test_cases(). All other reads of the /// test cases list should happen via the parent class' test_cases() method. model::test_cases_map _lazy_test_cases; public: /// Constructs a new test program. /// /// \param interface_name_ Name of the test program interface. /// \param binary_ The name of the test program binary relative to root_. /// \param root_ The root of the test suite containing the test program. /// \param test_suite_name_ The name of the test suite. /// \param metadata_ Metadata of the test program. /// \param test_cases_ The collection of test cases in the test program. lazy_test_program(const std::string& interface_name_, const utils::fs::path& binary_, const utils::fs::path& root_, const std::string& test_suite_name_, const model::metadata& metadata_, const model::test_cases_map& test_cases_) : test_program(interface_name_, binary_, root_, test_suite_name_, metadata_, model::test_cases_map()), _set_test_cases_called(false), _lazy_test_cases(test_cases_) { } /// Lazily sets the test cases on the parent and returns them. /// /// \return The list of test cases. const model::test_cases_map& test_cases(void) const { if (!_set_test_cases_called) { const_cast< lazy_test_program* >(this)->set_test_cases( _lazy_test_cases); _set_test_cases_called = true; } return test_program::test_cases(); } }; } // anonymous namespace /// Runs a ctor_and_getters test. /// /// \tparam TestProgram Either model::test_program or lazy_test_program. template< class TestProgram > static void check_ctor_and_getters(void) { const model::metadata tp_md = model::metadata_builder() .add_custom("first", "foo") .add_custom("second", "bar") .build(); const model::metadata tc_md = model::metadata_builder() .add_custom("first", "baz") .build(); const TestProgram test_program( "mock", fs::path("binary"), fs::path("root"), "suite-name", tp_md, model::test_cases_map_builder().add("foo", tc_md).build()); ATF_REQUIRE_EQ("mock", test_program.interface_name()); ATF_REQUIRE_EQ(fs::path("binary"), test_program.relative_path()); ATF_REQUIRE_EQ(fs::current_path() / "root/binary", test_program.absolute_path()); ATF_REQUIRE_EQ(fs::path("root"), test_program.root()); ATF_REQUIRE_EQ("suite-name", test_program.test_suite_name()); ATF_REQUIRE_EQ(tp_md, test_program.get_metadata()); const model::metadata exp_tc_md = model::metadata_builder() .add_custom("first", "baz") .add_custom("second", "bar") .build(); const model::test_cases_map exp_tcs = model::test_cases_map_builder() .add("foo", exp_tc_md) .build(); ATF_REQUIRE_EQ(exp_tcs, test_program.test_cases()); } ATF_TEST_CASE_WITHOUT_HEAD(ctor_and_getters); ATF_TEST_CASE_BODY(ctor_and_getters) { check_ctor_and_getters< model::test_program >(); } ATF_TEST_CASE_WITHOUT_HEAD(derived__ctor_and_getters); ATF_TEST_CASE_BODY(derived__ctor_and_getters) { check_ctor_and_getters< lazy_test_program >(); } /// Runs a find_ok test. /// /// \tparam TestProgram Either model::test_program or lazy_test_program. template< class TestProgram > static void check_find_ok(void) { const model::test_case test_case("main", model::metadata_builder().build()); const TestProgram test_program( "mock", fs::path("non-existent"), fs::path("."), "suite-name", model::metadata_builder().build(), model::test_cases_map_builder().add(test_case).build()); const model::test_case& found_test_case = test_program.find("main"); ATF_REQUIRE_EQ(test_case, found_test_case); } ATF_TEST_CASE_WITHOUT_HEAD(find__ok); ATF_TEST_CASE_BODY(find__ok) { check_find_ok< model::test_program >(); } ATF_TEST_CASE_WITHOUT_HEAD(derived__find__ok); ATF_TEST_CASE_BODY(derived__find__ok) { check_find_ok< lazy_test_program >(); } /// Runs a find_missing test. /// /// \tparam TestProgram Either model::test_program or lazy_test_program. template< class TestProgram > static void check_find_missing(void) { const TestProgram test_program( "mock", fs::path("non-existent"), fs::path("."), "suite-name", model::metadata_builder().build(), model::test_cases_map_builder().add("main").build()); ATF_REQUIRE_THROW_RE(model::not_found_error, "case.*abc.*program.*non-existent", test_program.find("abc")); } ATF_TEST_CASE_WITHOUT_HEAD(find__missing); ATF_TEST_CASE_BODY(find__missing) { check_find_missing< model::test_program >(); } ATF_TEST_CASE_WITHOUT_HEAD(derived__find__missing); ATF_TEST_CASE_BODY(derived__find__missing) { check_find_missing< lazy_test_program >(); } /// Runs a metadata_inheritance test. /// /// \tparam TestProgram Either model::test_program or lazy_test_program. template< class TestProgram > static void check_metadata_inheritance(void) { const model::test_cases_map test_cases = model::test_cases_map_builder() .add("inherit-all") .add("inherit-some", model::metadata_builder() .set_description("Overriden description") .build()) .add("inherit-none", model::metadata_builder() .add_allowed_architecture("overriden-arch") .add_allowed_platform("overriden-platform") .set_description("Overriden description") .build()) .build(); const model::metadata metadata = model::metadata_builder() .add_allowed_architecture("base-arch") .set_description("Base description") .build(); const TestProgram test_program( "plain", fs::path("non-existent"), fs::path("."), "suite-name", metadata, test_cases); { const model::metadata exp_metadata = model::metadata_builder() .add_allowed_architecture("base-arch") .set_description("Base description") .build(); ATF_REQUIRE_EQ(exp_metadata, test_program.find("inherit-all").get_metadata()); } { const model::metadata exp_metadata = model::metadata_builder() .add_allowed_architecture("base-arch") .set_description("Overriden description") .build(); ATF_REQUIRE_EQ(exp_metadata, test_program.find("inherit-some").get_metadata()); } { const model::metadata exp_metadata = model::metadata_builder() .add_allowed_architecture("overriden-arch") .add_allowed_platform("overriden-platform") .set_description("Overriden description") .build(); ATF_REQUIRE_EQ(exp_metadata, test_program.find("inherit-none").get_metadata()); } } ATF_TEST_CASE_WITHOUT_HEAD(metadata_inheritance); ATF_TEST_CASE_BODY(metadata_inheritance) { check_metadata_inheritance< model::test_program >(); } ATF_TEST_CASE_WITHOUT_HEAD(derived__metadata_inheritance); ATF_TEST_CASE_BODY(derived__metadata_inheritance) { check_metadata_inheritance< lazy_test_program >(); } /// Runs a operators_eq_and_ne__copy test. /// /// \tparam TestProgram Either model::test_program or lazy_test_program. template< class TestProgram > static void check_operators_eq_and_ne__copy(void) { const TestProgram tp1( "plain", fs::path("non-existent"), fs::path("."), "suite-name", model::metadata_builder().build(), model::test_cases_map()); const TestProgram tp2 = tp1; ATF_REQUIRE( tp1 == tp2); ATF_REQUIRE(!(tp1 != tp2)); } ATF_TEST_CASE_WITHOUT_HEAD(operators_eq_and_ne__copy); ATF_TEST_CASE_BODY(operators_eq_and_ne__copy) { check_operators_eq_and_ne__copy< model::test_program >(); } ATF_TEST_CASE_WITHOUT_HEAD(derived__operators_eq_and_ne__copy); ATF_TEST_CASE_BODY(derived__operators_eq_and_ne__copy) { check_operators_eq_and_ne__copy< lazy_test_program >(); } /// Runs a operators_eq_and_ne__not_copy test. /// /// \tparam TestProgram Either model::test_program or lazy_test_program. template< class TestProgram > static void check_operators_eq_and_ne__not_copy(void) { const std::string base_interface("plain"); const fs::path base_relative_path("the/test/program"); const fs::path base_root("/the/root"); const std::string base_test_suite("suite-name"); const model::metadata base_metadata = model::metadata_builder() .add_custom("foo", "bar") .build(); const model::test_cases_map base_tcs = model::test_cases_map_builder() .add("main", model::metadata_builder() .add_custom("second", "baz") .build()) .build(); const TestProgram base_tp( base_interface, base_relative_path, base_root, base_test_suite, base_metadata, base_tcs); // Construct with all same values. { const model::test_cases_map other_tcs = model::test_cases_map_builder() .add("main", model::metadata_builder() .add_custom("second", "baz") .build()) .build(); const TestProgram other_tp( base_interface, base_relative_path, base_root, base_test_suite, base_metadata, other_tcs); ATF_REQUIRE( base_tp == other_tp); ATF_REQUIRE(!(base_tp != other_tp)); } // Construct with same final metadata values but using a different // intermediate representation. The original test program has one property // in the base test program definition and another in the test case; here, // we put both definitions explicitly in the test case. { const model::test_cases_map other_tcs = model::test_cases_map_builder() .add("main", model::metadata_builder() .add_custom("foo", "bar") .add_custom("second", "baz") .build()) .build(); const TestProgram other_tp( base_interface, base_relative_path, base_root, base_test_suite, base_metadata, other_tcs); ATF_REQUIRE( base_tp == other_tp); ATF_REQUIRE(!(base_tp != other_tp)); } // Different interface. { const TestProgram other_tp( "atf", base_relative_path, base_root, base_test_suite, base_metadata, base_tcs); ATF_REQUIRE(!(base_tp == other_tp)); ATF_REQUIRE( base_tp != other_tp); } // Different relative path. { const TestProgram other_tp( base_interface, fs::path("a/b/c"), base_root, base_test_suite, base_metadata, base_tcs); ATF_REQUIRE(!(base_tp == other_tp)); ATF_REQUIRE( base_tp != other_tp); } // Different root. { const TestProgram other_tp( base_interface, base_relative_path, fs::path("."), base_test_suite, base_metadata, base_tcs); ATF_REQUIRE(!(base_tp == other_tp)); ATF_REQUIRE( base_tp != other_tp); } // Different test suite. { const TestProgram other_tp( base_interface, base_relative_path, base_root, "different-suite", base_metadata, base_tcs); ATF_REQUIRE(!(base_tp == other_tp)); ATF_REQUIRE( base_tp != other_tp); } // Different metadata. { const TestProgram other_tp( base_interface, base_relative_path, base_root, base_test_suite, model::metadata_builder().build(), base_tcs); ATF_REQUIRE(!(base_tp == other_tp)); ATF_REQUIRE( base_tp != other_tp); } // Different test cases. { const model::test_cases_map other_tcs = model::test_cases_map_builder() .add("foo").build(); const TestProgram other_tp( base_interface, base_relative_path, base_root, base_test_suite, base_metadata, other_tcs); ATF_REQUIRE(!(base_tp == other_tp)); ATF_REQUIRE( base_tp != other_tp); } } ATF_TEST_CASE_WITHOUT_HEAD(operators_eq_and_ne__not_copy); ATF_TEST_CASE_BODY(operators_eq_and_ne__not_copy) { check_operators_eq_and_ne__not_copy< model::test_program >(); } ATF_TEST_CASE_WITHOUT_HEAD(derived__operators_eq_and_ne__not_copy); ATF_TEST_CASE_BODY(derived__operators_eq_and_ne__not_copy) { check_operators_eq_and_ne__not_copy< lazy_test_program >(); } /// Runs a operator_lt test. /// /// \tparam TestProgram Either model::test_program or lazy_test_program. template< class TestProgram > static void check_operator_lt(void) { const TestProgram tp1( "plain", fs::path("a/b/c"), fs::path("/foo/bar"), "suite-name", model::metadata_builder().build(), model::test_cases_map()); const TestProgram tp2( "atf", fs::path("c"), fs::path("/foo/bar"), "suite-name", model::metadata_builder().build(), model::test_cases_map()); const TestProgram tp3( "plain", fs::path("a/b/c"), fs::path("/abc"), "suite-name", model::metadata_builder().build(), model::test_cases_map()); ATF_REQUIRE(!(tp1 < tp1)); ATF_REQUIRE( tp1 < tp2); ATF_REQUIRE(!(tp2 < tp1)); ATF_REQUIRE(!(tp1 < tp3)); ATF_REQUIRE( tp3 < tp1); // And now, test the actual reason why we want to have an < overload by // attempting to put the various programs in a set. std::set< TestProgram > programs; programs.insert(tp1); programs.insert(tp2); programs.insert(tp3); } ATF_TEST_CASE_WITHOUT_HEAD(operator_lt); ATF_TEST_CASE_BODY(operator_lt) { check_operator_lt< model::test_program >(); } ATF_TEST_CASE_WITHOUT_HEAD(derived__operator_lt); ATF_TEST_CASE_BODY(derived__operator_lt) { check_operator_lt< lazy_test_program >(); } /// Runs a output__no_test_cases test. /// /// \tparam TestProgram Either model::test_program or lazy_test_program. template< class TestProgram > static void check_output__no_test_cases(void) { TestProgram tp( "plain", fs::path("binary/path"), fs::path("/the/root"), "suite-name", model::metadata_builder().add_allowed_architecture("a").build(), model::test_cases_map()); std::ostringstream str; str << tp; ATF_REQUIRE_EQ( "test_program{interface='plain', binary='binary/path', " "root='/the/root', test_suite='suite-name', " "metadata=metadata{allowed_architectures='a', allowed_platforms='', " - "description='', has_cleanup='false', is_exclusive='false', " + "description='', execenv='', execenv_jail_params='', " + "has_cleanup='false', is_exclusive='false', " "required_configs='', required_disk_space='0', required_files='', " "required_memory='0', " "required_programs='', required_user='', timeout='300'}, " "test_cases=map()}", str.str()); } ATF_TEST_CASE_WITHOUT_HEAD(output__no_test_cases); ATF_TEST_CASE_BODY(output__no_test_cases) { check_output__no_test_cases< model::test_program >(); } ATF_TEST_CASE_WITHOUT_HEAD(derived__output__no_test_cases); ATF_TEST_CASE_BODY(derived__output__no_test_cases) { check_output__no_test_cases< lazy_test_program >(); } /// Runs a output__some_test_cases test. /// /// \tparam TestProgram Either model::test_program or lazy_test_program. template< class TestProgram > static void check_output__some_test_cases(void) { const model::test_cases_map test_cases = model::test_cases_map_builder() .add("the-name", model::metadata_builder() .add_allowed_platform("foo") .add_custom("bar", "baz") .build()) .add("another-name") .build(); const TestProgram tp = TestProgram( "plain", fs::path("binary/path"), fs::path("/the/root"), "suite-name", model::metadata_builder().add_allowed_architecture("a").build(), test_cases); std::ostringstream str; str << tp; ATF_REQUIRE_EQ( "test_program{interface='plain', binary='binary/path', " "root='/the/root', test_suite='suite-name', " "metadata=metadata{allowed_architectures='a', allowed_platforms='', " - "description='', has_cleanup='false', is_exclusive='false', " + "description='', execenv='', execenv_jail_params='', " + "has_cleanup='false', is_exclusive='false', " "required_configs='', required_disk_space='0', required_files='', " "required_memory='0', " "required_programs='', required_user='', timeout='300'}, " "test_cases=map(" "another-name=test_case{name='another-name', " "metadata=metadata{allowed_architectures='a', allowed_platforms='', " - "description='', has_cleanup='false', is_exclusive='false', " + "description='', execenv='', execenv_jail_params='', " + "has_cleanup='false', is_exclusive='false', " "required_configs='', required_disk_space='0', required_files='', " "required_memory='0', " "required_programs='', required_user='', timeout='300'}}, " "the-name=test_case{name='the-name', " "metadata=metadata{allowed_architectures='a', allowed_platforms='foo', " - "custom.bar='baz', description='', has_cleanup='false', " - "is_exclusive='false', " + "custom.bar='baz', description='', execenv='', execenv_jail_params='', " + "has_cleanup='false', is_exclusive='false', " "required_configs='', required_disk_space='0', required_files='', " "required_memory='0', " "required_programs='', required_user='', timeout='300'}})}", str.str()); } ATF_TEST_CASE_WITHOUT_HEAD(output__some_test_cases); ATF_TEST_CASE_BODY(output__some_test_cases) { check_output__some_test_cases< model::test_program >(); } ATF_TEST_CASE_WITHOUT_HEAD(derived__output__some_test_cases); ATF_TEST_CASE_BODY(derived__output__some_test_cases) { check_output__some_test_cases< lazy_test_program >(); } ATF_TEST_CASE_WITHOUT_HEAD(builder__defaults); ATF_TEST_CASE_BODY(builder__defaults) { const model::test_program expected( "mock", fs::path("non-existent"), fs::path("."), "suite-name", model::metadata_builder().build(), model::test_cases_map()); const model::test_program built = model::test_program_builder( "mock", fs::path("non-existent"), fs::path("."), "suite-name") .build(); ATF_REQUIRE_EQ(built, expected); } ATF_TEST_CASE_WITHOUT_HEAD(builder__overrides); ATF_TEST_CASE_BODY(builder__overrides) { const model::metadata md = model::metadata_builder() .add_custom("foo", "bar") .build(); const model::test_cases_map tcs = model::test_cases_map_builder() .add("first") .add("second", md) .build(); const model::test_program expected( "mock", fs::path("binary"), fs::path("root"), "suite-name", md, tcs); const model::test_program built = model::test_program_builder( "mock", fs::path("binary"), fs::path("root"), "suite-name") .add_test_case("first") .add_test_case("second", md) .set_metadata(md) .build(); ATF_REQUIRE_EQ(built, expected); } ATF_TEST_CASE_WITHOUT_HEAD(builder__ptr); ATF_TEST_CASE_BODY(builder__ptr) { const model::test_program expected( "mock", fs::path("non-existent"), fs::path("."), "suite-name", model::metadata_builder().build(), model::test_cases_map()); const model::test_program_ptr built = model::test_program_builder( "mock", fs::path("non-existent"), fs::path("."), "suite-name") .build_ptr(); ATF_REQUIRE_EQ(*built, expected); } ATF_INIT_TEST_CASES(tcs) { ATF_ADD_TEST_CASE(tcs, ctor_and_getters); ATF_ADD_TEST_CASE(tcs, find__ok); ATF_ADD_TEST_CASE(tcs, find__missing); ATF_ADD_TEST_CASE(tcs, metadata_inheritance); ATF_ADD_TEST_CASE(tcs, operators_eq_and_ne__copy); ATF_ADD_TEST_CASE(tcs, operators_eq_and_ne__not_copy); ATF_ADD_TEST_CASE(tcs, operator_lt); ATF_ADD_TEST_CASE(tcs, output__no_test_cases); ATF_ADD_TEST_CASE(tcs, output__some_test_cases); ATF_ADD_TEST_CASE(tcs, derived__ctor_and_getters); ATF_ADD_TEST_CASE(tcs, derived__find__ok); ATF_ADD_TEST_CASE(tcs, derived__find__missing); ATF_ADD_TEST_CASE(tcs, derived__metadata_inheritance); ATF_ADD_TEST_CASE(tcs, derived__operators_eq_and_ne__copy); ATF_ADD_TEST_CASE(tcs, derived__operators_eq_and_ne__not_copy); ATF_ADD_TEST_CASE(tcs, derived__operator_lt); ATF_ADD_TEST_CASE(tcs, derived__output__no_test_cases); ATF_ADD_TEST_CASE(tcs, derived__output__some_test_cases); ATF_ADD_TEST_CASE(tcs, builder__defaults); ATF_ADD_TEST_CASE(tcs, builder__overrides); ATF_ADD_TEST_CASE(tcs, builder__ptr); } diff --git a/contrib/kyua/main.cpp b/contrib/kyua/os/freebsd/execenv_jail.cpp similarity index 62% copy from contrib/kyua/main.cpp copy to contrib/kyua/os/freebsd/execenv_jail.cpp index 4344248f89db..04f44a412760 100644 --- a/contrib/kyua/main.cpp +++ b/contrib/kyua/os/freebsd/execenv_jail.cpp @@ -1,50 +1,78 @@ -// Copyright 2010 The Kyua Authors. +// Copyright 2024 The Kyua Authors. // All rights reserved. // // Redistribution and use in source and binary forms, with or without // modification, are permitted provided that the following conditions are // met: // // * Redistributions of source code must retain the above copyright // notice, this list of conditions and the following disclaimer. // * Redistributions in binary form must reproduce the above copyright // notice, this list of conditions and the following disclaimer in the // documentation and/or other materials provided with the distribution. // * Neither the name of Google Inc. nor the names of its contributors // may be used to endorse or promote products derived from this software // without specific prior written permission. // // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS // "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT // LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR // A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT // OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT // LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -#include "cli/main.hpp" - - -/// Program entry point. -/// -/// The whole purpose of this extremely-simple function is to delegate execution -/// to an internal module that does not contain a proper ::main() function. -/// This is to allow unit-testing of the internal code. -/// -/// \param argc The number of arguments passed on the command line. -/// \param argv NULL-terminated array containing the command line arguments. -/// -/// \return 0 on success, some other integer on error. -/// -/// \throw std::exception This throws any uncaught exception. Such exceptions -/// are bugs, but we let them propagate so that the runtime will abort and -/// dump core. -int -main(const int argc, const char* const* const argv) +#include "os/freebsd/execenv_jail.hpp" + +#include "model/metadata.hpp" +#include "model/test_case.hpp" +#include "os/freebsd/utils/jail.hpp" +#include "utils/fs/path.hpp" + + +namespace freebsd { + + +bool execenv_jail_supported = true; + + +static utils::jail jail = utils::jail(); + + +void +execenv_jail::init() const +{ + auto test_case = _test_program.find(_test_case_name); + + jail.create( + jail.make_name(_test_program.absolute_path(), _test_case_name), + test_case.get_metadata().execenv_jail_params() + ); +} + + +void +execenv_jail::cleanup() const +{ + jail.remove( + jail.make_name(_test_program.absolute_path(), _test_case_name) + ); +} + + +void +execenv_jail::exec(const args_vector& args) const { - return cli::main(argc, argv); + jail.exec( + jail.make_name(_test_program.absolute_path(), _test_case_name), + _test_program.absolute_path(), + args + ); } + + +} // namespace freebsd diff --git a/contrib/kyua/main.cpp b/contrib/kyua/os/freebsd/execenv_jail.hpp similarity index 64% copy from contrib/kyua/main.cpp copy to contrib/kyua/os/freebsd/execenv_jail.hpp index 4344248f89db..e6d2c2e42497 100644 --- a/contrib/kyua/main.cpp +++ b/contrib/kyua/os/freebsd/execenv_jail.hpp @@ -1,50 +1,65 @@ -// Copyright 2010 The Kyua Authors. +// Copyright 2024 The Kyua Authors. // All rights reserved. // // Redistribution and use in source and binary forms, with or without // modification, are permitted provided that the following conditions are // met: // // * Redistributions of source code must retain the above copyright // notice, this list of conditions and the following disclaimer. // * Redistributions in binary form must reproduce the above copyright // notice, this list of conditions and the following disclaimer in the // documentation and/or other materials provided with the distribution. // * Neither the name of Google Inc. nor the names of its contributors // may be used to endorse or promote products derived from this software // without specific prior written permission. // // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS // "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT // LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR // A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT // OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT // LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -#include "cli/main.hpp" - - -/// Program entry point. -/// -/// The whole purpose of this extremely-simple function is to delegate execution -/// to an internal module that does not contain a proper ::main() function. -/// This is to allow unit-testing of the internal code. -/// -/// \param argc The number of arguments passed on the command line. -/// \param argv NULL-terminated array containing the command line arguments. -/// -/// \return 0 on success, some other integer on error. -/// -/// \throw std::exception This throws any uncaught exception. Such exceptions -/// are bugs, but we let them propagate so that the runtime will abort and -/// dump core. -int -main(const int argc, const char* const* const argv) -{ - return cli::main(argc, argv); -} +/// \file os/freebsd/execenv_jail.hpp +/// FreeBSD jail execution environment. + +#if !defined(FREEBSD_EXECENV_JAIL_HPP) +#define FREEBSD_EXECENV_JAIL_HPP + +#include "engine/execenv/execenv.hpp" + +#include "utils/process/operations_fwd.hpp" + +namespace execenv = engine::execenv; + +using utils::process::args_vector; + + +namespace freebsd { + + +extern bool execenv_jail_supported; + + +class execenv_jail : public execenv::interface { +public: + execenv_jail(const model::test_program& test_program, + const std::string& test_case_name) : + execenv::interface(test_program, test_case_name) + {} + + void init() const; + void cleanup() const; + void exec(const args_vector& args) const UTILS_NORETURN; +}; + + +} // namespace freebsd + +#endif // !defined(FREEBSD_EXECENV_JAIL_HPP) diff --git a/contrib/kyua/main.cpp b/contrib/kyua/os/freebsd/execenv_jail_manager.cpp similarity index 63% copy from contrib/kyua/main.cpp copy to contrib/kyua/os/freebsd/execenv_jail_manager.cpp index 4344248f89db..18673f6b0faa 100644 --- a/contrib/kyua/main.cpp +++ b/contrib/kyua/os/freebsd/execenv_jail_manager.cpp @@ -1,50 +1,63 @@ -// Copyright 2010 The Kyua Authors. +// Copyright 2024 The Kyua Authors. // All rights reserved. // // Redistribution and use in source and binary forms, with or without // modification, are permitted provided that the following conditions are // met: // // * Redistributions of source code must retain the above copyright // notice, this list of conditions and the following disclaimer. // * Redistributions in binary form must reproduce the above copyright // notice, this list of conditions and the following disclaimer in the // documentation and/or other materials provided with the distribution. // * Neither the name of Google Inc. nor the names of its contributors // may be used to endorse or promote products derived from this software // without specific prior written permission. // // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS // "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT // LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR // A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT // OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT // LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -#include "cli/main.hpp" - - -/// Program entry point. -/// -/// The whole purpose of this extremely-simple function is to delegate execution -/// to an internal module that does not contain a proper ::main() function. -/// This is to allow unit-testing of the internal code. -/// -/// \param argc The number of arguments passed on the command line. -/// \param argv NULL-terminated array containing the command line arguments. -/// -/// \return 0 on success, some other integer on error. -/// -/// \throw std::exception This throws any uncaught exception. Such exceptions -/// are bugs, but we let them propagate so that the runtime will abort and -/// dump core. -int -main(const int argc, const char* const* const argv) +#include "os/freebsd/execenv_jail_manager.hpp" + +#include "model/metadata.hpp" +#include "model/test_case.hpp" +#include "os/freebsd/execenv_jail.hpp" + +static const std::string execenv_name = "jail"; + +const std::string& +freebsd::execenv_jail_manager::name() const +{ + return execenv_name; +} + + +bool +freebsd::execenv_jail_manager::is_supported() const +{ + return freebsd::execenv_jail_supported; +} + + +std::unique_ptr< execenv::interface > +freebsd::execenv_jail_manager::probe( + const model::test_program& test_program, + const std::string& test_case_name) const { - return cli::main(argc, argv); + auto test_case = test_program.find(test_case_name); + if (test_case.get_metadata().execenv() != execenv_name) + return nullptr; + + return std::unique_ptr< execenv::interface >( + new freebsd::execenv_jail(test_program, test_case_name) + ); } diff --git a/contrib/kyua/main.cpp b/contrib/kyua/os/freebsd/execenv_jail_manager.hpp similarity index 65% copy from contrib/kyua/main.cpp copy to contrib/kyua/os/freebsd/execenv_jail_manager.hpp index 4344248f89db..eee9da9ed7d0 100644 --- a/contrib/kyua/main.cpp +++ b/contrib/kyua/os/freebsd/execenv_jail_manager.hpp @@ -1,50 +1,54 @@ -// Copyright 2010 The Kyua Authors. +// Copyright 2024 The Kyua Authors. // All rights reserved. // // Redistribution and use in source and binary forms, with or without // modification, are permitted provided that the following conditions are // met: // // * Redistributions of source code must retain the above copyright // notice, this list of conditions and the following disclaimer. // * Redistributions in binary form must reproduce the above copyright // notice, this list of conditions and the following disclaimer in the // documentation and/or other materials provided with the distribution. // * Neither the name of Google Inc. nor the names of its contributors // may be used to endorse or promote products derived from this software // without specific prior written permission. // // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS // "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT // LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR // A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT // OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT // LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -#include "cli/main.hpp" - - -/// Program entry point. -/// -/// The whole purpose of this extremely-simple function is to delegate execution -/// to an internal module that does not contain a proper ::main() function. -/// This is to allow unit-testing of the internal code. -/// -/// \param argc The number of arguments passed on the command line. -/// \param argv NULL-terminated array containing the command line arguments. -/// -/// \return 0 on success, some other integer on error. -/// -/// \throw std::exception This throws any uncaught exception. Such exceptions -/// are bugs, but we let them propagate so that the runtime will abort and -/// dump core. -int -main(const int argc, const char* const* const argv) -{ - return cli::main(argc, argv); -} +/// \file os/freebsd/execenv_jail_manager.hpp +/// FreeBSD jail execution environment manager. + +#if !defined(FREEBSD_EXECENV_JAIL_MANAGER_HPP) +#define FREEBSD_EXECENV_JAIL_MANAGER_HPP + +#include "engine/execenv/execenv.hpp" + +namespace execenv = engine::execenv; + +namespace freebsd { + + +class execenv_jail_manager : public execenv::manager { +public: + const std::string& name() const; + bool is_supported() const; + std::unique_ptr< execenv::interface > probe( + const model::test_program& test_program, + const std::string& test_case_name) const; +}; + + +} // namespace freebsd + +#endif // !defined(FREEBSD_EXECENV_JAIL_MANAGER_HPP) diff --git a/contrib/kyua/main.cpp b/contrib/kyua/os/freebsd/execenv_jail_stub.cpp similarity index 66% copy from contrib/kyua/main.cpp copy to contrib/kyua/os/freebsd/execenv_jail_stub.cpp index 4344248f89db..9425618e2b5a 100644 --- a/contrib/kyua/main.cpp +++ b/contrib/kyua/os/freebsd/execenv_jail_stub.cpp @@ -1,50 +1,75 @@ -// Copyright 2010 The Kyua Authors. +// Copyright 2024 The Kyua Authors. // All rights reserved. // // Redistribution and use in source and binary forms, with or without // modification, are permitted provided that the following conditions are // met: // // * Redistributions of source code must retain the above copyright // notice, this list of conditions and the following disclaimer. // * Redistributions in binary form must reproduce the above copyright // notice, this list of conditions and the following disclaimer in the // documentation and/or other materials provided with the distribution. // * Neither the name of Google Inc. nor the names of its contributors // may be used to endorse or promote products derived from this software // without specific prior written permission. // // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS // "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT // LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR // A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT // OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT // LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -#include "cli/main.hpp" - - -/// Program entry point. -/// -/// The whole purpose of this extremely-simple function is to delegate execution -/// to an internal module that does not contain a proper ::main() function. -/// This is to allow unit-testing of the internal code. -/// -/// \param argc The number of arguments passed on the command line. -/// \param argv NULL-terminated array containing the command line arguments. -/// -/// \return 0 on success, some other integer on error. -/// -/// \throw std::exception This throws any uncaught exception. Such exceptions -/// are bugs, but we let them propagate so that the runtime will abort and -/// dump core. -int -main(const int argc, const char* const* const argv) +#include "os/freebsd/execenv_jail.hpp" + +#include + +#include "utils/process/operations_fwd.hpp" + +using utils::process::args_vector; + + +static inline void requires_freebsd(void) UTILS_NORETURN; + +static inline void +requires_freebsd(void) +{ + std::cerr << "execenv=\"jail\" requires FreeBSD with jail feature.\n"; + std::exit(EXIT_FAILURE); +} + + +namespace freebsd { + + +bool execenv_jail_supported = false; + + +void +execenv_jail::init() const +{ + requires_freebsd(); +} + + +void +execenv_jail::cleanup() const { - return cli::main(argc, argv); + requires_freebsd(); } + + +void +execenv_jail::exec(const args_vector&) const +{ + requires_freebsd(); +} + + +} // namespace freebsd diff --git a/contrib/kyua/main.cpp b/contrib/kyua/os/freebsd/main.cpp similarity index 82% copy from contrib/kyua/main.cpp copy to contrib/kyua/os/freebsd/main.cpp index 4344248f89db..13e5dcf0e023 100644 --- a/contrib/kyua/main.cpp +++ b/contrib/kyua/os/freebsd/main.cpp @@ -1,50 +1,54 @@ -// Copyright 2010 The Kyua Authors. +// Copyright 2024 The Kyua Authors. // All rights reserved. // // Redistribution and use in source and binary forms, with or without // modification, are permitted provided that the following conditions are // met: // // * Redistributions of source code must retain the above copyright // notice, this list of conditions and the following disclaimer. // * Redistributions in binary form must reproduce the above copyright // notice, this list of conditions and the following disclaimer in the // documentation and/or other materials provided with the distribution. // * Neither the name of Google Inc. nor the names of its contributors // may be used to endorse or promote products derived from this software // without specific prior written permission. // // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS // "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT // LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR // A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT // OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT // LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -#include "cli/main.hpp" +#include "os/freebsd/main.hpp" +#include "engine/execenv/execenv.hpp" +#include "os/freebsd/execenv_jail_manager.hpp" -/// Program entry point. -/// -/// The whole purpose of this extremely-simple function is to delegate execution -/// to an internal module that does not contain a proper ::main() function. -/// This is to allow unit-testing of the internal code. +namespace execenv = engine::execenv; + +/// FreeBSD related features initialization. /// /// \param argc The number of arguments passed on the command line. /// \param argv NULL-terminated array containing the command line arguments. /// /// \return 0 on success, some other integer on error. /// /// \throw std::exception This throws any uncaught exception. Such exceptions /// are bugs, but we let them propagate so that the runtime will abort and /// dump core. int -main(const int argc, const char* const* const argv) +freebsd::main(const int, const char* const* const) { - return cli::main(argc, argv); + execenv::register_execenv( + std::shared_ptr< execenv::manager >(new freebsd::execenv_jail_manager()) + ); + + return 0; } diff --git a/contrib/kyua/main.cpp b/contrib/kyua/os/freebsd/main.hpp similarity index 65% copy from contrib/kyua/main.cpp copy to contrib/kyua/os/freebsd/main.hpp index 4344248f89db..0581483e37e0 100644 --- a/contrib/kyua/main.cpp +++ b/contrib/kyua/os/freebsd/main.hpp @@ -1,50 +1,41 @@ -// Copyright 2010 The Kyua Authors. +// Copyright 2024 The Kyua Authors. // All rights reserved. // // Redistribution and use in source and binary forms, with or without // modification, are permitted provided that the following conditions are // met: // // * Redistributions of source code must retain the above copyright // notice, this list of conditions and the following disclaimer. // * Redistributions in binary form must reproduce the above copyright // notice, this list of conditions and the following disclaimer in the // documentation and/or other materials provided with the distribution. // * Neither the name of Google Inc. nor the names of its contributors // may be used to endorse or promote products derived from this software // without specific prior written permission. // // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS // "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT // LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR // A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT // OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT // LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -#include "cli/main.hpp" +/// \file os/freebsd/main.hpp +/// FreeBSD related features initialization. +#if !defined(FREEBSD_MAIN_HPP) +#define FREEBSD_MAIN_HPP -/// Program entry point. -/// -/// The whole purpose of this extremely-simple function is to delegate execution -/// to an internal module that does not contain a proper ::main() function. -/// This is to allow unit-testing of the internal code. -/// -/// \param argc The number of arguments passed on the command line. -/// \param argv NULL-terminated array containing the command line arguments. -/// -/// \return 0 on success, some other integer on error. -/// -/// \throw std::exception This throws any uncaught exception. Such exceptions -/// are bugs, but we let them propagate so that the runtime will abort and -/// dump core. -int -main(const int argc, const char* const* const argv) -{ - return cli::main(argc, argv); -} +namespace freebsd { + +int main(const int argc, const char* const* const argv); + +} // namespace freebsd + +#endif // !defined(FREEBSD_MAIN_HPP) diff --git a/contrib/kyua/os/freebsd/utils/jail.cpp b/contrib/kyua/os/freebsd/utils/jail.cpp new file mode 100644 index 000000000000..b39761f28e51 --- /dev/null +++ b/contrib/kyua/os/freebsd/utils/jail.cpp @@ -0,0 +1,306 @@ +// Copyright 2024 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "os/freebsd/utils/jail.hpp" + +extern "C" { +#include +#include +#include + +// FreeBSD sysctl facility +#include + +// FreeBSD Jail syscalls +#include +#include + +// FreeBSD Jail library +#include +} + +#include +#include +#include + +#include "model/metadata.hpp" +#include "model/test_case.hpp" +#include "model/test_program.hpp" +#include "utils/fs/path.hpp" +#include "utils/process/child.ipp" +#include "utils/format/macros.hpp" +#include "utils/process/operations.hpp" +#include "utils/process/status.hpp" + +namespace process = utils::process; +namespace fs = utils::fs; + +using utils::process::args_vector; +using utils::process::child; + + +static const size_t jail_name_max_len = MAXHOSTNAMELEN - 1; +static const char* jail_name_prefix = "kyua"; + + +/// Functor to run a program. +class run { + /// Program binary absolute path. + const utils::fs::path& _program; + + /// Program arguments. + const args_vector& _args; + +public: + /// Constructor. + /// + /// \param program Program binary absolute path. + /// \param args Program arguments. + run( + const utils::fs::path& program, + const args_vector& args) : + _program(program), + _args(args) + { + } + + /// Body of the subprocess. + void + operator()(void) + { + process::exec(_program, _args); + } +}; + + +namespace freebsd { +namespace utils { + + +std::vector< std::string > +jail::parse_params_string(const std::string& str) +{ + std::vector< std::string > params; + std::string p; + char quote = 0; + + std::istringstream iss(str); + while (iss >> p) { + if (p.front() == '"' || p.front() == '\'') { + quote = p.front(); + p.erase(p.begin()); + if (p.find(quote) == std::string::npos) { + std::string rest; + std::getline(iss, rest, quote); + p += rest; + iss.ignore(); + } + if (p.back() == quote) + p.erase(p.end() - 1); + } + params.push_back(p); + } + + return params; +} + + +/// Constructs a jail name based on program and test case. +/// +/// The formula is "kyua" + + "_" + . +/// All non-alphanumeric chars are replaced with "_". +/// +/// If a resulting string exceeds maximum allowed length of a jail name, +/// then it's shortened from the left side keeping the "kyua" prefix. +/// +/// \param program The test program. +/// \param test_case_name Name of the test case. +/// +/// \return A jail name string. +std::string +jail::make_name(const fs::path& program, + const std::string& test_case_name) +{ + std::string name = std::regex_replace( + program.str() + "_" + test_case_name, + std::regex(R"([^A-Za-z0-9_])"), + "_"); + + const std::string::size_type limit = + jail_name_max_len - strlen(jail_name_prefix); + if (name.length() > limit) + name.erase(0, name.length() - limit); + + return jail_name_prefix + name; +} + + +/// Create a jail with a given name and params string. +/// +/// A new jail will always be 'persist', thus the caller is expected to remove +/// the jail eventually via remove(). +/// +/// It's expected to be run in a subprocess. +/// +/// \param jail_name Name of a new jail. +/// \param jail_params String of jail parameters. +void +jail::create(const std::string& jail_name, + const std::string& jail_params) +{ + args_vector av; + + // creation flag + av.push_back("-qc"); + + // jail name + av.push_back("name=" + jail_name); + + // determine maximum allowed children.max + const char* const oid = "security.jail.children.max"; + int max; + size_t len = sizeof(max); + if (::sysctlbyname(oid, &max, &len, NULL, 0) != 0) { + std::cerr << "sysctlbyname(" << oid << ") errors: " + << strerror(errno) << ".\n"; + std::exit(EXIT_FAILURE); + } + if (len < sizeof(max)) { + std::cerr << "sysctlbyname(" << oid << ") provides less " + "data (" << len << ") than expected (" << sizeof(max) << ").\n"; + std::exit(EXIT_FAILURE); + } + if (max < 0) { + std::cerr << "sysctlbyname(" << oid << ") yields " + "abnormal " << max << ".\n"; + std::exit(EXIT_FAILURE); + } + if (max > 0) + max--; // a child jail must have less than parent's children.max + av.push_back("children.max=" + std::to_string(max)); + + // test defined jail params + const std::vector< std::string > params = parse_params_string(jail_params); + for (const std::string& p : params) + av.push_back(p); + + // it must be persist + av.push_back("persist"); + + // invoke jail + std::auto_ptr< process::child > child = child::fork_capture( + run(fs::path("/usr/sbin/jail"), av)); + process::status status = child->wait(); + + // expect success + if (status.exited() && status.exitstatus() == EXIT_SUCCESS) + return; + + // otherwise, let us know what jail thinks and fail fast + std::cerr << child->output().rdbuf(); + std::exit(EXIT_FAILURE); +} + + +/// Executes an external binary in a jail and replaces the current process. +/// +/// \param jail_name Name of the jail to run within. +/// \param program The test program binary absolute path. +/// \param args The arguments to pass to the binary, without the program name. +void +jail::exec(const std::string& jail_name, + const fs::path& program, + const args_vector& args) throw() +{ + // get work dir prepared by kyua + char cwd[PATH_MAX]; + if (::getcwd(cwd, sizeof(cwd)) == NULL) { + std::cerr << "jail::exec: getcwd() errors: " + << strerror(errno) << ".\n"; + std::exit(EXIT_FAILURE); + } + + // get jail id by its name + int jid = ::jail_getid(jail_name.c_str()); + if (jid == -1) { + std::cerr << "jail::exec: jail_getid() errors: " + << strerror(errno) << ": " << jail_errmsg << ".\n"; + std::exit(EXIT_FAILURE); + } + + // attach to the jail + if (::jail_attach(jid) == -1) { + std::cerr << "jail::exec: jail_attach() errors: " + << strerror(errno) << ".\n"; + std::exit(EXIT_FAILURE); + } + + // set back the expected work dir + if (::chdir(cwd) == -1) { + std::cerr << "jail::exec: chdir() errors: " + << strerror(errno) << ".\n"; + std::exit(EXIT_FAILURE); + } + + process::exec(program, args); +} + + +/// Removes a jail with a given name. +/// +/// It's expected to be run in a subprocess. +/// +/// \param jail_name Name of a jail to remove. +void +jail::remove(const std::string& jail_name) +{ + args_vector av; + + // removal flag + av.push_back("-r"); + + // jail name + av.push_back(jail_name); + + // invoke jail + std::auto_ptr< process::child > child = child::fork_capture( + run(fs::path("/usr/sbin/jail"), av)); + process::status status = child->wait(); + + // expect success + if (status.exited() && status.exitstatus() == EXIT_SUCCESS) + std::exit(EXIT_SUCCESS); + + // otherwise, let us know what jail thinks and fail fast + std::cerr << child->output().rdbuf(); + std::exit(EXIT_FAILURE); +} + + +} // namespace utils +} // namespace freebsd diff --git a/contrib/kyua/main.cpp b/contrib/kyua/os/freebsd/utils/jail.hpp similarity index 61% copy from contrib/kyua/main.cpp copy to contrib/kyua/os/freebsd/utils/jail.hpp index 4344248f89db..5b972155cd25 100644 --- a/contrib/kyua/main.cpp +++ b/contrib/kyua/os/freebsd/utils/jail.hpp @@ -1,50 +1,64 @@ -// Copyright 2010 The Kyua Authors. +// Copyright 2024 The Kyua Authors. // All rights reserved. // // Redistribution and use in source and binary forms, with or without // modification, are permitted provided that the following conditions are // met: // // * Redistributions of source code must retain the above copyright // notice, this list of conditions and the following disclaimer. // * Redistributions in binary form must reproduce the above copyright // notice, this list of conditions and the following disclaimer in the // documentation and/or other materials provided with the distribution. // * Neither the name of Google Inc. nor the names of its contributors // may be used to endorse or promote products derived from this software // without specific prior written permission. // // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS // "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT // LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR // A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT // OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT // LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -#include "cli/main.hpp" - - -/// Program entry point. -/// -/// The whole purpose of this extremely-simple function is to delegate execution -/// to an internal module that does not contain a proper ::main() function. -/// This is to allow unit-testing of the internal code. -/// -/// \param argc The number of arguments passed on the command line. -/// \param argv NULL-terminated array containing the command line arguments. -/// -/// \return 0 on success, some other integer on error. -/// -/// \throw std::exception This throws any uncaught exception. Such exceptions -/// are bugs, but we let them propagate so that the runtime will abort and -/// dump core. -int -main(const int argc, const char* const* const argv) -{ - return cli::main(argc, argv); -} +/// \file os/freebsd/utils/jail.hpp +/// FreeBSD jail utilities. + +#if !defined(FREEBSD_UTILS_JAIL_HPP) +#define FREEBSD_UTILS_JAIL_HPP + +#include "utils/defs.hpp" +#include "utils/fs/path_fwd.hpp" +#include "utils/process/operations_fwd.hpp" + +namespace fs = utils::fs; + +using utils::process::args_vector; + +namespace freebsd { +namespace utils { + + +class jail { +public: + std::vector< std::string > parse_params_string(const std::string& str); + std::string make_name(const fs::path& program, + const std::string& test_case_name); + void create(const std::string& jail_name, + const std::string& jail_params); + void exec(const std::string& jail_name, + const fs::path& program, + const args_vector& args) throw() UTILS_NORETURN; + void remove(const std::string& jail_name); +}; + + +} // namespace utils +} // namespace freebsd + +#endif // !defined(FREEBSD_UTILS_JAIL_HPP) diff --git a/contrib/kyua/utils/config/nodes.ipp b/contrib/kyua/utils/config/nodes.ipp index 9e0a1228cccd..0ec3832cc690 100644 --- a/contrib/kyua/utils/config/nodes.ipp +++ b/contrib/kyua/utils/config/nodes.ipp @@ -1,408 +1,413 @@ // Copyright 2012 The Kyua Authors. // All rights reserved. // // Redistribution and use in source and binary forms, with or without // modification, are permitted provided that the following conditions are // met: // // * Redistributions of source code must retain the above copyright // notice, this list of conditions and the following disclaimer. // * Redistributions in binary form must reproduce the above copyright // notice, this list of conditions and the following disclaimer in the // documentation and/or other materials provided with the distribution. // * Neither the name of Google Inc. nor the names of its contributors // may be used to endorse or promote products derived from this software // without specific prior written permission. // // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS // "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT // LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR // A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT // OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT // LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #include "utils/config/nodes.hpp" #if !defined(UTILS_CONFIG_NODES_IPP) #define UTILS_CONFIG_NODES_IPP #include #include #include "utils/config/exceptions.hpp" #include "utils/defs.hpp" #include "utils/format/macros.hpp" #include "utils/optional.ipp" #include "utils/text/exceptions.hpp" #include "utils/text/operations.ipp" #include "utils/sanity.hpp" namespace utils { namespace config { namespace detail { /// Type of the new_node() family of functions. typedef base_node* (*new_node_hook)(void); /// Creates a new leaf node of a given type. /// /// \tparam NodeType The type of the leaf node to create. /// /// \return A pointer to the newly-created node. template< class NodeType > base_node* new_node(void) { return new NodeType(); } /// Internal node of the tree. /// /// This abstract base class provides the mechanism to implement both static and /// dynamic nodes. Ideally, the implementation would be split in subclasses and /// this class would not include the knowledge of whether the node is dynamic or /// not. However, because the static/dynamic difference depends on the leaf /// types, we need to declare template functions and these cannot be virtual. class inner_node : public base_node { /// Whether the node is dynamic or not. bool _dynamic; protected: /// Type to represent the collection of children of this node. /// /// Note that these are one-level keys. They cannot contain dots, and thus /// is why we use a string rather than a tree_key. typedef std::map< std::string, base_node* > children_map; /// Mapping of keys to values that are descendants of this node. children_map _children; void copy_into(inner_node*) const; void combine_into(const tree_key&, const base_node*, inner_node*) const; private: void combine_children_into(const tree_key&, const children_map&, const children_map&, inner_node*) const; public: inner_node(const bool); virtual ~inner_node(void) = 0; const base_node* lookup_ro(const tree_key&, const tree_key::size_type) const; leaf_node* lookup_rw(const tree_key&, const tree_key::size_type, new_node_hook); void all_properties(properties_map&, const tree_key&) const; }; /// Static internal node of the tree. /// /// The direct children of this node must be pre-defined by calls to define(). /// Attempts to traverse this node and resolve a key that is not a pre-defined /// children will result in an "unknown key" error. class static_inner_node : public config::detail::inner_node { public: static_inner_node(void); virtual base_node* deep_copy(void) const; virtual base_node* combine(const tree_key&, const base_node*) const; void define(const tree_key&, const tree_key::size_type, new_node_hook); }; /// Dynamic internal node of the tree. /// /// The children of this node need not be pre-defined. Attempts to traverse /// this node and resolve a key will result in such key being created. Any /// intermediate non-existent nodes of the traversal will be created as dynamic /// inner nodes as well. class dynamic_inner_node : public config::detail::inner_node { public: virtual base_node* deep_copy(void) const; virtual base_node* combine(const tree_key&, const base_node*) const; dynamic_inner_node(void); }; } // namespace detail } // namespace config /// Constructor for a node with an undefined value. /// /// This should only be called by the tree's define() method as a way to /// register a node as known but undefined. The node will then serve as a /// placeholder for future values. template< typename ValueType > config::typed_leaf_node< ValueType >::typed_leaf_node(void) : _value(none) { } /// Checks whether the node has been set by the user. /// /// Nodes of the tree are predefined by the caller to specify the valid /// types of the leaves. Such predefinition results in the creation of /// nodes within the tree, but these nodes have not yet been set. /// Traversing these nodes is invalid and should result in an "unknown key" /// error. /// /// \return True if a value has been set in the node. template< typename ValueType > bool config::typed_leaf_node< ValueType >::is_set(void) const { return static_cast< bool >(_value); } /// Gets the value stored in the node. /// /// \pre The node must have a value. /// /// \return The value in the node. template< typename ValueType > const typename config::typed_leaf_node< ValueType >::value_type& config::typed_leaf_node< ValueType >::value(void) const { PRE(is_set()); return _value.get(); } /// Gets the read-write value stored in the node. /// /// \pre The node must have a value. /// /// \return The value in the node. template< typename ValueType > typename config::typed_leaf_node< ValueType >::value_type& config::typed_leaf_node< ValueType >::value(void) { PRE(is_set()); return _value.get(); } /// Sets the value of the node. /// /// \param value_ The new value to set the node to. /// /// \throw value_error If the value is invalid, according to validate(). template< typename ValueType > void config::typed_leaf_node< ValueType >::set(const value_type& value_) { validate(value_); _value = optional< value_type >(value_); } /// Checks a given value for validity. /// /// This is called internally by the node right before updating the recorded /// value. This method can be redefined by subclasses. /// /// \throw value_error If the value is not valid. template< typename ValueType > void config::typed_leaf_node< ValueType >::validate( const value_type& /* new_value */) const { } /// Sets the value of the node from a raw string representation. /// /// \param raw_value The value to set the node to. /// /// \throw value_error If the value is invalid. template< typename ValueType > void config::native_leaf_node< ValueType >::set_string(const std::string& raw_value) { try { typed_leaf_node< ValueType >::set(text::to_type< ValueType >( raw_value)); } catch (const text::value_error& e) { throw config::value_error(F("Failed to convert string value '%s' to " "the node's type") % raw_value); } } /// Converts the contents of the node to a string. /// /// \pre The node must have a value. /// /// \return A string representation of the value held by the node. template< typename ValueType > std::string config::native_leaf_node< ValueType >::to_string(void) const { PRE(typed_leaf_node< ValueType >::is_set()); return F("%s") % typed_leaf_node< ValueType >::value(); } /// Constructor for a node with an undefined value. /// /// This should only be called by the tree's define() method as a way to /// register a node as known but undefined. The node will then serve as a /// placeholder for future values. template< typename ValueType > config::base_set_node< ValueType >::base_set_node(void) : _value(none) { } /// Checks whether the node has been set. /// /// Remember that a node can exist before holding a value (i.e. when the node /// has been defined as "known" but not yet set by the user). This function /// checks whether the node laready holds a value. /// /// \return True if a value has been set in the node. template< typename ValueType > bool config::base_set_node< ValueType >::is_set(void) const { return static_cast< bool >(_value); } /// Gets the value stored in the node. /// /// \pre The node must have a value. /// /// \return The value in the node. template< typename ValueType > const typename config::base_set_node< ValueType >::value_type& config::base_set_node< ValueType >::value(void) const { PRE(is_set()); return _value.get(); } /// Gets the read-write value stored in the node. /// /// \pre The node must have a value. /// /// \return The value in the node. template< typename ValueType > typename config::base_set_node< ValueType >::value_type& config::base_set_node< ValueType >::value(void) { PRE(is_set()); return _value.get(); } /// Sets the value of the node. /// /// \param value_ The new value to set the node to. /// /// \throw value_error If the value is invalid, according to validate(). template< typename ValueType > void config::base_set_node< ValueType >::set(const value_type& value_) { validate(value_); _value = optional< value_type >(value_); } /// Sets the value of the node from a raw string representation. /// /// \param raw_value The value to set the node to. /// /// \throw value_error If the value is invalid. template< typename ValueType > void config::base_set_node< ValueType >::set_string(const std::string& raw_value) { std::set< ValueType > new_value; const std::vector< std::string > words = text::split(raw_value, ' '); for (std::vector< std::string >::const_iterator iter = words.begin(); iter != words.end(); ++iter) { if (!(*iter).empty()) new_value.insert(parse_one(*iter)); } set(new_value); } /// Converts the contents of the node to a string. /// /// \pre The node must have a value. /// /// \return A string representation of the value held by the node. template< typename ValueType > std::string config::base_set_node< ValueType >::to_string(void) const { PRE(is_set()); return text::join(_value.get(), " "); } /// Pushes the node's value onto the Lua stack. template< typename ValueType > void config::base_set_node< ValueType >::push_lua(lutok::state& /* state */) const { UNREACHABLE; } /// Sets the value of the node from an entry in the Lua stack. /// /// \throw value_error If the value in state(value_index) cannot be /// processed by this node. template< typename ValueType > void config::base_set_node< ValueType >::set_lua( - lutok::state& /* state */, - const int /* value_index */) + lutok::state& state, + const int value_index) { + if (state.is_string(value_index)) { + set_string(state.to_string(value_index)); + return; + } + UNREACHABLE; } /// Checks a given value for validity. /// /// This is called internally by the node right before updating the recorded /// value. This method can be redefined by subclasses. /// /// \throw value_error If the value is not valid. template< typename ValueType > void config::base_set_node< ValueType >::validate( const value_type& /* new_value */) const { } } // namespace utils #endif // !defined(UTILS_CONFIG_NODES_IPP) diff --git a/contrib/kyua/utils/process/executor.cpp b/contrib/kyua/utils/process/executor.cpp index dbdf31268f86..6904f5a8fab0 100644 --- a/contrib/kyua/utils/process/executor.cpp +++ b/contrib/kyua/utils/process/executor.cpp @@ -1,869 +1,911 @@ // Copyright 2015 The Kyua Authors. // All rights reserved. // // Redistribution and use in source and binary forms, with or without // modification, are permitted provided that the following conditions are // met: // // * Redistributions of source code must retain the above copyright // notice, this list of conditions and the following disclaimer. // * Redistributions in binary form must reproduce the above copyright // notice, this list of conditions and the following disclaimer in the // documentation and/or other materials provided with the distribution. // * Neither the name of Google Inc. nor the names of its contributors // may be used to endorse or promote products derived from this software // without specific prior written permission. // // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS // "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT // LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR // A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT // OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT // LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #include "utils/process/executor.ipp" #if defined(HAVE_CONFIG_H) #include "config.h" #endif extern "C" { #include #include #include } #include #include #include #include #include "utils/datetime.hpp" #include "utils/format/macros.hpp" #include "utils/fs/auto_cleaners.hpp" #include "utils/fs/exceptions.hpp" #include "utils/fs/operations.hpp" #include "utils/fs/path.hpp" #include "utils/logging/macros.hpp" #include "utils/logging/operations.hpp" #include "utils/noncopyable.hpp" #include "utils/optional.ipp" #include "utils/passwd.hpp" #include "utils/process/child.ipp" #include "utils/process/deadline_killer.hpp" #include "utils/process/isolation.hpp" #include "utils/process/operations.hpp" #include "utils/process/status.hpp" #include "utils/sanity.hpp" #include "utils/signals/interrupts.hpp" #include "utils/signals/timer.hpp" namespace datetime = utils::datetime; namespace executor = utils::process::executor; namespace fs = utils::fs; namespace logging = utils::logging; namespace passwd = utils::passwd; namespace process = utils::process; namespace signals = utils::signals; using utils::none; using utils::optional; namespace { /// Template for temporary directories created by the executor. static const char* work_directory_template = PACKAGE_TARNAME ".XXXXXX"; /// Mapping of active subprocess PIDs to their execution data. typedef std::map< int, executor::exec_handle > exec_handles_map; } // anonymous namespace /// Basename of the file containing the stdout of the subprocess. const char* utils::process::executor::detail::stdout_name = "stdout.txt"; /// Basename of the file containing the stderr of the subprocess. const char* utils::process::executor::detail::stderr_name = "stderr.txt"; /// Basename of the subdirectory in which the subprocess is actually executed. /// /// This is a subdirectory of the "unique work directory" generated for the /// subprocess so that our code can create control files on disk and not /// get them clobbered by the subprocess's activity. const char* utils::process::executor::detail::work_subdir = "work"; /// Prepares a subprocess to run a user-provided hook in a controlled manner. /// /// \param unprivileged_user User to switch to if not none. /// \param control_directory Path to the subprocess-specific control directory. /// \param work_directory Path to the subprocess-specific work directory. void utils::process::executor::detail::setup_child( const optional< passwd::user > unprivileged_user, const fs::path& control_directory, const fs::path& work_directory) { logging::set_inmemory(); process::isolate_path(unprivileged_user, control_directory); process::isolate_child(unprivileged_user, work_directory); } /// Internal implementation for the exit_handle class. struct utils::process::executor::exec_handle::impl : utils::noncopyable { /// PID of the process being run. int pid; /// Path to the subprocess-specific work directory. fs::path control_directory; /// Path to the subprocess's stdout file. const fs::path stdout_file; /// Path to the subprocess's stderr file. const fs::path stderr_file; /// Start time. datetime::timestamp start_time; /// User the subprocess is running as if different than the current one. const optional< passwd::user > unprivileged_user; /// Timer to kill the subprocess on activation. process::deadline_killer timer; /// Number of owners of the on-disk state. executor::detail::refcnt_t state_owners; /// Constructor. /// /// \param pid_ PID of the forked process. /// \param control_directory_ Path to the subprocess-specific work /// directory. /// \param stdout_file_ Path to the subprocess's stdout file. /// \param stderr_file_ Path to the subprocess's stderr file. /// \param start_time_ Timestamp of when this object was constructed. /// \param timeout Maximum amount of time the subprocess can run for. /// \param unprivileged_user_ User the subprocess is running as if /// different than the current one. /// \param [in,out] state_owners_ Number of owners of the on-disk state. /// For first-time processes, this should be a new counter set to 0; /// for followup processes, this should point to the same counter used /// by the preceding process. impl(const int pid_, const fs::path& control_directory_, const fs::path& stdout_file_, const fs::path& stderr_file_, const datetime::timestamp& start_time_, const datetime::delta& timeout, const optional< passwd::user > unprivileged_user_, executor::detail::refcnt_t state_owners_) : pid(pid_), control_directory(control_directory_), stdout_file(stdout_file_), stderr_file(stderr_file_), start_time(start_time_), unprivileged_user(unprivileged_user_), timer(timeout, pid_), state_owners(state_owners_) { (*state_owners)++; POST(*state_owners > 0); } }; /// Constructor. /// /// \param pimpl Constructed internal implementation. executor::exec_handle::exec_handle(std::shared_ptr< impl > pimpl) : _pimpl(pimpl) { } /// Destructor. executor::exec_handle::~exec_handle(void) { } /// Returns the PID of the process being run. /// /// \return A PID. int executor::exec_handle::pid(void) const { return _pimpl->pid; } /// Returns the path to the subprocess-specific control directory. /// /// This is where the executor may store control files. /// /// \return The path to a directory that exists until cleanup() is called. fs::path executor::exec_handle::control_directory(void) const { return _pimpl->control_directory; } /// Returns the path to the subprocess-specific work directory. /// /// This is guaranteed to be clear of files created by the executor. /// /// \return The path to a directory that exists until cleanup() is called. fs::path executor::exec_handle::work_directory(void) const { return _pimpl->control_directory / detail::work_subdir; } /// Returns the path to the subprocess's stdout file. /// /// \return The path to a file that exists until cleanup() is called. const fs::path& executor::exec_handle::stdout_file(void) const { return _pimpl->stdout_file; } /// Returns the path to the subprocess's stderr file. /// /// \return The path to a file that exists until cleanup() is called. const fs::path& executor::exec_handle::stderr_file(void) const { return _pimpl->stderr_file; } /// Internal implementation for the exit_handle class. struct utils::process::executor::exit_handle::impl : utils::noncopyable { /// Original PID of the terminated subprocess. /// /// Note that this PID is no longer valid and cannot be used on system /// tables! const int original_pid; /// Termination status of the subprocess, or none if it timed out. const optional< process::status > status; /// The user the process ran as, if different than the current one. const optional< passwd::user > unprivileged_user; /// Timestamp of when the subprocess was spawned. const datetime::timestamp start_time; /// Timestamp of when wait() or wait_any() returned this object. const datetime::timestamp end_time; /// Path to the subprocess-specific work directory. const fs::path control_directory; /// Path to the subprocess's stdout file. const fs::path stdout_file; /// Path to the subprocess's stderr file. const fs::path stderr_file; /// Number of owners of the on-disk state. /// /// This will be 1 if this exit_handle is the last holder of the on-disk /// state, in which case cleanup() invocations will wipe the disk state. /// For all other cases, this will hold a higher value. detail::refcnt_t state_owners; /// Mutable pointer to the corresponding executor state. /// /// This object references a member of the executor_handle that yielded this /// exit_handle instance. We need this direct access to clean up after /// ourselves when the handle is destroyed. exec_handles_map& all_exec_handles; /// Whether the subprocess state has been cleaned yet or not. /// /// Used to keep track of explicit calls to the public cleanup(). bool cleaned; /// Constructor. /// /// \param original_pid_ Original PID of the terminated subprocess. /// \param status_ Termination status of the subprocess, or none if /// timed out. /// \param unprivileged_user_ The user the process ran as, if different than /// the current one. /// \param start_time_ Timestamp of when the subprocess was spawned. /// \param end_time_ Timestamp of when wait() or wait_any() returned this /// object. /// \param control_directory_ Path to the subprocess-specific work /// directory. /// \param stdout_file_ Path to the subprocess's stdout file. /// \param stderr_file_ Path to the subprocess's stderr file. /// \param [in,out] state_owners_ Number of owners of the on-disk state. /// \param [in,out] all_exec_handles_ Global object keeping track of all /// active executions for an executor. This is a pointer to a member of /// the executor_handle object. impl(const int original_pid_, const optional< process::status > status_, const optional< passwd::user > unprivileged_user_, const datetime::timestamp& start_time_, const datetime::timestamp& end_time_, const fs::path& control_directory_, const fs::path& stdout_file_, const fs::path& stderr_file_, detail::refcnt_t state_owners_, exec_handles_map& all_exec_handles_) : original_pid(original_pid_), status(status_), unprivileged_user(unprivileged_user_), start_time(start_time_), end_time(end_time_), control_directory(control_directory_), stdout_file(stdout_file_), stderr_file(stderr_file_), state_owners(state_owners_), all_exec_handles(all_exec_handles_), cleaned(false) { } /// Destructor. ~impl(void) { if (!cleaned) { LW(F("Implicitly cleaning up exit_handle for exec_handle %s; " "ignoring errors!") % original_pid); try { cleanup(); } catch (const std::runtime_error& error) { LE(F("Subprocess cleanup failed: %s") % error.what()); } } } /// Cleans up the subprocess on-disk state. /// /// \throw engine::error If the cleanup fails, especially due to the /// inability to remove the work directory. void cleanup(void) { PRE(*state_owners > 0); if (*state_owners == 1) { LI(F("Cleaning up exit_handle for exec_handle %s") % original_pid); fs::rm_r(control_directory); } else { LI(F("Not cleaning up exit_handle for exec_handle %s; " "%s owners left") % original_pid % (*state_owners - 1)); } // We must decrease our reference only after we have successfully // cleaned up the control directory. Otherwise, the rm_r call would // throw an exception, which would in turn invoke the implicit cleanup // from the destructor, which would make us crash due to an invalid // reference count. (*state_owners)--; // Marking this object as clean here, even if we did not do actually the // cleaning above, is fine (albeit a bit confusing). Note that "another // owner" refers to a handle for a different PID, so that handle will be // the one issuing the cleanup. all_exec_handles.erase(original_pid); cleaned = true; } }; /// Constructor. /// /// \param pimpl Constructed internal implementation. executor::exit_handle::exit_handle(std::shared_ptr< impl > pimpl) : _pimpl(pimpl) { } /// Destructor. executor::exit_handle::~exit_handle(void) { } /// Cleans up the subprocess status. /// /// This function should be called explicitly as it provides the means to /// control any exceptions raised during cleanup. Do not rely on the destructor /// to clean things up. /// /// \throw engine::error If the cleanup fails, especially due to the inability /// to remove the work directory. void executor::exit_handle::cleanup(void) { PRE(!_pimpl->cleaned); _pimpl->cleanup(); POST(_pimpl->cleaned); } /// Gets the current number of owners of the on-disk data. /// /// \return A shared reference counter. Even though this function is marked as /// const, the return value is intentionally mutable because we need to update /// reference counts from different but related processes. This is why this /// method is not public. std::shared_ptr< std::size_t > executor::exit_handle::state_owners(void) const { return _pimpl->state_owners; } /// Returns the original PID corresponding to the terminated subprocess. /// /// \return An exec_handle. int executor::exit_handle::original_pid(void) const { return _pimpl->original_pid; } /// Returns the process termination status of the subprocess. /// /// \return A process termination status, or none if the subprocess timed out. const optional< process::status >& executor::exit_handle::status(void) const { return _pimpl->status; } /// Returns the user the process ran as if different than the current one. /// /// \return None if the credentials of the process were the same as the current /// one, or else a user. const optional< passwd::user >& executor::exit_handle::unprivileged_user(void) const { return _pimpl->unprivileged_user; } /// Returns the timestamp of when the subprocess was spawned. /// /// \return A timestamp. const datetime::timestamp& executor::exit_handle::start_time(void) const { return _pimpl->start_time; } /// Returns the timestamp of when wait() or wait_any() returned this object. /// /// \return A timestamp. const datetime::timestamp& executor::exit_handle::end_time(void) const { return _pimpl->end_time; } /// Returns the path to the subprocess-specific control directory. /// /// This is where the executor may store control files. /// /// \return The path to a directory that exists until cleanup() is called. fs::path executor::exit_handle::control_directory(void) const { return _pimpl->control_directory; } /// Returns the path to the subprocess-specific work directory. /// /// This is guaranteed to be clear of files created by the executor. /// /// \return The path to a directory that exists until cleanup() is called. fs::path executor::exit_handle::work_directory(void) const { return _pimpl->control_directory / detail::work_subdir; } /// Returns the path to the subprocess's stdout file. /// /// \return The path to a file that exists until cleanup() is called. const fs::path& executor::exit_handle::stdout_file(void) const { return _pimpl->stdout_file; } /// Returns the path to the subprocess's stderr file. /// /// \return The path to a file that exists until cleanup() is called. const fs::path& executor::exit_handle::stderr_file(void) const { return _pimpl->stderr_file; } /// Internal implementation for the executor_handle. /// /// Because the executor is a singleton, these essentially is a container for /// global variables. struct utils::process::executor::executor_handle::impl : utils::noncopyable { /// Numeric counter of executed subprocesses. /// /// This is used to generate a unique identifier for each subprocess as an /// easy mechanism to discern their unique work directories. size_t last_subprocess; /// Interrupts handler. std::auto_ptr< signals::interrupts_handler > interrupts_handler; /// Root work directory for all executed subprocesses. std::auto_ptr< fs::auto_directory > root_work_directory; /// Mapping of PIDs to the data required at run time. exec_handles_map all_exec_handles; /// Whether the executor state has been cleaned yet or not. /// /// Used to keep track of explicit calls to the public cleanup(). bool cleaned; /// Constructor. impl(void) : last_subprocess(0), interrupts_handler(new signals::interrupts_handler()), root_work_directory(new fs::auto_directory( fs::auto_directory::mkdtemp_public(work_directory_template))), cleaned(false) { } /// Destructor. ~impl(void) { if (!cleaned) { LW("Implicitly cleaning up executor; ignoring errors!"); try { cleanup(); cleaned = true; } catch (const std::runtime_error& error) { LE(F("Executor global cleanup failed: %s") % error.what()); } } } /// Cleans up the executor state. void cleanup(void) { PRE(!cleaned); for (exec_handles_map::const_iterator iter = all_exec_handles.begin(); iter != all_exec_handles.end(); ++iter) { const int& pid = (*iter).first; const exec_handle& data = (*iter).second; process::terminate_group(pid); int status; if (::waitpid(pid, &status, 0) == -1) { // Should not happen. LW(F("Failed to wait for PID %s") % pid); } try { fs::rm_r(data.control_directory()); } catch (const fs::error& e) { LE(F("Failed to clean up subprocess work directory %s: %s") % data.control_directory() % e.what()); } } all_exec_handles.clear(); try { // The following only causes the work directory to be deleted, not // any of its contents, so we expect this to always succeed. This // *should* be sufficient because, in the loop above, we have // individually wiped the subdirectories of any still-unclean // subprocesses. root_work_directory->cleanup(); } catch (const fs::error& e) { LE(F("Failed to clean up executor work directory %s: %s; this is " "an internal error") % root_work_directory->directory() % e.what()); } root_work_directory.reset(NULL); interrupts_handler->unprogram(); interrupts_handler.reset(NULL); } /// Common code to run after any of the wait calls. /// /// \param original_pid The PID of the terminated subprocess. /// \param status The exit status of the terminated subprocess. /// /// \return A pointer to an object describing the waited-for subprocess. executor::exit_handle post_wait(const int original_pid, const process::status& status) { PRE(original_pid == status.dead_pid()); LI(F("Waited for subprocess with exec_handle %s") % original_pid); process::terminate_group(status.dead_pid()); const exec_handles_map::iterator iter = all_exec_handles.find( original_pid); exec_handle& data = (*iter).second; data._pimpl->timer.unprogram(); // It is tempting to assert here (and old code did) that, if the timer // has fired, the process has been forcibly killed by us. This is not // always the case though: for short-lived processes and with very short // timeouts (think 1ms), it is possible for scheduling decisions to // allow the subprocess to finish while at the same time cause the timer // to fire. So we do not assert this any longer and just rely on the // timer expiration to check if the process timed out or not. If the // process did finish but the timer expired... oh well, we do not detect // this correctly but we don't care because this should not really // happen. if (!fs::exists(data.stdout_file())) { std::ofstream new_stdout(data.stdout_file().c_str()); } if (!fs::exists(data.stderr_file())) { std::ofstream new_stderr(data.stderr_file().c_str()); } return exit_handle(std::shared_ptr< exit_handle::impl >( new exit_handle::impl( data.pid(), data._pimpl->timer.fired() ? none : utils::make_optional(status), data._pimpl->unprivileged_user, data._pimpl->start_time, datetime::timestamp::now(), data.control_directory(), data.stdout_file(), data.stderr_file(), data._pimpl->state_owners, all_exec_handles))); } + + executor::exit_handle + reap(const pid_t original_pid) + { + const exec_handles_map::iterator iter = all_exec_handles.find( + original_pid); + exec_handle& data = (*iter).second; + data._pimpl->timer.unprogram(); + + if (!fs::exists(data.stdout_file())) { + std::ofstream new_stdout(data.stdout_file().c_str()); + } + if (!fs::exists(data.stderr_file())) { + std::ofstream new_stderr(data.stderr_file().c_str()); + } + + return exit_handle(std::shared_ptr< exit_handle::impl >( + new exit_handle::impl( + data.pid(), + none, + data._pimpl->unprivileged_user, + data._pimpl->start_time, datetime::timestamp::now(), + data.control_directory(), + data.stdout_file(), + data.stderr_file(), + data._pimpl->state_owners, + all_exec_handles))); + } }; /// Constructor. executor::executor_handle::executor_handle(void) throw() : _pimpl(new impl()) { } /// Destructor. executor::executor_handle::~executor_handle(void) { } /// Queries the path to the root of the work directory for all subprocesses. /// /// \return A path. const fs::path& executor::executor_handle::root_work_directory(void) const { return _pimpl->root_work_directory->directory(); } /// Cleans up the executor state. /// /// This function should be called explicitly as it provides the means to /// control any exceptions raised during cleanup. Do not rely on the destructor /// to clean things up. /// /// \throw engine::error If there are problems cleaning up the executor. void executor::executor_handle::cleanup(void) { PRE(!_pimpl->cleaned); _pimpl->cleanup(); _pimpl->cleaned = true; } /// Initializes the executor. /// /// \pre This function can only be called if there is no other executor_handle /// object alive. /// /// \return A handle to the operations of the executor. executor::executor_handle executor::setup(void) { return executor_handle(); } /// Pre-helper for the spawn() method. /// /// \return The created control directory for the subprocess. fs::path executor::executor_handle::spawn_pre(void) { signals::check_interrupt(); ++_pimpl->last_subprocess; const fs::path control_directory = _pimpl->root_work_directory->directory() / (F("%s") % _pimpl->last_subprocess); fs::mkdir_p(control_directory / detail::work_subdir, 0755); return control_directory; } /// Post-helper for the spawn() method. /// /// \param control_directory Control directory as returned by spawn_pre(). /// \param stdout_file Path to the subprocess' stdout. /// \param stderr_file Path to the subprocess' stderr. /// \param timeout Maximum amount of time the subprocess can run for. /// \param unprivileged_user If not none, user to switch to before execution. /// \param child The process created by spawn(). /// /// \return The execution handle of the started subprocess. executor::exec_handle executor::executor_handle::spawn_post( const fs::path& control_directory, const fs::path& stdout_file, const fs::path& stderr_file, const datetime::delta& timeout, const optional< passwd::user > unprivileged_user, std::auto_ptr< process::child > child) { const exec_handle handle(std::shared_ptr< exec_handle::impl >( new exec_handle::impl( child->pid(), control_directory, stdout_file, stderr_file, datetime::timestamp::now(), timeout, unprivileged_user, detail::refcnt_t(new detail::refcnt_t::element_type(0))))); INV_MSG(_pimpl->all_exec_handles.find(handle.pid()) == _pimpl->all_exec_handles.end(), F("PID %s already in all_exec_handles; not properly cleaned " "up or reused too fast") % handle.pid());; _pimpl->all_exec_handles.insert(exec_handles_map::value_type( handle.pid(), handle)); LI(F("Spawned subprocess with exec_handle %s") % handle.pid()); return handle; } /// Pre-helper for the spawn_followup() method. void executor::executor_handle::spawn_followup_pre(void) { signals::check_interrupt(); } /// Post-helper for the spawn_followup() method. /// /// \param base Exit handle of the subprocess to use as context. /// \param timeout Maximum amount of time the subprocess can run for. /// \param child The process created by spawn_followup(). /// /// \return The execution handle of the started subprocess. executor::exec_handle executor::executor_handle::spawn_followup_post( const exit_handle& base, const datetime::delta& timeout, std::auto_ptr< process::child > child) { INV(*base.state_owners() > 0); const exec_handle handle(std::shared_ptr< exec_handle::impl >( new exec_handle::impl( child->pid(), base.control_directory(), base.stdout_file(), base.stderr_file(), datetime::timestamp::now(), timeout, base.unprivileged_user(), base.state_owners()))); INV_MSG(_pimpl->all_exec_handles.find(handle.pid()) == _pimpl->all_exec_handles.end(), F("PID %s already in all_exec_handles; not properly cleaned " "up or reused too fast") % handle.pid());; _pimpl->all_exec_handles.insert(exec_handles_map::value_type( handle.pid(), handle)); LI(F("Spawned subprocess with exec_handle %s") % handle.pid()); return handle; } /// Waits for completion of any forked process. /// /// \param exec_handle The handle of the process to wait for. /// /// \return A pointer to an object describing the waited-for subprocess. executor::exit_handle executor::executor_handle::wait(const exec_handle exec_handle) { signals::check_interrupt(); const process::status status = process::wait(exec_handle.pid()); return _pimpl->post_wait(exec_handle.pid(), status); } /// Waits for completion of any forked process. /// /// \return A pointer to an object describing the waited-for subprocess. executor::exit_handle executor::executor_handle::wait_any(void) { signals::check_interrupt(); const process::status status = process::wait_any(); return _pimpl->post_wait(status.dead_pid(), status); } +/// Forms exit_handle for the given PID subprocess. +/// +/// Can be used in the cases when we want to do cleanup(s) of a killed test +/// subprocess, but we do not have exit handle as we usually do after normal +/// wait mechanism. +/// +/// \return A pointer to an object describing the subprocess. +executor::exit_handle +executor::executor_handle::reap(const int pid) +{ + return _pimpl->reap(pid); +} + + /// Checks if an interrupt has fired. /// /// Calls to this function should be sprinkled in strategic places through the /// code protected by an interrupts_handler object. /// /// This is just a wrapper over signals::check_interrupt() to avoid leaking this /// dependency to the caller. /// /// \throw signals::interrupted_error If there has been an interrupt. void executor::executor_handle::check_interrupt(void) const { signals::check_interrupt(); } diff --git a/contrib/kyua/utils/process/executor.hpp b/contrib/kyua/utils/process/executor.hpp index 858ad9c815aa..01a17ff8c681 100644 --- a/contrib/kyua/utils/process/executor.hpp +++ b/contrib/kyua/utils/process/executor.hpp @@ -1,231 +1,232 @@ // Copyright 2015 The Kyua Authors. // All rights reserved. // // Redistribution and use in source and binary forms, with or without // modification, are permitted provided that the following conditions are // met: // // * Redistributions of source code must retain the above copyright // notice, this list of conditions and the following disclaimer. // * Redistributions in binary form must reproduce the above copyright // notice, this list of conditions and the following disclaimer in the // documentation and/or other materials provided with the distribution. // * Neither the name of Google Inc. nor the names of its contributors // may be used to endorse or promote products derived from this software // without specific prior written permission. // // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS // "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT // LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR // A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT // OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT // LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. /// \file utils/process/executor.hpp /// Multiprogrammed process executor with isolation guarantees. /// /// This module provides a mechanism to invoke more than one process /// concurrently while at the same time ensuring that each process is run /// in a clean container and in a "safe" work directory that gets cleaned /// up automatically on termination. /// /// The intended workflow for using this module is the following: /// /// 1) Initialize the executor using setup(). Keep the returned object /// around through the lifetime of the next operations. Only one /// instance of the executor can be alive at once. /// 2) Spawn one or more processes with spawn(). On the caller side, keep /// track of any per-process data you may need using the returned /// exec_handle, which is unique among the set of active processes. /// 3) Call wait() or wait_any() to wait for completion of a process started /// in the previous step. Repeat as desired. /// 4) Use the returned exit_handle object by wait() or wait_any() to query /// the status of the terminated process and/or to access any of its /// data files. /// 5) Invoke cleanup() on the exit_handle to wipe any stale data. /// 6) Invoke cleanup() on the object returned by setup(). /// /// It is the responsibility of the caller to ensure that calls to /// spawn() and spawn_followup() are balanced with wait() and wait_any() calls. /// /// Processes executed in this manner have access to two different "unique" /// directories: the first is the "work directory", which is an empty directory /// that acts as the subprocess' work directory; the second is the "control /// directory", which is the location where the in-process code may place files /// that are not clobbered by activities in the work directory. #if !defined(UTILS_PROCESS_EXECUTOR_HPP) #define UTILS_PROCESS_EXECUTOR_HPP #include "utils/process/executor_fwd.hpp" #include #include #include "utils/datetime_fwd.hpp" #include "utils/fs/path_fwd.hpp" #include "utils/optional.hpp" #include "utils/passwd_fwd.hpp" #include "utils/process/child_fwd.hpp" #include "utils/process/status_fwd.hpp" namespace utils { namespace process { namespace executor { namespace detail { extern const char* stdout_name; extern const char* stderr_name; extern const char* work_subdir; /// Shared reference counter. typedef std::shared_ptr< std::size_t > refcnt_t; void setup_child(const utils::optional< utils::passwd::user >, const utils::fs::path&, const utils::fs::path&); } // namespace detail /// Maintenance data held while a subprocess is being executed. /// /// This data structure exists from the moment a subprocess is executed via /// executor::spawn() to when it is cleaned up with exit_handle::cleanup(). /// /// The caller NEED NOT maintain this object alive for the execution of the /// subprocess. However, the PID contained in here can be used to match /// exec_handle objects with corresponding exit_handle objects via their /// original_pid() method. /// /// Objects of this type can be copied around but their implementation is /// shared. The implication of this is that only the last copy of a given exit /// handle will execute the automatic cleanup() on destruction. class exec_handle { struct impl; /// Pointer to internal implementation. std::shared_ptr< impl > _pimpl; friend class executor_handle; exec_handle(std::shared_ptr< impl >); public: ~exec_handle(void); int pid(void) const; utils::fs::path control_directory(void) const; utils::fs::path work_directory(void) const; const utils::fs::path& stdout_file(void) const; const utils::fs::path& stderr_file(void) const; }; /// Container for the data of a process termination. /// /// This handle provides access to the details of the process that terminated /// and serves as the owner of the remaining on-disk files. The caller is /// expected to call cleanup() before destruction to remove the on-disk state. /// /// Objects of this type can be copied around but their implementation is /// shared. The implication of this is that only the last copy of a given exit /// handle will execute the automatic cleanup() on destruction. class exit_handle { struct impl; /// Pointer to internal implementation. std::shared_ptr< impl > _pimpl; friend class executor_handle; exit_handle(std::shared_ptr< impl >); detail::refcnt_t state_owners(void) const; public: ~exit_handle(void); void cleanup(void); int original_pid(void) const; const utils::optional< utils::process::status >& status(void) const; const utils::optional< utils::passwd::user >& unprivileged_user(void) const; const utils::datetime::timestamp& start_time() const; const utils::datetime::timestamp& end_time() const; utils::fs::path control_directory(void) const; utils::fs::path work_directory(void) const; const utils::fs::path& stdout_file(void) const; const utils::fs::path& stderr_file(void) const; }; /// Handler for the livelihood of the executor. /// /// Objects of this type can be copied around (because we do not have move /// semantics...) but their implementation is shared. Only one instance of the /// executor can exist at any point in time. class executor_handle { struct impl; /// Pointer to internal implementation. std::shared_ptr< impl > _pimpl; friend executor_handle setup(void); executor_handle(void) throw(); utils::fs::path spawn_pre(void); exec_handle spawn_post(const utils::fs::path&, const utils::fs::path&, const utils::fs::path&, const utils::datetime::delta&, const utils::optional< utils::passwd::user >, std::auto_ptr< utils::process::child >); void spawn_followup_pre(void); exec_handle spawn_followup_post(const exit_handle&, const utils::datetime::delta&, std::auto_ptr< utils::process::child >); public: ~executor_handle(void); const utils::fs::path& root_work_directory(void) const; void cleanup(void); template< class Hook > exec_handle spawn(Hook, const datetime::delta&, const utils::optional< utils::passwd::user >, const utils::optional< utils::fs::path > = utils::none, const utils::optional< utils::fs::path > = utils::none); template< class Hook > exec_handle spawn_followup(Hook, const exit_handle&, const datetime::delta&); exit_handle wait(const exec_handle); exit_handle wait_any(void); + exit_handle reap(const pid_t); void check_interrupt(void) const; }; executor_handle setup(void); } // namespace executor } // namespace process } // namespace utils #endif // !defined(UTILS_PROCESS_EXECUTOR_HPP) diff --git a/usr.bin/kyua/Makefile b/usr.bin/kyua/Makefile index 953bb46589b1..990a65f2ce56 100644 --- a/usr.bin/kyua/Makefile +++ b/usr.bin/kyua/Makefile @@ -1,212 +1,225 @@ .include KYUA_CONFDIR= /etc/kyua KYUA_DOCDIR= /usr/share/doc/kyua KYUA_EGDIR= /usr/share/examples/kyua KYUA_MISCDIR= /usr/share/kyua/misc KYUA_STOREDIR= /usr/share/kyua/store KYUA_VERSION= 0.13 KYUA_SRCDIR= ${SRCTOP}/contrib/kyua .PATH: ${KYUA_SRCDIR} PACKAGE= tests PROG_CXX= kyua SRCS= main.cpp LIBADD= lutok sqlite3 MAN= kyua-about.1 \ kyua-config.1 \ kyua-db-exec.1 \ kyua-db-migrate.1 \ kyua-debug.1 \ kyua-help.1 \ kyua-list.1 \ kyua-report-html.1 \ kyua-report-junit.1 \ kyua-report.1 \ kyua-test.1 \ kyua.1 \ kyua.conf.5 \ kyuafile.5 CFLAGS+= -I${KYUA_SRCDIR} -I${.CURDIR} CFLAGS+= -I${SRCTOP}/contrib/lutok/include CFLAGS+= -I${SRCTOP}/contrib/sqlite3 # kyua uses auto_ptr CFLAGS+= -Wno-deprecated-declarations CXXSTD= c++11 CFLAGS+= -DHAVE_CONFIG_H # We compile the kyua libraries as part of the main executable as this saves # compile time and we don't install them anyway. CFLAGS+= -DGDB=\"/usr/local/bin/gdb\" \ -DKYUA_ARCHITECTURE=\"${MACHINE_ARCH}\" \ -DKYUA_CONFDIR=\"${KYUA_CONFDIR}\" \ -DKYUA_DOCDIR=\"${KYUA_DOCDIR}\" \ -DKYUA_MISCDIR=\"${KYUA_MISCDIR}\" \ -DKYUA_PLATFORM=\"${MACHINE}\" \ -DKYUA_STOREDIR=\"${KYUA_STOREDIR}\" \ -DPACKAGE=\"kyua\" \ -DPACKAGE_NAME=\"Kyua\" \ -DPACKAGE_VERSION=\"${KYUA_VERSION}\" \ -DVERSION=\"${KYUA_VERSION}\" SRCS+= utils/datetime.cpp \ utils/env.cpp \ utils/memory.cpp \ utils/passwd.cpp \ utils/sanity.cpp \ utils/stacktrace.cpp \ utils/stream.cpp \ utils/units.cpp \ utils/cmdline/base_command.cpp \ utils/cmdline/exceptions.cpp \ utils/cmdline/globals.cpp \ utils/cmdline/options.cpp \ utils/cmdline/parser.cpp \ utils/cmdline/ui.cpp \ utils/cmdline/ui_mock.cpp \ utils/config/exceptions.cpp \ utils/config/keys.cpp \ utils/config/lua_module.cpp \ utils/config/nodes.cpp \ utils/config/parser.cpp \ utils/config/tree.cpp \ utils/format/exceptions.cpp \ utils/format/formatter.cpp \ utils/fs/auto_cleaners.cpp \ utils/fs/directory.cpp \ utils/fs/exceptions.cpp \ utils/fs/lua_module.cpp \ utils/fs/operations.cpp \ utils/fs/path.cpp \ utils/logging/operations.cpp \ utils/process/child.cpp \ utils/process/deadline_killer.cpp \ utils/process/exceptions.cpp \ utils/process/executor.cpp \ utils/process/fdstream.cpp \ utils/process/isolation.cpp \ utils/process/operations.cpp \ utils/process/status.cpp \ utils/process/system.cpp \ utils/process/systembuf.cpp \ utils/signals/exceptions.cpp \ utils/signals/interrupts.cpp \ utils/signals/misc.cpp \ utils/signals/programmer.cpp \ utils/signals/timer.cpp \ utils/sqlite/c_gate.cpp \ utils/sqlite/database.cpp \ utils/sqlite/exceptions.cpp \ utils/sqlite/statement.cpp \ utils/sqlite/transaction.cpp \ utils/text/exceptions.cpp \ utils/text/operations.cpp \ utils/text/regex.cpp \ utils/text/table.cpp \ utils/text/templates.cpp SRCS+= model/context.cpp \ model/exceptions.cpp \ model/metadata.cpp \ model/test_case.cpp \ model/test_program.cpp \ model/test_result.cpp SRCS+= engine/atf.cpp \ engine/atf_list.cpp \ engine/atf_result.cpp \ engine/config.cpp \ engine/exceptions.cpp \ engine/filters.cpp \ engine/kyuafile.cpp \ engine/plain.cpp \ engine/requirements.cpp \ engine/scanner.cpp \ engine/tap.cpp \ engine/tap_parser.cpp \ - engine/scheduler.cpp + engine/scheduler.cpp \ + engine/execenv/execenv.cpp \ + engine/execenv/execenv_host.cpp + +SRCS+= os/freebsd/execenv_jail_manager.cpp \ + os/freebsd/main.cpp SRCS+= store/dbtypes.cpp \ store/exceptions.cpp \ store/layout.cpp \ store/metadata.cpp \ store/migrate.cpp \ store/read_backend.cpp \ store/read_transaction.cpp \ store/write_backend.cpp \ store/write_transaction.cpp SRCS+= drivers/debug_test.cpp \ drivers/list_tests.cpp \ drivers/report_junit.cpp \ drivers/run_tests.cpp \ drivers/scan_results.cpp SRCS+= cli/cmd_about.cpp \ cli/cmd_config.cpp \ cli/cmd_db_exec.cpp \ cli/cmd_db_migrate.cpp \ cli/cmd_debug.cpp \ cli/cmd_help.cpp \ cli/cmd_list.cpp \ cli/cmd_report.cpp \ cli/cmd_report_html.cpp \ cli/cmd_report_junit.cpp \ cli/cmd_test.cpp \ cli/common.cpp \ cli/config.cpp \ cli/main.cpp +.if ${MK_JAIL} == "no" +SRCS+= os/freebsd/execenv_jail_stub.cpp +.else +SRCS+= os/freebsd/execenv_jail.cpp \ + os/freebsd/utils/jail.cpp +LIBADD+= jail +.endif + FILESGROUPS= DOCS MISC STORE .if ${MK_EXAMPLES} != "no" FILESGROUPS+= EXAMPLES .endif # Install a minimal default config that uses the 'tests' user. # The examples config is not appropriate for general use. CONFS= kyua.conf-default CONFSDIR= ${KYUA_CONFDIR} CONFSNAME= kyua.conf CONFSDIRTAGS= package=tests DOCS= AUTHORS CONTRIBUTORS LICENSE DOCSDIR= ${KYUA_DOCDIR} DOCSTAGS= package=tests EXAMPLES= Kyuafile.top kyua.conf EXAMPLESDIR= ${KYUA_EGDIR} EXAMPLESTAGS= package=tests .PATH: ${KYUA_SRCDIR}/examples MISC= context.html index.html report.css test_result.html MISCDIR= ${KYUA_MISCDIR} MISCTAGS= package=tests .PATH: ${KYUA_SRCDIR}/misc STORE= migrate_v1_v2.sql migrate_v2_v3.sql schema_v3.sql STOREDIR= ${KYUA_STOREDIR} STORETAGS= package=tests .PATH: ${KYUA_SRCDIR}/store CLEANFILES+= ${MAN} .PATH: ${KYUA_SRCDIR}/doc .for man in ${MAN} ${man}: ${man}.in sh ${KYUA_SRCDIR}/doc/manbuild.sh \ -v "CONFDIR=${KYUA_CONFDIR}" \ -v "DOCDIR=${KYUA_DOCDIR}" \ -v "EGDIR=${KYUA_EGDIR}" \ -v "MISCDIR=${KYUA_MISCDIR}" \ -v "PACKAGE=kyua" \ -v "STOREDIR=${KYUA_STOREDIR}" \ -v "TESTSDIR=${TESTSBASE}" \ -v "VERSION=${KYUA_VERSION}" \ ${.ALLSRC} ${.TARGET} .endfor .include