aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorKaren Arutyunov <karen@codesynthesis.com>2020-10-10 17:22:46 +0300
committerKaren Arutyunov <karen@codesynthesis.com>2020-11-06 19:32:09 +0300
commitf41599c8e9435f3dfec60b872c2b4ae31177efdd (patch)
tree088f8d9bf906e4a2ed734e034699163c9ccc7306
parentac76a4fd2afff48a0d5db84592babe5cabef3a2c (diff)
Add support for test timeouts
-rwxr-xr-xdoc/cli.sh1
-rw-r--r--doc/manual.cli22
-rw-r--r--doc/testscript.cli52
-rw-r--r--libbuild2/adhoc-rule-buildscript.cxx25
-rw-r--r--libbuild2/adhoc-rule-buildscript.hxx9
-rw-r--r--libbuild2/algorithm.cxx2
-rw-r--r--libbuild2/build/script/script.cxx28
-rw-r--r--libbuild2/build/script/script.hxx26
-rw-r--r--libbuild2/file.cxx1
-rw-r--r--libbuild2/rule.cxx7
-rw-r--r--libbuild2/rule.hxx16
-rw-r--r--libbuild2/script/builtin-options.cxx271
-rw-r--r--libbuild2/script/builtin-options.hxx58
-rw-r--r--libbuild2/script/builtin-options.ixx9
-rw-r--r--libbuild2/script/builtin.cli5
-rw-r--r--libbuild2/script/parser.cxx95
-rw-r--r--libbuild2/script/parser.hxx16
-rw-r--r--libbuild2/script/run.cxx639
-rw-r--r--libbuild2/script/script.cxx26
-rw-r--r--libbuild2/script/script.hxx53
-rw-r--r--libbuild2/script/script.ixx35
-rw-r--r--libbuild2/script/timeout.cxx26
-rw-r--r--libbuild2/script/timeout.hxx52
-rw-r--r--libbuild2/script/timeout.ixx44
-rw-r--r--libbuild2/test/common.cxx72
-rw-r--r--libbuild2/test/common.hxx39
-rw-r--r--libbuild2/test/init.cxx38
-rw-r--r--libbuild2/test/module.cxx12
-rw-r--r--libbuild2/test/module.hxx2
-rw-r--r--libbuild2/test/operation.cxx36
-rw-r--r--libbuild2/test/rule.cxx149
-rw-r--r--libbuild2/test/script/parser+env.test.testscript20
-rw-r--r--libbuild2/test/script/script.cxx106
-rw-r--r--libbuild2/test/script/script.hxx61
-rw-r--r--libbuild2/utility.cxx18
-rw-r--r--libbuild2/utility.hxx7
-rw-r--r--tests/recipe/buildscript/testscript120
-rw-r--r--tests/test/script/builtin/sleep.testscript18
-rw-r--r--tests/test/script/runner/driver.cxx30
-rw-r--r--tests/test/script/runner/env.testscript29
-rw-r--r--tests/test/script/runner/set.testscript118
-rw-r--r--tests/test/script/runner/timeout.testscript503
-rw-r--r--tests/test/simple/generated/buildfile3
-rw-r--r--tests/test/simple/generated/driver.cxx32
-rw-r--r--tests/test/simple/generated/testscript57
45 files changed, 2779 insertions, 209 deletions
diff --git a/doc/cli.sh b/doc/cli.sh
index 21d38d2..d1fcb77 100755
--- a/doc/cli.sh
+++ b/doc/cli.sh
@@ -101,6 +101,7 @@ function compile_doc () # <file> <prefix> <suffix>
--link-regex '%bpkg(#.+)?%../../bpkg/doc/build2-package-manager-manual.xhtml$1%' \
--link-regex '%bdep([-.].+)%../../bdep/doc/bdep$1%' \
--link-regex '%testscript(#.+)?%build2-testscript-manual.xhtml$1%' \
+--link-regex '%build2(#.+)?%build2-build-system-manual.xhtml$1%' \
--output-prefix "$2" \
--output-suffix "$3" \
"$1"
diff --git a/doc/manual.cli b/doc/manual.cli
index 73f15a5..4d4e67d 100644
--- a/doc/manual.cli
+++ b/doc/manual.cli
@@ -5595,6 +5595,28 @@ treated specially. It enables all the tests at and under its directory. This
special treatment can be inhibited by specifying the target type explicitly
(for example, \c{dir{foo/\}}).
+The test execution time can be limited using the \c{config.test.timeout}
+variable. Its value has the \c{<operation-timeout>/<test-timeout>} form where
+the timeouts are specified in seconds and either of them (but not both) can be
+omitted. The left hand side sets the timeout for the whole \c{test} operation
+and the right hand side \- for individual tests. The zero value clears the
+previously set timeout. For example:
+
+\
+b test config.test.timeout=20 # Test operation.
+b test config.test.timeout=20/5 # Test operation and individual tests.
+b test config.test.timeout=/5 # Individual tests.
+\
+
+The test timeout can be specified on multiple nested root scopes. For example,
+we can specify a greater timeout for the entire build configuration and lesser
+ones for individual projects. The tests must complete before the nearest of
+the enclosing scope timeouts. Failed that, the timed out tests are terminated
+forcibly causing the entire \c{test} operation to fail. See also the
+\l{testscript#builtins-timeout \c{timeout}} builtin for specifying timeouts
+from within the tests and test groups.
+
+
\h1#module-install|\c{install} Module|
\N{This chapter is a work in progress and is incomplete.}
diff --git a/doc/testscript.cli b/doc/testscript.cli
index d838cc0..b68844b 100644
--- a/doc/testscript.cli
+++ b/doc/testscript.cli
@@ -1206,7 +1206,7 @@ token is an unquoted word, then the second token of the line is examined in
the \c{second_token} mode (see below). If it is a variable assignment (either
\c{+=}, \c{=+}, or \c{=}), then the line type is a variable line. Otherwise,
it is a test command line. Note that variables with computed names can only
-be set using the \l{#builtins-set \c{set} pseudo-builtin}.
+be set using the \l{#builtins-set \c{set}} pseudo-builtin.
The Testscript language defines the following distinct lexing modes (or
contexts):
@@ -2429,10 +2429,11 @@ with a newline.
\h#builtins-env|\c{env}|
\
-env [-u <name>]... [-] [<name>=<value>]... -- <cmd>
+env [-t <sec>] [-u <name>]... [-] [<name>=<value>]... -- <cmd>
\
-Run a command adding/removing the variables to/from the environment.
+Run a command limiting its execution time and/or adding/removing the variables
+to/from the environment.
Note that \c{env} is a \i{pseudo-builtin}. In particular, its name and the
\c{--} separator must be specified \i{literally} on the command line.
@@ -2449,6 +2450,11 @@ env - --unset=FOO -- $*
\dl|
+\li|\n\c{-t|--timeout <sec>}
+
+ Terminate the command if it fails to complete within the specified number
+ of seconds. See also \l{#builtins-timeout \c{timeout}} builtin.|
+
\li|\n\c{-u|--unset <name>}
Remove the specified variable from the environment.||
@@ -2823,6 +2829,46 @@ Test the specified \i{path} according to one of the following options. Succeed
Note that tests dereference symbolic links.
+\h#builtins-timeout|\c{timeout}|
+
+\
+timeout [-s] [<group-timeout>]/[<test-timeout>]
+timeout [-s] <timeout>
+\
+
+Specify test and/or test group timeout.
+
+The first form sets the test group and/or individual test timeouts and can
+only be used as a setup command. Either of the timeouts (but not both) can be
+omitted.
+
+The second form sets the test group timeout if used as a setup or teardown
+command and the remaining test fragment timeout if used as a test command.
+
+In both forms the timeouts are specified in seconds with the zero value
+clearing the previously set timeout.
+
+Note that \c{timeout} is a \i{pseudo-builtin}. In particular, it must be the
+only command in the pipe expression, it either succeeds or terminates
+abnormally, and its standard streams cannot be redirected.
+
+The timeouts can be set on multiple levels: via the \c{config.test.timeout}
+variable on the (potentially nested) project root scopes (see
+\l{build2#module-test \c{test}} module for details), with the \c{timeout}
+builtin in the nested test group scopes and the test scope, and with the
+\c{env} builtin for individual commands. Each command must complete before the
+nearest timeout from its timeout hierarchy. Failed that, a command is
+terminated forcibly causing the entire \c{test} operation to fail unless the
+expired timeout was specified with the \c{--success} option, in which case the
+timed out command is assumed to have succeeded.
+
+\dl|
+
+\li|\n\c{-s|--success}
+
+ Assume a command terminated due to this timeout to have succeeded.||
+
+
\h#builtins-touch|\c{touch}|
\
diff --git a/libbuild2/adhoc-rule-buildscript.cxx b/libbuild2/adhoc-rule-buildscript.cxx
index 3dbe817..a09d2c7 100644
--- a/libbuild2/adhoc-rule-buildscript.cxx
+++ b/libbuild2/adhoc-rule-buildscript.cxx
@@ -124,8 +124,21 @@ namespace build2
}
recipe adhoc_buildscript_rule::
- apply (action a, target& t, match_extra&) const
+ apply (action a, target& t, match_extra& e) const
{
+ return apply (a, t, e, nullopt);
+ }
+
+ recipe adhoc_buildscript_rule::
+ apply (action a, target& t, match_extra&, const optional<timestamp>& d) const
+ {
+ // We don't support deadlines of any of these case (see below).
+ //
+ if (d && (a.outer () ||
+ t.data<bool> () ||
+ (a == perform_update_id && t.is_a<file> ())))
+ return empty_recipe;
+
// If this is an outer operation (e.g., update-for-test), then delegate to
// the inner.
//
@@ -171,9 +184,9 @@ namespace build2
}
else
{
- return [this] (action a, const target& t)
+ return [d, this] (action a, const target& t)
{
- return default_action (a, t);
+ return default_action (a, t, d);
};
}
}
@@ -584,7 +597,9 @@ namespace build2
}
target_state adhoc_buildscript_rule::
- default_action (action a, const target& t) const
+ default_action (action a,
+ const target& t,
+ const optional<timestamp>& deadline) const
{
tracer trace ("adhoc_buildscript_rule::default_action");
@@ -597,7 +612,7 @@ namespace build2
const scope& bs (t.base_scope ());
const scope& rs (*bs.root_scope ());
- build::script::environment e (a, t, script.temp_dir);
+ build::script::environment e (a, t, script.temp_dir, deadline);
build::script::parser p (ctx);
if (verb == 1)
diff --git a/libbuild2/adhoc-rule-buildscript.hxx b/libbuild2/adhoc-rule-buildscript.hxx
index d7543c4..89515eb 100644
--- a/libbuild2/adhoc-rule-buildscript.hxx
+++ b/libbuild2/adhoc-rule-buildscript.hxx
@@ -18,7 +18,8 @@ namespace build2
//
// Note: not exported and should not be used directly (i.e., registered).
//
- class adhoc_buildscript_rule: public adhoc_rule
+ class adhoc_buildscript_rule: public adhoc_rule,
+ public adhoc_rule_with_deadline
{
public:
virtual bool
@@ -28,11 +29,15 @@ namespace build2
virtual recipe
apply (action, target&, match_extra&) const override;
+ virtual recipe
+ apply (action, target&, match_extra&,
+ const optional<timestamp>&) const override;
+
target_state
perform_update_file (action, const target&) const;
target_state
- default_action (action, const target&) const;
+ default_action (action, const target&, const optional<timestamp>&) const;
adhoc_buildscript_rule (const location& l, size_t b)
: adhoc_rule ("<ad hoc buildscript recipe>", l, b) {}
diff --git a/libbuild2/algorithm.cxx b/libbuild2/algorithm.cxx
index b540138..e535af3 100644
--- a/libbuild2/algorithm.cxx
+++ b/libbuild2/algorithm.cxx
@@ -527,7 +527,7 @@ namespace build2
{
case target_decl::prereq_new:
{
- dr << info << "target " << t << " is no declared in any buildfile";
+ dr << info << "target " << t << " is not declared in any buildfile";
if (t.is_a<file> ())
dr << info << "perhaps it is a missing source file?";
diff --git a/libbuild2/build/script/script.cxx b/libbuild2/build/script/script.cxx
index 3485f54..c6b57c3 100644
--- a/libbuild2/build/script/script.cxx
+++ b/libbuild2/build/script/script.cxx
@@ -7,6 +7,8 @@
#include <libbuild2/target.hxx>
+#include <libbuild2/script/timeout.hxx>
+
#include <libbuild2/build/script/parser.hxx>
using namespace std;
@@ -17,12 +19,17 @@ namespace build2
{
namespace script
{
+ using build2::script::to_deadline;
+
// environment
//
static const optional<string> wd_name ("current directory");
environment::
- environment (action a, const target_type& t, bool temp)
+ environment (action a,
+ const target_type& t,
+ bool temp,
+ const optional<timestamp>& dl)
: build2::script::environment (
t.ctx,
cast<target_triplet> (t.ctx.global_scope["build.host"]),
@@ -32,7 +39,8 @@ namespace build2
redirect (redirect_type::merge, 2),
redirect (redirect_type::pass)),
target (t),
- vars (context, false /* global */)
+ vars (context, false /* global */),
+ script_deadline (to_deadline (dl, false /* success */))
{
// Set special variables.
//
@@ -58,8 +66,10 @@ namespace build2
//
names ns;
for (const target_type* pt: t.prerequisite_targets[a])
+ {
if (pt != nullptr)
pt->as_name (ns);
+ }
assign (var_pool.insert ("<")) = move (ns);
}
@@ -231,6 +241,20 @@ namespace build2
return r;
}
+
+ void environment::
+ set_timeout (const string& t, bool success, const location& l)
+ {
+ fragment_deadline =
+ to_deadline (parse_deadline (t, "buildscript timeout", l),
+ success);
+ }
+
+ optional<deadline> environment::
+ effective_deadline ()
+ {
+ return earlier (script_deadline, fragment_deadline);
+ }
}
}
}
diff --git a/libbuild2/build/script/script.hxx b/libbuild2/build/script/script.hxx
index f4d70ac..9284813 100644
--- a/libbuild2/build/script/script.hxx
+++ b/libbuild2/build/script/script.hxx
@@ -26,6 +26,8 @@ namespace build2
using build2::script::redirect_type;
using build2::script::expr_term;
using build2::script::command_expr;
+ using build2::script::deadline;
+ using build2::script::timeout;
// Notes:
//
@@ -83,7 +85,10 @@ namespace build2
public:
using target_type = build2::target;
- environment (action, const target_type&, bool temp_dir);
+ environment (action,
+ const target_type&,
+ bool temp_dir,
+ const optional<timestamp>& deadline = nullopt);
environment (environment&&) = delete;
environment (const environment&) = delete;
@@ -98,7 +103,7 @@ namespace build2
// Script-local variable pool and map.
//
// Note that it may be tempting to reuse the rule-specific variables
- // for this but they should no be modified during execution (i.e.,
+ // for this but they should not be modified during execution (i.e.,
// they are for intra-rule communication; perhaps we could have a
// special builtin that sets such variables during match).
//
@@ -125,12 +130,29 @@ namespace build2
//
auto_rmdir temp_dir;
+ // The whole script and the remaining script fragment execution
+ // deadlines (the latter is set by the timeout builtin).
+ //
+ optional<deadline> script_deadline;
+ optional<deadline> fragment_deadline;
+
virtual void
set_variable (string&& name,
names&&,
const string& attrs,
const location&) override;
+ // Parse the specified in seconds timeout and set the remaining script
+ // fragment execution deadline. Reset it to nullopt on zero.
+ //
+ virtual void
+ set_timeout (const string&, bool success, const location&) override;
+
+ // Return the nearest of the script and fragment execution deadlines.
+ //
+ virtual optional<deadline>
+ effective_deadline () override;
+
virtual void
create_temp_dir () override;
diff --git a/libbuild2/file.cxx b/libbuild2/file.cxx
index 701367f..23bd103 100644
--- a/libbuild2/file.cxx
+++ b/libbuild2/file.cxx
@@ -1767,6 +1767,7 @@ namespace build2
{
is.close ();
pr.kill ();
+ pr.wait ();
throw_generic_ios_failure (EFBIG, "output too large");
}
}
diff --git a/libbuild2/rule.cxx b/libbuild2/rule.cxx
index a671b04..6af074c 100644
--- a/libbuild2/rule.cxx
+++ b/libbuild2/rule.cxx
@@ -349,6 +349,13 @@ namespace build2
{
}
+ // adhoc_rule_with_deadline (vtable)
+ //
+ adhoc_rule_with_deadline::
+ ~adhoc_rule_with_deadline ()
+ {
+ }
+
// Scope operation callback that cleans up recipe builds.
//
target_state adhoc_rule::
diff --git a/libbuild2/rule.hxx b/libbuild2/rule.hxx
index fa61bc5..8796659 100644
--- a/libbuild2/rule.hxx
+++ b/libbuild2/rule.hxx
@@ -205,6 +205,22 @@ namespace build2
static target_state
clean_recipes_build (action, const scope&, const dir&);
};
+
+ // A mix-in interface for ad hoc rules that support recipes with deadlines.
+ //
+ class adhoc_rule_with_deadline
+ {
+ public:
+ virtual
+ ~adhoc_rule_with_deadline ();
+
+ // Return empty recipe if one with the deadline cannot be provided for
+ // this action. In this case the caller may fallback to the normal
+ // apply().
+ //
+ virtual recipe
+ apply (action, target&, match_extra&, const optional<timestamp>&) const = 0;
+ };
}
#endif // LIBBUILD2_RULE_HXX
diff --git a/libbuild2/script/builtin-options.cxx b/libbuild2/script/builtin-options.cxx
index abf325f..9b91bd2 100644
--- a/libbuild2/script/builtin-options.cxx
+++ b/libbuild2/script/builtin-options.cxx
@@ -652,6 +652,277 @@ namespace build2
return r;
}
+
+ // timeout_options
+ //
+
+ timeout_options::
+ timeout_options ()
+ : success_ ()
+ {
+ }
+
+ timeout_options::
+ timeout_options (int& argc,
+ char** argv,
+ bool erase,
+ ::build2::script::cli::unknown_mode opt,
+ ::build2::script::cli::unknown_mode arg)
+ : success_ ()
+ {
+ ::build2::script::cli::argv_scanner s (argc, argv, erase);
+ _parse (s, opt, arg);
+ }
+
+ timeout_options::
+ timeout_options (int start,
+ int& argc,
+ char** argv,
+ bool erase,
+ ::build2::script::cli::unknown_mode opt,
+ ::build2::script::cli::unknown_mode arg)
+ : success_ ()
+ {
+ ::build2::script::cli::argv_scanner s (start, argc, argv, erase);
+ _parse (s, opt, arg);
+ }
+
+ timeout_options::
+ timeout_options (int& argc,
+ char** argv,
+ int& end,
+ bool erase,
+ ::build2::script::cli::unknown_mode opt,
+ ::build2::script::cli::unknown_mode arg)
+ : success_ ()
+ {
+ ::build2::script::cli::argv_scanner s (argc, argv, erase);
+ _parse (s, opt, arg);
+ end = s.end ();
+ }
+
+ timeout_options::
+ timeout_options (int start,
+ int& argc,
+ char** argv,
+ int& end,
+ bool erase,
+ ::build2::script::cli::unknown_mode opt,
+ ::build2::script::cli::unknown_mode arg)
+ : success_ ()
+ {
+ ::build2::script::cli::argv_scanner s (start, argc, argv, erase);
+ _parse (s, opt, arg);
+ end = s.end ();
+ }
+
+ timeout_options::
+ timeout_options (::build2::script::cli::scanner& s,
+ ::build2::script::cli::unknown_mode opt,
+ ::build2::script::cli::unknown_mode arg)
+ : success_ ()
+ {
+ _parse (s, opt, arg);
+ }
+
+ typedef
+ std::map<std::string, void (*) (timeout_options&, ::build2::script::cli::scanner&)>
+ _cli_timeout_options_map;
+
+ static _cli_timeout_options_map _cli_timeout_options_map_;
+
+ struct _cli_timeout_options_map_init
+ {
+ _cli_timeout_options_map_init ()
+ {
+ _cli_timeout_options_map_["--success"] =
+ &::build2::script::cli::thunk< timeout_options, bool, &timeout_options::success_ >;
+ _cli_timeout_options_map_["-s"] =
+ &::build2::script::cli::thunk< timeout_options, bool, &timeout_options::success_ >;
+ }
+ };
+
+ static _cli_timeout_options_map_init _cli_timeout_options_map_init_;
+
+ bool timeout_options::
+ _parse (const char* o, ::build2::script::cli::scanner& s)
+ {
+ _cli_timeout_options_map::const_iterator i (_cli_timeout_options_map_.find (o));
+
+ if (i != _cli_timeout_options_map_.end ())
+ {
+ (*(i->second)) (*this, s);
+ return true;
+ }
+
+ return false;
+ }
+
+ bool timeout_options::
+ _parse (::build2::script::cli::scanner& s,
+ ::build2::script::cli::unknown_mode opt_mode,
+ ::build2::script::cli::unknown_mode arg_mode)
+ {
+ // Can't skip combined flags (--no-combined-flags).
+ //
+ assert (opt_mode != ::build2::script::cli::unknown_mode::skip);
+
+ bool r = false;
+ bool opt = true;
+
+ while (s.more ())
+ {
+ const char* o = s.peek ();
+
+ if (std::strcmp (o, "--") == 0)
+ {
+ opt = false;
+ s.skip ();
+ r = true;
+ continue;
+ }
+
+ if (opt)
+ {
+ if (_parse (o, s))
+ {
+ r = true;
+ continue;
+ }
+
+ if (std::strncmp (o, "-", 1) == 0 && o[1] != '\0')
+ {
+ // Handle combined option values.
+ //
+ std::string co;
+ if (const char* v = std::strchr (o, '='))
+ {
+ co.assign (o, 0, v - o);
+ ++v;
+
+ int ac (2);
+ char* av[] =
+ {
+ const_cast<char*> (co.c_str ()),
+ const_cast<char*> (v)
+ };
+
+ ::build2::script::cli::argv_scanner ns (0, ac, av);
+
+ if (_parse (co.c_str (), ns))
+ {
+ // Parsed the option but not its value?
+ //
+ if (ns.end () != 2)
+ throw ::build2::script::cli::invalid_value (co, v);
+
+ s.next ();
+ r = true;
+ continue;
+ }
+ else
+ {
+ // Set the unknown option and fall through.
+ //
+ o = co.c_str ();
+ }
+ }
+
+ // Handle combined flags.
+ //
+ char cf[3];
+ {
+ const char* p = o + 1;
+ for (; *p != '\0'; ++p)
+ {
+ if (!((*p >= 'a' && *p <= 'z') ||
+ (*p >= 'A' && *p <= 'Z') ||
+ (*p >= '0' && *p <= '9')))
+ break;
+ }
+
+ if (*p == '\0')
+ {
+ for (p = o + 1; *p != '\0'; ++p)
+ {
+ std::strcpy (cf, "-");
+ cf[1] = *p;
+ cf[2] = '\0';
+
+ int ac (1);
+ char* av[] =
+ {
+ cf
+ };
+
+ ::build2::script::cli::argv_scanner ns (0, ac, av);
+
+ if (!_parse (cf, ns))
+ break;
+ }
+
+ if (*p == '\0')
+ {
+ // All handled.
+ //
+ s.next ();
+ r = true;
+ continue;
+ }
+ else
+ {
+ // Set the unknown option and fall through.
+ //
+ o = cf;
+ }
+ }
+ }
+
+ switch (opt_mode)
+ {
+ case ::build2::script::cli::unknown_mode::skip:
+ {
+ s.skip ();
+ r = true;
+ continue;
+ }
+ case ::build2::script::cli::unknown_mode::stop:
+ {
+ break;
+ }
+ case ::build2::script::cli::unknown_mode::fail:
+ {
+ throw ::build2::script::cli::unknown_option (o);
+ }
+ }
+
+ break;
+ }
+ }
+
+ switch (arg_mode)
+ {
+ case ::build2::script::cli::unknown_mode::skip:
+ {
+ s.skip ();
+ r = true;
+ continue;
+ }
+ case ::build2::script::cli::unknown_mode::stop:
+ {
+ break;
+ }
+ case ::build2::script::cli::unknown_mode::fail:
+ {
+ throw ::build2::script::cli::unknown_argument (o);
+ }
+ }
+
+ break;
+ }
+
+ return r;
+ }
}
}
diff --git a/libbuild2/script/builtin-options.hxx b/libbuild2/script/builtin-options.hxx
index 5a3f153..d0d3c31 100644
--- a/libbuild2/script/builtin-options.hxx
+++ b/libbuild2/script/builtin-options.hxx
@@ -326,6 +326,64 @@ namespace build2
bool newline_;
bool whitespace_;
};
+
+ class timeout_options
+ {
+ public:
+ timeout_options ();
+
+ timeout_options (int& argc,
+ char** argv,
+ bool erase = false,
+ ::build2::script::cli::unknown_mode option = ::build2::script::cli::unknown_mode::fail,
+ ::build2::script::cli::unknown_mode argument = ::build2::script::cli::unknown_mode::stop);
+
+ timeout_options (int start,
+ int& argc,
+ char** argv,
+ bool erase = false,
+ ::build2::script::cli::unknown_mode option = ::build2::script::cli::unknown_mode::fail,
+ ::build2::script::cli::unknown_mode argument = ::build2::script::cli::unknown_mode::stop);
+
+ timeout_options (int& argc,
+ char** argv,
+ int& end,
+ bool erase = false,
+ ::build2::script::cli::unknown_mode option = ::build2::script::cli::unknown_mode::fail,
+ ::build2::script::cli::unknown_mode argument = ::build2::script::cli::unknown_mode::stop);
+
+ timeout_options (int start,
+ int& argc,
+ char** argv,
+ int& end,
+ bool erase = false,
+ ::build2::script::cli::unknown_mode option = ::build2::script::cli::unknown_mode::fail,
+ ::build2::script::cli::unknown_mode argument = ::build2::script::cli::unknown_mode::stop);
+
+ timeout_options (::build2::script::cli::scanner&,
+ ::build2::script::cli::unknown_mode option = ::build2::script::cli::unknown_mode::fail,
+ ::build2::script::cli::unknown_mode argument = ::build2::script::cli::unknown_mode::stop);
+
+ // Option accessors.
+ //
+ const bool&
+ success () const;
+
+ // Implementation details.
+ //
+ protected:
+ bool
+ _parse (const char*, ::build2::script::cli::scanner&);
+
+ private:
+ bool
+ _parse (::build2::script::cli::scanner&,
+ ::build2::script::cli::unknown_mode option,
+ ::build2::script::cli::unknown_mode argument);
+
+ public:
+ bool success_;
+ };
}
}
diff --git a/libbuild2/script/builtin-options.ixx b/libbuild2/script/builtin-options.ixx
index dc59f98..5edf31a 100644
--- a/libbuild2/script/builtin-options.ixx
+++ b/libbuild2/script/builtin-options.ixx
@@ -173,6 +173,15 @@ namespace build2
{
return this->whitespace_;
}
+
+ // timeout_options
+ //
+
+ inline const bool& timeout_options::
+ success () const
+ {
+ return this->success_;
+ }
}
}
diff --git a/libbuild2/script/builtin.cli b/libbuild2/script/builtin.cli
index 68db23e..1a6f523 100644
--- a/libbuild2/script/builtin.cli
+++ b/libbuild2/script/builtin.cli
@@ -17,5 +17,10 @@ namespace build2
bool --newline|-n;
bool --whitespace|-w;
};
+
+ class timeout_options
+ {
+ bool --success|-s;
+ };
}
}
diff --git a/libbuild2/script/parser.cxx b/libbuild2/script/parser.cxx
index d5cff1a..b4184ea 100644
--- a/libbuild2/script/parser.cxx
+++ b/libbuild2/script/parser.cxx
@@ -1027,7 +1027,9 @@ namespace build2
bool env (false);
if (prog && tt == type::word && t.value == "env")
{
- c.variables = parse_env_builtin (t, tt);
+ parsed_env r (parse_env_builtin (t, tt));
+ c.variables = move (r.variables);
+ c.timeout = r.timeout;
env = true;
}
@@ -1287,7 +1289,7 @@ namespace build2
return make_pair (move (expr), move (hd));
}
- environment_vars parser::
+ parser::parsed_env parser::
parse_env_builtin (token& t, token_type& tt)
{
// enter: 'env' word token
@@ -1295,11 +1297,10 @@ namespace build2
next (t, tt); // Skip 'env'.
- // Note that the -u option and its value can belong to the different
- // name chunks. That's why we parse the env builtin arguments in the
- // chunking mode into the argument/location pair list up to the '--'
- // separator and parse this list into the variable sets/unsets
- // afterwords.
+ // Note that an option name and value can belong to different name
+ // chunks. That's why we parse the env builtin arguments in the chunking
+ // mode into the argument/location pair list up to the '--' separator
+ // and parse this list into the variable sets/unsets afterwords.
//
// Align the size with environment_vars (double because of -u <var>
// which is two arguments).
@@ -1352,13 +1353,13 @@ namespace build2
// Parse the env builtin options and arguments.
//
- environment_vars r;
+ parsed_env r;
// Note: args is empty in the pre-parse mode.
//
auto i (as.begin ()), e (as.end ());
- // Parse the variable unsets (from options).
+ // Parse options (the timeout and variable unsets).
//
for (; i != e; ++i)
{
@@ -1372,32 +1373,76 @@ namespace build2
break;
}
- // Unset the variable, adding its name to the resulting variable list.
+ // We should probably switch to CLI if we need anything more
+ // elaborate. Note however, that we will have no precise error
+ // location then.
//
- auto unset = [&r, &i, this] (string&& v, const char* o)
+
+ // If this is an option represented with its long or short name, then
+ // return its value as string and nullopt otherwise. In the former
+ // case strip the value assignment from the option, if it is in the
+ // <name>=<value> form, and fail if the option value is empty.
+ //
+ auto str = [&i, &e, &o, &l, this] (const char* lo, const char* so)
{
- if (v.empty ())
+ optional<string> r;
+
+ if (o == lo || o == so)
+ {
+ if (++i == e)
+ fail (l) << "env: missing value for option '" << o << "'";
+
+ r = move (i->first);
+ }
+ else
+ {
+ size_t n (string::traits_type::length (lo));
+
+ if (o.compare (0, n, lo) == 0 && o[n] == '=')
+ {
+ r = string (o, n + 1);
+ o.resize (n);
+ }
+ }
+
+ if (r && r->empty ())
fail (i->second) << "env: empty value for option '" << o << "'";
- if (v.find ('=') != string::npos)
- fail (i->second) << "env: invalid value '" << v << "' for "
- << "option '" << o << "': contains '='";
+ return r;
+ };
+
+ // As above but convert the option value to a number and fail on
+ // error.
+ //
+ auto num = [&i, &o, &str, this] (const char* ln, const char* sn)
+ {
+ optional<uint64_t> r;
+ if (optional<string> s = str (ln, sn))
+ {
+ r = parse_number (*s);
+
+ if (!r)
+ fail (i->second) << "env: invalid value '" << *s
+ << "' for option '" << o << "'";
+ }
- r.push_back (move (v));
+ return r;
};
- // If this is the --unset|-u option then add the variable unset and
- // bail out to parsing the variable sets otherwise.
+ // Parse a known option or bail out to parsing the variable sets.
//
- if (o == "--unset" || o == "-u")
+ if (optional<uint64_t> v = num ("--timeout", "-t"))
{
- if (++i == e)
- fail (l) << "env: missing value for option '" << o << "'";
+ r.timeout = chrono::seconds (*v);
+ }
+ else if (optional<string> v = str ("--unset", "-u"))
+ {
+ if (v->find ('=') != string::npos)
+ fail (i->second) << "env: invalid value '" << *v << "' for "
+ << "option '" << o << "': contains '='";
- unset (move (i->first), o.c_str ());
+ r.variables.push_back (move (*v));
}
- else if (o.compare (0, 8, "--unset=") == 0)
- unset (string (o, 8), "--unset");
else
break;
}
@@ -1421,7 +1466,7 @@ namespace build2
// Add the variable set to the resulting list.
//
- r.push_back (move (a));
+ r.variables.push_back (move (a));
}
return r;
diff --git a/libbuild2/script/parser.hxx b/libbuild2/script/parser.hxx
index da69591..9098b3c 100644
--- a/libbuild2/script/parser.hxx
+++ b/libbuild2/script/parser.hxx
@@ -130,12 +130,18 @@ namespace build2
pre_parse_line_start (token&, token_type&, lexer_mode);
// Parse the env pseudo-builtin arguments up to the program name. Return
- // the list of the variables that should be unset ("name") and/or set
- // ("name=value") in the command environment and the token/type that
- // starts the program name. Note that the variable unsets come first, if
- // present.
+ // the program execution timeout, the list of the variables that should
+ // be unset ("name") and/or set ("name=value") in the command
+ // environment, and the token/type that starts the program name. Note
+ // that the variable unsets come first, if present.
//
- environment_vars
+ struct parsed_env
+ {
+ optional<duration> timeout;
+ environment_vars variables;
+ };
+
+ parsed_env
parse_env_builtin (token&, token_type&);
// Execute.
diff --git a/libbuild2/script/run.cxx b/libbuild2/script/run.cxx
index 8c71b32..f13359f 100644
--- a/libbuild2/script/run.cxx
+++ b/libbuild2/script/run.cxx
@@ -3,6 +3,12 @@
#include <libbuild2/script/run.hxx>
+#ifndef _WIN32
+# include <signal.h> // SIG*
+#else
+# include <libbutl/win32-utility.hxx> // DBG_TERMINATE_PROCESS
+#endif
+
#include <ios> // streamsize
#include <libbutl/regex.mxx>
@@ -15,6 +21,7 @@
#include <libbuild2/diagnostics.hxx>
#include <libbuild2/script/regex.hxx>
+#include <libbuild2/script/timeout.hxx>
#include <libbuild2/script/builtin-options.hxx>
using namespace std;
@@ -757,6 +764,39 @@ namespace build2
return false;
}
+ // The timeout pseudo-builtin: set the script timeout. See the script-
+ // specific set_timeout() implementations for the exact semantics.
+ //
+ // timeout [--success|-s] <timeout>
+ //
+ static void
+ timeout_builtin (environment& env,
+ const strings& args,
+ const location& ll)
+ {
+ try
+ {
+ // Parse arguments.
+ //
+ cli::vector_scanner scan (args);
+ timeout_options ops (scan);
+
+ if (!scan.more ())
+ fail (ll) << "missing timeout";
+
+ string a (scan.next ());
+
+ if (scan.more ())
+ fail (ll) << "unexpected argument '" << scan.next () << "'";
+
+ env.set_timeout (a, ops.success (), ll);
+ }
+ catch (const cli::exception& e)
+ {
+ fail (ll) << "timeout: " << e;
+ }
+ }
+
// The exit pseudo-builtin: exit the script successfully, or print the
// diagnostics and exit the script unsuccessfully. Always throw exit
// exception.
@@ -786,6 +826,16 @@ namespace build2
throw exit (false);
}
+ // Return the command program path for diagnostics.
+ //
+ static inline path
+ cmd_path (const command& c)
+ {
+ return c.program.initial == nullptr // Not pre-searched?
+ ? c.program.recall
+ : path (c.program.recall_string ());
+ }
+
// The set pseudo-builtin: set variable from the stdin input.
//
// set [-e|--exact] [(-n|--newline)|(-w|--whitespace)] [<attr>] <var>
@@ -794,16 +844,12 @@ namespace build2
set_builtin (environment& env,
const strings& args,
auto_fd in,
+ const optional<deadline>& dl,
+ const command& deadline_cmd,
const location& ll)
{
try
{
- // Do not throw when eofbit is set (end of stream reached), and
- // when failbit is set (read operation failed to extract any
- // character).
- //
- ifdstream cin (move (in), ifdstream::badbit);
-
// Parse arguments.
//
cli::vector_scanner scan (args);
@@ -828,62 +874,136 @@ namespace build2
if (vname.empty ())
fail (ll) << "empty variable name";
- // Read the input.
+ // Read out the stream content into a string while keeping an eye on
+ // the deadline. Then parse it according to the split mode.
//
- cin.peek (); // Sets eofbit for an empty stream.
-
- names ns;
- while (!cin.eof ())
+ string s;
{
- // Read next element that depends on the whitespace mode being
- // enabled or not. For the later case it also make sense to strip
- // the trailing CRs that can appear while, for example,
- // cross-testing Windows target or as a part of msvcrt junk
- // production (see above).
+ ifdstream cin;
+
+ // If the execution deadline is specified, then turn the stream into
+ // the non-blocking mode reading its content in chunks and with a
+ // single operation otherwise. If the specified deadline is reached
+ // while reading the stream, then bail out for the successful
+ // deadline and fail otherwise. Note that in the former case the
+ // variable value will be incomplete, but we leave it to the caller
+ // to handle that.
//
- string s;
- if (ops.whitespace ())
- cin >> s;
- else
+ if (dl)
{
- getline (cin, s);
+ fdselect_set fds {in.get ()};
+ cin.open (move (in), fdstream_mode::non_blocking);
- while (!s.empty () && s.back () == '\r')
- s.pop_back ();
- }
+ const timestamp& dlt (dl->value);
- // If failbit is set then we read nothing into the string as eof is
- // reached. That in particular means that the stream has trailing
- // whitespaces (possibly including newlines) if the whitespace mode
- // is enabled, or the trailing newline otherwise. If so then
- // we append the "blank" to the variable value in the exact mode
- // prior to bailing out.
- //
- if (cin.fail ())
- {
- if (ops.exact ())
+ for (char buf[4096];; )
{
- if (ops.whitespace () || ops.newline ())
- ns.emplace_back (move (s)); // Reuse empty string.
- else if (ns.empty ())
- ns.emplace_back ("\n");
- else
- ns[0].value += '\n';
- }
+ timestamp now (system_clock::now ());
- break;
- }
+ if (dlt <= now || ifdselect (fds, dlt - now) == 0)
+ {
+ if (!dl->success)
+ fail (ll) << cmd_path (deadline_cmd)
+ << " terminated: execution timeout expired";
+ else
+ break;
+ }
+
+ streamsize n (cin.readsome (buf, sizeof (buf)));
+
+ // Bail out if eos is reached.
+ //
+ if (n == 0)
+ break;
- if (ops.whitespace () || ops.newline () || ns.empty ())
- ns.emplace_back (move (s));
+ s.append (buf, n);
+ }
+ }
else
{
- ns[0].value += '\n';
- ns[0].value += s;
+ cin.open (move (in));
+ s = cin.read_text ();
}
+
+ cin.close ();
}
- cin.close ();
+ // Parse the stream content into the variable value.
+ //
+ names ns;
+
+ if (!s.empty ())
+ {
+ if (ops.whitespace ()) // The whitespace mode.
+ {
+ // Note that we collapse multiple consecutive whitespaces.
+ //
+ for (size_t p (0); p != string::npos; )
+ {
+ // Skip the whitespaces.
+ //
+ const char* sep (" \n\r\t");
+ size_t b (s.find_first_not_of (sep, p));
+
+ if (b != string::npos) // Word beginning.
+ {
+ size_t e (s.find_first_of (sep, b)); // Find the word end.
+ ns.emplace_back (string (s, b, e != string::npos ? e - b : e));
+
+ p = e;
+ }
+ else // Trailings whitespaces.
+ {
+ // Append the trailing "blank" after the trailing whitespaces
+ // in the exact mode.
+ //
+ if (ops.exact ())
+ ns.emplace_back (empty_string);
+
+ // Bail out since the end of the string is reached.
+ //
+ break;
+ }
+ }
+ }
+ else // The newline or no-split mode.
+ {
+ // Note that we don't collapse multiple consecutive newlines.
+ //
+ // Note also that we always sanitize CRs so this loop is always
+ // needed.
+ //
+ for (size_t p (0); p != string::npos; )
+ {
+ size_t e (s.find ('\n', p));
+ string l (s, p, e != string::npos ? e - p : e);
+
+ // Strip the trailing CRs that can appear while, for example,
+ // cross-testing Windows target or as a part of msvcrt junk
+ // production (see above).
+ //
+ while (!l.empty () && l.back () == '\r')
+ l.pop_back ();
+
+ // Append the line.
+ //
+ if (!l.empty () || // Non-empty.
+ e != string::npos || // Empty, non-trailing.
+ ops.exact ()) // Empty, trailing, in the exact mode.
+ {
+ if (ops.newline () || ns.empty ())
+ ns.emplace_back (move (l));
+ else
+ {
+ ns[0].value += '\n';
+ ns[0].value += l;
+ }
+ }
+
+ p = e != string::npos ? e + 1 : e;
+ }
+ }
+ }
env.set_variable (move (vname),
move (ns),
@@ -915,24 +1035,70 @@ namespace build2
name);
}
+ // Stack-allocated linked list of information about the running pipeline
+ // processes and builtins.
+ //
+ struct pipe_command
+ {
+ // We could probably use a union here, but let's keep it simple for now
+ // (one is NULL).
+ //
+ process* proc;
+ builtin* bltn;
+
+ // True if this command has been terminated.
+ //
+ bool terminated = false;
+
+ // Only for diagnostics.
+ //
+ const command& cmd;
+ const location& loc;
+
+ pipe_command* prev; // NULL for the left-most command.
+
+ pipe_command (process& p,
+ const command& c,
+ const location& l,
+ pipe_command* v)
+ : proc (&p), bltn (nullptr), cmd (c), loc (l), prev (v) {}
+
+ pipe_command (builtin& b,
+ const command& c,
+ const location& l,
+ pipe_command* v)
+ : proc (nullptr), bltn (&b), cmd (c), loc (l), prev (v) {}
+ };
+
static bool
run_pipe (environment& env,
command_pipe::const_iterator bc,
command_pipe::const_iterator ec,
auto_fd ifd,
size_t ci, size_t li, const location& ll,
- bool diag)
+ bool diag,
+ optional<deadline> dl = nullopt,
+ const command* dl_cmd = nullptr, // env -t <cmd>
+ pipe_command* prev_cmd = nullptr)
{
+ tracer trace ("script::run_pipe");
+
if (bc == ec) // End of the pipeline.
return true;
- // The overall plan is to run the first command in the pipe, reading
- // its input from the file descriptor passed (or, for the first
- // command, according to stdin redirect specification) and redirecting
- // its output to the right-hand part of the pipe recursively. Fail if
- // the right-hand part fails. Otherwise check the process exit code,
- // match stderr (and stdout for the last command in the pipe) according
- // to redirect specification(s) and fail if any of the above fails.
+ // The overall plan is to run the first command in the pipe, reading its
+ // input from the file descriptor passed (or, for the first command,
+ // according to stdin redirect specification) and redirecting its output
+ // to the right-hand part of the pipe recursively. Fail if the
+ // right-hand part fails. Otherwise check the process exit code, match
+ // stderr (and stdout for the last command in the pipe) according to
+ // redirect specification(s) and fail if any of the above fails.
+ //
+ // If the command has a deadline, then terminate the whole pipeline when
+ // the deadline is reached. This way the pipeline processes get a chance
+ // to terminate gracefully, which in particular may require to interrupt
+ // their IO operations, closing their standard streams readers and
+ // writers.
//
const command& c (*bc);
@@ -996,48 +1162,69 @@ namespace build2
return args;
};
- // Prior to opening file descriptors for command input/output
- // redirects let's check if the command is the exit builtin. Being a
- // builtin syntactically it differs from the regular ones in a number
- // of ways. It doesn't communicate with standard streams, so
- // redirecting them is meaningless. It may appear only as a single
- // command in a pipeline. It doesn't return any value and stops the
- // script execution, so checking its exit status is meaningless as
- // well. That all means we can short-circuit here calling the builtin
- // and bailing out right after that. Checking that the user didn't
- // specify any redirects or exit code check sounds like a right thing
- // to do.
+ // Prior to opening file descriptors for command input/output redirects
+ // let's check if the command is the timeout or exit builtin. Being a
+ // builtin syntactically they differ from the regular ones in a number
+ // of ways. They don't communicate with standard streams, so redirecting
+ // them is meaningless. They may appear only as a single command in a
+ // pipeline. They don't return any value, so checking their exit status
+ // is meaningless as well. That all means we can short-circuit here
+ // calling the builtin and bailing out right after that. Checking that
+ // the user didn't specify any variables, timeout, redirects, or exit
+ // code check sounds like a right thing to do.
//
- if (resolve && program == "exit")
+ if (resolve && (program == "timeout" || program == "exit"))
{
// In case the builtin is erroneously pipelined from the other
// command, we will close stdin gracefully (reading out the stream
- // content), to make sure that the command doesn't print any
- // unwanted diagnostics about IO operation failure.
+ // content), to make sure that the command doesn't print any unwanted
+ // diagnostics about IO operation failure.
//
- // Note that dtor will ignore any errors (which is what we want).
+ // Note though, that doing so would be a bad idea if the deadline is
+ // specified, since we can block on read and miss the deadline.
//
- ifdstream is (move (ifd), fdstream_mode::skip);
+ if (!dl)
+ {
+ // Note that dtor will ignore any errors (which is what we want).
+ //
+ ifdstream (move (ifd), fdstream_mode::skip);
+ }
if (!first || !last)
- fail (ll) << "exit builtin must be the only pipe command";
+ fail (ll) << program << " builtin must be the only pipe command";
+
+ if (!c.variables.empty ())
+ fail (ll) << "environment variables cannot be (un)set for "
+ << program << " builtin";
+
+ if (c.timeout)
+ fail (ll) << "timeout cannot be specified for " << program
+ << " builtin";
if (c.in)
- fail (ll) << "exit builtin stdin cannot be redirected";
+ fail (ll) << program << " builtin stdin cannot be redirected";
if (c.out)
- fail (ll) << "exit builtin stdout cannot be redirected";
+ fail (ll) << program << " builtin stdout cannot be redirected";
if (c.err)
- fail (ll) << "exit builtin stderr cannot be redirected";
+ fail (ll) << program << " builtin stderr cannot be redirected";
if (c.exit)
- fail (ll) << "exit builtin exit code cannot be checked";
+ fail (ll) << program << " builtin exit code cannot be checked";
if (verb >= 2)
print_process (process_args ());
- exit_builtin (c.arguments, ll); // Throws exit exception.
+ if (program == "timeout")
+ {
+ timeout_builtin (env, c.arguments, ll);
+ return true;
+ }
+ else if (program == "exit")
+ exit_builtin (c.arguments, ll); // Throws exit exception.
+ else
+ assert (false);
}
// Create a unique path for a command standard stream cache file.
@@ -1121,6 +1308,9 @@ namespace build2
// process to hang which can be interpreted as a command failure.
// @@ Both ways are quite ugly. Is there some better way to do
// this?
+ // @@ Maybe we can create a pipe, write a byte into it, close the
+ // writing end, and after the process terminates make sure we can
+ // still read this byte out?
//
// Fall through.
//
@@ -1163,6 +1353,24 @@ namespace build2
assert (ifd.get () != -1);
+ // Calculate the process/builtin execution deadline. Note that we should
+ // also consider the left-hand side processes deadlines, not to keep
+ // them waiting for us and allow them to terminate not later than their
+ // deadlines. Thus, let's also track which command has introduced the
+ // deadline, so we can report it if the deadline is missed.
+ //
+ dl = earlier (dl, env.effective_deadline ());
+
+ if (c.timeout)
+ {
+ deadline d (system_clock::now () + *c.timeout, false /* success */);
+ if (!dl || d < *dl)
+ {
+ dl = d;
+ dl_cmd = &c;
+ }
+ }
+
// Prior to opening file descriptors for command outputs redirects
// let's check if the command is the set builtin. Being a builtin
// syntactically it differs from the regular ones in a number of ways.
@@ -1190,7 +1398,10 @@ namespace build2
if (verb >= 2)
print_process (process_args ());
- set_builtin (env, c.arguments, move (ifd), ll);
+ set_builtin (env, c.arguments, move (ifd),
+ dl, dl_cmd != nullptr ? *dl_cmd : c,
+ ll);
+
return true;
}
@@ -1344,10 +1555,119 @@ namespace build2
//
assert (ofd.out.get () != -1 && efd.get () != -1);
+ // Wait for a process/builtin to complete until the deadline is reached
+ // and return the underlying wait function result (optional<something>).
+ //
+ auto timed_wait = [] (auto& p, const timestamp& deadline)
+ {
+ timestamp now (system_clock::now ());
+ return deadline > now ? p.timed_wait (deadline - now) : p.try_wait ();
+ };
+
+ // Terminate the pipeline processes starting from the specified one and
+ // up to the leftmost one and then kill those which didn't terminate
+ // after 1 second.
+ //
+ // After that wait for the pipeline builtins completion. Since their
+ // standard streams should no longer be written to or read from by any
+ // process, that shouldn't take long. If, however, they won't be able to
+ // complete in 1 second, then some of them have probably stuck while
+ // communicating with a slow filesystem device or similar, and since we
+ // currently have no way to terminate asynchronous builtins, we have no
+ // choice but to abort.
+ //
+ // Issue diagnostics and fail if something goes wrong, but still try to
+ // terminate/kill all the pipe processes.
+ //
+ auto term_pipe = [&timed_wait, &trace] (pipe_command* pc)
+ {
+ diag_record dr;
+
+ auto prog = [] (pipe_command* c) {return cmd_path (c->cmd);};
+
+ // Terminate processes gracefully and set the terminate flag for the
+ // pipe commands.
+ //
+ for (pipe_command* c (pc); c != nullptr; c = c->prev)
+ {
+ if (process* p = c->proc)
+ try
+ {
+ l5 ([&]{trace (c->loc) << "terminating: " << c->cmd;});
+
+ p->term ();
+ }
+ catch (const process_error& e)
+ {
+ // If unable to terminate the process for any reason (the process
+ // is exiting on Windows, etc) then just ignore this, postponing
+ // the potential failure till the kill() call.
+ //
+ l5 ([&]{trace (c->loc) <<"unable to terminate " << prog (c)
+ << ": " << e;});
+ }
+
+ c->terminated = true;
+ }
+
+ // Wait a bit for the processes to terminate and kill the remaining
+ // ones.
+ //
+ timestamp dl (system_clock::now () + chrono::seconds (1));
+
+ for (pipe_command* c (pc); c != nullptr; c = c->prev)
+ {
+ if (process* p = c->proc)
+ try
+ {
+ l5 ([&]{trace (c->loc) << "waiting: " << c->cmd;});
+
+ if (!timed_wait (*p, dl))
+ {
+ l5 ([&]{trace (c->loc) << "killing: " << c->cmd;});
+
+ p->kill ();
+ p->wait ();
+ }
+ }
+ catch (const process_error& e)
+ {
+ dr << fail (c->loc) << "unable to wait/kill " << prog (c) << ": "
+ << e;
+ }
+ }
+
+ // Wait a bit for the builtins to complete and abort if any remain
+ // running.
+ //
+ dl = system_clock::now () + chrono::seconds (1);
+
+ for (pipe_command* c (pc); c != nullptr; c = c->prev)
+ {
+ if (builtin* b = c->bltn)
+ try
+ {
+ l5 ([&]{trace (c->loc) << "waiting: " << c->cmd;});
+
+ if (!timed_wait (*b, dl))
+ {
+ error (c->loc) << prog (c) << " builtin hanged, aborting";
+ terminate (false /* trace */);
+ }
+ }
+ catch (const system_error& e)
+ {
+ dr << fail (c->loc) << "unable to wait for " << prog (c) << ": "
+ << e;
+ }
+ }
+ };
+
+ // Absent if the process/builtin misses the "unsuccessful" deadline.
+ //
optional<process_exit> exit;
- const builtin_info* bi (resolve
- ? builtins.find (program)
- : nullptr);
+
+ const builtin_info* bi (resolve ? builtins.find (program) : nullptr);
bool success;
@@ -1394,6 +1714,21 @@ namespace build2
if (cleanup_builtin (program))
cln = cleanup ();
+ // We also extend the sleep builtin, deactivating the thread before
+ // going to sleep and waking up before the deadline is reached.
+ //
+ // Let's "wrap up" the sleep-related values into the single object to
+ // rely on "small function object" optimization.
+ //
+ struct sleep
+ {
+ optional<timestamp> deadline;
+ bool terminated = false;
+
+ sleep (const optional<timestamp>& d): deadline (d) {}
+ };
+ sleep slp (dl ? dl->value : optional<timestamp> ());
+
builtin_callbacks bcs {
// create
@@ -1555,14 +1890,29 @@ namespace build2
// sleep
//
- // Deactivate the thread before going to sleep.
- //
- [&env] (const duration& d)
+ [&env, &slp] (const duration& d)
{
+ duration t (d);
+ const optional<timestamp>& dl (slp.deadline);
+
+ if (dl)
+ {
+ timestamp now (system_clock::now ());
+
+ slp.terminated = now + t > *dl;
+
+ if (*dl <= now)
+ return;
+
+ duration d (*dl - now);
+ if (t > d)
+ t = d;
+ }
+
// If/when required we could probably support the precise sleep
// mode (e.g., via an option).
//
- env.context.sched.sleep (d);
+ env.context.sched.sleep (t);
}
};
@@ -1575,13 +1925,49 @@ namespace build2
*env.work_dir.path,
bcs));
+ pipe_command pc (b, c, ll, prev_cmd);
+
+ // If the deadline is specified, then make sure we don't miss it
+ // waiting indefinitely in the builtin destructor on the right-hand
+ // side of the pipe failure.
+ //
+ auto g (make_exception_guard ([&dl, &pc, &term_pipe] ()
+ {
+ if (dl)
+ try
+ {
+ term_pipe (&pc);
+ }
+ catch (const failed&)
+ {
+ // We can't do much here.
+ }
+ }));
+
success = run_pipe (env,
- nc,
- ec,
+ nc, ec,
move (ofd.in),
- ci + 1, li, ll, diag);
+ ci + 1, li, ll, diag,
+ dl, dl_cmd,
+ &pc);
+
+ if (!dl)
+ b.wait ();
+ else if (!timed_wait (b, dl->value))
+ term_pipe (&pc);
+
+ // Note that this also handles ad hoc termination (without the call
+ // to term_pipe()) by the sleep builtin (see above).
+ //
+ if (pc.terminated || slp.terminated)
+ {
+ assert (dl);
- exit = process_exit (b.wait ());
+ if (dl->success)
+ exit = process_exit (0);
+ }
+ else
+ exit = process_exit (r);
}
catch (const system_error& e)
{
@@ -1654,19 +2040,60 @@ namespace build2
env.work_dir.path->string ().c_str (),
pe.vars);
+ // Can't throw.
+ //
ifd.reset ();
ofd.out.reset ();
efd.reset ();
+ pipe_command pc (pr, c, ll, prev_cmd);
+
+ // If the deadline is specified, then make sure we don't miss it
+ // waiting indefinitely in the process destructor on the right-hand
+ // part of the pipe failure.
+ //
+ auto g (make_exception_guard ([&dl, &pc, &term_pipe] ()
+ {
+ if (dl)
+ try
+ {
+ term_pipe (&pc);
+ }
+ catch (const failed&)
+ {
+ // We can't do much here.
+ }
+ }));
+
success = run_pipe (env,
- nc,
- ec,
+ nc, ec,
move (ofd.in),
- ci + 1, li, ll, diag);
-
- pr.wait ();
+ ci + 1, li, ll, diag,
+ dl, dl_cmd,
+ &pc);
+
+ if (!dl)
+ pr.wait ();
+ else if (!timed_wait (pr, dl->value))
+ term_pipe (&pc);
+
+#ifndef _WIN32
+ if (pc.terminated &&
+ !pr.exit->normal () &&
+ pr.exit->signal () == SIGTERM)
+#else
+ if (pc.terminated &&
+ !pr.exit->normal () &&
+ pr.exit->status == DBG_TERMINATE_PROCESS)
+#endif
+ {
+ assert (dl);
- exit = move (pr.exit);
+ if (dl->success)
+ exit = process_exit (0);
+ }
+ else
+ exit = pr.exit;
}
catch (const process_error& e)
{
@@ -1679,19 +2106,19 @@ namespace build2
}
}
- assert (exit);
-
// If the righ-hand side pipeline failed than the whole pipeline fails,
// and no further checks are required.
//
if (!success)
return false;
- // Use the program path for diagnostics (print relative, etc).
+ // Fail if the process is terminated due to reaching the deadline.
//
- const path& pr (resolve
- ? c.program.recall
- : path (c.program.recall_string ())); // Can't throw.
+ if (!exit)
+ fail (ll) << cmd_path (dl_cmd != nullptr ? *dl_cmd : c)
+ << " terminated: execution timeout expired";
+
+ path pr (cmd_path (c));
// If there is no valid exit code available by whatever reason then we
// print the proper diagnostics, dump stderr (if cached and not too
@@ -1794,7 +2221,7 @@ namespace build2
// pipe that "switches on" the diagnostics potential printing.
//
command_expr::const_iterator trailing_ands; // Undefined if diag is
- // disallowed.
+ // disallowed.
if (diag)
{
auto i (expr.crbegin ());
@@ -1817,8 +2244,10 @@ namespace build2
// with false.
//
if (!((or_op && r) || (!or_op && !r)))
- r = run_pipe (
- env, p.begin (), p.end (), auto_fd (), ci, li, ll, print);
+ r = run_pipe (env,
+ p.begin (), p.end (),
+ auto_fd (),
+ ci, li, ll, print);
ci += p.size ();
}
diff --git a/libbuild2/script/script.cxx b/libbuild2/script/script.cxx
index 6ee702e..f540687 100644
--- a/libbuild2/script/script.cxx
+++ b/libbuild2/script/script.cxx
@@ -3,6 +3,7 @@
#include <libbuild2/script/script.hxx>
+#include <chrono>
#include <sstream>
#include <cstring> // strchr()
@@ -408,13 +409,20 @@ namespace build2
if ((m & command_to_stream::header) == command_to_stream::header)
{
- // Print the env builtin arguments, if any environment variable
- // (un)sets are present.
+ // Print the env builtin if any of its options/arguments are present.
//
- if (!c.variables.empty ())
+ if (!c.variables.empty () || c.timeout)
{
o << "env";
+ // Timeout.
+ //
+ if (c.timeout)
+ o << " -t "
+ << chrono::duration_cast<chrono::seconds> (*c.timeout).count ();
+
+ // Variable unsets/sets.
+ //
auto b (c.variables.begin ()), i (b), e (c.variables.end ());
// Print a variable name or assignment to the stream, quoting it if
@@ -456,8 +464,7 @@ namespace build2
//
// Print the variable unsets as the -u options until a variable set
// is encountered (contains '=') or the end of the variable list is
- // reached. In the former case, to avoid a potential ambiguity add
- // the '-' separator, if there are any options.
+ // reached.
//
// Note that we rely on the fact that unsets come first, which is
// guaranteed by parser::parse_env_builtin().
@@ -471,16 +478,15 @@ namespace build2
o << " -u "; print (v, true /* name*/);
}
else // Variable set.
- {
- if (i != b)
- o << " -";
-
break;
- }
}
// Variable sets.
//
+ // Note that we don't add the '-' separator since we always use the
+ // `-* <value>` option notation and so there can't be any ambiguity
+ // with a variable set.
+ //
for (; i != e; ++i)
{
o << ' '; print (*i, false /* name */);
diff --git a/libbuild2/script/script.hxx b/libbuild2/script/script.hxx
index 4a62c77..ecd2c2b 100644
--- a/libbuild2/script/script.hxx
+++ b/libbuild2/script/script.hxx
@@ -308,8 +308,9 @@ namespace build2
//
process_path program;
- strings arguments;
- environment_vars variables;
+ strings arguments;
+ environment_vars variables; // From env builtin.
+ optional<duration> timeout; // From env builtin.
optional<redirect> in;
optional<redirect> out;
@@ -363,6 +364,37 @@ namespace build2
ostream&
operator<< (ostream&, const command_expr&);
+ struct timeout
+ {
+ duration value;
+ bool success;
+
+ timeout (duration d, bool s): value (d), success (s) {}
+ };
+
+ struct deadline
+ {
+ timestamp value;
+ bool success;
+
+ deadline (timestamp t, bool s): value (t), success (s) {}
+ };
+
+ // If timestamps/durations are equal, the failure is less than the
+ // success.
+ //
+ bool
+ operator< (const deadline&, const deadline&);
+
+ bool
+ operator< (const timeout&, const timeout&);
+
+ optional<deadline>
+ to_deadline (const optional<timestamp>&, bool success);
+
+ optional<timeout>
+ to_timeout (const optional<duration>&, bool success);
+
// Script execution environment.
//
class environment
@@ -469,6 +501,23 @@ namespace build2
const string& attrs,
const location&) = 0;
+ // Set the script execution timeout from the timeout builtin call.
+ //
+ // The builtin argument semantics is script implementation-dependent. If
+ // success is true then a process missing this deadline should not be
+ // considered as failed unless it didn't terminate gracefully and had to
+ // be killed.
+ //
+ virtual void
+ set_timeout (const string& arg, bool success, const location&) = 0;
+
+ // Return the script execution deadline which can potentially rely on
+ // factors besides the latest timeout builtin call (variables, scoping,
+ // etc).
+ //
+ virtual optional<deadline>
+ effective_deadline () = 0;
+
// Create the temporary directory and set the temp_dir reference target
// to its path. Must only be called if temp_dir is empty.
//
diff --git a/libbuild2/script/script.ixx b/libbuild2/script/script.ixx
index 56043b2..37c77ec 100644
--- a/libbuild2/script/script.ixx
+++ b/libbuild2/script/script.ixx
@@ -25,7 +25,6 @@ namespace build2
inline command_to_stream
operator| (command_to_stream x, command_to_stream y) {return x |= y;}
-
// command
//
inline ostream&
@@ -52,5 +51,39 @@ namespace build2
to_stream (o, e, command_to_stream::all);
return o;
}
+
+ // deadline
+ //
+ inline bool
+ operator< (const deadline& x, const deadline& y)
+ {
+ if (x.value != y.value)
+ return x.value < y.value;
+
+ return x.success < y.success;
+ }
+
+ inline optional<deadline>
+ to_deadline (const optional<timestamp>& ts, bool success)
+ {
+ return ts ? deadline (*ts, success) : optional<deadline> ();
+ }
+
+ // timeout
+ //
+ inline bool
+ operator< (const timeout& x, const timeout& y)
+ {
+ if (x.value != y.value)
+ return x.value < y.value;
+
+ return x.success < y.success;
+ }
+
+ inline optional<timeout>
+ to_timeout (const optional<duration>& d, bool success)
+ {
+ return d ? timeout (*d, success) : optional<timeout> ();
+ }
}
}
diff --git a/libbuild2/script/timeout.cxx b/libbuild2/script/timeout.cxx
new file mode 100644
index 0000000..a44e1bb
--- /dev/null
+++ b/libbuild2/script/timeout.cxx
@@ -0,0 +1,26 @@
+// file : libbuild2/script/timeout.cxx -*- C++ -*-
+// license : MIT; see accompanying LICENSE file
+
+#include <libbuild2/script/timeout.hxx>
+
+#include <chrono>
+
+#include <libbuild2/diagnostics.hxx>
+
+using namespace std;
+
+namespace build2
+{
+ optional<duration>
+ parse_timeout (const string& s, const char* what, const location& l)
+ {
+ if (optional<uint64_t> n = parse_number (s))
+ {
+ return *n != 0
+ ? chrono::duration_cast<duration> (chrono::seconds (*n))
+ : optional<duration> ();
+ }
+ else
+ fail (l) << "invalid " << what << " '" << s << "'" << endf;
+ }
+}
diff --git a/libbuild2/script/timeout.hxx b/libbuild2/script/timeout.hxx
new file mode 100644
index 0000000..9991ad6
--- /dev/null
+++ b/libbuild2/script/timeout.hxx
@@ -0,0 +1,52 @@
+// file : libbuild2/script/timeout.hxx -*- C++ -*-
+// license : MIT; see accompanying LICENSE file
+
+#ifndef LIBBUILD2_SCRIPT_TIMEOUT_HXX
+#define LIBBUILD2_SCRIPT_TIMEOUT_HXX
+
+#include <libbuild2/types.hxx>
+#include <libbuild2/utility.hxx>
+
+namespace build2
+{
+ // Parse the specified in seconds timeout returning it if the value is not
+ // zero and nullopt otherwise. Issue diagnostics and fail if the argument is
+ // not a valid timeout.
+ //
+ optional<duration>
+ parse_timeout (const string&,
+ const char* what,
+ const location& = location ());
+
+ // As above, but return the timepoint which is away from now by the
+ // specified timeout.
+ //
+ optional<timestamp>
+ parse_deadline (const string&,
+ const char* what,
+ const location& = location ());
+
+ // Return the earlier timeout/deadline of two values, if any is present.
+ //
+ // Note that earlier(nullopt, v) and earlier(v, nullopt) return v.
+ //
+ template <typename T>
+ T
+ earlier (const T&, const T&);
+
+ template <typename T>
+ T
+ earlier (const optional<T>&, const T&);
+
+ template <typename T>
+ T
+ earlier (const T&, const optional<T>&);
+
+ template <typename T>
+ optional<T>
+ earlier (const optional<T>&, const optional<T>&);
+}
+
+#include <libbuild2/script/timeout.ixx>
+
+#endif // LIBBUILD2_SCRIPT_TIMEOUT_HXX
diff --git a/libbuild2/script/timeout.ixx b/libbuild2/script/timeout.ixx
new file mode 100644
index 0000000..755af17
--- /dev/null
+++ b/libbuild2/script/timeout.ixx
@@ -0,0 +1,44 @@
+// file : libbuild2/script/timeout.ixx -*- C++ -*-
+// license : MIT; see accompanying LICENSE file
+
+namespace build2
+{
+ inline optional<timestamp>
+ parse_deadline (const string& s, const char* what, const location& l)
+ {
+ if (optional<duration> t = parse_timeout (s, what, l))
+ return system_clock::now () + *t;
+ else
+ return nullopt;
+ }
+
+ template <typename T>
+ inline T
+ earlier (const T& x, const T& y)
+ {
+ return x < y ? x : y;
+ }
+
+ template <typename T>
+ inline T
+ earlier (const T& x, const optional<T>& y)
+ {
+ return y ? earlier (x, *y) : x;
+ }
+
+ template <typename T>
+ inline T
+ earlier (const optional<T>& x, const T& y)
+ {
+ return earlier (y, x);
+ }
+
+ template <typename T>
+ inline optional<T>
+ earlier (const optional<T>& x, const optional<T>& y)
+ {
+ return x ? earlier (*x, y) :
+ y ? earlier (*y, x) :
+ optional<T> ();
+ }
+}
diff --git a/libbuild2/test/common.cxx b/libbuild2/test/common.cxx
index f50d289..7fdb347 100644
--- a/libbuild2/test/common.cxx
+++ b/libbuild2/test/common.cxx
@@ -6,6 +6,10 @@
#include <libbuild2/target.hxx>
#include <libbuild2/algorithm.hxx>
+#include <libbuild2/script/timeout.hxx>
+
+#include <libbuild2/test/module.hxx>
+
using namespace std;
namespace build2
@@ -215,5 +219,73 @@ namespace build2
return r;
}
+
+ optional<timestamp> common::
+ operation_deadline () const
+ {
+ if (!operation_timeout)
+ return nullopt;
+
+ duration::rep r (operation_deadline_.load (memory_order_consume));
+
+ if (r == timestamp_unknown_rep)
+ {
+ duration::rep t (timestamp (system_clock::now () + *operation_timeout).
+ time_since_epoch ().count ());
+
+ if (operation_deadline_.compare_exchange_strong (r,
+ t,
+ memory_order_release,
+ memory_order_consume))
+ r = t;
+ }
+
+ return timestamp (duration (r));
+ }
+
+ // Helpers.
+ //
+ optional<timestamp>
+ operation_deadline (const target& t)
+ {
+ optional<timestamp> r;
+
+ for (const scope* s (t.base_scope ().root_scope ());
+ s != nullptr;
+ s = s->parent_scope ()->root_scope ())
+ {
+ if (auto* m = s->find_module<module> (module::name))
+ r = earlier (r, m->operation_deadline ());
+ }
+
+ return r;
+ }
+
+ optional<duration>
+ test_timeout (const target& t)
+ {
+ optional<duration> r;
+
+ for (const scope* s (t.base_scope ().root_scope ());
+ s != nullptr;
+ s = s->parent_scope ()->root_scope ())
+ {
+ if (auto* m = s->find_module<module> (module::name))
+ r = earlier (r, m->test_timeout);
+ }
+
+ return r;
+ }
+
+ optional<timestamp>
+ test_deadline (const target& t)
+ {
+ optional<timestamp> r (operation_deadline (t));
+
+ if (optional<duration> d = test_timeout (t))
+ r = earlier (r, system_clock::now () + *d);
+
+ return r;
+ }
}
}
diff --git a/libbuild2/test/common.hxx b/libbuild2/test/common.hxx
index 01628fd..a43b2b1 100644
--- a/libbuild2/test/common.hxx
+++ b/libbuild2/test/common.hxx
@@ -20,6 +20,7 @@ namespace build2
{
const variable& config_test;
const variable& config_test_output;
+ const variable& config_test_timeout;
const variable& var_test;
const variable& test_options;
@@ -40,11 +41,28 @@ namespace build2
output_before before = output_before::warn;
output_after after = output_after::clean;
+ // The config.test.timeout values.
+ //
+ optional<duration> operation_timeout;
+ optional<duration> test_timeout;
+
// The config.test query interface.
//
const names* test_ = nullptr; // The config.test value if any.
scope* root_ = nullptr; // The root scope for target resolution.
+ // Store it as the underlying representation and use the release-consume
+ // ordering (see mtime_target for the reasoning).
+ //
+ mutable atomic<timestamp::rep> operation_deadline_ {
+ timestamp_unknown_rep};
+
+ // Return the test operation deadline, calculating it on the first call
+ // as an offset from now by the operation timeout.
+ //
+ optional<timestamp>
+ operation_deadline () const;
+
// Return true if the specified alias target should pass-through to its
// prerequisites.
//
@@ -65,6 +83,27 @@ namespace build2
explicit
common (common_data&& d): common_data (move (d)) {}
};
+
+ // Helpers.
+ //
+
+ // Return the nearest of the target-enclosing root scopes test operation
+ // deadlines.
+ //
+ optional<timestamp>
+ operation_deadline (const target&);
+
+ // Return the lesser of the target-enclosing root scopes test timeouts.
+ //
+ optional<duration>
+ test_timeout (const target&);
+
+ // Convert the test timeouts in the target-enclosing root scopes into
+ // deadlines and return the nearest between them and the operation
+ // deadlines in the enclosing root scopes.
+ //
+ optional<timestamp>
+ test_deadline (const target&);
}
}
diff --git a/libbuild2/test/init.cxx b/libbuild2/test/init.cxx
index aaacdc6..0a47842 100644
--- a/libbuild2/test/init.cxx
+++ b/libbuild2/test/init.cxx
@@ -10,6 +10,8 @@
#include <libbuild2/config/utility.hxx>
+#include <libbuild2/script/timeout.hxx>
+
#include <libbuild2/test/module.hxx>
#include <libbuild2/test/target.hxx>
#include <libbuild2/test/operation.hxx>
@@ -44,8 +46,8 @@ namespace build2
//
// Specified as <target>@<path-id> pairs with both sides being
// optional. The variable is untyped (we want a list of name-pairs),
- // overridable, and with global visibiility. The target is relative
- // (in essence a prerequisite) which is resolved from the (root) scope
+ // overridable, and with global visibility. The target is relative (in
+ // essence a prerequisite) which is resolved from the (root) scope
// where the config.test value is defined.
//
vp.insert ("config.test"),
@@ -55,6 +57,11 @@ namespace build2
//
vp.insert<name_pair> ("config.test.output"),
+ // Test operation and individual test execution timeouts (see the
+ // manual for semantics).
+ //
+ vp.insert<string> ("config.test.timeout"),
+
// The test variable is a name which can be a path (with the
// true/false special values) or a target name.
//
@@ -189,6 +196,33 @@ namespace build2
else fail << "invalid config.test.output before value '" << b << "'";
}
+ // config.test.timeout
+ //
+ if (lookup l = lookup_config (rs, m.config_test_timeout))
+ {
+ const string& t (cast<string> (l));
+
+ const char* ot ("config.test.timeout test operation timeout value");
+ const char* tt ("config.test.timeout test timeout value");
+
+ size_t p (t.find ('/'));
+ if (p != string::npos)
+ {
+ // Note: either of the timeouts can be omitted but not both.
+ //
+ if (t.size () == 1)
+ fail << "invalid config.test.timeout value '" << t << "'";
+
+ if (p != 0)
+ m.operation_timeout = parse_timeout (string (t, 0, p), ot);
+
+ if (p != t.size () - 1)
+ m.test_timeout = parse_timeout (string (t, p + 1), tt);
+ }
+ else
+ m.test_timeout = parse_timeout (t, ot);
+ }
+
//@@ TODO: Need ability to specify extra diff options (e.g.,
// --strip-trailing-cr, now hardcoded).
//
diff --git a/libbuild2/test/module.cxx b/libbuild2/test/module.cxx
new file mode 100644
index 0000000..6b2cbdf
--- /dev/null
+++ b/libbuild2/test/module.cxx
@@ -0,0 +1,12 @@
+// file : libbuild2/test/module.cxx -*- C++ -*-
+// license : MIT; see accompanying LICENSE file
+
+#include <libbuild2/test/module.hxx>
+
+namespace build2
+{
+ namespace test
+ {
+ const string module::name ("test");
+ }
+}
diff --git a/libbuild2/test/module.hxx b/libbuild2/test/module.hxx
index 7635f01..c278f5c 100644
--- a/libbuild2/test/module.hxx
+++ b/libbuild2/test/module.hxx
@@ -21,6 +21,8 @@ namespace build2
default_rule,
group_rule
{
+ static const string name;
+
const test::group_rule&
group_rule () const
{
diff --git a/libbuild2/test/operation.cxx b/libbuild2/test/operation.cxx
index e9635cf..0a65bed 100644
--- a/libbuild2/test/operation.cxx
+++ b/libbuild2/test/operation.cxx
@@ -3,7 +3,11 @@
#include <libbuild2/test/operation.hxx>
+#include <libbuild2/rule.hxx>
#include <libbuild2/variable.hxx>
+#include <libbuild2/algorithm.hxx>
+
+#include <libbuild2/test/common.hxx> // test_deadline()
using namespace std;
using namespace butl;
@@ -23,6 +27,36 @@ namespace build2
return mo != disfigure_id ? update_id : 0;
}
+ // Ad hoc rule apply callback.
+ //
+ // If this is not perform(test) or there is no deadline set for the test
+ // execution, then forward the call to the ad hoc rule's apply().
+ // Otherwise, return a recipe that will execute with the deadline if we
+ // can get it and return the noop recipe that just issues a warning if we
+ // can't.
+ //
+ static recipe
+ adhoc_apply (const adhoc_rule& ar, action a, target& t, match_extra& me)
+ {
+ optional<timestamp> d;
+
+ if (a != perform_test_id || !(d = test_deadline (t)))
+ return ar.apply (a, t, me);
+
+ if (const auto* dr = dynamic_cast<const adhoc_rule_with_deadline*> (&ar))
+ {
+ if (recipe r = dr->apply (a, t, me, d))
+ return r;
+ }
+
+ return [] (action a, const target& t)
+ {
+ warn << "unable to impose timeout on test for target " << t
+ << ", skipping";
+ return noop_action (a, t);
+ };
+ }
+
const operation_info op_test {
test_id,
0,
@@ -36,7 +70,7 @@ namespace build2
&test_pre,
nullptr,
nullptr,
- nullptr
+ &adhoc_apply
};
// Also the explicit update-for-test operation alias.
diff --git a/libbuild2/test/rule.cxx b/libbuild2/test/rule.cxx
index db490d9..df2d5ba 100644
--- a/libbuild2/test/rule.cxx
+++ b/libbuild2/test/rule.cxx
@@ -3,6 +3,12 @@
#include <libbuild2/test/rule.hxx>
+#ifndef _WIN32
+# include <signal.h> // SIG*
+#else
+# include <libbutl/win32-utility.hxx> // DBG_TERMINATE_PROCESS
+#endif
+
#include <libbuild2/scope.hxx>
#include <libbuild2/target.hxx>
#include <libbuild2/context.hxx>
@@ -632,11 +638,30 @@ namespace build2
// ...
// nameN arg arg ... nullptr nullptr
//
+ // Stack-allocated linked list of information about the running pipeline
+ // processes.
+ //
+ struct pipe_process
+ {
+ process& proc;
+ const char* prog; // Only for diagnostics.
+
+ // True if this process has been terminated.
+ //
+ bool terminated = false;
+
+ pipe_process* prev; // NULL for the left-most program.
+
+ pipe_process (process& p, const char* g, pipe_process* r)
+ : proc (p), prog (g), prev (r) {}
+ };
+
static bool
run_test (const target& t,
diag_record& dr,
char const** args,
- process* prev = nullptr)
+ const optional<timestamp>& deadline,
+ pipe_process* prev = nullptr)
{
// Find the next process, if any.
//
@@ -648,19 +673,116 @@ namespace build2
//
int out (*next != nullptr ? -1 : 1);
bool pr;
- process_exit pe;
+
+ // Absent if the process misses the deadline.
+ //
+ optional<process_exit> pe;
try
{
+ // Wait for a process to complete until the deadline is reached and
+ // return the underlying wait function result.
+ //
+ auto timed_wait = [] (process& p, const timestamp& deadline)
+ {
+ timestamp now (system_clock::now ());
+ return deadline > now
+ ? p.timed_wait (deadline - now)
+ : p.try_wait ();
+ };
+
+ // Terminate the pipeline processes starting from the specified one
+ // and up to the leftmost one and then kill those which didn't
+ // terminate in 1 second. Issue diagnostics and fail if something goes
+ // wrong, but still try to terminate all processes.
+ //
+ auto term_pipe = [&timed_wait] (pipe_process* pp)
+ {
+ diag_record dr;
+
+ // Terminate processes gracefully and set the terminate flag for
+ // them.
+ //
+ for (pipe_process* p (pp); p != nullptr; p = p->prev)
+ {
+ try
+ {
+ p->proc.term ();
+ }
+ catch (const process_error& e)
+ {
+ dr << fail << "unable to terminate " << p->prog << ": " << e;
+ }
+
+ p->terminated = true;
+ }
+
+ // Wait a bit for the processes to terminate and kill the remaining
+ // ones.
+ //
+ timestamp deadline (system_clock::now () + chrono::seconds (1));
+
+ for (pipe_process* p (pp); p != nullptr; p = p->prev)
+ {
+ process& pr (p->proc);
+
+ try
+ {
+ if (!timed_wait (pr, deadline))
+ {
+ pr.kill ();
+ pr.wait ();
+ }
+ }
+ catch (const process_error& e)
+ {
+ dr << fail << "unable to wait/kill " << p->prog << ": " << e;
+ }
+ }
+ };
+
process p (prev == nullptr
- ? process (args, 0, out) // First process.
- : process (args, *prev, out)); // Next process.
+ ? process (args, 0, out) // First process.
+ : process (args, prev->proc, out)); // Next process.
- pr = *next == nullptr || run_test (t, dr, next, &p);
- p.wait ();
+ pipe_process pp (p, args[0], prev);
+
+ // If the deadline is specified, then make sure we don't miss it
+ // waiting indefinitely in the process destructor on the right-hand
+ // part of the pipe failure.
+ //
+ auto g (make_exception_guard ([&deadline, &pp, &term_pipe] ()
+ {
+ if (deadline)
+ try
+ {
+ term_pipe (&pp);
+ }
+ catch (const failed&)
+ {
+ // We can't do much here.
+ }
+ }));
+
+ pr = *next == nullptr || run_test (t, dr, next, deadline, &pp);
+
+ if (!deadline)
+ p.wait ();
+ else if (!timed_wait (p, *deadline))
+ term_pipe (&pp);
assert (p.exit);
- pe = *p.exit;
+
+#ifndef _WIN32
+ if (!(pp.terminated &&
+ !p.exit->normal () &&
+ p.exit->signal () == SIGTERM))
+#else
+ if (!(pp.terminated &&
+ !p.exit->normal () &&
+ p.exit->status == DBG_TERMINATE_PROCESS))
+#endif
+ pe = *p.exit;
}
catch (const process_error& e)
{
@@ -672,7 +794,7 @@ namespace build2
throw failed ();
}
- bool wr (pe.normal () && pe.code () == 0);
+ bool wr (pe && pe->normal () && pe->code () == 0);
if (!wr)
{
@@ -681,7 +803,11 @@ namespace build2
dr << error;
print_process (dr, args);
- dr << " " << pe;
+
+ if (pe)
+ dr << " " << *pe;
+ else
+ dr << " terminated: execution timeout expired";
}
return pr && wr;
@@ -896,10 +1022,13 @@ namespace build2
if (!ctx.dry_run)
{
diag_record dr;
+ pipe_process pp (cat, "cat", nullptr);
+
if (!run_test (tt,
dr,
args.data () + (sin ? 3 : 0), // Skip cat.
- sin ? &cat : nullptr))
+ test_deadline (tt),
+ sin ? &pp : nullptr))
{
dr << info << "test command line: ";
print_process (dr, args);
diff --git a/libbuild2/test/script/parser+env.test.testscript b/libbuild2/test/script/parser+env.test.testscript
index b1e864c..b6fb305 100644
--- a/libbuild2/test/script/parser+env.test.testscript
+++ b/libbuild2/test/script/parser+env.test.testscript
@@ -48,10 +48,10 @@
: set
:
{
- $* <'env a=b -- cmd' >'env a=b -- cmd' : var
- $* <'env -u a b=c -- cmd' >'env -u a - b=c -- cmd' : opt-var
- $* <'env a="b c" -- cmd' >"env a='b c' -- cmd" : quote
- $* <'env "a b"=c -- cmd' >"env 'a b=c' -- cmd" : quote-name
+ $* <'env a=b -- cmd' >'env a=b -- cmd' : var
+ $* <'env -u a b=c -- cmd' >'env -u a b=c -- cmd' : opt-var
+ $* <'env a="b c" -- cmd' >"env a='b c' -- cmd" : quote
+ $* <'env "a b"=c -- cmd' >"env 'a b=c' -- cmd" : quote-name
: double-quote
:
@@ -66,9 +66,19 @@
EOE
}
+: timeout
+:
+{
+ $* <'env -t 5 -- cmd' >'env -t 5 -- cmd' : short-opt
+ $* <'env --timeout 5 -- cmd' >'env -t 5 -- cmd' : long-opt
+ $* <'env --timeout=5 -- cmd' >'env -t 5 -- cmd' : long-opt-eq
+ $* <'env -u a -t 5 -- cmd' >'env -t 5 -u a -- cmd' : mult-opt
+ $* <'env -t 5 a=b -- cmd' >'env -t 5 a=b -- cmd' : args
+}
+
: non-first
:
-$* <'cmd1 && env -u a b=c -- cmd2' >'cmd1 && env -u a - b=c -- cmd2'
+$* <'cmd1 && env -u a b=c -- cmd2' >'cmd1 && env -u a b=c -- cmd2'
: no-cmd
:
diff --git a/libbuild2/test/script/script.cxx b/libbuild2/test/script/script.cxx
index 34d4723..3f615ee 100644
--- a/libbuild2/test/script/script.cxx
+++ b/libbuild2/test/script/script.cxx
@@ -8,6 +8,10 @@
#include <libbuild2/target.hxx>
#include <libbuild2/algorithm.hxx>
+#include <libbuild2/script/timeout.hxx>
+
+#include <libbuild2/test/common.hxx> // operation_deadline(),
+ // test_timeout()
#include <libbuild2/test/script/parser.hxx>
using namespace std;
@@ -18,6 +22,9 @@ namespace build2
{
namespace script
{
+ using build2::script::to_deadline;
+ using build2::script::to_timeout;
+
// scope_base
//
scope_base::
@@ -188,11 +195,14 @@ namespace build2
// script
//
script::
- script (const target& tt,
- const testscript& st,
- const dir_path& rwd)
+ script (const target& tt, const testscript& st, const dir_path& rwd)
: script_base (tt, st),
- group (st.name == "testscript" ? string () : st.name, *this)
+ group (st.name == "testscript" ? string () : st.name, *this),
+ operation_deadline (
+ to_deadline (build2::test::operation_deadline (tt),
+ false /* success */)),
+ test_timeout (to_timeout (build2::test::test_timeout (tt),
+ false /* success */))
{
// Set the script working dir ($~) to $out_base/test/<id> (id_path
// for root is just the id which is empty if st is 'testscript').
@@ -282,6 +292,14 @@ namespace build2
reset_special ();
}
+ optional<deadline> script::
+ effective_deadline ()
+ {
+ return earlier (operation_deadline, group_deadline);
+ }
+
+ // scope
+ //
lookup scope::
lookup (const variable& var) const
{
@@ -409,6 +427,86 @@ namespace build2
//
assign (root.cmd_var) = move (s);
}
+
+ // group
+ //
+ void group::
+ set_timeout (const string& t, bool success, const location& l)
+ {
+ const char* gt (parent != nullptr
+ ? "test group timeout"
+ : "testscript timeout");
+
+ const char* tt ("test timeout");
+
+ size_t p (t.find ('/'));
+ if (p != string::npos)
+ {
+ // Note: either of the timeouts can be omitted but not both.
+ //
+ if (t.size () == 1)
+ fail (l) << "invalid timeout '" << t << "'";
+
+ if (p != 0)
+ group_deadline =
+ to_deadline (parse_deadline (string (t, 0, p), gt, l),
+ success);
+
+ if (p != t.size () - 1)
+ test_timeout =
+ to_timeout (parse_timeout (string (t, p + 1), tt, l), success);
+ }
+ else
+ group_deadline = to_deadline (parse_deadline (t, gt, l), success);
+ }
+
+ optional<deadline> group::
+ effective_deadline ()
+ {
+ return parent != nullptr
+ ? earlier (parent->effective_deadline (), group_deadline)
+ : group_deadline;
+ }
+
+ // test
+ //
+ void test::
+ set_timeout (const string& t, bool success, const location& l)
+ {
+ fragment_deadline =
+ to_deadline (parse_deadline (t, "test fragment timeout", l),
+ success);
+ }
+
+ optional<deadline> test::
+ effective_deadline ()
+ {
+ if (!test_deadline)
+ {
+ assert (parent != nullptr); // Test is always inside a group scope.
+
+ test_deadline = parent->effective_deadline ();
+
+ // Calculate the minimum timeout and factor it into the resulting
+ // deadline.
+ //
+ optional<timeout> t (root.test_timeout); // config.test.timeout
+ for (const scope* p (parent); p != nullptr; p = p->parent)
+ {
+ const group* g (dynamic_cast<const group*> (p));
+ assert (g != nullptr);
+
+ t = earlier (t, g->test_timeout);
+ }
+
+ if (t)
+ test_deadline =
+ earlier (*test_deadline,
+ deadline (system_clock::now () + t->value, t->success));
+ }
+
+ return earlier (*test_deadline, fragment_deadline);
+ }
}
}
}
diff --git a/libbuild2/test/script/script.hxx b/libbuild2/test/script/script.hxx
index 6356501..2789cab 100644
--- a/libbuild2/test/script/script.hxx
+++ b/libbuild2/test/script/script.hxx
@@ -28,6 +28,8 @@ namespace build2
using build2::script::redirect_type;
using build2::script::line_type;
using build2::script::command_expr;
+ using build2::script::deadline;
+ using build2::script::timeout;
class parser; // Required by VC for 'friend class parser' declaration.
@@ -168,10 +170,29 @@ namespace build2
class group: public scope
{
public:
- vector<unique_ptr<scope>> scopes;
+ group (const string& id, group& p): scope (id, &p, p.root) {}
public:
- group (const string& id, group& p): scope (id, &p, p.root) {}
+ vector<unique_ptr<scope>> scopes;
+
+ // The test group execution deadline and the individual test timeout.
+ //
+ optional<deadline> group_deadline;
+ optional<timeout> test_timeout;
+
+ // Parse the argument having the '[<group-timeout>]/[<test-timeout>]'
+ // form, where the values are expressed in seconds and either of them
+ // (but not both) can be omitted, and set the group deadline and test
+ // timeout respectively, if specified. Reset them to nullopt on zero.
+ //
+ virtual void
+ set_timeout (const string&, bool success, const location&) override;
+
+ // Return the nearest of the own deadline and the enclosing groups
+ // deadlines.
+ //
+ virtual optional<deadline>
+ effective_deadline () override;
protected:
group (const string& id, script& r): scope (id, nullptr, r) {}
@@ -207,6 +228,29 @@ namespace build2
public:
test (const string& id, group& p): scope (id, &p, p.root) {}
+ public:
+ // The whole test and the remaining test fragment execution deadlines.
+ //
+ // The former is based on the minimum of the test timeouts set for the
+ // enclosing scopes and is calculated on the first deadline() call.
+ // The later is set by set_timeout() from the timeout builtin call
+ // during the test execution.
+ //
+ optional<optional<deadline>> test_deadline; // calculated<specified<>>
+ optional<deadline> fragment_deadline;
+
+ // Parse the specified in seconds timeout and set the remaining test
+ // fragment execution deadline. Reset it to nullopt on zero.
+ //
+ virtual void
+ set_timeout (const string&, bool success, const location&) override;
+
+ // Return the nearest of the test and fragment execution deadlines,
+ // calculating the former on the first call.
+ //
+ virtual optional<deadline>
+ effective_deadline () override;
+
// Pre-parse data.
//
public:
@@ -254,6 +298,13 @@ namespace build2
class script: public script_base, public group
{
public:
+ // The test operation deadline and the individual test timeout (see
+ // the config.test.timeout variable for details).
+ //
+ optional<deadline> operation_deadline;
+ optional<timeout> test_timeout;
+
+ public:
script (const target& test_target,
const testscript& script_target,
const dir_path& root_wd);
@@ -263,6 +314,12 @@ namespace build2
script& operator= (script&&) = delete;
script& operator= (const script&) = delete;
+ // Return the nearest of the test operation and group execution
+ // deadlines.
+ //
+ virtual optional<deadline>
+ effective_deadline () override;
+
// Pre-parse data.
//
private:
diff --git a/libbuild2/utility.cxx b/libbuild2/utility.cxx
index 81f6809..193610f 100644
--- a/libbuild2/utility.cxx
+++ b/libbuild2/utility.cxx
@@ -593,4 +593,22 @@ namespace build2
script::regex::init ();
}
+
+ optional<uint64_t>
+ parse_number (const string& s, uint64_t max_num)
+ {
+ optional<uint64_t> r;
+
+ if (!s.empty ())
+ {
+ const char* b (s.c_str ());
+ char* e (nullptr);
+ uint64_t v (strtoull (b, &e, 10)); // Can't throw.
+
+ if (errno != ERANGE && e == b + s.size () && v <= max_num)
+ r = v;
+ }
+
+ return r;
+ }
}
diff --git a/libbuild2/utility.hxx b/libbuild2/utility.hxx
index 4560a73..7dbff2f 100644
--- a/libbuild2/utility.hxx
+++ b/libbuild2/utility.hxx
@@ -798,6 +798,13 @@ namespace build2
{
return apply_pattern (s, p.c_str ());
}
+
+ // Try to parse a string as a non-negative number returning nullopt if the
+ // argument is not a valid number or the number is greater than the
+ // specified maximum.
+ //
+ optional<uint64_t>
+ parse_number (const string&, uint64_t max = UINT64_MAX);
}
#include <libbuild2/utility.ixx>
diff --git a/tests/recipe/buildscript/testscript b/tests/recipe/buildscript/testscript
index 551f64a..787bafd 100644
--- a/tests/recipe/buildscript/testscript
+++ b/tests/recipe/buildscript/testscript
@@ -107,3 +107,123 @@
$* clean 2>-
}
+
+: timeout
+:
+if ($cxx.target.class != 'windows')
+{
+ : update
+ :
+ {
+ : expired
+ :
+ {
+ echo 'bar' >=bar;
+
+ cat <<EOI >=buildfile;
+ foo: bar
+ % [diag=update]
+ {{
+ cp $path($<) $path($>)
+ timeout 1
+ ^sleep 5
+ }}
+ EOI
+
+ $* 2>>~%EOE% != 0;
+ update file{foo}
+ buildfile:6:3: error: ^sleep terminated: execution timeout expired
+ info: while updating file{foo}
+ %.+
+ EOE
+
+ $* clean 2>-
+ }
+
+ : successful-timeout
+ :
+ {
+ echo 'bar' >=bar;
+
+ cat <<EOI >=buildfile;
+ foo: bar
+ % [diag=update]
+ {{
+ cp $path($<) $path($>)
+ timeout --success 1
+ ^sleep 5
+ }}
+ EOI
+
+ $* 2>>EOE;
+ update file{foo}
+ EOE
+
+ $* clean 2>-
+ }
+ }
+
+ : test
+ :
+ {
+ : expired
+ :
+ {
+ echo 'bar' >=bar;
+
+ cat <<EOI >=buildfile;
+ foo: bar
+ {{
+ cp $path($<) $path($>)
+ }}
+ % [diag=test] test
+ {{
+ ^sleep 5
+ }}
+ EOI
+
+ $* test config.test.timeout=1 2>>~%EOE% != 0;
+ cp file{foo}
+ test file{foo}
+ buildfile:7:3: error: ^sleep terminated: execution timeout expired
+ info: while testing file{foo}
+ %.+
+ EOE
+
+ $* test config.test.timeout=/1 2>>~%EOE% != 0;
+ test file{foo}
+ buildfile:7:3: error: ^sleep terminated: execution timeout expired
+ info: while testing file{foo}
+ %.+
+ EOE
+
+ $* clean 2>-
+ }
+
+ : not-expired
+ :
+ {
+ echo 'bar' >=bar;
+
+ cat <<EOI >=buildfile;
+ foo: bar
+ % [diag=cp]
+ {{
+ ^sleep 4
+ cp $path($<) $path($>)
+ }}
+ % [diag=test] test
+ {{
+ ^sleep 1
+ }}
+ EOI
+
+ $* test config.test.timeout=3 2>>EOE;
+ cp file{foo}
+ test file{foo}
+ EOE
+
+ $* clean 2>-
+ }
+ }
+}
diff --git a/tests/test/script/builtin/sleep.testscript b/tests/test/script/builtin/sleep.testscript
index 21ed07b..e1410ac 100644
--- a/tests/test/script/builtin/sleep.testscript
+++ b/tests/test/script/builtin/sleep.testscript
@@ -6,3 +6,21 @@
: success
:
$c <'sleep 1' && $b
+
+: timeout
+:
+{
+ : failure
+ :
+ $c <'env -t 1 -- sleep 86400' && $b 2>>~%EOE% != 0
+ %testscript:.*: error: sleep terminated: execution timeout expired%
+ %.
+ EOE
+
+ : success
+ :
+ $c <<EOI && $b
+ timeout --success 1;
+ sleep 86400
+ EOI
+}
diff --git a/tests/test/script/runner/driver.cxx b/tests/test/script/runner/driver.cxx
index d5a74a4..935541d 100644
--- a/tests/test/script/runner/driver.cxx
+++ b/tests/test/script/runner/driver.cxx
@@ -1,6 +1,13 @@
// file : tests/test/script/runner/driver.cxx -*- C++ -*-
// license : MIT; see accompanying LICENSE file
+#ifndef _WIN32
+# include <thread> // this_thread::sleep_for()
+# include <chrono>
+#else
+# include <libbutl/win32-utility.hxx>
+#endif
+
#include <limits> // numeric_limits
#include <string>
#include <cstdlib> // abort()
@@ -36,10 +43,10 @@ main (int argc, char* argv[])
// Usage: driver [-i <int>] (-o <string>)* (-e <string>)* (-f <file>)*
// (-d <dir>)* (-v <name>)* [(-t (a|m|s|z)) | (-s <int>)]
//
- // Execute actions specified by -i, -o, -e, -f, -d, and -v options in the
- // order as they appear on the command line. After that terminate abnormally
- // if -t option is provided, otherwise exit normally with the status
- // specified by -s option (0 by default).
+ // Execute actions specified by -i, -o, -e, -f, -d, -v, and -l options in
+ // the order as they appear on the command line. After that terminate
+ // abnormally if -t option is provided, otherwise exit normally with the
+ // status specified by -s option (0 by default).
//
// -i <fd>
// Forward stdin data to the standard stream denoted by the file
@@ -62,6 +69,9 @@ main (int argc, char* argv[])
// If the specified variable is set the print its value to stdout and the
// string '<none>' otherwise.
//
+ // -l <sec>
+ // Sleep the specified number of seconds.
+ //
// -t <method>
// Abnormally terminate itself using one of the following methods:
//
@@ -144,6 +154,18 @@ main (int argc, char* argv[])
optional<string> var (getenv (v));
cout << (var ? *var : "<none>") << endl;
}
+ else if (o == "-l")
+ {
+ size_t t (toi (v));
+
+ // MinGW GCC 4.9 doesn't implement this_thread so use Win32 Sleep().
+ //
+#ifndef _WIN32
+ this_thread::sleep_for (chrono::seconds (t));
+#else
+ Sleep (static_cast<DWORD> (t * 1000));
+#endif
+ }
else if (o == "-t")
{
assert (aterm == '\0' && !status); // Make sure exit method is not set.
diff --git a/tests/test/script/runner/env.testscript b/tests/test/script/runner/env.testscript
index 6fcedfa..ef90c3b 100644
--- a/tests/test/script/runner/env.testscript
+++ b/tests/test/script/runner/env.testscript
@@ -3,5 +3,30 @@
.include ../common.testscript
-$c <'env abc=xyz -- $* -v abc >xyz' && $b : set
-$c <'env --unset=abc -- $* -v abc >"<none>"' && env abc=xyz -- $b : unset
+: variables
+:
+{
+ $c <'env abc=xyz -- $* -v abc >xyz' && $b : set
+ $c <'env --unset=abc -- $* -v abc >"<none>"' && env abc=xyz -- $b : unset
+}
+
+: timeout
+:
+{
+ : expired
+ :
+ $c <'env --timeout 1 -- $* -l 5' && $b 2>>~%EOE% != 0
+ %testscript:1:1: error: .+ terminated: execution timeout expired%
+ info: test id: 1
+ EOE
+
+ : not-expired
+ :
+ $c <'env --timeout 5 -- $* -l 1' && $b
+
+ : invalid
+ :
+ $c <'env --timeout a -- $*' && $b 2>>EOE != 0
+ testscript:1:15: error: env: invalid value 'a' for option '--timeout'
+ EOE
+}
diff --git a/tests/test/script/runner/set.testscript b/tests/test/script/runner/set.testscript
index 9219cbb..41709e4 100644
--- a/tests/test/script/runner/set.testscript
+++ b/tests/test/script/runner/set.testscript
@@ -94,10 +94,28 @@
{
: non-exact
:
- $c <<EOI && $b
- set -w baz <' foo bar ';
- echo $baz >'foo bar'
- EOI
+ {
+ : non-empty
+ :
+ $c <<EOI && $b
+ set -w baz <' foo bar ';
+ echo $regex.apply($baz, '^(.*)$', '"\1"') >'"foo" "bar"'
+ EOI
+
+ : empty
+ :
+ $c <<EOI && $b
+ set -w baz <:'';
+ echo $regex.apply($baz, '^(.*)$', '"\1"') >''
+ EOI
+
+ : spaces
+ :
+ $c <<EOI && $b
+ set -w baz <' ';
+ echo $regex.apply($baz, '^(.*)$', '"\1"') >''
+ EOI
+ }
: exact
:
@@ -106,7 +124,7 @@
:
$c <<EOI && $b
set --exact --whitespace baz <' foo bar ';
- echo $baz >'foo bar '
+ echo $regex.apply($baz, '^(.*)$', '"\1"') >'"foo" "bar" ""'
EOI
: no-trailing-ws
@@ -115,8 +133,22 @@
: ':' modifier.
:
$c <<EOI && $b
- set --exact --whitespace baz <:' foo bar';
- echo $baz >'foo bar'
+ set -e -w baz <:' foo bar';
+ echo $regex.apply($baz, '^(.*)$', '"\1"') >'"foo" "bar"'
+ EOI
+
+ : empty
+ :
+ $c <<EOI && $b
+ set -e -w baz <:'';
+ echo $regex.apply($baz, '^(.*)$', '"\1"') >''
+ EOI
+
+ : spaces
+ :
+ $c <<EOI && $b
+ set -e -w baz <' ';
+ echo $regex.apply($baz, '^(.*)$', '"\1"') >'""'
EOI
}
}
@@ -134,7 +166,7 @@
bar
EOF
- echo $baz >' foo bar '
+ echo $regex.apply($baz, '^(.*)$', '"\1"') >'"" "foo" "" "bar" ""'
EOI
: exact
@@ -150,7 +182,7 @@
bar
EOF
- echo $baz >' foo bar '
+ echo $regex.apply($baz, '^(.*)$', '"\1"') >'"" "foo" "" "bar" "" ""'
EOI
: no-trailing-newline
@@ -162,7 +194,7 @@
bar
EOF
- echo $baz >' foo bar'
+ echo $regex.apply($baz, '^(.*)$', '"\1"') >'"" "foo" "" "bar"'
EOI
}
}
@@ -180,7 +212,7 @@
bar
EOF
- echo $baz >>EOO
+ echo ($baz[0]) >>EOO
foo
@@ -209,7 +241,7 @@
bar
EOF
- echo "$baz" >>EOO
+ echo ($baz[0]) >>EOO
foo
@@ -227,7 +259,7 @@
bar
EOF
- echo "$baz" >>EOO
+ echo ($baz[0]) >>EOO
foo
@@ -237,6 +269,66 @@
}
}
+: deadline
+:
+{
+ : not-reached
+ :
+ $c <<EOI && $b
+ env -t 10 -- $* -o 'foo' | set bar;
+ echo $bar >foo 2>|
+ EOI
+
+ : set-reached
+ :
+ $c <<EOI && $b 2>>~%EOE% != 0
+ $* -o 'foo' -l 10 | env -t 1 -- set bar
+ EOI
+ %testscript:.*: error: set terminated: execution timeout expired%
+ %.
+ EOE
+
+ : driver-reached
+ :
+ $c <<EOI && $b 2>>~%EOE% != 0
+ env -t 1 -- $* -o 'foo' -l 10 | set bar
+ EOI
+ %testscript:.*: error: .+driver.* terminated: execution timeout expired%
+ %.
+ EOE
+
+ : read-some-data
+ :
+ {
+ s="----------------------------------------------------------------------"
+ s="$s$s$s$s$s$s$s$s$s$s$s$s$s$s$s$s$s$s$s$s$s$s$s$s$s$s$s$s$s$s$s$s$s$s$s"
+ s="$s$s$s$s$s$s$s$s$s$s$s$s$s$s$s$s$s$s$s$s$s$s$s$s$s$s$s$s$s$s$s$s$s$s$s"
+
+ : failure
+ :
+ $c <<EOI && $b 2>>~%EOE% != 0
+ echo "$s" >=f;
+ $* -o 'foo' -l 10 | cat f - | env -t 2 -- set bar
+ EOI
+ %testscript:.*: error: set terminated: execution timeout expired%
+ %.
+ EOE
+
+ : success
+ :
+ : Note that the cat builtin ends up with the 'broken pipe' diagnostics or
+ : similar.
+ :
+ $c <<EOI && $b
+ echo "$s" >=f;
+ timeout --success 2;
+ $* -o 'foo' -l 10 | cat f - 2>>~%EOE% | set bar
+ %cat: .+%
+ EOE
+ EOI
+ }
+}
+
: attributes
:
{
diff --git a/tests/test/script/runner/timeout.testscript b/tests/test/script/runner/timeout.testscript
new file mode 100644
index 0000000..ae8f535
--- /dev/null
+++ b/tests/test/script/runner/timeout.testscript
@@ -0,0 +1,503 @@
+# file : tests/test/script/runner/timeout.testscript
+# license : MIT; see accompanying LICENSE file
+
+.include ../common.testscript
+
+: test
+:
+{
+ : fragment-timeout
+ :
+ {
+ : set
+ :
+ $c <<EOI && $b 2>>~%EOE% != 0
+ timeout 1;
+ $* -l 3
+ EOI
+ %testscript:.*: error: .+ terminated: execution timeout expired%
+ %.
+ EOE
+
+ : reset
+ :
+ $c <<EOI && $b
+ timeout 1;
+ timeout 0;
+ $* -l 3
+ EOI
+
+ : override
+ :
+ $c <<EOI && $b 2>>~%EOE% != 0
+ +timeout /10
+
+ {
+ +timeout /10
+
+ timeout 1;
+ env -t 10 -- $* -l 3
+ }
+ EOI
+ %testscript:.*: error: .+ terminated: execution timeout expired%
+ %.
+ EOE
+
+ : successful
+ :
+ $c <<EOI && $b
+ timeout --success 1;
+ $* -l 3
+ EOI
+ }
+
+ : missing
+ :
+ $c <'timeout' && $b 2>>~%EOE% != 0
+ testscript:1:1: error: missing timeout
+ %.
+ EOE
+
+ : invalid
+ :
+ $c <'timeout foo' && $b 2>>~%EOE% != 0
+ testscript:1:1: error: invalid test fragment timeout 'foo'
+ %.
+ EOE
+}
+
+: group
+:
+{
+ : group-timeout
+ :
+ {
+ : set
+ :
+ $c <<EOI && $b 2>>~%EOE% != 0
+ {
+ +timeout 1
+
+ $* -l 3
+ }
+ EOI
+ %testscript:.*: error: .+ terminated: execution timeout expired%
+ %.
+ EOE
+
+ : reset
+ :
+ $c <<EOI && $b
+ {
+ +timeout 1
+ +timeout 0
+
+ $* -l 3
+ }
+ EOI
+
+ : override
+ :
+ : Also test slash usage inside the timeout value.
+ :
+ $c <<EOI && $b 2>>~%EOE% != 0
+ +timeout 10/10
+
+ {
+ +timeout 1/
+
+ timeout 10;
+ env -t 10 -- $* -l 3
+ }
+ EOI
+ %testscript:.*: error: .+ terminated: execution timeout expired%
+ %.
+ EOE
+
+ : invalid
+ :
+ $c <<EOI && $b 2>>~%EOE% != 0
+ {
+ +timeout foo/
+ }
+ EOI
+ testscript:2:4: error: invalid test group timeout 'foo'
+ %.
+ EOE
+ }
+
+ : test-timeout
+ :
+ {
+ : set
+ :
+ $c <<EOI && $b 2>>~%EOE% != 0
+ {
+ +timeout /1
+
+ $* -l 3
+ }
+ EOI
+ %testscript:.*: error: .+ terminated: execution timeout expired%
+ %.
+ EOE
+
+ : reset
+ :
+ $c <<EOI && $b
+ {
+ +timeout /1
+ +timeout /0
+
+ $* -l 3
+ }
+ EOI
+
+ : override
+ :
+ $c <<EOI && $b 2>>~%EOE% != 0
+ +timeout 10/10
+
+ {
+ +timeout /1
+
+ timeout 10;
+ env -t 10 -- $* -l 3
+ }
+ EOI
+ %testscript:.*: error: .+ terminated: execution timeout expired%
+ %.
+ EOE
+
+ : invalid
+ :
+ $c <<EOI && $b 2>>~%EOE% != 0
+ {
+ +timeout /foo
+ }
+ EOI
+ testscript:2:4: error: invalid test timeout 'foo'
+ %.
+ EOE
+ }
+}
+
+: script
+:
+{
+ : group-timeout
+ :
+ {
+ : set
+ :
+ $c <<EOI && $b 2>>~%EOE% != 0
+ +timeout 1
+
+ $* -l 3
+ EOI
+ %testscript:.*: error: .+ terminated: execution timeout expired%
+ %.
+ EOE
+
+ : reset
+ :
+ $c <<EOI && $b
+ +timeout 1
+ +timeout 0
+
+ $* -l 3
+ EOI
+
+ : override
+ :
+ $c <<EOI && $b 2>>~%EOE% != 0
+ +timeout 1
+
+ {
+ +timeout 10
+
+ timeout 10;
+ env -t 10 -- $* -l 3
+ }
+ EOI
+ %testscript:.*: error: .+ terminated: execution timeout expired%
+ %.
+ EOE
+
+ : invalid
+ :
+ $c <<EOI && $b 2>>~%EOE% != 0
+ +timeout foo/
+ EOI
+ testscript:1:2: error: invalid testscript timeout 'foo'
+ %.
+ EOE
+
+ : successful
+ :
+ $c <<EOI && $b
+ +timeout --success 1
+
+ $* -l 3
+ EOI
+ }
+
+ : test-timeout
+ :
+ {
+ : set
+ :
+ $c <<EOI && $b 2>>~%EOE% != 0
+ +timeout /1
+
+ $* -l 3
+ EOI
+ %testscript:.*: error: .+ terminated: execution timeout expired%
+ %.
+ EOE
+
+ : reset
+ :
+ $c <<EOI && $b
+ +timeout /1
+ +timeout /0
+
+ $* -l 3
+ EOI
+
+ : override
+ :
+ $c <<EOI && $b 2>>~%EOE% != 0
+ +timeout /1
+
+ {
+ +timeout --success /1
+
+ {
+ +timeout 10/10
+
+ timeout 10;
+ env -t 10 -- $* -l 3
+ }
+ }
+ EOI
+ %testscript:.*: error: .+ terminated: execution timeout expired%
+ %.
+ EOE
+
+ : successful
+ :
+ $c <<EOI && $b
+ +timeout --success /1
+
+ $* -l 3
+ EOI
+
+ : invalid
+ :
+ $c <<EOI && $b 2>>~%EOE% != 0
+ {
+ +timeout /foo
+ }
+ EOI
+ testscript:2:4: error: invalid test timeout 'foo'
+ %.
+ EOE
+ }
+}
+
+: config
+:
+{
+ : operation
+ :
+ {
+ : set
+ :
+ $c <<EOI && $b config.test.timeout=1/10 2>>~%EOE% != 0
+ $* -l 3
+ EOI
+ %testscript:.*: error: .+ terminated: execution timeout expired%
+ %.
+ EOE
+
+ : reset
+ :
+ $c <<EOI && $b config.test.timeout=1/10 config.test.timeout=0/10
+ $* -l 3
+ EOI
+
+ : override
+ :
+ $c <<EOI && $b config.test.timeout=1/10 2>>~%EOE% != 0
+ +timeout 10
+
+ {
+ +timeout 10/10
+
+ timeout 10;
+ env -t 10 -- $* -l 3
+ }
+ EOI
+ %testscript:.*: error: .+ terminated: execution timeout expired%
+ %.
+ EOE
+
+ : invalid
+ :
+ $c && $b config.test.timeout=foo 2>>EOE != 0
+ error: invalid config.test.timeout test operation timeout value 'foo'
+ EOE
+ }
+
+ : test
+ :
+ {
+ : set
+ :
+ $c <<EOI && $b config.test.timeout=10/1 2>>~%EOE% != 0
+ $* -l 3
+ EOI
+ %testscript:.*: error: .+ terminated: execution timeout expired%
+ %.
+ EOE
+
+ : reset
+ :
+ $c <<EOI && $b config.test.timeout=10/1 config.test.timeout=10/0
+ $* -l 3
+ EOI
+
+ : override
+ :
+ $c <<EOI && $b config.test.timeout=10/1 2>>~%EOE% != 0
+ +timeout 10
+
+ {
+ +timeout 10/10
+
+ timeout 10;
+ env -t 10 -- $* -l 3
+ }
+ EOI
+ %testscript:.*: error: .+ terminated: execution timeout expired%
+ %.
+ EOE
+
+ : invalid
+ :
+ $c && $b config.test.timeout=/foo 2>>EOE != 0
+ error: invalid config.test.timeout test timeout value 'foo'
+ EOE
+ }
+}
+
+: failures
+:
+: Here we test that the left-hand side processes are terminated on failure.
+:
+{
+ : set
+ :
+ $c <<EOI && $b 2>>~%EOE% != 0
+ env -t 1 -- $* -l 86400 -o 'foo' | set --foo bar
+ EOI
+ %testscript:.*: error: set: unknown option '--foo'%
+ %.
+ EOE
+
+ : exit
+ :
+ $c <<EOI && $b 2>>~%EOE% != 0
+ env -t 1 -- $* -l 86400 -o 'foo' | exit 0
+ EOI
+ %testscript:.*: error: exit builtin must be the only pipe command%
+ %.
+ EOE
+
+ : redirect
+ :
+ $c <<EOI && $b 2>>~%EOE% != 0
+ env -t 1 -- $* -l 86400 -o 'foo' | touch $~/foo/bar
+ EOI
+ %testscript:.*: error: touch exited with code 1%
+ %.+
+ EOE
+}
+
+: pipeline
+:
+{
+ : prog-tm-prog
+ :
+ $c <'$* -l 10 | env -t 1 -- $* -i 0' && $b 2>>~%EOE% != 0
+ %testscript:.*: error: .+driver.* terminated: execution timeout expired%
+ %.
+ EOE
+
+ : tm-prog-prog
+ :
+ $c <'env -t 1 -- $* -l 10 | $* -i 0' && $b 2>>~%EOE% != 0
+ %testscript:.*: error: .+driver.* terminated: execution timeout expired%
+ %.
+ EOE
+
+ : tm-cat-prog
+ :
+ $c <'env -t 1 -- cat <"test" | $* -l 10' && $b 2>>~%EOE% != 0
+ %testscript:.*: error: cat terminated: execution timeout expired%
+ %.
+ EOE
+
+ : cat-tm-prog
+ :
+ $c <'cat <"test" | env -t 1 -- $* -l 10' && $b 2>>~%EOE% != 0
+ %testscript:.*: error: .+driver.* terminated: execution timeout expired%
+ %.
+ EOE
+
+ : tm-prog-cat
+ :
+ $c <'env -t 1 -- $* -l 10 | cat >-' && $b 2>>~%EOE% != 0
+ %testscript:.*: error: .+driver.* terminated: execution timeout expired%
+ %.
+ EOE
+
+ : tm-echo-prog
+ :
+ $c <'env -t 1 -- echo "test" | $* -l 10' && $b 2>>~%EOE% != 0
+ %testscript:.*: error: echo terminated: execution timeout expired%
+ %.
+ EOE
+
+ : successful
+ :
+ {
+ : prog-prog
+ :
+ $c <<EOI && $b
+ timeout --success 1;
+ $* -l 10 | $* -i 0
+ EOI
+
+ : prog-cat
+ :
+ $c <<EOI && $b
+ timeout --success 1;
+ $* -l 10 | cat
+ EOI
+
+ : cat-prog
+ :
+ $c <<EOI && $b
+ s="----------------------------------------------------------------------";
+ s="$s$s$s$s$s$s$s$s$s$s$s$s$s$s$s$s$s$s$s$s$s$s$s$s$s$s$s$s$s$s$s$s$s$s$s";
+ s="$s$s$s$s$s$s$s$s$s$s$s$s$s$s$s$s$s$s$s$s$s$s$s$s$s$s$s$s$s$s$s$s$s$s$s";
+ s="$s$s$s$s$s$s$s$s$s$s$s$s$s$s$s$s$s$s$s$s$s$s$s$s$s$s$s$s$s$s$s$s$s$s$s";
+ timeout --success 1;
+ cat <"$s" 2>>~%EOE% | $* -l 10 -i 0
+ %cat: unable to print stdin: .+%
+ EOE
+ EOI
+ }
+}
diff --git a/tests/test/simple/generated/buildfile b/tests/test/simple/generated/buildfile
index 0809bdf..0344891 100644
--- a/tests/test/simple/generated/buildfile
+++ b/tests/test/simple/generated/buildfile
@@ -6,4 +6,5 @@
./: testscript exe{driver} $b file{*.in}
-exe{driver}: cxx{driver}
+import libs = libbutl%lib{butl}
+exe{driver}: cxx{driver} $libs
diff --git a/tests/test/simple/generated/driver.cxx b/tests/test/simple/generated/driver.cxx
index 1d911df..18fd0ae 100644
--- a/tests/test/simple/generated/driver.cxx
+++ b/tests/test/simple/generated/driver.cxx
@@ -1,24 +1,52 @@
// file : tests/test/simple/generated/driver.cxx -*- C++ -*-
// license : MIT; see accompanying LICENSE file
+#ifndef _WIN32
+# include <chrono>
+# include <thread>
+#else
+# include <libbutl/win32-utility.hxx>
+#endif
+
#include <string>
#include <fstream>
#include <iostream>
using namespace std;
+// If the -s option is specified, then also sleep for 5 seconds.
+//
int
main (int argc, char* argv[])
{
+ int i (1);
+ for (; i != argc; ++i)
+ {
+ string a (argv[i]);
+
+ if (a == "-s")
+ {
+ // MINGW GCC 4.9 doesn't implement this_thread so use Win32 Sleep().
+ //
+#ifndef _WIN32
+ this_thread::sleep_for (chrono::seconds (5));
+#else
+ Sleep (5000);
+#endif
+ }
+ else
+ break;
+ }
+
int r (0);
- if (argc == 1)
+ if (i == argc)
{
cout << "1.2.3" << endl;
}
else
{
- ifstream ifs (argv[1]);
+ ifstream ifs (argv[i]);
if (!ifs.is_open ())
cerr << "unable to open " << argv[1] << endl;
diff --git a/tests/test/simple/generated/testscript b/tests/test/simple/generated/testscript
index a04dccc..f6a89d8 100644
--- a/tests/test/simple/generated/testscript
+++ b/tests/test/simple/generated/testscript
@@ -42,3 +42,60 @@ driver = $src_root/../exe{driver}
./: file{output}: test.stdout = true
file{output}: in{output} $src_root/manifest #@@ in module
EOI
+
+: timeout
+:
+{
+ : operation
+ :
+ {
+ : no-output
+ :
+ ln -s $src_base/output.in ./;
+ $* config.test.timeout=1 <<EOI 2>>/~%EOE% != 0
+ driver = $src_root/../exe{driver}
+ ./: test = $driver
+ ./: test.options = -s
+ ./: $driver
+ EOI
+ error: test dir{./} failed
+ % error: .+ -s terminated: execution timeout expired%
+ % info: test command line: .+%
+ EOE
+
+ : output
+ :
+ ln -s $src_base/output.in ./;
+ $* config.test.timeout=1 &output &output.d <<EOI 2>>/~%EOE% != 0
+ driver = $src_root/../exe{driver}
+ ./: test = $driver
+ ./: test.options = -s
+ ./: $driver
+ ./: file{output}: test.stdout = true
+ file{output}: in{output} $src_root/manifest #@@ in module
+ EOI
+ error: test dir{./} failed
+ % error: diff .+ terminated: execution timeout expired%
+ % error: .+ -s terminated: execution timeout expired%
+ % info: test command line: .+%
+ EOE
+ }
+
+ : test
+ :
+ {
+ : no-output
+ :
+ ln -s $src_base/output.in ./;
+ $* config.test.timeout=/1 <<EOI 2>>/~%EOE% != 0
+ driver = $src_root/../exe{driver}
+ ./: test = $driver
+ ./: test.options = -s
+ ./: $driver
+ EOI
+ error: test dir{./} failed
+ % error: .+ -s terminated: execution timeout expired%
+ % info: test command line: .+%
+ EOE
+ }
+}