diff --git a/contrib/kyua/.cirrus.yml b/contrib/kyua/.cirrus.yml new file mode 100644 index 000000000000..855693b34fa1 --- /dev/null +++ b/contrib/kyua/.cirrus.yml @@ -0,0 +1,25 @@ +env: + ARCH: amd64 + +task: + matrix: + - name: 14.0-RELEASE + freebsd_instance: + image_family: freebsd-14-0 + - name: 14.0-STABLE + freebsd_instance: + image_family: freebsd-14-0-snap + - name: 13.2-RELEASE + freebsd_instance: + image_family: freebsd-13-2 + - name: 13.2-STABLE + freebsd_instance: + image_family: freebsd-13-2-snap + env: + DO: distcheck + install_script: + - sed -i.bak -e 's,pkg+http://pkg.FreeBSD.org/\${ABI}/quarterly,pkg+http://pkg.FreeBSD.org/\${ABI}/latest,' /etc/pkg/FreeBSD.conf + - ASSUME_ALWAYS_YES=yes pkg bootstrap -f + - pkg install -y autoconf automake atf lutok pkgconf sqlite3 + script: + - ./admin/travis-build.sh diff --git a/contrib/kyua/AUTHORS b/contrib/kyua/AUTHORS index ac0998fb937c..c7bd72ce776b 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 +* The FreeBSD Foundation * Google Inc. diff --git a/contrib/kyua/admin/travis-build.sh b/contrib/kyua/admin/travis-build.sh index e69f271c13f1..0ccf1cc41efb 100755 --- a/contrib/kyua/admin/travis-build.sh +++ b/contrib/kyua/admin/travis-build.sh @@ -1,98 +1,98 @@ #! /bin/sh # 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. set -e -x run_autoreconf() { if [ -d /usr/local/share/aclocal ]; then autoreconf -isv -I/usr/local/share/aclocal else autoreconf -isv fi } do_apidocs() { run_autoreconf || return 1 ./configure --with-doxygen || return 1 make check-api-docs } do_distcheck() { run_autoreconf || return 1 ./configure || return 1 sudo sysctl -w "kernel.core_pattern=core.%p" local archflags= [ "${ARCH?}" != i386 ] || archflags=-m32 cat >kyua.conf <>kyua.conf + echo "unprivileged_user = 'nobody'" >>kyua.conf local f= f="${f} CFLAGS='${archflags}'" f="${f} CPPFLAGS='-I/usr/local/include'" f="${f} CXXFLAGS='${archflags}'" f="${f} LDFLAGS='-L/usr/local/lib -Wl,-R/usr/local/lib'" f="${f} PKG_CONFIG_PATH='/usr/local/lib/pkgconfig'" f="${f} KYUA_CONFIG_FILE_FOR_CHECK=$(pwd)/kyua.conf" if [ "${AS_ROOT:-no}" = yes ]; then sudo -H PATH="${PATH}" make distcheck DISTCHECK_CONFIGURE_FLAGS="${f}" else make distcheck DISTCHECK_CONFIGURE_FLAGS="${f}" fi } do_style() { run_autoreconf || return 1 mkdir build cd build ../configure || return 1 make check-style } main() { if [ -z "${DO}" ]; then echo "DO must be defined" 1>&2 exit 1 fi for step in ${DO}; do "do_${DO}" || exit 1 done } main "${@}" diff --git a/contrib/kyua/cli/cmd_report_html.cpp b/contrib/kyua/cli/cmd_report_html.cpp index b2133a8de047..9c99e4348252 100644 --- a/contrib/kyua/cli/cmd_report_html.cpp +++ b/contrib/kyua/cli/cmd_report_html.cpp @@ -1,474 +1,475 @@ // 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 "cli/cmd_report_html.hpp" #include #include #include #include #include #include "cli/common.ipp" #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/layout.hpp" #include "store/read_transaction.hpp" #include "utils/cmdline/options.hpp" #include "utils/cmdline/parser.ipp" #include "utils/cmdline/ui.hpp" #include "utils/datetime.hpp" #include "utils/env.hpp" #include "utils/format/macros.hpp" #include "utils/fs/exceptions.hpp" #include "utils/fs/operations.hpp" #include "utils/fs/path.hpp" #include "utils/optional.ipp" +#include "utils/text/operations.hpp" #include "utils/text/templates.hpp" namespace cmdline = utils::cmdline; namespace config = utils::config; namespace datetime = utils::datetime; namespace fs = utils::fs; namespace layout = store::layout; namespace text = utils::text; using utils::optional; namespace { /// Creates the report's top directory and fails if it exists. /// /// \param directory The directory to create. /// \param force Whether to wipe an existing directory or not. /// /// \throw std::runtime_error If the directory already exists; this is a user /// error that the user must correct. /// \throw fs::error If the directory creation fails for any other reason. static void create_top_directory(const fs::path& directory, const bool force) { if (force) { if (fs::exists(directory)) fs::rm_r(directory); } try { fs::mkdir(directory, 0755); } catch (const fs::system_error& e) { if (e.original_errno() == EEXIST) throw std::runtime_error(F("Output directory '%s' already exists; " "maybe use --force?") % directory); else throw e; } } /// Generates a flat unique filename for a given test case. /// /// \param test_program The test program for which to genereate the name. /// \param test_case_name The test case name. /// /// \return A filename unique within a directory with a trailing HTML extension. static std::string test_case_filename(const model::test_program& test_program, const std::string& test_case_name) { static const char* special_characters = "/:"; std::string name = cli::format_test_case_id(test_program, test_case_name); std::string::size_type pos = name.find_first_of(special_characters); while (pos != std::string::npos) { name.replace(pos, 1, "_"); pos = name.find_first_of(special_characters, pos + 1); } return name + ".html"; } /// Adds a string to string map to the templates. /// /// \param [in,out] templates The templates to add the map to. /// \param props The map to add to the templates. /// \param key_vector Name of the template vector that holds the keys. /// \param value_vector Name of the template vector that holds the values. static void add_map(text::templates_def& templates, const config::properties_map& props, const std::string& key_vector, const std::string& value_vector) { templates.add_vector(key_vector); templates.add_vector(value_vector); for (config::properties_map::const_iterator iter = props.begin(); iter != props.end(); ++iter) { templates.add_to_vector(key_vector, (*iter).first); templates.add_to_vector(value_vector, (*iter).second); } } /// Generates an HTML report. class html_hooks : public drivers::scan_results::base_hooks { /// User interface object where to report progress. cmdline::ui* _ui; /// The top directory in which to create the HTML files. fs::path _directory; /// Collection of result types to include in the report. const cli::result_types& _results_filters; /// The start time of the first test. optional< utils::datetime::timestamp > _start_time; /// The end time of the last test. optional< utils::datetime::timestamp > _end_time; /// The total run time of the tests. Note that we cannot subtract _end_time /// from _start_time to compute this due to parallel execution. utils::datetime::delta _runtime; /// Templates accumulator to generate the index.html file. text::templates_def _summary_templates; /// Mapping of result types to the amount of tests with such result. std::map< model::test_result_type, std::size_t > _types_count; /// Generates a common set of templates for all of our files. /// /// \return A new templates object with common parameters. static text::templates_def common_templates(void) { text::templates_def templates; templates.add_variable("css", "report.css"); return templates; } /// Adds a test case result to the summary. /// /// \param test_program The test program with the test case to be added. /// \param test_case_name Name of the test case. /// \param result The result of the test case. /// \param has_detail If true, the result of the test case has not been /// filtered and therefore there exists a separate file for the test /// with all of its information. void add_to_summary(const model::test_program& test_program, const std::string& test_case_name, const model::test_result& result, const bool has_detail) { ++_types_count[result.type()]; if (!has_detail) return; std::string test_cases_vector; std::string test_cases_file_vector; switch (result.type()) { case model::test_result_broken: test_cases_vector = "broken_test_cases"; test_cases_file_vector = "broken_test_cases_file"; break; case model::test_result_expected_failure: test_cases_vector = "xfail_test_cases"; test_cases_file_vector = "xfail_test_cases_file"; break; case model::test_result_failed: test_cases_vector = "failed_test_cases"; test_cases_file_vector = "failed_test_cases_file"; break; case model::test_result_passed: test_cases_vector = "passed_test_cases"; test_cases_file_vector = "passed_test_cases_file"; break; case model::test_result_skipped: test_cases_vector = "skipped_test_cases"; test_cases_file_vector = "skipped_test_cases_file"; break; } INV(!test_cases_vector.empty()); INV(!test_cases_file_vector.empty()); _summary_templates.add_to_vector( test_cases_vector, cli::format_test_case_id(test_program, test_case_name)); _summary_templates.add_to_vector( test_cases_file_vector, test_case_filename(test_program, test_case_name)); } /// Instantiate a template to generate an HTML file in the output directory. /// /// \param templates The templates to use. /// \param template_name The name of the template. This is automatically /// searched for in the installed directory, so do not provide a path. /// \param output_name The name of the output file. This is a basename to /// be created within the output directory. /// /// \throw text::error If there is any problem applying the templates. void generate(const text::templates_def& templates, const std::string& template_name, const std::string& output_name) const { const fs::path miscdir(utils::getenv_with_default( "KYUA_MISCDIR", KYUA_MISCDIR)); const fs::path template_file = miscdir / template_name; const fs::path output_path(_directory / output_name); _ui->out(F("Generating %s") % output_path); text::instantiate(templates, template_file, output_path); } /// Gets the number of tests with a given result type. /// /// \param type The type to be queried. /// /// \return The number of tests of the given type, or 0 if none have yet /// been registered by add_to_summary(). std::size_t get_count(const model::test_result_type type) const { const std::map< model::test_result_type, std::size_t >::const_iterator iter = _types_count.find(type); if (iter == _types_count.end()) return 0; else return (*iter).second; } public: /// Constructor for the hooks. /// /// \param ui_ User interface object where to report progress. /// \param directory_ The directory in which to create the HTML files. /// \param results_filters_ The result types to include in the report. /// Cannot be empty. html_hooks(cmdline::ui* ui_, const fs::path& directory_, const cli::result_types& results_filters_) : _ui(ui_), _directory(directory_), _results_filters(results_filters_), _summary_templates(common_templates()) { PRE(!results_filters_.empty()); // Keep in sync with add_to_summary(). _summary_templates.add_vector("broken_test_cases"); _summary_templates.add_vector("broken_test_cases_file"); _summary_templates.add_vector("xfail_test_cases"); _summary_templates.add_vector("xfail_test_cases_file"); _summary_templates.add_vector("failed_test_cases"); _summary_templates.add_vector("failed_test_cases_file"); _summary_templates.add_vector("passed_test_cases"); _summary_templates.add_vector("passed_test_cases_file"); _summary_templates.add_vector("skipped_test_cases"); _summary_templates.add_vector("skipped_test_cases_file"); } /// Callback executed when the context is loaded. /// /// \param context The context loaded from the database. void got_context(const model::context& context) { text::templates_def templates = common_templates(); templates.add_variable("cwd", context.cwd().str()); add_map(templates, context.env(), "env_var", "env_var_value"); generate(templates, "context.html", "context.html"); } /// Callback executed when a test results is found. /// /// \param iter Container for the test result's data. void got_result(store::results_iterator& iter) { const model::test_program_ptr test_program = iter.test_program(); const std::string& test_case_name = iter.test_case_name(); const model::test_result result = iter.result(); if (std::find(_results_filters.begin(), _results_filters.end(), result.type()) == _results_filters.end()) { add_to_summary(*test_program, test_case_name, result, false); return; } add_to_summary(*test_program, test_case_name, result, true); if (!_start_time || _start_time.get() > iter.start_time()) _start_time = iter.start_time(); if (!_end_time || _end_time.get() < iter.end_time()) _end_time = iter.end_time(); const datetime::delta duration = iter.end_time() - iter.start_time(); _runtime += duration; text::templates_def templates = common_templates(); templates.add_variable("test_case", cli::format_test_case_id(*test_program, test_case_name)); templates.add_variable("test_program", test_program->absolute_path().str()); templates.add_variable("result", cli::format_result(result)); templates.add_variable("start_time", iter.start_time().to_iso8601_in_utc()); templates.add_variable("end_time", iter.end_time().to_iso8601_in_utc()); templates.add_variable("duration", cli::format_delta(duration)); const model::test_case& test_case = test_program->find(test_case_name); add_map(templates, test_case.get_metadata().to_properties(), "metadata_var", "metadata_value"); { const std::string stdout_text = iter.stdout_contents(); if (!stdout_text.empty()) - templates.add_variable("stdout", stdout_text); + templates.add_variable("stdout", text::escape_xml(stdout_text)); } { const std::string stderr_text = iter.stderr_contents(); if (!stderr_text.empty()) - templates.add_variable("stderr", stderr_text); + templates.add_variable("stderr", text::escape_xml(stderr_text)); } generate(templates, "test_result.html", test_case_filename(*test_program, test_case_name)); } /// Writes the index.html file in the output directory. /// /// This should only be called once all the processing has been done; /// i.e. when the scan_results driver returns. void write_summary(void) { const std::size_t n_passed = get_count(model::test_result_passed); const std::size_t n_failed = get_count(model::test_result_failed); const std::size_t n_skipped = get_count(model::test_result_skipped); const std::size_t n_xfail = get_count( model::test_result_expected_failure); const std::size_t n_broken = get_count(model::test_result_broken); const std::size_t n_bad = n_broken + n_failed; if (_start_time) { INV(_end_time); _summary_templates.add_variable( "start_time", _start_time.get().to_iso8601_in_utc()); _summary_templates.add_variable( "end_time", _end_time.get().to_iso8601_in_utc()); } else { _summary_templates.add_variable("start_time", "No tests run"); _summary_templates.add_variable("end_time", "No tests run"); } _summary_templates.add_variable("duration", cli::format_delta(_runtime)); _summary_templates.add_variable("passed_tests_count", F("%s") % n_passed); _summary_templates.add_variable("failed_tests_count", F("%s") % n_failed); _summary_templates.add_variable("skipped_tests_count", F("%s") % n_skipped); _summary_templates.add_variable("xfail_tests_count", F("%s") % n_xfail); _summary_templates.add_variable("broken_tests_count", F("%s") % n_broken); _summary_templates.add_variable("bad_tests_count", F("%s") % n_bad); generate(text::templates_def(), "report.css", "report.css"); generate(_summary_templates, "index.html", "index.html"); } }; } // anonymous namespace /// Default constructor for cmd_report_html. cli::cmd_report_html::cmd_report_html(void) : cli_command( "report-html", "", 0, 0, "Generates an HTML report with the result of a test suite run") { add_option(results_file_open_option); add_option(cmdline::bool_option( "force", "Wipe the output directory before generating the new report; " "use care")); add_option(cmdline::path_option( "output", "The directory in which to store the HTML files", "path", "html")); add_option(cmdline::list_option( "results-filter", "Comma-separated list of result types to include in " "the report", "types", "skipped,xfail,broken,failed")); } /// Entry point for the "report-html" subcommand. /// /// \param ui Object to interact with the I/O of the program. /// \param cmdline Representation of the command line to the subcommand. /// /// \return 0 if everything is OK, 1 if the statement is invalid or if there is /// any other problem. int cli::cmd_report_html::run(cmdline::ui* ui, const cmdline::parsed_cmdline& cmdline, const config::tree& /* user_config */) { const result_types types = get_result_types(cmdline); const fs::path results_file = layout::find_results( results_file_open(cmdline)); const fs::path directory = cmdline.get_option< cmdline::path_option >("output"); create_top_directory(directory, cmdline.has_option("force")); html_hooks hooks(ui, directory, types); drivers::scan_results::drive(results_file, std::set< engine::test_filter >(), hooks); hooks.write_summary(); return EXIT_SUCCESS; } diff --git a/contrib/kyua/doc/kyua.1.in b/contrib/kyua/doc/kyua.1.in index 2fca5eb09f9f..4a3ab7852b11 100644 --- a/contrib/kyua/doc/kyua.1.in +++ b/contrib/kyua/doc/kyua.1.in @@ -1,400 +1,405 @@ .\" 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. .Dd May 12, 2015 .Dt KYUA 1 .Os .Sh NAME .Nm kyua .Nd Testing framework for infrastructure software .Sh SYNOPSIS .Nm .Op Fl -config Ar file .Op Fl -logfile Ar file .Op Fl -loglevel Ar level .Op Fl -variable Ar name=value .Ar command .Op Ar command_options .Op Ar command_arguments .Sh DESCRIPTION .Em If you are here looking for details on how to run the test suite in .Pa /usr/tests .Em ( or .Pa __TESTSDIR__ ) , .Em please start by reading the .Xr tests 7 .Em manual page that should be supplied by your system . .Pp Kyua is a testing framework for infrastructure software, originally designed to equip BSD-based operating systems with a test suite. This means that Kyua is lightweight and simple, and that Kyua integrates well with various build systems and continuous integration frameworks. .Pp Kyua features an expressive test suite definition language, a safe runtime engine for test suites and a powerful report generation engine. .Pp Kyua is for both developers and users, from the developer applying a simple fix to a library to the system administrator deploying a new release on a production machine. .Pp Kyua is able to execute test programs written with a plethora of testing libraries and languages. The test program library of choice is ATF, which .Nm Ns 's design originated from. However, framework-less test programs and TAP-compliant test programs can also be executed through .Nm .Ss Overview As can be observed in the synopsis, the interface of .Nm implements a common subcommand-based interface. The arguments to the tool specify, in this order: a set of common options that all the commands accept, a required .Ar command name that specifies what .Nm should do, and a set of possibly-optional .Ar command_options and .Ar command_arguments that are specific to the chosen command. .Pp The following options are recognized by all the commands. Keep in mind that these must always be specified before the command name. .Bl -tag -width XX .It Fl -config Ar path , Fl c Ar path Specifies the configuration file to process, which must be in the format described in .Xr kyua.conf 5 . The special value .Sq none explicitly disables the loading of any configuration file. .Pp Defaults to .Pa ~/.kyua/kyua.conf if it exists, otherwise to .Pa __CONFDIR__/kyua.conf if it exists, or else to .Sq none . .It Fl -logfile Ar path Specifies the location of the file to which .Nm will log run time events useful for postmortem debugging. .Pp The default depends on different environment variables as described in .Sx Logging , but typically the file will be stored within the user's home directory. .It Fl -loglevel Ar level Specifies the maximum logging level to record in the log file. See .Sx Logging for more details. .Pp The default is .Sq info . .It Fl -variable Ar name=value , Fl v Ar name=value Sets the .Ar name configuration variable to .Ar value . The values set through this option have preference over the values set in the configuration file. .Pp The specified variable can either be a builtin variable or a test-suite specific variable. See .Xr kyua.conf 5 for more details. .El .Pp The following commands are generic and do not have any relation to the execution of tests or the inspection of their results: .Bl -tag -width reportXjunitXX -offset indent .It Ar about Shows general program information. See .Xr kyua-about 1 . .It Ar config Inspects the values of the configuration variables. See .Xr kyua-config 1 . .It Ar db-exec Executes an arbitrary SQL statement on a results file and prints the resulting table. See .Xr kyua-db-exec 1 . .It Ar help Shows usage information. See .Xr kyua-help 1 . .El .Pp The following commands are used to generate reports based on the data previously recorded in a results file: .Bl -tag -width reportXjunitXX -offset indent .It Ar report Generates a plaintext report. Combined with its .Fl -verbose flag and the ability to only display specific test cases, this command can also be used to debug test failures post-facto on the console. See .Xr kyua-report 1 . .It Ar report-html Generates an HTML report. See .Xr kyua-report-html 1 . .It Ar report-junit Generates a JUnit report. See .Xr kyua-report-junit 1 . .El .Pp The following commands are used to interact with a test suite: .Bl -tag -width reportXjunitXX -offset indent .It Ar debug Executes a single test case in a controlled environment for debugging purposes. See .Xr kyua-debug 1 . .It Ar list Lists test cases defined in a test suite by a .Xr kyuafile 5 and, optionally, displays their metadata. See .Xr kyua-list 1 . .It Ar test Runs tests defined in a test suite by a .Xr kyuafile 5 . See .Xr kyua-test 1 . .El .Ss Logging .Nm has a logging facility that collects all kinds of events at run time. These events are always logged to a file so that the log is available when it is most needed: right after a non-reproducible problem happens. The only way to disable logging is by sending the log to .Pa /dev/null . .Pp The location of the log file can be manually specified with the .Fl -logfile option, which applies to all commands. If no file is explicitly specified, the location of the log files is chosen in this order: .Bl -enum -offset indent .It .Pa ${HOME}/.kyua/logs/ if .Va HOME is defined. .It .Pa ${TMPDIR}/ if .Va TMPDIR is defined. .It .Pa /tmp/ . .El .Pp And the default naming scheme of the log files is: .Sq ..log . .Pp The messages stored in the log file have a level (or severity) attached to them. These are: .Bl -tag -width warningXX -offset indent .It error Fatal error messages. The program generally terminates after these, either in a clean manner or by crashing. .It warning Non-fatal error messages. These generally report a condition that must be addressed but the application can continue to run. .It info Informational messages. These tell the user what the program was doing at a general level of operation. .It debug Detailed informational messages. These are often useful when debugging problems in the application, as they contain lots of internal details. .El .Pp The default log level is .Sq info unless explicitly overridden with .Fl -loglevel . .Pp The log file is a plain text file containing one line per log record. The format of each line is as follows: .Bd -literal -offset indent timestamp entry_type pid file:line: message .Ed .Pp .Ar entry_type can be one of: .Sq E for an error, .Sq W for a warning, .Sq I for an informational message and .Sq D for a debug message. .Ss Bug reporting If you think you have encountered a bug in .Nm , please take the time to let the developers know about it. This will ensure that the bug is addressed and potentially fixed in the next Kyua release. .Pp The first step in reporting a bug is to check if there already is a similar bug in the database. You can check what issues are currently in the database by going to: .Bd -literal -offset indent https://github.com/jmmv/kyua/issues/ .Ed .Pp If there is no existing issue that describes an issue similar to the one you are experiencing, you can open a new one by visiting: .Bd -literal -offset indent https://github.com/jmmv/kyua/issues/new/ .Ed .Pp When doing so, please include as much detail as possible. Among other things, explain what operating system and platform you are running .Nm on, what were you trying to do, what exact messages you saw on the screen, how did you expect the program to behave, and any other details that you may find relevant. .Pp Also, please include a copy of the log file corresponding to the problem you are experiencing. Unless you have changed the location of the log files, you can most likely find them in .Pa ~/.kyua/logs/ . If the problem is reproducible, it is good idea to regenerate the log file with an increased log level so as to provide more information. For example: .Bd -literal -offset indent $ kyua --logfile=problem.log --loglevel=debug \\ [rest of the command line] .Ed .Sh ENVIRONMENT The following variables are recognized and can be freely tuned by the end user: .Bl -tag -width COLUMNSXX .It Va COLUMNS The width of the screen, in number of characters. .Nm uses this to wrap long lines. If not present, the width of the screen is determined from the terminal stdout is connected to, and, if the guessing fails, this defaults to infinity. .It Va HOME Path to the user's home directory. .Nm uses this location to determine paths to configuration files and default log files. .It Va TMPDIR Path to the system-wide temporary directory. .Nm uses this location to place the work directory of test cases, among other things. .Pp The default value of this variable depends on the operating system. In general, it is .Pa /tmp . .El .Pp The following variables are also recognized, but you should not need to set them during normal operation. They are only provided to override the value of built-in values, which is useful when testing .Nm itself: .Bl -tag -width KYUAXCONFDIRXX .It Va KYUA_CONFDIR Path to the system-wide configuration files for .Nm . .Pp Defaults to .Pa __CONFDIR__ . .It Va KYUA_DOCDIR Path to the location of installed documentation. .Pp Defaults to .Pa __DOCDIR__ . .It Va KYUA_MISCDIR Path to the location of the installed miscellaneous scripts and data files provided by .Nm . .Pp Defaults to .Pa __MISCDIR__ . .It Va KYUA_STOREDIR Path to the location of the installed store support files; e.g., the directory containing the SQL database schema. .Pp Defaults to .Pa __STOREDIR__ . .El .Sh FILES .Bl -tag -width XXXX .It Pa ~/.kyua/store/ Default location for the results files. .It Pa ~/.kyua/kyua.conf User-specific configuration file. .It Pa ~/.kyua/logs/ Default location for the collected log files. .It Pa __CONFDIR__/kyua.conf System-wide configuration file. .El .Sh EXIT STATUS .Nm returns 0 on success, 1 on a controlled error condition in the given subcommand, 2 on a general unexpected error and 3 on a usage error. .Pp The documentation of the subcommands in the corresponding manual pages only details the difference between a successful exit (0) and the detection of a controlled error (1). Even though when those manual pages do not describe any other exit statuses, codes above 1 can be returned. .Sh SEE ALSO .Xr kyua.conf 5 , .Xr kyuafile 5 , .Xr atf 7 , .Xr tests 7 .Sh AUTHORS +The original author of +.Nm +was +.An Julio Merino . +.Pp For more details on the people that made .Nm possible and the license terms, run: .Bd -literal -offset indent $ kyua about .Ed diff --git a/contrib/kyua/doc/manbuild_test.sh b/contrib/kyua/doc/manbuild_test.sh old mode 100755 new mode 100644 diff --git a/contrib/kyua/integration/cmd_about_test.sh b/contrib/kyua/integration/cmd_about_test.sh old mode 100755 new mode 100644 diff --git a/contrib/kyua/integration/cmd_config_test.sh b/contrib/kyua/integration/cmd_config_test.sh old mode 100755 new mode 100644 diff --git a/contrib/kyua/integration/cmd_db_exec_test.sh b/contrib/kyua/integration/cmd_db_exec_test.sh old mode 100755 new mode 100644 diff --git a/contrib/kyua/integration/cmd_db_migrate_test.sh b/contrib/kyua/integration/cmd_db_migrate_test.sh old mode 100755 new mode 100644 diff --git a/contrib/kyua/integration/cmd_debug_test.sh b/contrib/kyua/integration/cmd_debug_test.sh old mode 100755 new mode 100644 diff --git a/contrib/kyua/integration/cmd_help_test.sh b/contrib/kyua/integration/cmd_help_test.sh old mode 100755 new mode 100644 diff --git a/contrib/kyua/integration/cmd_list_test.sh b/contrib/kyua/integration/cmd_list_test.sh old mode 100755 new mode 100644 diff --git a/contrib/kyua/integration/cmd_report_html_test.sh b/contrib/kyua/integration/cmd_report_html_test.sh old mode 100755 new mode 100644 diff --git a/contrib/kyua/integration/cmd_report_junit_test.sh b/contrib/kyua/integration/cmd_report_junit_test.sh old mode 100755 new mode 100644 diff --git a/contrib/kyua/integration/cmd_report_test.sh b/contrib/kyua/integration/cmd_report_test.sh old mode 100755 new mode 100644 diff --git a/contrib/kyua/integration/cmd_test_test.sh b/contrib/kyua/integration/cmd_test_test.sh old mode 100755 new mode 100644 diff --git a/contrib/kyua/integration/global_test.sh b/contrib/kyua/integration/global_test.sh old mode 100755 new mode 100644 diff --git a/contrib/kyua/integration/utils.sh b/contrib/kyua/integration/utils.sh old mode 100755 new mode 100644 index 99565a1c9857..d1462a5a9b01 --- a/contrib/kyua/integration/utils.sh +++ b/contrib/kyua/integration/utils.sh @@ -1,177 +1,177 @@ # 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. # Subcommand to strip out the durations and timestamps in a report. # # This is to make the reports deterministic and thus easily testable. The # time deltas are replaced by the fixed string S.UUU and the timestamps are # replaced by the fixed strings YYYYMMDD.HHMMSS.ssssss and # YYYY-MM-DDTHH:MM:SS.ssssssZ depending on their original format. # # This variable should be used as shown here: # # atf_check ... -x kyua report "| ${utils_strip_times}" # # Use the utils_install_times_wrapper function to create a 'kyua' wrapper # script that automatically does this. # CHECK_STYLE_DISABLE utils_strip_times='sed -E \ - -e "s,( |\[|\")[0-9][0-9]*.[0-9][0-9][0-9](s]|s|\"),\1S.UUU\2,g" \ + -e "s,( |\[|\")[0-9][0-9]*\.[0-9][0-9][0-9](s]|s|\"),\1S.UUU\2,g" \ -e "s,[0-9]{8}-[0-9]{6}-[0-9]{6},YYYYMMDD-HHMMSS-ssssss,g" \ -e "s,[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}\.[0-9]{6}Z,YYYY-MM-DDTHH:MM:SS.ssssssZ,g"' # CHECK_STYLE_ENABLE # Same as utils_strip_times but avoids stripping timestamp-based report IDs. # # This is to make the reports deterministic and thus easily testable. The # time deltas are replaced by the fixed string S.UUU and the timestamps are # replaced by the fixed string YYYY-MM-DDTHH:MM:SS.ssssssZ. # CHECK_STYLE_DISABLE utils_strip_times_but_not_ids='sed -E \ - -e "s,( |\[|\")[0-9][0-9]*.[0-9][0-9][0-9](s]|s|\"),\1S.UUU\2,g" \ + -e "s,( |\[|\")[0-9][0-9]*\.[0-9][0-9][0-9](s]|s|\"),\1S.UUU\2,g" \ -e "s,[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}\.[0-9]{6}Z,YYYY-MM-DDTHH:MM:SS.ssssssZ,g"' # CHECK_STYLE_ENABLE # Computes the results id for a test suite run. # # The computed path is "generic" in the sense that it does not include a # real timestamp: it only includes a placeholder. This function should be # used along the utils_strip_times function so that the timestamps of # the real results files are stripped out. # # \param path Optional path to use; if not given, use the cwd. utils_results_id() { local test_suite_id="$(utils_test_suite_id "${@}")" echo "${test_suite_id}.YYYYMMDD-HHMMSS-ssssss" } # Computes the results file for a test suite run. # # The computed path is "generic" in the sense that it does not include a # real timestamp: it only includes a placeholder. This function should be # used along the utils_strip_times function so that the timestampts of the # real results files are stripped out. # # \param path Optional path to use; if not given, use the cwd. utils_results_file() { echo "${HOME}/.kyua/store/results.$(utils_results_id "${@}").db" } # Copies a helper binary from the source directory to the work directory. # # \param name The name of the binary to copy. # \param destination The target location for the binary; can be either # a directory name or a file name. utils_cp_helper() { local name="${1}"; shift local destination="${1}"; shift ln -s "$(atf_get_srcdir)"/helpers/"${name}" "${destination}" } # Creates a 'kyua' binary in the path that strips timing data off the output. # # Call this on test cases that wish to replace timing data in the *stdout* of # Kyua with the deterministic strings. This is to be used by tests that # validate the 'test' and 'report' subcommands. utils_install_times_wrapper() { [ ! -x kyua ] || return cat >kyua <kyua.tmpout result=\${?} cat kyua.tmpout | ${utils_strip_times} exit \${result} EOF chmod +x kyua PATH="$(pwd):${PATH}" } # Creates a 'kyua' binary in the path that makes the output of 'test' stable. # # Call this on test cases that wish to replace timing data with deterministic # strings and that need the result lines in the output to be sorted # lexicographically. The latter hides the indeterminism caused by parallel # execution so that the output can be verified. For these reasons, this is to # be used exclusively by tests that validate the 'test' subcommand. utils_install_stable_test_wrapper() { [ ! -x kyua ] || return cat >kyua <kyua.tmpout result=\${?} cat kyua.tmpout | ${utils_strip_times} >kyua.tmpout2 # Sort the test result lines but keep the rest intact. grep '[^ ]*:[^ ]*' kyua.tmpout2 | sort >kyua.tmpout3 grep -v '[^ ]*:[^ ]*' kyua.tmpout2 >kyua.tmpout4 cat kyua.tmpout3 kyua.tmpout4 exit \${result} EOF chmod +x kyua PATH="$(pwd):${PATH}" } # Defines a test case with a default head. utils_test_case() { local name="${1}"; shift atf_test_case "${name}" eval "${name}_head() { atf_set require.progs kyua }" } # Computes the test suite identifier for results files files. # # \param path Optional path to use; if not given, use the cwd. utils_test_suite_id() { local path= if [ ${#} -gt 0 ]; then path="$(cd ${1} && pwd)"; shift else path="$(pwd)" fi echo "${path}" | sed -e 's,^/,,' -e 's,/,_,g' } diff --git a/contrib/kyua/utils/datetime.cpp b/contrib/kyua/utils/datetime.cpp index ae3fdb62fe55..174d830031d2 100644 --- a/contrib/kyua/utils/datetime.cpp +++ b/contrib/kyua/utils/datetime.cpp @@ -1,613 +1,614 @@ // 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 "utils/datetime.hpp" extern "C" { #include #include } #include #include "utils/format/macros.hpp" #include "utils/optional.ipp" #include "utils/noncopyable.hpp" #include "utils/sanity.hpp" namespace datetime = utils::datetime; using utils::none; using utils::optional; namespace { /// Fake value for the current time. static optional< datetime::timestamp > mock_now = none; } // anonymous namespace /// Creates a zero time delta. datetime::delta::delta(void) : seconds(0), useconds(0) { } /// Creates a time delta. /// /// \param seconds_ The seconds in the delta. /// \param useconds_ The microseconds in the delta. /// /// \throw std::runtime_error If the input delta is negative. datetime::delta::delta(const int64_t seconds_, const unsigned long useconds_) : seconds(seconds_), useconds(useconds_) { if (seconds_ < 0) { throw std::runtime_error(F("Negative deltas are not supported by the " "datetime::delta class; got: %s") % (*this)); } } /// Converts a time expressed in microseconds to a delta. /// /// \param useconds The amount of microseconds representing the delta. /// /// \return A new delta object. /// /// \throw std::runtime_error If the input delta is negative. datetime::delta datetime::delta::from_microseconds(const int64_t useconds) { if (useconds < 0) { throw std::runtime_error(F("Negative deltas are not supported by the " "datetime::delta class; got: %sus") % useconds); } return delta(useconds / 1000000, useconds % 1000000); } /// Convers the delta to a flat representation expressed in microseconds. /// /// \return The amount of microseconds that corresponds to this delta. int64_t datetime::delta::to_microseconds(void) const { return seconds * 1000000 + useconds; } /// Checks if two time deltas are equal. /// /// \param other The object to compare to. /// /// \return True if the two time deltas are equals; false otherwise. bool datetime::delta::operator==(const datetime::delta& other) const { return seconds == other.seconds && useconds == other.useconds; } /// Checks if two time deltas are different. /// /// \param other The object to compare to. /// /// \return True if the two time deltas are different; false otherwise. bool datetime::delta::operator!=(const datetime::delta& other) const { return !(*this == other); } /// Checks if this time delta is shorter than another one. /// /// \param other The object to compare to. /// /// \return True if this time delta is shorter than other; false otherwise. bool datetime::delta::operator<(const datetime::delta& other) const { return seconds < other.seconds || (seconds == other.seconds && useconds < other.useconds); } /// Checks if this time delta is shorter than or equal to another one. /// /// \param other The object to compare to. /// /// \return True if this time delta is shorter than or equal to; false /// otherwise. bool datetime::delta::operator<=(const datetime::delta& other) const { return (*this) < other || (*this) == other; } /// Checks if this time delta is larger than another one. /// /// \param other The object to compare to. /// /// \return True if this time delta is larger than other; false otherwise. bool datetime::delta::operator>(const datetime::delta& other) const { return seconds > other.seconds || (seconds == other.seconds && useconds > other.useconds); } /// Checks if this time delta is larger than or equal to another one. /// /// \param other The object to compare to. /// /// \return True if this time delta is larger than or equal to; false /// otherwise. bool datetime::delta::operator>=(const datetime::delta& other) const { return (*this) > other || (*this) == other; } /// Adds a time delta to this one. /// /// \param other The time delta to add. /// /// \return The addition of this time delta with the other time delta. datetime::delta datetime::delta::operator+(const datetime::delta& other) const { return delta::from_microseconds(to_microseconds() + other.to_microseconds()); } /// Adds a time delta to this one and updates this with the result. /// /// \param other The time delta to add. /// /// \return The addition of this time delta with the other time delta. datetime::delta& datetime::delta::operator+=(const datetime::delta& other) { *this = *this + other; return *this; } /// Scales this delta by a positive integral factor. /// /// \param factor The scaling factor. /// /// \return The scaled delta. datetime::delta datetime::delta::operator*(const std::size_t factor) const { return delta::from_microseconds(to_microseconds() * factor); } /// Scales this delta by and updates this delta with the result. /// /// \param factor The scaling factor. /// /// \return The scaled delta as a reference to the input object. datetime::delta& datetime::delta::operator*=(const std::size_t factor) { *this = *this * factor; return *this; } /// 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& datetime::operator<<(std::ostream& output, const delta& object) { return (output << object.to_microseconds() << "us"); } namespace utils { namespace datetime { /// Internal representation for datetime::timestamp. struct timestamp::impl : utils::noncopyable { /// The raw timestamp as provided by libc. ::timeval data; /// Constructs an impl object from initialized data. /// /// \param data_ The raw timestamp to use. impl(const ::timeval& data_) : data(data_) { } }; } // namespace datetime } // namespace utils /// Constructs a new timestamp. /// /// \param pimpl_ An existing impl representation. datetime::timestamp::timestamp(std::shared_ptr< impl > pimpl_) : _pimpl(pimpl_) { } /// Constructs a timestamp from the amount of microseconds since the epoch. /// /// \param value Microseconds since the epoch in UTC. Must be positive. /// /// \return A new timestamp. datetime::timestamp datetime::timestamp::from_microseconds(const int64_t value) { PRE(value >= 0); ::timeval data; data.tv_sec = static_cast< time_t >(value / 1000000); data.tv_usec = static_cast< suseconds_t >(value % 1000000); return timestamp(std::shared_ptr< impl >(new impl(data))); } /// Constructs a timestamp based on user-friendly values. /// /// \param year The year in the [1900,inf) range. /// \param month The month in the [1,12] range. /// \param day The day in the [1,30] range. /// \param hour The hour in the [0,23] range. /// \param minute The minute in the [0,59] range. /// \param second The second in the [0,60] range. Yes, that is 60, which can be /// the case on leap seconds. /// \param microsecond The microsecond in the [0,999999] range. /// /// \return A new timestamp. datetime::timestamp datetime::timestamp::from_values(const int year, const int month, const int day, const int hour, const int minute, const int second, const int microsecond) { PRE(year >= 1900); PRE(month >= 1 && month <= 12); PRE(day >= 1 && day <= 30); PRE(hour >= 0 && hour <= 23); PRE(minute >= 0 && minute <= 59); PRE(second >= 0 && second <= 60); PRE(microsecond >= 0 && microsecond <= 999999); // The code below is quite convoluted. The problem is that we can't assume // that some fields (like tm_zone) of ::tm exist, and thus we can't blindly // set them from the code. Instead of detecting their presence in the // configure script, we just query the current time to initialize such // fields and then we override the ones we are interested in. (There might // be some better way to do this, but I don't know it and the documentation // does not shed much light into how to create your own fake date.) const time_t current_time = ::time(NULL); ::tm timedata; if (::gmtime_r(¤t_time, &timedata) == NULL) UNREACHABLE; timedata.tm_sec = second; timedata.tm_min = minute; timedata.tm_hour = hour; timedata.tm_mday = day; timedata.tm_mon = month - 1; timedata.tm_year = year - 1900; // Ignored: timedata.tm_wday // Ignored: timedata.tm_yday ::timeval data; data.tv_sec = ::mktime(&timedata); data.tv_usec = static_cast< suseconds_t >(microsecond); return timestamp(std::shared_ptr< impl >(new impl(data))); } /// Constructs a new timestamp representing the current time in UTC. /// /// \return A new timestamp. datetime::timestamp datetime::timestamp::now(void) { if (mock_now) return mock_now.get(); ::timeval data; { const int ret = ::gettimeofday(&data, NULL); INV(ret != -1); } return timestamp(std::shared_ptr< impl >(new impl(data))); } /// Formats a timestamp. /// /// \param format The format string to use as consumed by strftime(3). /// /// \return The formatted time. std::string datetime::timestamp::strftime(const std::string& format) const { ::tm timedata; // This conversion to time_t is necessary because tv_sec is not guaranteed // to be a time_t. For example, it isn't in NetBSD 5.x ::time_t epoch_seconds; epoch_seconds = _pimpl->data.tv_sec; if (::gmtime_r(&epoch_seconds, &timedata) == NULL) UNREACHABLE_MSG("gmtime_r(3) did not accept the value returned by " "gettimeofday(2)"); char buf[128]; if (::strftime(buf, sizeof(buf), format.c_str(), &timedata) == 0) UNREACHABLE_MSG("Arbitrary-long format strings are unimplemented"); return buf; } /// Formats a timestamp with the ISO 8601 standard and in UTC. /// /// \return A string with the formatted timestamp. std::string datetime::timestamp::to_iso8601_in_utc(void) const { return F("%s.%06sZ") % strftime("%Y-%m-%dT%H:%M:%S") % _pimpl->data.tv_usec; } /// Returns the number of microseconds since the epoch in UTC. /// /// \return A number of microseconds. int64_t datetime::timestamp::to_microseconds(void) const { return static_cast< int64_t >(_pimpl->data.tv_sec) * 1000000 + _pimpl->data.tv_usec; } /// Returns the number of seconds since the epoch in UTC. /// /// \return A number of seconds. int64_t datetime::timestamp::to_seconds(void) const { return static_cast< int64_t >(_pimpl->data.tv_sec); } /// Sets the current time for testing purposes. void datetime::set_mock_now(const int year, const int month, const int day, const int hour, const int minute, const int second, const int microsecond) { mock_now = timestamp::from_values(year, month, day, hour, minute, second, microsecond); } /// Sets the current time for testing purposes. /// /// \param mock_now_ The mock timestamp to set the time to. void datetime::set_mock_now(const timestamp& mock_now_) { mock_now = mock_now_; } /// Checks if two timestamps are equal. /// /// \param other The object to compare to. /// /// \return True if the two timestamps are equals; false otherwise. bool datetime::timestamp::operator==(const datetime::timestamp& other) const { return _pimpl->data.tv_sec == other._pimpl->data.tv_sec && _pimpl->data.tv_usec == other._pimpl->data.tv_usec; } /// Checks if two timestamps are different. /// /// \param other The object to compare to. /// /// \return True if the two timestamps are different; false otherwise. bool datetime::timestamp::operator!=(const datetime::timestamp& other) const { return !(*this == other); } /// Checks if a timestamp is before another. /// /// \param other The object to compare to. /// /// \return True if this timestamp comes before other; false otherwise. bool datetime::timestamp::operator<(const datetime::timestamp& other) const { return to_microseconds() < other.to_microseconds(); } /// Checks if a timestamp is before or equal to another. /// /// \param other The object to compare to. /// /// \return True if this timestamp comes before other or is equal to it; /// false otherwise. bool datetime::timestamp::operator<=(const datetime::timestamp& other) const { return to_microseconds() <= other.to_microseconds(); } /// Checks if a timestamp is after another. /// /// \param other The object to compare to. /// /// \return True if this timestamp comes after other; false otherwise; bool datetime::timestamp::operator>(const datetime::timestamp& other) const { return to_microseconds() > other.to_microseconds(); } /// Checks if a timestamp is after or equal to another. /// /// \param other The object to compare to. /// /// \return True if this timestamp comes after other or is equal to it; /// false otherwise. bool datetime::timestamp::operator>=(const datetime::timestamp& other) const { return to_microseconds() >= other.to_microseconds(); } /// Calculates the addition of a delta to a timestamp. /// /// \param other The delta to add. /// /// \return A new timestamp in the future. datetime::timestamp datetime::timestamp::operator+(const datetime::delta& other) const { return datetime::timestamp::from_microseconds(to_microseconds() + other.to_microseconds()); } /// Calculates the addition of a delta to this timestamp. /// /// \param other The delta to add. /// /// \return A reference to the modified timestamp. datetime::timestamp& datetime::timestamp::operator+=(const datetime::delta& other) { *this = *this + other; return *this; } /// Calculates the subtraction of a delta from a timestamp. /// /// \param other The delta to subtract. /// /// \return A new timestamp in the past. datetime::timestamp datetime::timestamp::operator-(const datetime::delta& other) const { return datetime::timestamp::from_microseconds(to_microseconds() - other.to_microseconds()); } /// Calculates the subtraction of a delta from this timestamp. /// /// \param other The delta to subtract. /// /// \return A reference to the modified timestamp. datetime::timestamp& datetime::timestamp::operator-=(const datetime::delta& other) { *this = *this - other; return *this; } /// Calculates the delta between two timestamps. /// /// \param other The subtrahend. /// /// \return The difference between this object and the other object. /// /// \throw std::runtime_error If the subtraction would result in a negative time /// delta, which are currently not supported. datetime::delta datetime::timestamp::operator-(const datetime::timestamp& other) const { - if ((*this) < other) { - throw std::runtime_error( - F("Cannot subtract %s from %s as it would result in a negative " - "datetime::delta, which are not supported") % other % (*this)); - } + /* + * XXX-BD: gettimeofday isn't necessarily monotonic so return the + * smallest non-zero delta if time went backwards. + */ + if ((*this) < other) + return datetime::delta::from_microseconds(1); return datetime::delta::from_microseconds(to_microseconds() - other.to_microseconds()); } /// 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& datetime::operator<<(std::ostream& output, const timestamp& object) { return (output << object.to_microseconds() << "us"); } diff --git a/contrib/kyua/utils/datetime_test.cpp b/contrib/kyua/utils/datetime_test.cpp index 9f8ff50cd0f8..a2d3a3c0cdad 100644 --- a/contrib/kyua/utils/datetime_test.cpp +++ b/contrib/kyua/utils/datetime_test.cpp @@ -1,593 +1,593 @@ // 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 "utils/datetime.hpp" extern "C" { #include #include } #include #include #include namespace datetime = utils::datetime; ATF_TEST_CASE_WITHOUT_HEAD(delta__defaults); ATF_TEST_CASE_BODY(delta__defaults) { const datetime::delta delta; ATF_REQUIRE_EQ(0, delta.seconds); ATF_REQUIRE_EQ(0, delta.useconds); } ATF_TEST_CASE_WITHOUT_HEAD(delta__overrides); ATF_TEST_CASE_BODY(delta__overrides) { const datetime::delta delta(1, 2); ATF_REQUIRE_EQ(1, delta.seconds); ATF_REQUIRE_EQ(2, delta.useconds); ATF_REQUIRE_THROW_RE( std::runtime_error, "Negative.*not supported.*-4999997us", datetime::delta(-5, 3)); } ATF_TEST_CASE_WITHOUT_HEAD(delta__from_microseconds); ATF_TEST_CASE_BODY(delta__from_microseconds) { { const datetime::delta delta = datetime::delta::from_microseconds(0); ATF_REQUIRE_EQ(0, delta.seconds); ATF_REQUIRE_EQ(0, delta.useconds); } { const datetime::delta delta = datetime::delta::from_microseconds( 999999); ATF_REQUIRE_EQ(0, delta.seconds); ATF_REQUIRE_EQ(999999, delta.useconds); } { const datetime::delta delta = datetime::delta::from_microseconds( 1000000); ATF_REQUIRE_EQ(1, delta.seconds); ATF_REQUIRE_EQ(0, delta.useconds); } { const datetime::delta delta = datetime::delta::from_microseconds( 10576293); ATF_REQUIRE_EQ(10, delta.seconds); ATF_REQUIRE_EQ(576293, delta.useconds); } { const datetime::delta delta = datetime::delta::from_microseconds( 123456789123456LL); ATF_REQUIRE_EQ(123456789, delta.seconds); ATF_REQUIRE_EQ(123456, delta.useconds); } ATF_REQUIRE_THROW_RE( std::runtime_error, "Negative.*not supported.*-12345us", datetime::delta::from_microseconds(-12345)); } ATF_TEST_CASE_WITHOUT_HEAD(delta__to_microseconds); ATF_TEST_CASE_BODY(delta__to_microseconds) { ATF_REQUIRE_EQ(0, datetime::delta(0, 0).to_microseconds()); ATF_REQUIRE_EQ(999999, datetime::delta(0, 999999).to_microseconds()); ATF_REQUIRE_EQ(1000000, datetime::delta(1, 0).to_microseconds()); ATF_REQUIRE_EQ(10576293, datetime::delta(10, 576293).to_microseconds()); ATF_REQUIRE_EQ(11576293, datetime::delta(10, 1576293).to_microseconds()); } ATF_TEST_CASE_WITHOUT_HEAD(delta__equals); ATF_TEST_CASE_BODY(delta__equals) { ATF_REQUIRE(datetime::delta() == datetime::delta()); ATF_REQUIRE(datetime::delta() == datetime::delta(0, 0)); ATF_REQUIRE(datetime::delta(1, 2) == datetime::delta(1, 2)); ATF_REQUIRE(!(datetime::delta() == datetime::delta(0, 1))); ATF_REQUIRE(!(datetime::delta() == datetime::delta(1, 0))); ATF_REQUIRE(!(datetime::delta(1, 2) == datetime::delta(2, 1))); } ATF_TEST_CASE_WITHOUT_HEAD(delta__differs); ATF_TEST_CASE_BODY(delta__differs) { ATF_REQUIRE(!(datetime::delta() != datetime::delta())); ATF_REQUIRE(!(datetime::delta() != datetime::delta(0, 0))); ATF_REQUIRE(!(datetime::delta(1, 2) != datetime::delta(1, 2))); ATF_REQUIRE(datetime::delta() != datetime::delta(0, 1)); ATF_REQUIRE(datetime::delta() != datetime::delta(1, 0)); ATF_REQUIRE(datetime::delta(1, 2) != datetime::delta(2, 1)); } ATF_TEST_CASE_WITHOUT_HEAD(delta__sorting); ATF_TEST_CASE_BODY(delta__sorting) { ATF_REQUIRE(!(datetime::delta() < datetime::delta())); ATF_REQUIRE( datetime::delta() <= datetime::delta()); ATF_REQUIRE(!(datetime::delta() > datetime::delta())); ATF_REQUIRE( datetime::delta() >= datetime::delta()); ATF_REQUIRE(!(datetime::delta(9, 8) < datetime::delta(9, 8))); ATF_REQUIRE( datetime::delta(9, 8) <= datetime::delta(9, 8)); ATF_REQUIRE(!(datetime::delta(9, 8) > datetime::delta(9, 8))); ATF_REQUIRE( datetime::delta(9, 8) >= datetime::delta(9, 8)); ATF_REQUIRE( datetime::delta(2, 5) < datetime::delta(4, 8)); ATF_REQUIRE( datetime::delta(2, 5) <= datetime::delta(4, 8)); ATF_REQUIRE(!(datetime::delta(2, 5) > datetime::delta(4, 8))); ATF_REQUIRE(!(datetime::delta(2, 5) >= datetime::delta(4, 8))); ATF_REQUIRE( datetime::delta(2, 5) < datetime::delta(2, 8)); ATF_REQUIRE( datetime::delta(2, 5) <= datetime::delta(2, 8)); ATF_REQUIRE(!(datetime::delta(2, 5) > datetime::delta(2, 8))); ATF_REQUIRE(!(datetime::delta(2, 5) >= datetime::delta(2, 8))); ATF_REQUIRE(!(datetime::delta(4, 8) < datetime::delta(2, 5))); ATF_REQUIRE(!(datetime::delta(4, 8) <= datetime::delta(2, 5))); ATF_REQUIRE( datetime::delta(4, 8) > datetime::delta(2, 5)); ATF_REQUIRE( datetime::delta(4, 8) >= datetime::delta(2, 5)); ATF_REQUIRE(!(datetime::delta(2, 8) < datetime::delta(2, 5))); ATF_REQUIRE(!(datetime::delta(2, 8) <= datetime::delta(2, 5))); ATF_REQUIRE( datetime::delta(2, 8) > datetime::delta(2, 5)); ATF_REQUIRE( datetime::delta(2, 8) >= datetime::delta(2, 5)); } ATF_TEST_CASE_WITHOUT_HEAD(delta__addition); ATF_TEST_CASE_BODY(delta__addition) { using datetime::delta; ATF_REQUIRE_EQ(delta(), delta() + delta()); ATF_REQUIRE_EQ(delta(0, 10), delta() + delta(0, 10)); ATF_REQUIRE_EQ(delta(10, 0), delta(10, 0) + delta()); ATF_REQUIRE_EQ(delta(1, 234567), delta(0, 1234567) + delta()); ATF_REQUIRE_EQ(delta(12, 34), delta(10, 20) + delta(2, 14)); } ATF_TEST_CASE_WITHOUT_HEAD(delta__addition_and_set); ATF_TEST_CASE_BODY(delta__addition_and_set) { using datetime::delta; { delta d; d += delta(3, 5); ATF_REQUIRE_EQ(delta(3, 5), d); } { delta d(1, 2); d += delta(3, 5); ATF_REQUIRE_EQ(delta(4, 7), d); } { delta d(1, 2); ATF_REQUIRE_EQ(delta(4, 7), (d += delta(3, 5))); } } ATF_TEST_CASE_WITHOUT_HEAD(delta__scale); ATF_TEST_CASE_BODY(delta__scale) { using datetime::delta; ATF_REQUIRE_EQ(delta(), delta() * 0); ATF_REQUIRE_EQ(delta(), delta() * 5); ATF_REQUIRE_EQ(delta(0, 30), delta(0, 10) * 3); ATF_REQUIRE_EQ(delta(17, 500000), delta(3, 500000) * 5); } ATF_TEST_CASE_WITHOUT_HEAD(delta__scale_and_set); ATF_TEST_CASE_BODY(delta__scale_and_set) { using datetime::delta; { delta d(3, 5); d *= 2; ATF_REQUIRE_EQ(delta(6, 10), d); } { delta d(8, 0); d *= 8; ATF_REQUIRE_EQ(delta(64, 0), d); } { delta d(3, 5); ATF_REQUIRE_EQ(delta(9, 15), (d *= 3)); } } ATF_TEST_CASE_WITHOUT_HEAD(delta__output); ATF_TEST_CASE_BODY(delta__output) { { std::ostringstream str; str << datetime::delta(15, 8791); ATF_REQUIRE_EQ("15008791us", str.str()); } { std::ostringstream str; str << datetime::delta(12345678, 0); ATF_REQUIRE_EQ("12345678000000us", str.str()); } } ATF_TEST_CASE_WITHOUT_HEAD(timestamp__copy); ATF_TEST_CASE_BODY(timestamp__copy) { const datetime::timestamp ts1 = datetime::timestamp::from_values( 2011, 2, 16, 19, 15, 30, 0); { const datetime::timestamp ts2 = ts1; const datetime::timestamp ts3 = datetime::timestamp::from_values( 2012, 2, 16, 19, 15, 30, 0); ATF_REQUIRE_EQ("2011", ts1.strftime("%Y")); ATF_REQUIRE_EQ("2011", ts2.strftime("%Y")); ATF_REQUIRE_EQ("2012", ts3.strftime("%Y")); } ATF_REQUIRE_EQ("2011", ts1.strftime("%Y")); } ATF_TEST_CASE_WITHOUT_HEAD(timestamp__from_microseconds); ATF_TEST_CASE_BODY(timestamp__from_microseconds) { const datetime::timestamp ts = datetime::timestamp::from_microseconds( 1328829351987654LL); ATF_REQUIRE_EQ("2012-02-09 23:15:51", ts.strftime("%Y-%m-%d %H:%M:%S")); ATF_REQUIRE_EQ(1328829351987654LL, ts.to_microseconds()); ATF_REQUIRE_EQ(1328829351, ts.to_seconds()); } ATF_TEST_CASE_WITHOUT_HEAD(timestamp__now__mock); ATF_TEST_CASE_BODY(timestamp__now__mock) { datetime::set_mock_now(2011, 2, 21, 18, 5, 10, 0); ATF_REQUIRE_EQ("2011-02-21 18:05:10", datetime::timestamp::now().strftime("%Y-%m-%d %H:%M:%S")); datetime::set_mock_now(datetime::timestamp::from_values( 2012, 3, 22, 19, 6, 11, 54321)); ATF_REQUIRE_EQ("2012-03-22 19:06:11", datetime::timestamp::now().strftime("%Y-%m-%d %H:%M:%S")); ATF_REQUIRE_EQ("2012-03-22 19:06:11", datetime::timestamp::now().strftime("%Y-%m-%d %H:%M:%S")); } ATF_TEST_CASE_WITHOUT_HEAD(timestamp__now__real); ATF_TEST_CASE_BODY(timestamp__now__real) { // This test is might fail if we happen to run at the crossing of one // day to the other and the two measures we pick of the current time // differ. This is so unlikely that I haven't bothered to do this in any // other way. const time_t just_before = ::time(NULL); const datetime::timestamp now = datetime::timestamp::now(); ::tm data; char buf[1024]; ATF_REQUIRE(::gmtime_r(&just_before, &data) != 0); ATF_REQUIRE(::strftime(buf, sizeof(buf), "%Y-%m-%d", &data) != 0); ATF_REQUIRE_EQ(buf, now.strftime("%Y-%m-%d")); ATF_REQUIRE(now.strftime("%Z") == "GMT" || now.strftime("%Z") == "UTC"); } ATF_TEST_CASE_WITHOUT_HEAD(timestamp__now__granularity); ATF_TEST_CASE_BODY(timestamp__now__granularity) { const datetime::timestamp first = datetime::timestamp::now(); ::usleep(1); const datetime::timestamp second = datetime::timestamp::now(); ATF_REQUIRE(first.to_microseconds() != second.to_microseconds()); } ATF_TEST_CASE_WITHOUT_HEAD(timestamp__strftime); ATF_TEST_CASE_BODY(timestamp__strftime) { const datetime::timestamp ts1 = datetime::timestamp::from_values( 2010, 12, 10, 8, 45, 50, 0); ATF_REQUIRE_EQ("2010-12-10", ts1.strftime("%Y-%m-%d")); ATF_REQUIRE_EQ("08:45:50", ts1.strftime("%H:%M:%S")); const datetime::timestamp ts2 = datetime::timestamp::from_values( 2011, 2, 16, 19, 15, 30, 0); ATF_REQUIRE_EQ("2011-02-16T19:15:30", ts2.strftime("%Y-%m-%dT%H:%M:%S")); } ATF_TEST_CASE_WITHOUT_HEAD(timestamp__to_iso8601_in_utc); ATF_TEST_CASE_BODY(timestamp__to_iso8601_in_utc) { const datetime::timestamp ts1 = datetime::timestamp::from_values( 2010, 12, 10, 8, 45, 50, 0); ATF_REQUIRE_EQ("2010-12-10T08:45:50.000000Z", ts1.to_iso8601_in_utc()); const datetime::timestamp ts2= datetime::timestamp::from_values( 2016, 7, 11, 17, 51, 28, 123456); ATF_REQUIRE_EQ("2016-07-11T17:51:28.123456Z", ts2.to_iso8601_in_utc()); } ATF_TEST_CASE_WITHOUT_HEAD(timestamp__to_microseconds); ATF_TEST_CASE_BODY(timestamp__to_microseconds) { const datetime::timestamp ts1 = datetime::timestamp::from_values( 2010, 12, 10, 8, 45, 50, 123456); ATF_REQUIRE_EQ(1291970750123456LL, ts1.to_microseconds()); } ATF_TEST_CASE_WITHOUT_HEAD(timestamp__to_seconds); ATF_TEST_CASE_BODY(timestamp__to_seconds) { const datetime::timestamp ts1 = datetime::timestamp::from_values( 2010, 12, 10, 8, 45, 50, 123456); ATF_REQUIRE_EQ(1291970750, ts1.to_seconds()); } ATF_TEST_CASE_WITHOUT_HEAD(timestamp__leap_second); ATF_TEST_CASE_BODY(timestamp__leap_second) { // This is actually a test for from_values(), which is the function that // includes assertions to validate the input parameters. const datetime::timestamp ts1 = datetime::timestamp::from_values( 2012, 6, 30, 23, 59, 60, 543); ATF_REQUIRE_EQ(1341100800, ts1.to_seconds()); } ATF_TEST_CASE_WITHOUT_HEAD(timestamp__equals); ATF_TEST_CASE_BODY(timestamp__equals) { ATF_REQUIRE(datetime::timestamp::from_microseconds(1291970750123456LL) == datetime::timestamp::from_microseconds(1291970750123456LL)); } ATF_TEST_CASE_WITHOUT_HEAD(timestamp__differs); ATF_TEST_CASE_BODY(timestamp__differs) { ATF_REQUIRE(datetime::timestamp::from_microseconds(1291970750123456LL) != datetime::timestamp::from_microseconds(1291970750123455LL)); } ATF_TEST_CASE_WITHOUT_HEAD(timestamp__sorting); ATF_TEST_CASE_BODY(timestamp__sorting) { { const datetime::timestamp ts1 = datetime::timestamp::from_microseconds( 1291970750123455LL); const datetime::timestamp ts2 = datetime::timestamp::from_microseconds( 1291970750123455LL); ATF_REQUIRE(!(ts1 < ts2)); ATF_REQUIRE( ts1 <= ts2); ATF_REQUIRE(!(ts1 > ts2)); ATF_REQUIRE( ts1 >= ts2); } { const datetime::timestamp ts1 = datetime::timestamp::from_microseconds( 1291970750123455LL); const datetime::timestamp ts2 = datetime::timestamp::from_microseconds( 1291970759123455LL); ATF_REQUIRE( ts1 < ts2); ATF_REQUIRE( ts1 <= ts2); ATF_REQUIRE(!(ts1 > ts2)); ATF_REQUIRE(!(ts1 >= ts2)); } { const datetime::timestamp ts1 = datetime::timestamp::from_microseconds( 1291970759123455LL); const datetime::timestamp ts2 = datetime::timestamp::from_microseconds( 1291970750123455LL); ATF_REQUIRE(!(ts1 < ts2)); ATF_REQUIRE(!(ts1 <= ts2)); ATF_REQUIRE( ts1 > ts2); ATF_REQUIRE( ts1 >= ts2); } } ATF_TEST_CASE_WITHOUT_HEAD(timestamp__add_delta); ATF_TEST_CASE_BODY(timestamp__add_delta) { using datetime::delta; using datetime::timestamp; ATF_REQUIRE_EQ(timestamp::from_values(2014, 12, 11, 21, 43, 30, 1234), timestamp::from_values(2014, 12, 11, 21, 43, 0, 0) + delta(30, 1234)); ATF_REQUIRE_EQ(timestamp::from_values(2014, 12, 11, 22, 43, 7, 100), timestamp::from_values(2014, 12, 11, 21, 43, 0, 0) + delta(3602, 5000100)); } ATF_TEST_CASE_WITHOUT_HEAD(timestamp__add_delta_and_set); ATF_TEST_CASE_BODY(timestamp__add_delta_and_set) { using datetime::delta; using datetime::timestamp; { timestamp ts = timestamp::from_values(2014, 12, 11, 21, 43, 0, 0); ts += delta(30, 1234); ATF_REQUIRE_EQ(timestamp::from_values(2014, 12, 11, 21, 43, 30, 1234), ts); } { timestamp ts = timestamp::from_values(2014, 12, 11, 21, 43, 0, 0); ATF_REQUIRE_EQ(timestamp::from_values(2014, 12, 11, 22, 43, 7, 100), ts += delta(3602, 5000100)); } } ATF_TEST_CASE_WITHOUT_HEAD(timestamp__subtract_delta); ATF_TEST_CASE_BODY(timestamp__subtract_delta) { using datetime::delta; using datetime::timestamp; ATF_REQUIRE_EQ(timestamp::from_values(2014, 12, 11, 21, 43, 10, 4321), timestamp::from_values(2014, 12, 11, 21, 43, 40, 5555) - delta(30, 1234)); ATF_REQUIRE_EQ(timestamp::from_values(2014, 12, 11, 20, 43, 1, 300), timestamp::from_values(2014, 12, 11, 21, 43, 8, 400) - delta(3602, 5000100)); } ATF_TEST_CASE_WITHOUT_HEAD(timestamp__subtract_delta_and_set); ATF_TEST_CASE_BODY(timestamp__subtract_delta_and_set) { using datetime::delta; using datetime::timestamp; { timestamp ts = timestamp::from_values(2014, 12, 11, 21, 43, 40, 5555); ts -= delta(30, 1234); ATF_REQUIRE_EQ(timestamp::from_values(2014, 12, 11, 21, 43, 10, 4321), ts); } { timestamp ts = timestamp::from_values(2014, 12, 11, 21, 43, 8, 400); ATF_REQUIRE_EQ(timestamp::from_values(2014, 12, 11, 20, 43, 1, 300), ts -= delta(3602, 5000100)); } } ATF_TEST_CASE_WITHOUT_HEAD(timestamp__subtraction); ATF_TEST_CASE_BODY(timestamp__subtraction) { const datetime::timestamp ts1 = datetime::timestamp::from_microseconds( 1291970750123456LL); const datetime::timestamp ts2 = datetime::timestamp::from_microseconds( 1291970750123468LL); const datetime::timestamp ts3 = datetime::timestamp::from_microseconds( 1291970850123456LL); ATF_REQUIRE_EQ(datetime::delta(0, 0), ts1 - ts1); ATF_REQUIRE_EQ(datetime::delta(0, 12), ts2 - ts1); ATF_REQUIRE_EQ(datetime::delta(100, 0), ts3 - ts1); ATF_REQUIRE_EQ(datetime::delta(99, 999988), ts3 - ts2); - ATF_REQUIRE_THROW_RE( - std::runtime_error, - "Cannot subtract 1291970850123456us from 1291970750123468us " - ".*negative datetime::delta.*not supported", - ts2 - ts3); + /* + * NOTE (ngie): behavior change for + * https://github.com/jmmv/kyua/issues/155 . + */ + ATF_REQUIRE_EQ(datetime::delta::from_microseconds(1), ts2 - ts3); } ATF_TEST_CASE_WITHOUT_HEAD(timestamp__output); ATF_TEST_CASE_BODY(timestamp__output) { { std::ostringstream str; str << datetime::timestamp::from_microseconds(1291970750123456LL); ATF_REQUIRE_EQ("1291970750123456us", str.str()); } { std::ostringstream str; str << datetime::timestamp::from_microseconds(1028309798759812LL); ATF_REQUIRE_EQ("1028309798759812us", str.str()); } } ATF_INIT_TEST_CASES(tcs) { ATF_ADD_TEST_CASE(tcs, delta__defaults); ATF_ADD_TEST_CASE(tcs, delta__overrides); ATF_ADD_TEST_CASE(tcs, delta__from_microseconds); ATF_ADD_TEST_CASE(tcs, delta__to_microseconds); ATF_ADD_TEST_CASE(tcs, delta__equals); ATF_ADD_TEST_CASE(tcs, delta__differs); ATF_ADD_TEST_CASE(tcs, delta__sorting); ATF_ADD_TEST_CASE(tcs, delta__addition); ATF_ADD_TEST_CASE(tcs, delta__addition_and_set); ATF_ADD_TEST_CASE(tcs, delta__scale); ATF_ADD_TEST_CASE(tcs, delta__scale_and_set); ATF_ADD_TEST_CASE(tcs, delta__output); ATF_ADD_TEST_CASE(tcs, timestamp__copy); ATF_ADD_TEST_CASE(tcs, timestamp__from_microseconds); ATF_ADD_TEST_CASE(tcs, timestamp__now__mock); ATF_ADD_TEST_CASE(tcs, timestamp__now__real); ATF_ADD_TEST_CASE(tcs, timestamp__now__granularity); ATF_ADD_TEST_CASE(tcs, timestamp__strftime); ATF_ADD_TEST_CASE(tcs, timestamp__to_iso8601_in_utc); ATF_ADD_TEST_CASE(tcs, timestamp__to_microseconds); ATF_ADD_TEST_CASE(tcs, timestamp__to_seconds); ATF_ADD_TEST_CASE(tcs, timestamp__leap_second); ATF_ADD_TEST_CASE(tcs, timestamp__equals); ATF_ADD_TEST_CASE(tcs, timestamp__differs); ATF_ADD_TEST_CASE(tcs, timestamp__sorting); ATF_ADD_TEST_CASE(tcs, timestamp__add_delta); ATF_ADD_TEST_CASE(tcs, timestamp__add_delta_and_set); ATF_ADD_TEST_CASE(tcs, timestamp__subtract_delta); ATF_ADD_TEST_CASE(tcs, timestamp__subtract_delta_and_set); ATF_ADD_TEST_CASE(tcs, timestamp__subtraction); ATF_ADD_TEST_CASE(tcs, timestamp__output); } diff --git a/contrib/kyua/utils/process/Kyuafile b/contrib/kyua/utils/process/Kyuafile index 92e62cfac6fc..37ab662982d5 100644 --- a/contrib/kyua/utils/process/Kyuafile +++ b/contrib/kyua/utils/process/Kyuafile @@ -1,13 +1,14 @@ syntax(2) test_suite("kyua") atf_test_program{name="child_test"} atf_test_program{name="deadline_killer_test"} atf_test_program{name="exceptions_test"} atf_test_program{name="executor_test"} +atf_test_program{name="executor_pid_test"} atf_test_program{name="fdstream_test"} atf_test_program{name="isolation_test"} atf_test_program{name="operations_test"} atf_test_program{name="status_test"} atf_test_program{name="systembuf_test"} diff --git a/contrib/kyua/utils/process/Makefile.am.inc b/contrib/kyua/utils/process/Makefile.am.inc index 3cff02e7e455..5ce894091a53 100644 --- a/contrib/kyua/utils/process/Makefile.am.inc +++ b/contrib/kyua/utils/process/Makefile.am.inc @@ -1,113 +1,118 @@ # 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. libutils_a_SOURCES += utils/process/child.cpp libutils_a_SOURCES += utils/process/child.hpp libutils_a_SOURCES += utils/process/child.ipp libutils_a_SOURCES += utils/process/child_fwd.hpp libutils_a_SOURCES += utils/process/deadline_killer.cpp libutils_a_SOURCES += utils/process/deadline_killer.hpp libutils_a_SOURCES += utils/process/deadline_killer_fwd.hpp libutils_a_SOURCES += utils/process/exceptions.cpp libutils_a_SOURCES += utils/process/exceptions.hpp libutils_a_SOURCES += utils/process/executor.cpp libutils_a_SOURCES += utils/process/executor.hpp libutils_a_SOURCES += utils/process/executor.ipp libutils_a_SOURCES += utils/process/executor_fwd.hpp libutils_a_SOURCES += utils/process/fdstream.cpp libutils_a_SOURCES += utils/process/fdstream.hpp libutils_a_SOURCES += utils/process/fdstream_fwd.hpp libutils_a_SOURCES += utils/process/isolation.cpp libutils_a_SOURCES += utils/process/isolation.hpp libutils_a_SOURCES += utils/process/operations.cpp libutils_a_SOURCES += utils/process/operations.hpp libutils_a_SOURCES += utils/process/operations_fwd.hpp libutils_a_SOURCES += utils/process/status.cpp libutils_a_SOURCES += utils/process/status.hpp libutils_a_SOURCES += utils/process/status_fwd.hpp libutils_a_SOURCES += utils/process/system.cpp libutils_a_SOURCES += utils/process/system.hpp libutils_a_SOURCES += utils/process/systembuf.cpp libutils_a_SOURCES += utils/process/systembuf.hpp libutils_a_SOURCES += utils/process/systembuf_fwd.hpp if WITH_ATF tests_utils_processdir = $(pkgtestsdir)/utils/process tests_utils_process_DATA = utils/process/Kyuafile EXTRA_DIST += $(tests_utils_process_DATA) tests_utils_process_PROGRAMS = utils/process/child_test utils_process_child_test_SOURCES = utils/process/child_test.cpp utils_process_child_test_CXXFLAGS = $(UTILS_CFLAGS) $(ATF_CXX_CFLAGS) utils_process_child_test_LDADD = $(UTILS_LIBS) $(ATF_CXX_LIBS) tests_utils_process_PROGRAMS += utils/process/deadline_killer_test utils_process_deadline_killer_test_SOURCES = \ utils/process/deadline_killer_test.cpp utils_process_deadline_killer_test_CXXFLAGS = $(UTILS_CFLAGS) $(ATF_CXX_CFLAGS) utils_process_deadline_killer_test_LDADD = $(UTILS_LIBS) $(ATF_CXX_LIBS) tests_utils_process_PROGRAMS += utils/process/exceptions_test utils_process_exceptions_test_SOURCES = utils/process/exceptions_test.cpp utils_process_exceptions_test_CXXFLAGS = $(UTILS_CFLAGS) $(ATF_CXX_CFLAGS) utils_process_exceptions_test_LDADD = $(UTILS_LIBS) $(ATF_CXX_LIBS) tests_utils_process_PROGRAMS += utils/process/executor_test utils_process_executor_test_SOURCES = utils/process/executor_test.cpp utils_process_executor_test_CXXFLAGS = $(UTILS_CFLAGS) $(ATF_CXX_CFLAGS) utils_process_executor_test_LDADD = $(UTILS_LIBS) $(ATF_CXX_LIBS) +tests_utils_process_PROGRAMS += utils/process/executor_pid_test +utils_process_executor_pid_test_SOURCES = utils/process/executor_pid_test.cpp +utils_process_executor_pid_test_CXXFLAGS = $(UTILS_CFLAGS) $(ATF_CXX_CFLAGS) +utils_process_executor_pid_test_LDADD = $(UTILS_LIBS) $(ATF_CXX_LIBS) + tests_utils_process_PROGRAMS += utils/process/fdstream_test utils_process_fdstream_test_SOURCES = utils/process/fdstream_test.cpp utils_process_fdstream_test_CXXFLAGS = $(UTILS_CFLAGS) $(ATF_CXX_CFLAGS) utils_process_fdstream_test_LDADD = $(UTILS_LIBS) $(ATF_CXX_LIBS) tests_utils_process_PROGRAMS += utils/process/isolation_test utils_process_isolation_test_SOURCES = utils/process/isolation_test.cpp utils_process_isolation_test_CXXFLAGS = $(UTILS_CFLAGS) $(ATF_CXX_CFLAGS) utils_process_isolation_test_LDADD = $(UTILS_LIBS) $(ATF_CXX_LIBS) tests_utils_process_PROGRAMS += utils/process/helpers utils_process_helpers_SOURCES = utils/process/helpers.cpp tests_utils_process_PROGRAMS += utils/process/operations_test utils_process_operations_test_SOURCES = utils/process/operations_test.cpp utils_process_operations_test_CXXFLAGS = $(UTILS_CFLAGS) $(ATF_CXX_CFLAGS) utils_process_operations_test_LDADD = $(UTILS_LIBS) $(ATF_CXX_LIBS) tests_utils_process_PROGRAMS += utils/process/status_test utils_process_status_test_SOURCES = utils/process/status_test.cpp utils_process_status_test_CXXFLAGS = $(UTILS_CFLAGS) $(ATF_CXX_CFLAGS) utils_process_status_test_LDADD = $(UTILS_LIBS) $(ATF_CXX_LIBS) tests_utils_process_PROGRAMS += utils/process/systembuf_test utils_process_systembuf_test_SOURCES = utils/process/systembuf_test.cpp utils_process_systembuf_test_CXXFLAGS = $(UTILS_CFLAGS) $(ATF_CXX_CFLAGS) utils_process_systembuf_test_LDADD = $(UTILS_LIBS) $(ATF_CXX_LIBS) endif diff --git a/contrib/kyua/utils/process/executor.cpp b/contrib/kyua/utils/process/executor.cpp index dbdf31268f86..a00632614737 100644 --- a/contrib/kyua/utils/process/executor.cpp +++ b/contrib/kyua/utils/process/executor.cpp @@ -1,869 +1,895 @@ // 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 +#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. +/// Internal implementation for the exec_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; + /// Former members of all_exec_handles removed due to PID reuse. + std::forward_list stale_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))), + all_exec_handles(), + stale_exec_handles(), 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(); + for (auto iter : stale_exec_handles) { + // The process already exited, so no need to kill and wait. + try { + fs::rm_r(iter.control_directory()); + } catch (const fs::error& e) { + LE(F("Failed to clean up stale subprocess work directory " + "%s: %s") % iter.control_directory() % e.what()); + } + } + stale_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()); + LE(F("Failed to clean up executor work directory %s: %s; " + "this could be an internal error or a buggy test") % + 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))); } }; /// 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)); + const auto value = exec_handles_map::value_type(handle.pid(), handle); + auto insert_pair = _pimpl->all_exec_handles.insert(value); + if (!insert_pair.second) { + LI(F("PID %s already in all_exec_handles") % handle.pid()); + _pimpl->stale_exec_handles.push_front(insert_pair.first->second); + _pimpl->all_exec_handles.erase(insert_pair.first); + insert_pair = _pimpl->all_exec_handles.insert(value); + INV_MSG(insert_pair.second, F("PID %s still in all_exec_handles") % + handle.pid()); + } 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)); + const auto value = exec_handles_map::value_type(handle.pid(), handle); + auto insert_pair = _pimpl->all_exec_handles.insert(value); + if (!insert_pair.second) { + LI(F("PID %s already in all_exec_handles") % handle.pid()); + _pimpl->stale_exec_handles.push_front(insert_pair.first->second); + _pimpl->all_exec_handles.erase(insert_pair.first); + insert_pair = _pimpl->all_exec_handles.insert(value); + INV_MSG(insert_pair.second, F("PID %s still in all_exec_handles") % + handle.pid()); + } 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); } /// 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_pid_test.cpp b/contrib/kyua/utils/process/executor_pid_test.cpp new file mode 100644 index 000000000000..22e0b90ba14b --- /dev/null +++ b/contrib/kyua/utils/process/executor_pid_test.cpp @@ -0,0 +1,208 @@ +/*- + * SPDX-License-Identifier: BSD-2-Clause + * + * Copyright (c) 2022 Dell Inc. + * Author: Eric van Gyzen + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. 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. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR 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 AUTHOR 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. + */ + +#if 0 + +1. Run some "bad" tests that prevent kyua from removing the work directory. + We use "chflags uunlink". Mounting a file system from an md(4) device + is another common use case. +2. Fork a lot, nearly wrapping the PID number space, so step 3 will re-use + a PID from step 1. Running the entire FreeBSD test suite is a more + realistic scenario for this step. +3. Run some more tests. If the stars align, the bug is not fixed yet, and + kyua is built with debugging, kyua will abort with the following messages. + Without debugging, the tests in step 3 will reuse the context from step 1, + including stdout, stderr, and working directory, which are still populated + with stuff from step 1. When I found this bug, step 3 was + __test_cases_list__, which expects a certain format in stdout and failed + when it found something completely unrelated. +4. You can clean up with: chflags -R nouunlink /tmp/kyua.*; rm -rf /tmp/kyua.* + +$ cc -o pid_wrap -latf-c pid_wrap.c +$ kyua test +pid_wrap:leak_0 -> passed [0.001s] +pid_wrap:leak_1 -> passed [0.001s] +pid_wrap:leak_2 -> passed [0.001s] +pid_wrap:leak_3 -> passed [0.001s] +pid_wrap:leak_4 -> passed [0.001s] +pid_wrap:leak_5 -> passed [0.001s] +pid_wrap:leak_6 -> passed [0.001s] +pid_wrap:leak_7 -> passed [0.001s] +pid_wrap:leak_8 -> passed [0.001s] +pid_wrap:leak_9 -> passed [0.001s] +pid_wrap:pid_wrap -> passed [1.113s] +pid_wrap:pid_wrap_0 -> passed [0.001s] +pid_wrap:pid_wrap_1 -> passed [0.001s] +pid_wrap:pid_wrap_2 -> passed [0.001s] +pid_wrap:pid_wrap_3 -> *** /usr/src/main/contrib/kyua/utils/process/executor.cpp:779: Invariant check failed: PID 60876 already in all_exec_handles; not properly cleaned up or reused too fast +*** Fatal signal 6 received +*** Log file is /home/vangyzen/.kyua/logs/kyua.20221006-193544.log +*** Please report this problem to kyua-discuss@googlegroups.com detailing what you were doing before the crash happened; if possible, include the log file mentioned above +Abort trap (core dumped) + +#endif + +#include + +#include + +#include +#include +#include + +#include +#include + +void +leak_work_dir() +{ + int fd; + + ATF_REQUIRE((fd = open("unforgettable", O_CREAT|O_EXCL|O_WRONLY, 0600)) + >= 0); + ATF_REQUIRE_EQ(0, fchflags(fd, UF_NOUNLINK)); + ATF_REQUIRE_EQ(0, close(fd)); +} + +void +wrap_pids() +{ + pid_t begin, current, target; + bool wrapped; + + begin = getpid(); + target = begin - 15; + if (target <= 1) { + target += 99999; // PID_MAX + wrapped = true; + } else { + wrapped = false; + } + + ATF_REQUIRE(signal(SIGCHLD, SIG_IGN) != SIG_ERR); + + do { + current = vfork(); + if (current == 0) { + _exit(0); + } + ATF_REQUIRE(current != -1); + if (current < begin) { + wrapped = true; + } + } while (!wrapped || current < target); +} + +void +test_work_dir_reuse() +{ + // If kyua is built with debugging, it would abort here before the fix. +} + +void +clean_up() +{ + (void)system("chflags -R nouunlink ../.."); +} + +ATF_TEST_CASE_WITHOUT_HEAD(leak_0); +ATF_TEST_CASE_BODY(leak_0) { leak_work_dir(); } +ATF_TEST_CASE_WITHOUT_HEAD(leak_1); +ATF_TEST_CASE_BODY(leak_1) { leak_work_dir(); } +ATF_TEST_CASE_WITHOUT_HEAD(leak_2); +ATF_TEST_CASE_BODY(leak_2) { leak_work_dir(); } +ATF_TEST_CASE_WITHOUT_HEAD(leak_3); +ATF_TEST_CASE_BODY(leak_3) { leak_work_dir(); } +ATF_TEST_CASE_WITHOUT_HEAD(leak_4); +ATF_TEST_CASE_BODY(leak_4) { leak_work_dir(); } +ATF_TEST_CASE_WITHOUT_HEAD(leak_5); +ATF_TEST_CASE_BODY(leak_5) { leak_work_dir(); } +ATF_TEST_CASE_WITHOUT_HEAD(leak_6); +ATF_TEST_CASE_BODY(leak_6) { leak_work_dir(); } +ATF_TEST_CASE_WITHOUT_HEAD(leak_7); +ATF_TEST_CASE_BODY(leak_7) { leak_work_dir(); } +ATF_TEST_CASE_WITHOUT_HEAD(leak_8); +ATF_TEST_CASE_BODY(leak_8) { leak_work_dir(); } +ATF_TEST_CASE_WITHOUT_HEAD(leak_9); +ATF_TEST_CASE_BODY(leak_9) { leak_work_dir(); } + +ATF_TEST_CASE_WITHOUT_HEAD(pid_wrap); +ATF_TEST_CASE_BODY(pid_wrap) { wrap_pids(); } + +ATF_TEST_CASE_WITHOUT_HEAD(pid_wrap_0); +ATF_TEST_CASE_BODY(pid_wrap_0) { test_work_dir_reuse(); } +ATF_TEST_CASE_WITHOUT_HEAD(pid_wrap_1); +ATF_TEST_CASE_BODY(pid_wrap_1) { test_work_dir_reuse(); } +ATF_TEST_CASE_WITHOUT_HEAD(pid_wrap_2); +ATF_TEST_CASE_BODY(pid_wrap_2) { test_work_dir_reuse(); } +ATF_TEST_CASE_WITHOUT_HEAD(pid_wrap_3); +ATF_TEST_CASE_BODY(pid_wrap_3) { test_work_dir_reuse(); } +ATF_TEST_CASE_WITHOUT_HEAD(pid_wrap_4); +ATF_TEST_CASE_BODY(pid_wrap_4) { test_work_dir_reuse(); } +ATF_TEST_CASE_WITHOUT_HEAD(pid_wrap_5); +ATF_TEST_CASE_BODY(pid_wrap_5) { test_work_dir_reuse(); } +ATF_TEST_CASE_WITHOUT_HEAD(pid_wrap_6); +ATF_TEST_CASE_BODY(pid_wrap_6) { test_work_dir_reuse(); } +ATF_TEST_CASE_WITHOUT_HEAD(pid_wrap_7); +ATF_TEST_CASE_BODY(pid_wrap_7) { test_work_dir_reuse(); } +ATF_TEST_CASE_WITHOUT_HEAD(pid_wrap_8); +ATF_TEST_CASE_BODY(pid_wrap_8) { test_work_dir_reuse(); } +ATF_TEST_CASE_WITHOUT_HEAD(pid_wrap_9); +ATF_TEST_CASE_BODY(pid_wrap_9) { test_work_dir_reuse(); } + +ATF_TEST_CASE_WITHOUT_HEAD(really_clean_up); +ATF_TEST_CASE_BODY(really_clean_up) { clean_up(); } + +ATF_INIT_TEST_CASES(tcs) +{ + ATF_ADD_TEST_CASE(tcs, leak_0); + ATF_ADD_TEST_CASE(tcs, leak_1); + ATF_ADD_TEST_CASE(tcs, leak_2); + ATF_ADD_TEST_CASE(tcs, leak_3); + ATF_ADD_TEST_CASE(tcs, leak_4); + ATF_ADD_TEST_CASE(tcs, leak_5); + ATF_ADD_TEST_CASE(tcs, leak_6); + ATF_ADD_TEST_CASE(tcs, leak_7); + ATF_ADD_TEST_CASE(tcs, leak_8); + ATF_ADD_TEST_CASE(tcs, leak_9); + + ATF_ADD_TEST_CASE(tcs, pid_wrap); + + ATF_ADD_TEST_CASE(tcs, pid_wrap_0); + ATF_ADD_TEST_CASE(tcs, pid_wrap_1); + ATF_ADD_TEST_CASE(tcs, pid_wrap_2); + ATF_ADD_TEST_CASE(tcs, pid_wrap_3); + ATF_ADD_TEST_CASE(tcs, pid_wrap_4); + ATF_ADD_TEST_CASE(tcs, pid_wrap_5); + ATF_ADD_TEST_CASE(tcs, pid_wrap_6); + ATF_ADD_TEST_CASE(tcs, pid_wrap_7); + ATF_ADD_TEST_CASE(tcs, pid_wrap_8); + ATF_ADD_TEST_CASE(tcs, pid_wrap_9); + + ATF_ADD_TEST_CASE(tcs, really_clean_up); +}