diff options
Diffstat (limited to 'libbuild2/test')
28 files changed, 3439 insertions, 391 deletions
diff --git a/libbuild2/test/common.cxx b/libbuild2/test/common.cxx index f50d289..89f3dd6 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 @@ -146,8 +150,7 @@ namespace build2 t.name == n->value && // Name matches. tt.name == n->type && // Target type matches. d == n->dir && // Directory matches. - (search_existing (*n, *root_) == &t || - search_existing (*n, *root_, d) == &t); + search_existing (*n, *root_) == &t; if (r) break; @@ -194,8 +197,7 @@ namespace build2 t.name == n->value && tt.name == n->type && d == n->dir && - (search_existing (*n, *root_) == &t || - search_existing (*n, *root_, d) == &t); + search_existing (*n, *root_) == &t; if (!r) continue; // Not our target. @@ -215,5 +217,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..958b541 100644 --- a/libbuild2/test/common.hxx +++ b/libbuild2/test/common.hxx @@ -20,11 +20,16 @@ namespace build2 { const variable& config_test; const variable& config_test_output; + const variable& config_test_timeout; + const variable& config_test_runner; const variable& var_test; const variable& test_options; const variable& test_arguments; + const variable& test_runner_path; + const variable& test_runner_options; + const variable& test_stdin; const variable& test_stdout; const variable& test_roundtrip; @@ -40,11 +45,34 @@ 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 test.runner.{path,options} values extracted from the + // config.test.runner value, if any. + // + const process_path* runner_path = nullptr; + const strings* runner_options = nullptr; + // 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 +93,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..32548f4 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> @@ -21,6 +23,8 @@ namespace build2 { namespace test { + static const file_rule file_rule_ (true /* check_type */); + void boot (scope& rs, const location&, module_boot_extra& extra) { @@ -28,15 +32,14 @@ namespace build2 l5 ([&]{trace << "for " << rs;}); - // Register our operations. - // - rs.insert_operation (test_id, op_test); - rs.insert_operation (update_for_test_id, op_update_for_test); - // Enter module variables. Do it during boot in case they get assigned // in bootstrap.build. // - auto& vp (rs.var_pool ()); + // Most of the variables we enter are qualified so go straight for the + // public variable pool. + // + auto& vp (rs.var_pool (true /* public */)); + auto& pvp (rs.var_pool ()); // For `test` and `for_test`. common_data d { @@ -44,8 +47,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,13 +58,28 @@ 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"), + + // Test command runner path and options (see the manual for semantics). + // + vp.insert<strings> ("config.test.runner"), + // The test variable is a name which can be a path (with the // true/false special values) or a target name. // - vp.insert<name> ("test", variable_visibility::target), + pvp.insert<name> ("test", variable_visibility::target), vp.insert<strings> ("test.options"), vp.insert<strings> ("test.arguments"), + // Test command runner path and options extracted from + // config.test.runner. + // + vp.insert<process_path> ("test.runner.path"), + vp.insert<strings> ("test.runner.options"), + // Prerequisite-specific. // // test.stdin and test.stdout can be used to mark a prerequisite as a @@ -94,12 +112,12 @@ namespace build2 // This one is used by other modules/rules. // - vp.insert<bool> ("for_test", variable_visibility::prereq); + pvp.insert<bool> ("for_test", variable_visibility::prereq); // These are only used in testscript. // - vp.insert<strings> ("test.redirects"); - vp.insert<strings> ("test.cleanups"); + vp.insert<cmdline> ("test.redirects"); + vp.insert<cmdline> ("test.cleanups"); // Unless already set, default test.target to build.host. Note that it // can still be overriden by the user, e.g., in root.build. @@ -108,9 +126,14 @@ namespace build2 value& v (rs.assign (d.test_target)); if (!v || v.empty ()) - v = cast<target_triplet> (rs.ctx.global_scope["build.host"]); + v = *rs.ctx.build_host; } + // Register our operations. + // + rs.insert_operation (test_id, op_test, &d.var_test); + rs.insert_operation (update_for_test_id, op_update_for_test, &d.var_test); + extra.set_module (new module (move (d))); } @@ -189,11 +212,84 @@ 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.operation_timeout = parse_timeout (t, ot); + } + + // config.test.runner + // + { + value& pv (rs.assign (m.test_runner_path)); + value& ov (rs.assign (m.test_runner_options)); + + if (lookup l = lookup_config (rs, m.config_test_runner)) + { + const strings& args (cast<strings> (l)); + + // Extract the runner process path. + // + { + const string& s (args.empty () ? empty_string : args.front ()); + + path p; + try { p = path (s); } catch (const invalid_path&) {} + + if (p.empty ()) + fail << "invalid runner path '" << s << "' in " + << m.config_test_runner; + + pv = run_search (p, false /* init */); + m.runner_path = &pv.as<process_path> (); + } + + // Extract the runner options. + // + { + ov = strings (++args.begin (), args.end ()); + m.runner_options = &ov.as<strings> (); + } + } + else + { + pv = nullptr; + ov = nullptr; + } + } + //@@ TODO: Need ability to specify extra diff options (e.g., // --strip-trailing-cr, now hardcoded). // //@@ TODO: Pring report. + // Environment. + // + // The only external program that we currently use is diff. None of the + // implementations we looked at (GNU diffutils, FreeBSD) use any + // environment variables. + // Register target types. // { @@ -206,18 +302,18 @@ namespace build2 { default_rule& dr (m); - // Note: register for mtime_target to take priority over the fallback - // rule below. - // - rs.insert_rule<target> (perform_test_id, "test", dr); - rs.insert_rule<mtime_target> (perform_test_id, "test", dr); - rs.insert_rule<alias> (perform_test_id, "test", dr); + rs.insert_rule<target> (perform_test_id, "test", dr); + rs.insert_rule<alias> (perform_test_id, "test", dr); // Register the fallback file rule for the update-for-test operation, // similar to update. // - rs.global_scope ().insert_rule<mtime_target> ( - perform_test_id, "test.file", file_rule::instance); + // Note: use target instead of anything more specific (such as + // mtime_target) in order not to take precedence over the "test" rule + // above. + // + rs.global_scope ().insert_rule<target> ( + perform_test_id, "test.file", file_rule_); } return true; 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 d06bd7c..2535adb 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; @@ -13,16 +17,43 @@ namespace build2 namespace test { static operation_id - test_pre (const values& params, meta_operation_id mo, const location& l) + pre_test (context&, const values&, meta_operation_id mo, const location&) { - if (!params.empty ()) - fail (l) << "unexpected parameters for operation test"; - // Run update as a pre-operation, unless we are disfiguring. // 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, @@ -33,8 +64,12 @@ namespace build2 "has nothing to test", // We cannot "be tested". execution_mode::first, 1 /* concurrency */, - &test_pre, - nullptr + &pre_test, + nullptr, + nullptr, + nullptr, + nullptr, + &adhoc_apply }; // Also the explicit update-for-test operation alias. @@ -49,8 +84,12 @@ namespace build2 op_update.name_done, op_update.mode, op_update.concurrency, - op_update.pre, - op_update.post + op_update.pre_operation, + op_update.post_operation, + op_update.operation_pre, + op_update.operation_post, + op_update.adhoc_match, + op_update.adhoc_apply }; } } diff --git a/libbuild2/test/rule.cxx b/libbuild2/test/rule.cxx index db490d9..28eb35b 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> @@ -24,7 +30,7 @@ namespace build2 namespace test { bool rule:: - match (action, target&, const string&) const + match (action, target&) const { // We always match, even if this target is not testable (so that we can // ignore it; see apply()). @@ -60,11 +66,11 @@ namespace build2 // Resolve group members. // - if (!see_through || t.type ().see_through) + if (!see_through_only || t.type ().see_through ()) { // Remember that we are called twice: first during update for test // (pre-operation) and then during test. During the former, we rely on - // the normall update rule to resolve the group members. During the + // the normal update rule to resolve the group members. During the // latter, there will be no rule to do this but the group will already // have been resolved by the pre-operation. // @@ -534,11 +540,19 @@ namespace build2 if (verb) { - diag_record dr (text); - dr << "test " << ts; - - if (!t.is_a<alias> ()) - dr << ' ' << t; + // If the target is an alias, then testscript itself is the + // target. + // + if (t.is_a<alias> ()) + print_diag ("test", ts); + else + { + // In this case the test is really a combination of the target + // and testscript and using "->" feels off. Also, let's list the + // testscript after the target even though its a source. + // + print_diag ("test", t, ts, "+"); + } } res.push_back (ctx.dry_run @@ -549,22 +563,22 @@ namespace build2 { scope_state& r (res.back ()); - if (!ctx.sched.async (ctx.count_busy (), - t[a].task_count, - [this] (const diag_frame* ds, - scope_state& r, - const target& t, - const testscript& ts, - const dir_path& wd) - { - diag_frame::stack_guard dsg (ds); - r = perform_script_impl (t, ts, wd, *this); - }, - diag_frame::stack (), - ref (r), - cref (t), - cref (ts), - cref (wd))) + if (!ctx.sched->async (ctx.count_busy (), + t[a].task_count, + [this] (const diag_frame* ds, + scope_state& r, + const target& t, + const testscript& ts, + const dir_path& wd) + { + diag_frame::stack_guard dsg (ds); + r = perform_script_impl (t, ts, wd, *this); + }, + diag_frame::stack (), + ref (r), + cref (t), + cref (ts), + cref (wd))) { // Executed synchronously. If failed and we were not asked to // keep going, bail out. @@ -632,11 +646,55 @@ namespace build2 // ... // nameN arg arg ... nullptr nullptr // - static bool + // Stack-allocated linked list of information about the running pipeline + // processes. + // + // Note: constructed incrementally. + // + struct pipe_process + { + // Initially NULL. Set to the address of the process object when it is + // created. Reset back to NULL when the process is executed and its exit + // status is collected (see complete_pipe() for details). + // + process* proc = nullptr; + + char const** args; // Only for diagnostics. + + diag_buffer dbuf; + bool force_dbuf; + + // True if this process has been terminated. + // + bool terminated = false; + + // True if this process has been terminated but we failed to read out + // its stderr stream in the reasonable timeframe (2 seconds) after the + // termination. + // + // Note that this may happen if there is a still running child process + // of the terminated process which has inherited the parent's stderr + // file descriptor. + // + bool unread_stderr = false; + + pipe_process* prev; // NULL for the left-most program. + pipe_process* next; // Left-most program for the right-most program. + + pipe_process (context& x, + char const** as, + bool fb, + pipe_process* p, + pipe_process* f) + : args (as), dbuf (x), force_dbuf (fb), prev (p), next (f) {} + }; + + static void run_test (const target& t, - diag_record& dr, char const** args, - process* prev = nullptr) + int ofd, + const optional<timestamp>& deadline, + pipe_process* prev = nullptr) { // Find the next process, if any. // @@ -644,23 +702,410 @@ namespace build2 for (next++; *next != nullptr; next++) ; next++; + bool last (*next == nullptr); + // Redirect stdout to a pipe unless we are last. // - int out (*next != nullptr ? -1 : 1); - bool pr; - process_exit pe; + int out (last ? ofd : -1); + + // Propagate the pointer to the left-most program. + // + // Also force diag buffering for the trailing diff process, so it's + // stderr is never printed if the test program fails (see + // complete_pipe() for details). + // + pipe_process pp (t.ctx, + args, + last && ofd == 2, + prev, + prev != nullptr ? prev->next : nullptr); + + if (prev != nullptr) + prev->next = &pp; + else + pp.next = &pp; // Points to itself. try { - process p (prev == nullptr - ? process (args, 0, out) // First process. - : process (args, *prev, out)); // Next process. + // 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 2 seconds. 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->args[0] << ": " << e; + } + + p->terminated = true; + } + + // Wait a bit for the processes to terminate and kill the remaining + // ones. + // + timestamp deadline (system_clock::now () + chrono::seconds (2)); + + 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->args[0] << ": " << e; + } + } + }; - pr = *next == nullptr || run_test (t, dr, next, &p); - p.wait (); + // Read out all the pipeline's buffered strerr streams watching for + // the deadline, if specified. If the deadline is reached, then + // terminate the whole pipeline, move the deadline by another 2 + // seconds, and continue reading. + // + // Note that we assume that this timeout increment is normally + // sufficient to read out the buffered data written by the already + // terminated processes. If, however, that's not the case (see + // pipe_process for the possible reasons), then we just set + // unread_stderr flag to true for such processes and bail out. + // + // Also note that this implementation is inspired by the + // script::run_pipe::read_pipe() lambda. + // + auto read_pipe = [&pp, &deadline, &term_pipe] () + { + fdselect_set fds; + for (pipe_process* p (&pp); p != nullptr; p = p->prev) + { + diag_buffer& b (p->dbuf); + + if (b.is.is_open ()) + fds.emplace_back (b.is.fd (), p); + } - assert (p.exit); - pe = *p.exit; + optional<timestamp> dl (deadline); + bool terminated (false); + + for (size_t unread (fds.size ()); unread != 0;) + { + try + { + // If a deadline is specified, then pass the timeout to + // fdselect(). + // + if (dl) + { + timestamp now (system_clock::now ()); + + if (*dl <= now || ifdselect (fds, *dl - now) == 0) + { + if (!terminated) + { + term_pipe (&pp); + terminated = true; + + dl = system_clock::now () + chrono::seconds (2); + continue; + } + else + { + for (fdselect_state& s: fds) + { + if (s.fd != nullfd) + { + pipe_process* p (static_cast<pipe_process*> (s.data)); + + p->unread_stderr = true; + + // Let's also close the stderr stream not to confuse + // diag_buffer::close() (see script::read() for + // details). + // + try + { + p->dbuf.is.close (); + } + catch (const io_error&) {} + } + } + + break; + } + } + } + else + ifdselect (fds); + + for (fdselect_state& s: fds) + { + if (s.ready) + { + pipe_process* p (static_cast<pipe_process*> (s.data)); + + if (!p->dbuf.read (p->force_dbuf)) + { + s.fd = nullfd; + --unread; + } + } + } + } + catch (const io_error& e) + { + fail << "io error reading pipeline streams: " << e; + } + } + }; + + // Wait for the pipeline processes to complete, watching for the + // deadline, if specified. If the deadline is reached, then terminate + // the whole pipeline. + // + // Note: must be called after read_pipe(). + // + auto wait_pipe = [&pp, &deadline, &timed_wait, &term_pipe] () + { + for (pipe_process* p (&pp); p != nullptr; p = p->prev) + { + try + { + if (!deadline) + p->proc->wait (); + else if (!timed_wait (*p->proc, *deadline)) + term_pipe (p); + } + catch (const process_error& e) + { + fail << "unable to wait " << p->args[0] << ": " << e; + } + } + }; + + // Iterate over the pipeline processes left to right, printing their + // stderr if buffered and issuing the diagnostics if the exit code is + // not available (terminated abnormally or due to a deadline), is + // non-zero, or stderr was not fully read. Afterwards, fail if any of + // such a faulty processes were encountered. + // + // Note that we only issue diagnostics for the first failure. + // + // Note: must be called after wait_pipe() and only once. + // + auto complete_pipe = [&pp, &t] () + { + pipe_process* b (pp.next); // Left-most program. + assert (b != nullptr); // The lambda can only be called once. + pp.next = nullptr; + + bool fail (false); + for (pipe_process* p (b); p != nullptr; p = p->next) + { + assert (p->proc != nullptr); // The lambda can only be called once. + + // Collect the exit status, if present. + // + // Absent if the process misses the deadline. + // + optional<process_exit> pe; + + const process& pr (*p->proc); + +#ifndef _WIN32 + if (!(p->terminated && + !pr.exit->normal () && + pr.exit->signal () == SIGTERM)) +#else + if (!(p->terminated && + !pr.exit->normal () && + pr.exit->status == DBG_TERMINATE_PROCESS)) +#endif + pe = pr.exit; + + p->proc = nullptr; + + // Verify the exit status and issue the diagnostics on failure. + // + // Note that we only issue diagnostics for the first failure but + // continue iterating to reset process pointers to NULL. Also note + // that if the test program fails, then the potential diff's + // diagnostics is suppressed since it is always buffered. + // + if (!fail) + { + diag_record dr; + + // Note that there can be a race, so that the process we have + // terminated due to reaching the deadline has in fact exited + // normally. Thus, the 'unread stderr' situation can also happen + // to a successfully terminated process. If that's the case, we + // report this problem as the main error and the secondary error + // otherwise. + // + if (!pe || + !pe->normal () || + pe->code () != 0 || + p->unread_stderr) + { + fail = true; + + dr << error << "test " << t << " failed" // Multi test: test 1. + << error << "process " << p->args[0] << ' '; + + if (!pe) + { + dr << "terminated: execution timeout expired"; + + if (p->unread_stderr) + dr << error << "stderr not closed after exit"; + } + else if (!pe->normal () || pe->code () != 0) + { + dr << *pe; + + if (p->unread_stderr) + dr << error << "stderr not closed after exit"; + } + else + { + assert (p->unread_stderr); + + dr << "stderr not closed after exit"; + } + + if (verb == 1) + { + dr << info << "test command line: "; + + for (pipe_process* p (b); p != nullptr; p = p->next) + { + if (p != b) + dr << " | "; + + print_process (dr, p->args); + } + } + } + + // Now print the buffered stderr, if present, and/or flush the + // diagnostics, if issued. + // + if (p->dbuf.is_open ()) + p->dbuf.close (move (dr)); + } + } + + if (fail) + throw failed (); + }; + + process p; + { + process::pipe ep; + { + fdpipe p; + if (diag_buffer::pipe (t.ctx, pp.force_dbuf) == -1) // Buffering? + { + try + { + p = fdopen_pipe (); + } + catch (const io_error& e) + { + fail << "unable to redirect stderr: " << e; + } + + // Note that we must return non-owning fd to our end of the pipe + // (see the process class for details). + // + ep = process::pipe (p.in.get (), move (p.out)); + } + else + ep = process::pipe (-1, 2); + + // Note that we must open the diag buffer regardless of the + // diag_buffer::pipe() result. + // + pp.dbuf.open (args[0], move (p.in), fdstream_mode::non_blocking); + } + + p = (prev == nullptr + ? process (args, 0, out, move (ep)) // First process. + : process (args, *prev->proc, out, move (ep))); // Next process. + } + + pp.proc = &p; + + // If the right-hand part of the pipe fails, then make sure we don't + // wait indefinitely in the process destructor if the deadline is + // specified or just because a process is blocked on stderr. + // + auto g (make_exception_guard ([&pp, &term_pipe] () + { + if (pp.proc != nullptr) + try + { + // Close all buffered pipeline stderr streams ignoring io_error + // exceptions. + // + for (pipe_process* p (&pp); p != nullptr; p = p->prev) + { + if (p->dbuf.is.is_open ()) + try + { + p->dbuf.is.close(); + } + catch (const io_error&) {} + } + + term_pipe (&pp); + } + catch (const failed&) + { + // We can't do much here. + } + })); + + if (!last) + run_test (t, next, ofd, deadline, &pp); + + // Complete the pipeline execution, if not done yet. + // + if (pp.proc != nullptr) + { + read_pipe (); + wait_pipe (); + complete_pipe (); + } } catch (const process_error& e) { @@ -671,20 +1116,6 @@ namespace build2 throw failed (); } - - bool wr (pe.normal () && pe.code () == 0); - - if (!wr) - { - if (pr) // First failure? - dr << fail << "test " << t << " failed"; // Multi test: test 1. - - dr << error; - print_process (dr, args); - dr << " " << pe; - } - - return pr && wr; } target_state rule:: @@ -730,7 +1161,7 @@ namespace build2 fail << "invalid test executable override: '" << *n << "'"; else { - // Must be a target name. + // Must be a target name. Could be from src (e.g., a script). // // @@ OUT: what if this is a @-qualified pair of names? // @@ -811,12 +1242,32 @@ namespace build2 args.push_back (nullptr); } - // If dry-run, the target may not exist. + process_path pp; + + // Do we have a test runner? // - process_path pp (!ctx.dry_run - ? run_search (p, true /* init */) - : run_try_search (p, true)); - args.push_back (pp.empty () ? p.string ().c_str () : pp.recall_string ()); + if (runner_path == nullptr) + { + // If dry-run, the target may not exist. + // + pp = process_path (!ctx.dry_run + ? run_search (p, true /* init */) + : run_try_search (p, true)); + + args.push_back (pp.empty () + ? p.string ().c_str () + : pp.recall_string ()); + } + else + { + args.push_back (runner_path->recall_string ()); + + append_options (args, *runner_options); + + // Leave it to the runner to resolve the test program path. + // + args.push_back (p.string ().c_str ()); + } // Do we have options and/or arguments? // @@ -840,10 +1291,19 @@ namespace build2 // Do we have stdout? // + // If we do, then match it using diff. Also redirect the diff's stdout + // to stderr, similar to how we do that for the script (see + // script::check_output() for the reasoning). That will also prevent the + // diff's output from interleaving with any other output. + // path dp ("diff"); process_path dpp; + int ofd (1); + if (pass_n != pts_n && pts[pass_n + 1] != nullptr) { + ofd = 2; + const file& ot (pts[pass_n + 1]->as<file> ()); const path& op (ot.path ()); assert (!op.empty ()); // Should have been assigned by update. @@ -889,22 +1349,29 @@ namespace build2 args.push_back (nullptr); // Second. if (verb >= 2) - print_process (args); + print_process (args); // Note: prints the whole pipeline. else if (verb) - text << "test " << tt; + print_diag ("test", tt); if (!ctx.dry_run) { - diag_record dr; - if (!run_test (tt, - dr, - args.data () + (sin ? 3 : 0), // Skip cat. - sin ? &cat : nullptr)) + pipe_process pp (tt.ctx, + args.data (), // Note: only cat's args are considered. + false /* force_dbuf */, + nullptr /* prev */, + nullptr /* next */); + + if (sin) { - dr << info << "test command line: "; - print_process (dr, args); - dr << endf; // return + pp.next = &pp; // Points to itself. + pp.proc = &cat; } + + run_test (tt, + args.data () + (sin ? 3 : 0), // Skip cat. + ofd, + test_deadline (tt), + sin ? &pp : nullptr); } return target_state::changed; diff --git a/libbuild2/test/rule.hxx b/libbuild2/test/rule.hxx index e96b68b..6fcf208 100644 --- a/libbuild2/test/rule.hxx +++ b/libbuild2/test/rule.hxx @@ -20,7 +20,7 @@ namespace build2 { public: virtual bool - match (action, target&, const string&) const override; + match (action, target&) const override; virtual recipe apply (action, target&) const override; @@ -34,10 +34,10 @@ namespace build2 target_state perform_script (action, const target&, size_t) const; - rule (common_data&& d, bool see_through_only) - : common (move (d)), see_through (see_through_only) {} + rule (common_data&& d, bool sto) + : common (move (d)), see_through_only (sto) {} - bool see_through; + bool see_through_only; }; class default_rule: public rule diff --git a/libbuild2/test/script/lexer+for-loop.test.testscript b/libbuild2/test/script/lexer+for-loop.test.testscript new file mode 100644 index 0000000..fcd12f7 --- /dev/null +++ b/libbuild2/test/script/lexer+for-loop.test.testscript @@ -0,0 +1,231 @@ +# file : libbuild2/test/script/lexer+for-loop.test.testscript +# license : MIT; see accompanying LICENSE file + +test.arguments = for-loop + +: semi +{ + : immediate + : + $* <"cmd;" >>EOO + 'cmd' + ; + <newline> + EOO + + : separated + : + $* <"cmd ;" >>EOO + 'cmd' + ; + <newline> + EOO + + : only + : + $* <";" >>EOO + ; + <newline> + EOO +} + +: colon +: +{ + : immediate + : + $* <"cmd: dsc" >>EOO + 'cmd' + : + 'dsc' + <newline> + EOO + + : separated + : + $* <"cmd :dsc" >>EOO + 'cmd' + : + 'dsc' + <newline> + EOO + + : only + : + $* <":" >>EOO + : + <newline> + EOO +} + +: redirect +: +{ + : pass + : + $* <"cmd <| 1>|" >>EOO + 'cmd' + <| + '1' + >| + <newline> + EOO + + : null + : + $* <"cmd <- 1>-" >>EOO + 'cmd' + <- + '1' + >- + <newline> + EOO + + : trace + : + $* <"cmd 1>!" >>EOO + 'cmd' + '1' + >! + <newline> + EOO + + : merge + : + $* <"cmd 1>&2" >>EOO + 'cmd' + '1' + >& + '2' + <newline> + EOO + + : str + : + $* <"cmd <a 1>b" >>EOO + 'cmd' + < + 'a' + '1' + > + 'b' + <newline> + EOO + + : str-nn + : + $* <"cmd <:a 1>:b" >>EOO + 'cmd' + <: + 'a' + '1' + >: + 'b' + <newline> + EOO + + : doc + : + $* <"cmd <<EOI 1>>EOO" >>EOO + 'cmd' + << + 'EOI' + '1' + >> + 'EOO' + <newline> + EOO + + : doc-nn + : + $* <"cmd <<:EOI 1>>:EOO" >>EOO + 'cmd' + <<: + 'EOI' + '1' + >>: + 'EOO' + <newline> + EOO + + : file-cmp + : + $* <"cmd <<<in >>>out 2>>>err" >>EOO + 'cmd' + <<< + 'in' + >>> + 'out' + '2' + >>> + 'err' + <newline> + EOO + + : file-write + : + $* <"cmd >=out 2>+err" >>EOO + 'cmd' + >= + 'out' + '2' + >+ + 'err' + <newline> + EOO +} + +: cleanup +: +{ + : always + : + $* <"cmd &file" >>EOO + 'cmd' + & + 'file' + <newline> + EOO + + : maybe + : + $* <"cmd &?file" >>EOO + 'cmd' + &? + 'file' + <newline> + EOO + + : never + : + $* <"cmd &!file" >>EOO + 'cmd' + &! + 'file' + <newline> + EOO +} + +: for +: +{ + : form-1 + : + $* <"for x: a" >>EOO + 'for' + 'x' + : + 'a' + <newline> + EOO + + : form-3 + : + $* <"for <<<a x" >>EOO + 'for' + <<< + 'a' + 'x' + <newline> + EOO +} diff --git a/libbuild2/test/script/lexer.cxx b/libbuild2/test/script/lexer.cxx index c23dea4..aec91fc 100644 --- a/libbuild2/test/script/lexer.cxx +++ b/libbuild2/test/script/lexer.cxx @@ -34,13 +34,16 @@ namespace build2 bool q (true); // quotes if (!esc) - { - assert (!state_.empty ()); - esc = state_.top ().escapes; - } + esc = current_state ().escapes; switch (m) { + case lexer_mode::for_loop: + { + // Leading tokens of the for-loop. Like command_line but also + // recognizes lsbrace like value. + } + // Fall through. case lexer_mode::command_line: { s1 = ":;=!|&<> $(#\t\n"; @@ -107,7 +110,7 @@ namespace build2 } assert (ps == '\0'); - state_.push ( + mode_impl ( state {m, data, nullopt, false, false, ps, s, n, q, *esc, s1, s2}); } @@ -116,12 +119,13 @@ namespace build2 { token r; - switch (state_.top ().mode) + switch (mode ()) { case lexer_mode::command_line: case lexer_mode::first_token: case lexer_mode::second_token: case lexer_mode::variable_line: + case lexer_mode::for_loop: r = next_line (); break; case lexer_mode::description_line: @@ -144,7 +148,7 @@ namespace build2 xchar c (get ()); uint64_t ln (c.line), cn (c.column); - state st (state_.top ()); // Make copy (see first/second_token). + state st (current_state ()); // Make copy (see first/second_token). lexer_mode m (st.mode); auto make_token = [&sep, ln, cn] (type t) @@ -157,9 +161,10 @@ namespace build2 // if (st.lsbrace) { - assert (m == lexer_mode::variable_line); + assert (m == lexer_mode::variable_line || + m == lexer_mode::for_loop); - state_.top ().lsbrace = false; // Note: st is a copy. + current_state ().lsbrace = false; // Note: st is a copy. if (c == '[' && (!st.lsbrace_unsep || !sep)) return make_token (type::lsbrace); @@ -172,7 +177,7 @@ namespace build2 // we push any new mode (e.g., double quote). // if (m == lexer_mode::first_token || m == lexer_mode::second_token) - state_.pop (); + expire_mode (); // NOTE: remember to update mode() if adding new special characters. @@ -183,7 +188,7 @@ namespace build2 // Expire variable value mode at the end of the line. // if (m == lexer_mode::variable_line) - state_.pop (); + expire_mode (); sep = true; // Treat newline as always separated. return make_token (type::newline); @@ -197,10 +202,11 @@ namespace build2 // Line separators. // - if (m == lexer_mode::command_line || - m == lexer_mode::first_token || - m == lexer_mode::second_token || - m == lexer_mode::variable_line) + if (m == lexer_mode::command_line || + m == lexer_mode::first_token || + m == lexer_mode::second_token || + m == lexer_mode::variable_line || + m == lexer_mode::for_loop) { switch (c) { @@ -210,7 +216,8 @@ namespace build2 if (m == lexer_mode::command_line || m == lexer_mode::first_token || - m == lexer_mode::second_token) + m == lexer_mode::second_token || + m == lexer_mode::for_loop) { switch (c) { @@ -222,7 +229,8 @@ namespace build2 // if (m == lexer_mode::command_line || m == lexer_mode::first_token || - m == lexer_mode::second_token) + m == lexer_mode::second_token || + m == lexer_mode::for_loop) { switch (c) { @@ -244,7 +252,8 @@ namespace build2 // if (m == lexer_mode::command_line || m == lexer_mode::first_token || - m == lexer_mode::second_token) + m == lexer_mode::second_token || + m == lexer_mode::for_loop) { if (optional<token> t = next_cmd_op (c, sep)) return move (*t); @@ -310,7 +319,7 @@ namespace build2 if (c == '\n') { get (); - state_.pop (); // Expire the description mode. + expire_mode (); // Expire the description mode. return token (type::newline, true, ln, cn, token_printer); } @@ -324,22 +333,23 @@ namespace build2 lexeme += c; } - return token (move (lexeme), - false, - quote_type::unquoted, false, + return token (move (lexeme), false, + quote_type::unquoted, false, false, ln, cn); } token lexer:: - word (state st, bool sep) + word (const state& st, bool sep) { - lexer_mode m (st.mode); + lexer_mode m (st.mode); // Save. token r (base_lexer::word (st, sep)); if (m == lexer_mode::variable) { - if (r.value.size () == 1 && digit (r.value[0])) // $N + if (r.type == type::word && + r.value.size () == 1 && + digit (r.value[0])) // $N { xchar c (peek ()); diff --git a/libbuild2/test/script/lexer.hxx b/libbuild2/test/script/lexer.hxx index 452e794..39b950a 100644 --- a/libbuild2/test/script/lexer.hxx +++ b/libbuild2/test/script/lexer.hxx @@ -24,10 +24,11 @@ namespace build2 enum { command_line = base_type::value_next, - first_token, // Expires at the end of the token. - second_token, // Expires at the end of the token. - variable_line, // Expires at the end of the line. - description_line // Expires at the end of the line. + first_token, // Expires at the end of the token. + second_token, // Expires at the end of the token. + variable_line, // Expires at the end of the line. + description_line, // Expires at the end of the line. + for_loop // Used for sensing the for-loop leading tokens. }; lexer_mode () = default; @@ -67,6 +68,8 @@ namespace build2 static redirect_aliases_type redirect_aliases; private: + using build2::script::lexer::mode; // Getter. + token next_line (); @@ -74,7 +77,7 @@ namespace build2 next_description (); virtual token - word (state, bool) override; + word (const state&, bool) override; }; } } diff --git a/libbuild2/test/script/lexer.test.cxx b/libbuild2/test/script/lexer.test.cxx index 9c64616..ef3ce4d 100644 --- a/libbuild2/test/script/lexer.test.cxx +++ b/libbuild2/test/script/lexer.test.cxx @@ -1,7 +1,6 @@ // file : libbuild2/test/script/lexer.test.cxx -*- C++ -*- // license : MIT; see accompanying LICENSE file -#include <cassert> #include <iostream> #include <libbuild2/types.hxx> @@ -10,6 +9,9 @@ #include <libbuild2/test/script/token.hxx> #include <libbuild2/test/script/lexer.hxx> +#undef NDEBUG +#include <cassert> + using namespace std; namespace build2 @@ -34,6 +36,7 @@ namespace build2 else if (s == "variable-line") m = lexer_mode::variable_line; else if (s == "description-line") m = lexer_mode::description_line; else if (s == "variable") m = lexer_mode::variable; + else if (s == "for-loop") m = lexer_mode::for_loop; else assert (false); } diff --git a/libbuild2/test/script/parser+command-if.test.testscript b/libbuild2/test/script/parser+command-if.test.testscript index 0b72b4a..9e223dd 100644 --- a/libbuild2/test/script/parser+command-if.test.testscript +++ b/libbuild2/test/script/parser+command-if.test.testscript @@ -315,6 +315,7 @@ } : end +: { : without-if : @@ -322,7 +323,7 @@ cmd end EOI - testscript:2:1: error: 'end' without preceding 'if' + testscript:2:1: error: 'end' without preceding 'if', 'for', or 'while' EOE : without-if-semi @@ -331,10 +332,11 @@ cmd; end EOI - testscript:2:1: error: 'end' without preceding 'if' + testscript:2:1: error: 'end' without preceding 'if', 'for', or 'while' EOE : before + : { : semi : diff --git a/libbuild2/test/script/parser+command-re-parse.test.testscript b/libbuild2/test/script/parser+command-re-parse.test.testscript index 84465b3..5a082eb 100644 --- a/libbuild2/test/script/parser+command-re-parse.test.testscript +++ b/libbuild2/test/script/parser+command-re-parse.test.testscript @@ -4,7 +4,7 @@ : double-quote : $* <<EOI >>EOO -x = cmd \">-\" "'<-'" +x = [cmdline] cmd \">-\" "'<-'" $x EOI cmd '>-' '<-' diff --git a/libbuild2/test/script/parser+description.test.testscript b/libbuild2/test/script/parser+description.test.testscript index cee540f..f656b7d 100644 --- a/libbuild2/test/script/parser+description.test.testscript +++ b/libbuild2/test/script/parser+description.test.testscript @@ -313,7 +313,7 @@ x = y end EOI - testscript:2:1: error: description before/after setup/teardown variable-if + testscript:2:1: error: description before/after setup/teardown variable-only 'if' EOE : var-if-after @@ -323,7 +323,7 @@ x = y end : foo EOI - testscript:1:1: error: description before/after setup/teardown variable-if + testscript:1:1: error: description before/after setup/teardown variable-only 'if' EOE : test diff --git a/libbuild2/test/script/parser+env.test.testscript b/libbuild2/test/script/parser+env.test.testscript index b1e864c..efa5dec 100644 --- a/libbuild2/test/script/parser+env.test.testscript +++ b/libbuild2/test/script/parser+env.test.testscript @@ -31,7 +31,7 @@ : invalid-val : $* <'env --unset=a=b -- cmd' 2>>EOE != 0 - testscript:1:5: error: env: invalid value 'a=b' for option '--unset': contains '=' + testscript:1:5: error: env: invalid value 'a=b' for option --unset: contains '=' EOE : no-sep @@ -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/parser+expansion.test.testscript b/libbuild2/test/script/parser+expansion.test.testscript index 77a7d6d..c31b0ad 100644 --- a/libbuild2/test/script/parser+expansion.test.testscript +++ b/libbuild2/test/script/parser+expansion.test.testscript @@ -27,7 +27,7 @@ EOE : invalid-redirect : $* <<EOI 2>>EOE != 0 -x = "1>&a" +x = [cmdline] "1>&a" cmd $x EOI <string>:1:4: error: stdout merge redirect file descriptor must be 2 diff --git a/libbuild2/test/script/parser+for.test.testscript b/libbuild2/test/script/parser+for.test.testscript new file mode 100644 index 0000000..985f9c9 --- /dev/null +++ b/libbuild2/test/script/parser+for.test.testscript @@ -0,0 +1,1029 @@ +# file : libbuild2/test/script/parser+for.test.testscript +# license : MIT; see accompanying LICENSE file + +: form-1 +: +: for x: ... +: +{ + : for + : + { + : no-var + : + $* <<EOI 2>>EOE != 0 + for + cmd + end + EOI + testscript:1:1: error: for: missing variable name + EOE + + : untyped + : + $* <<EOI >>EOO + for x: a b + cmd $x + end + EOI + cmd a + cmd b + EOO + + : null + : + $* <<EOI >:'' + for x: [null] + cmd $x + end + EOI + + : empty + : + $* <<EOI >:'' + for x: + cmd $x + end + EOI + + : expansion + : + $* <<EOI >>EOO + vs = a b + for x: $vs + cmd $x + end + EOI + cmd a + cmd b + EOO + + : typed-values + : + $* <<EOI >>~%EOO% + for x: [dir_paths] a b + cmd $x + end + EOI + %cmd (a/|'a\\')% + %cmd (b/|'b\\')% + EOO + + : typed-elem + : + $* <<EOI >>~%EOO% + for x [dir_path]: a b + cmd $x + end + EOI + %cmd (a/|'a\\')% + %cmd (b/|'b\\')% + EOO + + : typed-elem-value + : + $* <<EOI >>~%EOO% + for x [dir_path]: [strings] a b + cmd $x + end + EOI + %cmd (a/|'a\\')% + %cmd (b/|'b\\')% + EOO + + : scope-var + : + $* <<EOI >>EOO + x = x + + for x: a b + cmd $x + end + + -cmd $x + EOI + cmd a + cmd b + -cmd x + EOO + } + + : after-semi + : + $* -s <<EOI >>EOO + cmd1; + for x: a b + cmd2 $x + end + EOI + { + { + cmd1 + cmd2 a + cmd2 b + } + } + EOO + + : setup + : + $* -s <<EOI >>EOO + +for x: a b + cmd $x + end + EOI + { + +cmd a + +cmd b + } + EOO + + : tdown + : + $* -s <<EOI >>EOO + -for x: a b + cmd $x + end + EOI + { + -cmd a + -cmd b + } + EOO + + : end + : + { + : without-end + : + $* <<EOI 2>>EOE != 0 + for x: a b + cmd + EOI + testscript:3:1: error: expected closing 'end' + EOE + } + + : elif + : + { + : without-if + : + $* <<EOI 2>>EOE != 0 + for x: a b + elif true + cmd + end + end + EOI + testscript:2:3: error: 'elif' without preceding 'if' + EOE + } + + : nested + : + { + $* -l -r <<EOI >>EOO + for x: a b + cmd1 $x # 1 + if ($x == "a") # 2 + cmd2 # 3 + for y: x y + cmd3 # 4 + end + else + cmd4 # 5 + end + cmd5 # 6 + end; + cmd6 # 7 + EOI + cmd1 a # 1 i1 + ? true # 2 i1 + cmd2 # 3 i1 + cmd3 # 4 i1 i1 + cmd3 # 4 i1 i2 + cmd5 # 6 i1 + cmd1 b # 1 i2 + ? false # 2 i2 + cmd4 # 5 i2 + cmd5 # 6 i2 + cmd6 # 7 + EOO + } + + : contained + : + { + : semi + : + $* <<EOI 2>>EOE != 0 + for x: + cmd; + cmd + end + EOI + testscript:2:3: error: ';' inside 'for' + EOE + + : colon-leading + : + $* <<EOI 2>>EOE != 0 + for x: + : foo + cmd + end + EOI + testscript:2:3: error: description inside 'for' + EOE + + : colon-trailing + : + $* <<EOI 2>>EOE != 0 + for x: + cmd : foo + end + EOI + testscript:2:3: error: description inside 'for' + EOE + + : eos + : + $* <<EOI 2>>EOE != 0 + for x: + EOI + testscript:2:1: error: expected closing 'end' + EOE + + : scope + : + $* <<EOI 2>>EOE != 0 + for x: + cmd + { + } + end + EOI + testscript:3:3: error: expected closing 'end' + EOE + + : setup + : + $* <<EOI 2>>EOE != 0 + for x: + +cmd + end + EOI + testscript:2:3: error: setup command inside 'for' + EOE + + : tdown + : + $* <<EOI 2>>EOE != 0 + for x: + -cmd + end + EOI + testscript:2:3: error: teardown command inside 'for' + EOE + } + + : var + : + $* <<EOI >>EOO + for x: a b + cmd1 $x + end; + cmd2 $x + EOI + cmd1 a + cmd1 b + cmd2 b + EOO + + : leading-and-trailing-description + : + $* <<EOI 2>>EOE != 0 + : foo + for x: a b + cmd + end : bar + EOI + testscript:4:1: error: both leading and trailing descriptions + EOE +} + +: form-2 +: +: ... | for x +: +{ + : for + : + { + : status + : + $* <<EOI 2>>EOE != 0 + echo 'a b' | for x != 0 + cmd + end + EOI + testscript:1:20: error: for-loop exit code cannot be checked + EOE + + : not-last + : + $* <<EOI 2>>EOE != 0 + echo 'a b' | for x | echo x + cmd + end + EOI + testscript:1:20: error: for-loop must be last command in a pipe + EOE + + : not-last-relex + : + $* <<EOI 2>>EOE != 0 + echo 'a b' | for x|echo x + cmd + end + EOI + testscript:1:19: error: for-loop must be last command in a pipe + EOE + + : expression-after + : + $* <<EOI 2>>EOE != 0 + echo 'a b' | for x && echo x + cmd + end + EOI + testscript:1:20: error: command expression involving for-loop + EOE + + : expression-after-relex + : + $* <<EOI 2>>EOE != 0 + echo 'a b' | for x&&echo x + cmd + end + EOI + testscript:1:19: error: command expression involving for-loop + EOE + + : expression-before + : + $* <<EOI 2>>EOE != 0 + echo 'a b' && echo x | for x + cmd + end + EOI + testscript:1:24: error: command expression involving for-loop + EOE + + : expression-before-relex + : + $* <<EOI 2>>EOE != 0 + echo 'a b' && echo x|for x + cmd + end + EOI + testscript:1:22: error: command expression involving for-loop + EOE + + : cleanup + : + $* <<EOI 2>>EOE != 0 + echo 'a b' | for x &f + cmd + end + EOI + testscript:1:20: error: cleanup in for-loop + EOE + + : cleanup-relex + : + $* <<EOI 2>>EOE != 0 + echo 'a b' | for x&f + cmd + end + EOI + testscript:1:19: error: cleanup in for-loop + EOE + + : stdout-redirect + : + $* <<EOI 2>>EOE != 0 + echo 'a b' | for x >a + cmd + end + EOI + testscript:1:20: error: output redirect in for-loop + EOE + + : stdout-redirect-relex + : + $* <<EOI 2>>EOE != 0 + echo 'a b' | for x>a + cmd + end + EOI + testscript:1:19: error: output redirect in for-loop + EOE + + : stdin-redirect + : + $* <<EOI 2>>EOE != 0 + echo 'a b' | for x <a + cmd + end + EOI + testscript:1:20: error: stdin is both piped and redirected + EOE + + : no-var + : + $* <<EOI 2>>EOE != 0 + echo 'a b' | for + cmd + end + EOI + testscript:1:1: error: for: missing variable name + EOE + + : untyped + : + $* <<EOI >>EOO + echo 'a b' | for -w x + cmd $x + end + EOI + echo 'a b' | for -w x + EOO + + : expansion + : + $* <<EOI >>EOO + vs = a b + echo $vs | for x + cmd $x + end + EOI + echo a b | for x + EOO + + : typed-elem + : + $* <<EOI >>EOO + echo 'a b' | for -w x [dir_path] + cmd $x + end + EOI + echo 'a b' | for -w x [dir_path] + EOO + } + + : after-semi + : + $* -s <<EOI >>EOO + cmd1; + echo 'a b' | for x + cmd2 $x + end + EOI + { + { + cmd1 + echo 'a b' | for x + } + } + EOO + + : setup + : + $* -s <<EOI >>EOO + +echo 'a b' | for x + cmd $x + end + EOI + { + +echo 'a b' | for x + } + EOO + + : tdown + : + $* -s <<EOI >>EOO + -echo 'a b' | for x + cmd $x + end + EOI + { + -echo 'a b' | for x + } + EOO + + : end + : + { + : without-end + : + $* <<EOI 2>>EOE != 0 + echo 'a b' | for x + cmd + EOI + testscript:3:1: error: expected closing 'end' + EOE + } + + : elif + : + { + : without-if + : + $* <<EOI 2>>EOE != 0 + echo 'a b' | for x + elif true + cmd + end + end + EOI + testscript:2:3: error: 'elif' without preceding 'if' + EOE + } + + : nested + : + { + $* -l -r <<EOI >>EOO + echo 'a b' | for x # 1 + cmd1 $x # 2 + if ($x == "a") # 3 + cmd2 # 4 + echo x y | for y # 5 + cmd3 # 6 + end + else + cmd4 # 7 + end + cmd5 # 8 + end; + cmd6 # 9 + EOI + echo 'a b' | for x # 1 + cmd6 # 9 + EOO + } + + : contained + : + { + : semi + : + $* <<EOI 2>>EOE != 0 + echo 'a b' | for x + cmd; + cmd + end + EOI + testscript:2:3: error: ';' inside 'for' + EOE + + : colon-leading + : + $* <<EOI 2>>EOE != 0 + echo 'a b' | for x + : foo + cmd + end + EOI + testscript:2:3: error: description inside 'for' + EOE + + : colon-trailing + : + $* <<EOI 2>>EOE != 0 + echo 'a b' | for x + cmd : foo + end + EOI + testscript:2:3: error: description inside 'for' + EOE + + : eos + : + $* <<EOI 2>>EOE != 0 + echo 'a b' | for x + EOI + testscript:2:1: error: expected closing 'end' + EOE + + : scope + : + $* <<EOI 2>>EOE != 0 + echo 'a b' | for x + cmd + { + } + end + EOI + testscript:3:3: error: expected closing 'end' + EOE + + : setup + : + $* <<EOI 2>>EOE != 0 + echo 'a b' | for x + +cmd + end + EOI + testscript:2:3: error: setup command inside 'for' + EOE + + : tdown + : + $* <<EOI 2>>EOE != 0 + echo 'a b' | for x + -cmd + end + EOI + testscript:2:3: error: teardown command inside 'for' + EOE + } + + : leading-and-trailing-description + : + $* <<EOI 2>>EOE != 0 + : foo + echo 'a b' | for x + cmd + end : bar + EOI + testscript:4:1: error: both leading and trailing descriptions + EOE +} + +: form-3 +: +: for x <... +: +{ + : for + : + { + : status + : + $* <<EOI 2>>EOE != 0 + for x <a != 0 + cmd + end + EOI + testscript:1:10: error: for-loop exit code cannot be checked + EOE + + : not-last + : + $* <<EOI 2>>EOE != 0 + for x <a | echo x + cmd + end + EOI + testscript:1:10: error: for-loop must be last command in a pipe + EOE + + : not-last-relex + : + $* <<EOI 2>>EOE != 0 + for <a x|echo x + cmd + end + EOI + testscript:1:9: error: for-loop must be last command in a pipe + EOE + + : expression-after + : + $* <<EOI 2>>EOE != 0 + for x <a && echo x + cmd + end + EOI + testscript:1:10: error: command expression involving for-loop + EOE + + : expression-after-relex + : + $* <<EOI 2>>EOE != 0 + for <a x&&echo x + cmd + end + EOI + testscript:1:9: error: command expression involving for-loop + EOE + + : expression-before + : + $* <<EOI 2>>EOE != 0 + echo 'a b' && for x <a + cmd + end + EOI + testscript:1:15: error: command expression involving for-loop + EOE + + : cleanup + : + $* <<EOI 2>>EOE != 0 + for x <a &f + cmd + end + EOI + testscript:1:10: error: cleanup in for-loop + EOE + + : cleanup-before-var + : + $* <<EOI 2>>EOE != 0 + for &f x <a + cmd + end + EOI + testscript:1:5: error: cleanup in for-loop + EOE + + : cleanup-relex + : + $* <<EOI 2>>EOE != 0 + for <a x&f + cmd + end + EOI + testscript:1:9: error: cleanup in for-loop + EOE + + : stdout-redirect + : + $* <<EOI 2>>EOE != 0 + for x >a + cmd + end + EOI + testscript:1:7: error: output redirect in for-loop + EOE + + : stdout-redirect-before-var + : + $* <<EOI 2>>EOE != 0 + for >a x + cmd + end + EOI + testscript:1:5: error: output redirect in for-loop + EOE + + : stdout-redirect-relex + : + $* <<EOI 2>>EOE != 0 + for x>a + cmd + end + EOI + testscript:1:6: error: output redirect in for-loop + EOE + + : no-var + : + $* <<EOI 2>>EOE != 0 + for <a + cmd + end + EOI + testscript:1:1: error: for: missing variable name + EOE + + : quoted-opt + : + $* <<EOI >>EOO + o = -w + for "$o" x <'a b' + cmd $x + end; + for "($o)" x <'a b' + cmd $x + end + EOI + for -w x <'a b' + for -w x <'a b' + EOO + + : untyped + : + $* <<EOI >>EOO + for -w x <'a b' + cmd $x + end + EOI + for -w x <'a b' + EOO + + : expansion + : + $* <<EOI >>EOO + vs = a b + for x <$vs + cmd $x + end + EOI + for x b <a + EOO + + : typed-elem + : + $* <<EOI >>EOO + for -w x [dir_path] <'a b' + cmd $x + end + EOI + for -w x [dir_path] <'a b' + EOO + } + + : after-semi + : + $* -s <<EOI >>EOO + cmd1; + for x <'a b' + cmd2 $x + end + EOI + { + { + cmd1 + for x <'a b' + } + } + EOO + + : setup + : + $* -s <<EOI >>EOO + +for x <'a b' + cmd $x + end + EOI + { + +for x <'a b' + } + EOO + + : tdown + : + $* -s <<EOI >>EOO + -for x <'a b' + cmd $x + end + EOI + { + -for x <'a b' + } + EOO + + : end + : + { + : without-end + : + $* <<EOI 2>>EOE != 0 + for x <'a b' + cmd + EOI + testscript:3:1: error: expected closing 'end' + EOE + } + + : elif + : + { + : without-if + : + $* <<EOI 2>>EOE != 0 + for x <'a b' + elif true + cmd + end + end + EOI + testscript:2:3: error: 'elif' without preceding 'if' + EOE + } + + : nested + : + { + $* -l -r <<EOI >>EOO + for -w x <'a b' # 1 + cmd1 $x # 2 + if ($x == "a") # 3 + cmd2 # 4 + for -w y <'x y' # 5 + cmd3 # 6 + end + else + cmd4 # 7 + end + cmd5 # 8 + end; + cmd6 # 9 + EOI + for -w x <'a b' # 1 + cmd6 # 9 + EOO + } + + : contained + : + { + : semi + : + $* <<EOI 2>>EOE != 0 + for x <'a b' + cmd; + cmd + end + EOI + testscript:2:3: error: ';' inside 'for' + EOE + + : colon-leading + : + $* <<EOI 2>>EOE != 0 + for x <'a b' + : foo + cmd + end + EOI + testscript:2:3: error: description inside 'for' + EOE + + : colon-trailing + : + $* <<EOI 2>>EOE != 0 + for x <'a b' + cmd : foo + end + EOI + testscript:2:3: error: description inside 'for' + EOE + + : eos + : + $* <<EOI 2>>EOE != 0 + for x <'a b' + EOI + testscript:2:1: error: expected closing 'end' + EOE + + : scope + : + $* <<EOI 2>>EOE != 0 + for x <'a b' + cmd + { + } + end + EOI + testscript:3:3: error: expected closing 'end' + EOE + + : setup + : + $* <<EOI 2>>EOE != 0 + for x <'a b' + +cmd + end + EOI + testscript:2:3: error: setup command inside 'for' + EOE + + : tdown + : + $* <<EOI 2>>EOE != 0 + for x <'a b' + -cmd + end + EOI + testscript:2:3: error: teardown command inside 'for' + EOE + } + + : leading-and-trailing-description + : + $* <<EOI 2>>EOE != 0 + : foo + for x <'a b' + cmd + end : bar + EOI + testscript:4:1: error: both leading and trailing descriptions + EOE +} diff --git a/libbuild2/test/script/parser+while.test.testscript b/libbuild2/test/script/parser+while.test.testscript new file mode 100644 index 0000000..b1a2b44 --- /dev/null +++ b/libbuild2/test/script/parser+while.test.testscript @@ -0,0 +1,265 @@ +# file : libbuild2/test/script/parser+while.test.testscript +# license : MIT; see accompanying LICENSE file + +: while +: +{ + : true + : + $* <<EOI >>EOO + while ($v != "aa") + cmd "$v" + v = "$(v)a" + end + EOI + ? true + cmd '' + ? true + cmd a + ? false + EOO + + : false + : + $* <<EOI >>EOO + while ($v == "aa") + cmd "$v" + v = "$(v)a" + end + EOI + ? false + EOO + + : without-command + : + $* <<EOI 2>>EOE != 0 + while + cmd + end + EOI + testscript:1:6: error: missing program + EOE + + : after-semi + : + $* -s <<EOI >>EOO + cmd1; + while ($v != "aa") + cmd2 "$v" + v = "$(v)a" + end + EOI + { + { + cmd1 + ? true + cmd2 '' + ? true + cmd2 a + ? false + } + } + EOO + + : setup + : + $* -s <<EOI >>EOO + +while ($v != "aa") + cmd2 "$v" + v = "$(v)a" + end + EOI + { + ? true + +cmd2 '' + ? true + +cmd2 a + ? false + } + EOO + + : tdown + : + $* -s <<EOI >>EOO + -while ($v != "aa") + cmd2 "$v" + v = "$(v)a" + end + EOI + { + ? true + -cmd2 '' + ? true + -cmd2 a + ? false + } + EOO +} + +: end +: +{ + : without-end + : + $* <<EOI 2>>EOE != 0 + while true + cmd + EOI + testscript:3:1: error: expected closing 'end' + EOE +} + +: elif +: +{ + : without-if + : + $* <<EOI 2>>EOE != 0 + while false + elif true + cmd + end + end + EOI + testscript:2:3: error: 'elif' without preceding 'if' + EOE +} + +: nested +: +{ + $* -l -r <<EOI >>EOO + while ($v != "aa") # 1 + cmd1 "$v" # 2 + if ($v == "a") # 3 + cmd2 # 4 + while ($v2 != "$v") # 5 + cmd3 # 6 + v2=$v + end + else + cmd4 # 7 + end + cmd5 # 8 + v = "$(v)a" + end; + cmd6 + EOI + ? true # 1 i1 + cmd1 '' # 2 i1 + ? false # 3 i1 + cmd4 # 7 i1 + cmd5 # 8 i1 + ? true # 1 i2 + cmd1 a # 2 i2 + ? true # 3 i2 + cmd2 # 4 i2 + ? true # 5 i2 i1 + cmd3 # 6 i2 i1 + ? false # 5 i2 i2 + cmd5 # 8 i2 + ? false # 1 i3 + cmd6 # 9 + EOO +} + +: contained +: +{ + : semi + : + $* <<EOI 2>>EOE != 0 + while + cmd; + cmd + end + EOI + testscript:2:3: error: ';' inside 'while' + EOE + + : colon-leading + : + $* <<EOI 2>>EOE != 0 + while + : foo + cmd + end + EOI + testscript:2:3: error: description inside 'while' + EOE + + : colon-trailing + : + $* <<EOI 2>>EOE != 0 + while + cmd : foo + end + EOI + testscript:2:3: error: description inside 'while' + EOE + + : eos + : + $* <<EOI 2>>EOE != 0 + while + EOI + testscript:2:1: error: expected closing 'end' + EOE + + : scope + : + $* <<EOI 2>>EOE != 0 + while + cmd + { + } + end + EOI + testscript:3:3: error: expected closing 'end' + EOE + + : setup + : + $* <<EOI 2>>EOE != 0 + while + +cmd + end + EOI + testscript:2:3: error: setup command inside 'while' + EOE + + : tdown + : + $* <<EOI 2>>EOE != 0 + while + -cmd + end + EOI + testscript:2:3: error: teardown command inside 'while' + EOE +} + +: var +: +$* <<EOI >>EOO +while ($v1 != "a") + v1 = "$(v1)a" + v2 = "$v1" +end +cmd $v1 +EOI +? true +? false +cmd a +EOO + +: leading-and-trailing-description +: +$* <<EOI 2>>EOE != 0 +: foo +while false + cmd +end : bar +EOI +testscript:4:1: error: both leading and trailing descriptions +EOE diff --git a/libbuild2/test/script/parser.cxx b/libbuild2/test/script/parser.cxx index 944e1c8..337b162 100644 --- a/libbuild2/test/script/parser.cxx +++ b/libbuild2/test/script/parser.cxx @@ -293,22 +293,30 @@ namespace build2 } // Parse a logical line (as well as scope-if since the only way to - // recognize it is to parse the if line). + // recognize it is to parse the if line), handling the flow control + // constructs recursively. // // If one is true then only parse one line returning an indication of - // whether the line ended with a semicolon. If if_line is true then this - // line can be an if-else construct flow control line (else, end, etc). + // whether the line ended with a semicolon. If the flow control + // construct type is specified, then this line is assumed to belong to + // such construct. // bool parser:: pre_parse_line (token& t, type& tt, optional<description>& d, lines* ls, bool one, - bool if_line) + optional<line_type> fct) { // enter: next token is peeked at (type in tt) // leave: newline + assert (!fct || + *fct == line_type::cmd_if || + *fct == line_type::cmd_while || + *fct == line_type::cmd_for_stream || + *fct == line_type::cmd_for_args); + // Note: token is only peeked at. // const location ll (get_location (peeked ())); @@ -317,6 +325,52 @@ namespace build2 // line_type lt; type st (type::eos); // Later, can only be set to plus or minus. + bool semi (false); + + // Parse the command line tail, starting from the newline or the + // potential colon/semicolon token. + // + // Note that colon and semicolon are only valid in test command lines + // and after 'end' in flow control constructs. Note that we always + // recognize them lexically, even when they are not valid tokens per + // the grammar. + // + auto parse_command_tail = [&t, &tt, &st, <, &d, &semi, &ll, this] () + { + if (tt != type::newline) + { + if (lt != line_type::cmd && lt != line_type::cmd_end) + fail (t) << "expected newline instead of " << t; + + switch (st) + { + case type::plus: fail (t) << t << " after setup command" << endf; + case type::minus: fail (t) << t << " after teardown command" << endf; + } + } + + switch (tt) + { + case type::colon: + { + if (d) + fail (ll) << "both leading and trailing descriptions"; + + d = parse_trailing_description (t, tt); + break; + } + case type::semi: + { + semi = true; + replay_pop (); // See above for the reasoning. + next (t, tt); // Get newline. + break; + } + } + + if (tt != type::newline) + fail (t) << "expected newline instead of " << t; + }; switch (tt) { @@ -364,8 +418,12 @@ namespace build2 { const string& n (t.value); - if (n == "if") lt = line_type::cmd_if; - else if (n == "if!") lt = line_type::cmd_ifn; + // Handle the for-loop consistently with pre_parse_line_start(). + // + if (n == "if") lt = line_type::cmd_if; + else if (n == "if!") lt = line_type::cmd_ifn; + else if (n == "while") lt = line_type::cmd_while; + else if (n == "for") lt = line_type::cmd_for_stream; } break; @@ -379,8 +437,6 @@ namespace build2 // Pre-parse the line keeping track of whether it ends with a semi. // - bool semi (false); - line ln; switch (lt) { @@ -407,76 +463,147 @@ namespace build2 mode (lexer_mode::variable_line); parse_variable_line (t, tt); + // Note that the semicolon token is only required during + // pre-parsing to decide which line list the current line should + // go to and provides no additional semantics during the + // execution. Moreover, build2::script::parser::exec_lines() + // doesn't expect this token to be present. Thus, we just drop + // this token from the saved tokens. + // semi = (tt == type::semi); - if (tt == type::semi) + if (semi) + { + replay_pop (); next (t, tt); + } if (tt != type::newline) fail (t) << "expected newline instead of " << t; break; } + // + // See pre_parse_line_start() for details. + // + case line_type::cmd_for_args: assert (false); break; + case line_type::cmd_for_stream: + { + // First we need to sense the next few tokens and detect which + // form of the for-loop that actually is (see + // libbuild2/build/script/parser.cxx for details). + // + token pt (t); + assert (pt.type == type::word && pt.value == "for"); + + mode (lexer_mode::for_loop); + next (t, tt); + + string& n (t.value); + + if (tt == type::word && t.qtype == quote_type::unquoted && + (n[0] == '_' || alpha (n[0]) || // Variable. + n == "*" || n == "~" || n == "@")) // Special variable. + { + // Detect patterns analogous to parse_variable_name() (so we + // diagnose `for x[string]: ...`). + // + if (n.find_first_of ("[*?") != string::npos) + fail (t) << "expected variable name instead of " << n; + + if (special_variable (n)) + fail (t) << "attempt to set '" << n << "' variable directly"; + + if (lexer_->peek_char ().first == '[') + { + token vt (move (t)); + next_with_attributes (t, tt); + + attributes_push (t, tt, + true /* standalone */, + false /* next_token */); + + t = move (vt); + tt = t.type; + } + + if (lexer_->peek_char ().first == ':') + lt = line_type::cmd_for_args; + } + + if (lt == line_type::cmd_for_stream) // for x <... + { + ln.var = nullptr; + + expire_mode (); + + parse_command_expr_result r ( + parse_command_expr (t, tt, + lexer::redirect_aliases, + move (pt))); + + assert (r.for_loop); + + parse_command_tail (); + parse_here_documents (t, tt, r); + } + else // for x: ... + { + ln.var = &script_->var_pool.insert (move (n)); + + next (t, tt); + + assert (tt == type::colon); + + expire_mode (); + + // Parse the value similar to the var line type (see above), + // except for the fact that we don't expect a trailing semicolon. + // + mode (lexer_mode::variable_line); + parse_variable_line (t, tt); + + if (tt != type::newline) + fail (t) << "expected newline instead of " << t << " after for"; + } + + break; + } case line_type::cmd_elif: case line_type::cmd_elifn: case line_type::cmd_else: - case line_type::cmd_end: { - if (!if_line) - { + if (!fct || *fct != line_type::cmd_if) fail (t) << lt << " without preceding 'if'"; - } + } + // Fall through. + case line_type::cmd_end: + { + if (!fct) + fail (t) << lt << " without preceding 'if', 'for', or 'while'"; } // Fall through. case line_type::cmd_if: case line_type::cmd_ifn: + case line_type::cmd_while: next (t, tt); // Skip to start of command. // Fall through. case line_type::cmd: { - pair<command_expr, here_docs> p; + parse_command_expr_result r; if (lt != line_type::cmd_else && lt != line_type::cmd_end) - p = parse_command_expr (t, tt, lexer::redirect_aliases); + r = parse_command_expr (t, tt, lexer::redirect_aliases); - // Colon and semicolon are only valid in test command lines and - // after 'end' in if-else. Note that we still recognize them - // lexically, they are just not valid tokens per the grammar. - // - if (tt != type::newline) + if (r.for_loop) { - if (lt != line_type::cmd && lt != line_type::cmd_end) - fail (t) << "expected newline instead of " << t; - - switch (st) - { - case type::plus: fail (t) << t << " after setup command" << endf; - case type::minus: fail (t) << t << " after teardown command" << endf; - } + lt = line_type::cmd_for_stream; + ln.var = nullptr; } - switch (tt) - { - case type::colon: - { - if (d) - fail (ll) << "both leading and trailing descriptions"; - - d = parse_trailing_description (t, tt); - break; - } - case type::semi: - { - semi = true; - next (t, tt); // Get newline. - break; - } - } + parse_command_tail (); + parse_here_documents (t, tt, r); - if (tt != type::newline) - fail (t) << "expected newline instead of " << t; - - parse_here_documents (t, tt, p); break; } } @@ -494,24 +621,39 @@ namespace build2 ln.tokens = replay_data (); ls->push_back (move (ln)); - if (lt == line_type::cmd_if || lt == line_type::cmd_ifn) + switch (lt) { - semi = pre_parse_if_else (t, tt, d, *ls); + case line_type::cmd_if: + case line_type::cmd_ifn: + { + semi = pre_parse_if_else (t, tt, d, *ls); - // If this turned out to be scope-if, then ls is empty, semi is - // false, and none of the below logic applies. - // - if (ls->empty ()) - return semi; + // If this turned out to be scope-if, then ls is empty, semi is + // false, and none of the below logic applies. + // + if (ls->empty ()) + return semi; + + break; + } + case line_type::cmd_while: + case line_type::cmd_for_stream: + case line_type::cmd_for_args: + { + semi = pre_parse_loop (t, tt, lt, d, *ls); + break; + } + default: break; } // Unless we were told where to put it, decide where it actually goes. // if (ls == &ls_data) { - // First pre-check variable and variable-if: by themselves (i.e., - // without a trailing semicolon) they are treated as either setup or - // teardown without plus/minus. Also handle illegal line types. + // First pre-check variables and variable-only flow control + // constructs: by themselves (i.e., without a trailing semicolon) + // they are treated as either setup or teardown without + // plus/minus. Also handle illegal line types. // switch (lt) { @@ -524,8 +666,11 @@ namespace build2 } case line_type::cmd_if: case line_type::cmd_ifn: + case line_type::cmd_while: + case line_type::cmd_for_stream: + case line_type::cmd_for_args: { - // See if this is a variable-only command-if. + // See if this is a variable-only flow control construct. // if (find_if (ls_data.begin (), ls_data.end (), [] (const line& l) { @@ -549,7 +694,7 @@ namespace build2 fail (ll) << "description before setup/teardown variable"; else fail (ll) << "description before/after setup/teardown " - << "variable-if"; + << "variable-only " << lt; } // If we don't have any nested scopes or teardown commands, @@ -793,7 +938,7 @@ namespace build2 td, &ls, true /* one */, - true /* if_line */)); + line_type::cmd_if)); assert (ls.size () == 1 && ls.back ().type == lt); assert (tt == type::newline); @@ -831,6 +976,99 @@ namespace build2 return false; // We never end with a semi. } + // Pre-parse the flow control construct block line. Fail if the line is + // unexpectedly followed with a semicolon or test description. + // + bool parser:: + pre_parse_block_line (token& t, type& tt, + line_type bt, + optional<description>& d, + lines& ls) + { + // enter: peeked first token of the line (type in tt) + // leave: newline + + const location ll (get_location (peeked ())); + + switch (tt) + { + case type::colon: + fail (ll) << "description inside " << bt << endf; + case type::eos: + case type::rcbrace: + case type::lcbrace: + fail (ll) << "expected closing 'end'" << endf; + case type::plus: + fail (ll) << "setup command inside " << bt << endf; + case type::minus: + fail (ll) << "teardown command inside " << bt << endf; + } + + // Parse one line. Note that this one line can still be multiple lines + // in case of a flow control construct. In this case we want to view + // it as, for example, cmd_if, not cmd_end. Thus remember the start + // position of the next logical line. + // + size_t i (ls.size ()); + + line_type fct; // Flow control construct type the block type relates to. + + switch (bt) + { + case line_type::cmd_if: + case line_type::cmd_ifn: + case line_type::cmd_elif: + case line_type::cmd_elifn: + case line_type::cmd_else: + { + fct = line_type::cmd_if; + break; + } + case line_type::cmd_while: + case line_type::cmd_for_stream: + case line_type::cmd_for_args: + { + fct = bt; + break; + } + default: assert(false); + } + + optional<description> td; + bool semi (pre_parse_line (t, tt, td, &ls, true /* one */, fct)); + + assert (tt == type::newline); + + line_type lt (ls[i].type); + + // First take care of 'end'. + // + if (lt == line_type::cmd_end) + { + if (td) + { + if (d) + fail (ll) << "both leading and trailing descriptions"; + + d = move (td); + } + + return semi; + } + + // For any other line trailing semi or description is illegal. + // + // @@ Not the exact location of semi/colon. + // + if (semi) + fail (ll) << "';' inside " << bt; + + if (td) + fail (ll) << "description inside " << bt; + + return false; + } + bool parser:: pre_parse_if_else_command (token& t, type& tt, optional<description>& d, @@ -839,70 +1077,23 @@ namespace build2 // enter: peeked first token of next line (type in tt) // leave: newline - // Parse lines until we see closing 'end'. Nested if-else blocks are - // handled recursively. + // Parse lines until we see closing 'end'. // for (line_type bt (line_type::cmd_if); // Current block. ; tt = peek (lexer_mode::first_token)) { const location ll (get_location (peeked ())); - - switch (tt) - { - case type::colon: - fail (ll) << "description inside " << bt << endf; - case type::eos: - case type::rcbrace: - case type::lcbrace: - fail (ll) << "expected closing 'end'" << endf; - case type::plus: - fail (ll) << "setup command inside " << bt << endf; - case type::minus: - fail (ll) << "teardown command inside " << bt << endf; - } - - // Parse one line. Note that this one line can still be multiple - // lines in case of if-else. In this case we want to view it as - // cmd_if, not cmd_end. Thus remember the start position of the - // next logical line. - // size_t i (ls.size ()); - optional<description> td; - bool semi (pre_parse_line (t, tt, - td, - &ls, - true /* one */, - true /* if_line */)); - assert (tt == type::newline); + bool semi (pre_parse_block_line (t, tt, bt, d, ls)); line_type lt (ls[i].type); // First take care of 'end'. // if (lt == line_type::cmd_end) - { - if (td) - { - if (d) - fail (ll) << "both leading and trailing descriptions"; - - d = move (td); - } - return semi; - } - - // For any other line trailing semi or description is illegal. - // - // @@ Not the exact location of semi/colon. - // - if (semi) - fail (ll) << "';' inside " << bt; - - if (td) - fail (ll) << "description inside " << bt; // Check if-else block sequencing. // @@ -924,6 +1115,40 @@ namespace build2 default: break; } } + + assert (false); // Can't be here. + return false; + } + + bool parser:: + pre_parse_loop (token& t, type& tt, + line_type lt, + optional<description>& d, + lines& ls) + { + // enter: <newline> (previous line) + // leave: <newline> + + assert (lt == line_type::cmd_while || + lt == line_type::cmd_for_stream || + lt == line_type::cmd_for_args); + + tt = peek (lexer_mode::first_token); + + // Parse lines until we see closing 'end'. + // + for (;; tt = peek (lexer_mode::first_token)) + { + size_t i (ls.size ()); + + bool semi (pre_parse_block_line (t, tt, lt, d, ls)); + + if (ls[i].type == line_type::cmd_end) + return semi; + } + + assert (false); // Can't be here. + return false; } void parser:: @@ -945,7 +1170,7 @@ namespace build2 { pre_parse_ = false; args = parse_names (t, tt, - pattern_mode::expand, + pattern_mode::ignore, false, "directive argument", nullptr); @@ -1057,7 +1282,7 @@ namespace build2 diag_record dr (fail (dl)); dr << "invalid testscript include path "; - to_stream (dr.os, n, true); // Quote. + to_stream (dr.os, n, quote_mode::normal); } } @@ -1266,21 +1491,56 @@ namespace build2 // Note: this one is only used during execution. - pair<command_expr, here_docs> p ( + parse_command_expr_result pr ( parse_command_expr (t, tt, lexer::redirect_aliases)); - switch (tt) - { - case type::colon: parse_trailing_description (t, tt); break; - case type::semi: next (t, tt); break; // Get newline. - } + if (tt == type::colon) + parse_trailing_description (t, tt); assert (tt == type::newline); - parse_here_documents (t, tt, p); + parse_here_documents (t, tt, pr); assert (tt == type::newline); - return move (p.first); + command_expr r (move (pr.expr)); + + // If the test program runner is specified, then adjust the + // expressions to run test programs via this runner. + // + pair<const process_path*, const strings*> tr ( + runner_->test_runner ()); + + if (tr.first != nullptr) + { + for (expr_term& t: r) + { + for (command& c: t.pipe) + { + if (scope_->test_program (c.program.recall)) + { + // Append the runner options and the test program path to the + // the arguments list and rotate the list to the left, so that + // it starts from the runner options. This should probably be + // not less efficient than inserting the program path and then + // the runner options at the beginning of the list. + // + strings& args (c.arguments); + size_t n (args.size ()); + + args.insert (args.end (), + tr.second->begin (), tr.second->end ()); + + args.push_back (c.program.recall.string ()); + + rotate (args.begin (), args.begin () + n, args.end ()); + + c.program = process_path (*tr.first, false /* init */); + } + } + } + } + + return r; } // @@ -1349,6 +1609,17 @@ namespace build2 { runner_->enter (*scope_, scope_->start_loc_); + // Set thread-specific current directory override. In particular, this + // makes sure functions like $path.complete() work correctly. + // + auto wdg = make_guard ( + [old = path_traits::thread_current_directory ()] () + { + path_traits::thread_current_directory (old); + }); + + path_traits::thread_current_directory (&scope_->work_dir.path->string ()); + // Note that we rely on "small function object" optimization for the // exec_*() lambdas. // @@ -1364,9 +1635,6 @@ namespace build2 mode (lexer_mode::variable_line); value rhs (parse_variable_line (t, tt)); - if (tt == type::semi) - next (t, tt); - assert (tt == type::newline); // Assign. @@ -1377,17 +1645,8 @@ namespace build2 apply_value_attributes (&var, lhs, move (rhs), kind); - // If we change any of the test.* values, then reset the $*, $N - // special aliases. - // - if (var.name == script_->test_var.name || - var.name == script_->options_var.name || - var.name == script_->arguments_var.name || - var.name == script_->redirects_var.name || - var.name == script_->cleanups_var.name) - { + if (script_->test_command_var (var.name)) scope_->reset_special (); - } }; // Is set later, right before the exec_lines() call. @@ -1395,8 +1654,9 @@ namespace build2 command_type ct; auto exec_cmd = [&ct, this] (token& t, build2::script::token_type& tt, - size_t li, + const iteration_index* ii, size_t li, bool single, + const function<command_function>& cf, const location& ll) { // We use the 0 index to signal that this is the only command. @@ -1408,19 +1668,35 @@ namespace build2 command_expr ce ( parse_command_line (t, static_cast<token_type&> (tt))); - runner_->run (*scope_, ce, ct, li, ll); + runner_->run (*scope_, ce, ct, ii, li, cf, ll); }; - auto exec_if = [this] (token& t, build2::script::token_type& tt, - size_t li, - const location& ll) + auto exec_cond = [this] (token& t, build2::script::token_type& tt, + const iteration_index* ii, size_t li, + const location& ll) { command_expr ce ( parse_command_line (t, static_cast<token_type&> (tt))); - // Assume if-else always involves multiple commands. + // Assume a flow control construct always involves multiple + // commands. // - return runner_->run_if (*scope_, ce, li, ll); + return runner_->run_cond (*scope_, ce, ii, li, ll); + }; + + auto exec_for = [this] (const variable& var, + value&& val, + const attributes& val_attrs, + const location&) + { + value& lhs (scope_->assign (var)); + + attributes_.push_back (val_attrs); + + apply_value_attributes (&var, lhs, move (val), type::assign); + + if (script_->test_command_var (var.name)) + scope_->reset_special (); }; size_t li (1); @@ -1430,16 +1706,17 @@ namespace build2 ct = command_type::test; exec_lines (t->tests_.begin (), t->tests_.end (), - exec_set, exec_cmd, exec_if, - li); + exec_set, exec_cmd, exec_cond, exec_for, + nullptr /* iteration_index */, li); } else if (group* g = dynamic_cast<group*> (scope_)) { ct = command_type::setup; - bool exec_scope (exec_lines (g->setup_.begin (), g->setup_.end (), - exec_set, exec_cmd, exec_if, - li)); + bool exec_scope ( + exec_lines (g->setup_.begin (), g->setup_.end (), + exec_set, exec_cmd, exec_cond, exec_for, + nullptr /* iteration_index */, li)); if (exec_scope) { @@ -1497,7 +1774,8 @@ namespace build2 try { - take = runner_->run_if (*scope_, ce, li++, ll); + take = runner_->run_cond ( + *scope_, ce, nullptr /* iteration_index */, li++, ll); } catch (const exit_scope& e) { @@ -1564,24 +1842,24 @@ namespace build2 // UBSan workaround. // const diag_frame* df (diag_frame::stack ()); - if (!ctx.sched.async (task_count, - [] (const diag_frame* ds, - scope& s, - script& scr, - runner& r) - { - diag_frame::stack_guard dsg (ds); - execute_impl (s, scr, r); - }, - df, - ref (*chain), - ref (*script_), - ref (*runner_))) + if (!ctx->sched->async (task_count, + [] (const diag_frame* ds, + scope& s, + script& scr, + runner& r) + { + diag_frame::stack_guard dsg (ds); + execute_impl (s, scr, r); + }, + df, + ref (*chain), + ref (*script_), + ref (*runner_))) { // Bail out if the scope has failed and we weren't instructed // to keep going. // - if (chain->state == scope_state::failed && !ctx.keep_going) + if (chain->state == scope_state::failed && !ctx->keep_going) throw failed (); } } @@ -1608,8 +1886,8 @@ namespace build2 ct = command_type::teardown; exec_lines (g->tdown_.begin (), g->tdown_.end (), - exec_set, exec_cmd, exec_if, - li); + exec_set, exec_cmd, exec_cond, exec_for, + nullptr /* iteration_index */, li); } else assert (false); @@ -1623,7 +1901,8 @@ namespace build2 // The rest. // - // When add a special variable don't forget to update lexer::word(). + // When add a special variable don't forget to update lexer::word() and + // for-loop parsing in pre_parse_line(). // bool parser:: special_variable (const string& n) noexcept @@ -1632,7 +1911,7 @@ namespace build2 } lookup parser:: - lookup_variable (name&& qual, string&& name, const location& loc) + lookup_variable (names&& qual, string&& name, const location& loc) { if (pre_parse_) return lookup (); diff --git a/libbuild2/test/script/parser.hxx b/libbuild2/test/script/parser.hxx index 0c467a5..6fe46e2 100644 --- a/libbuild2/test/script/parser.hxx +++ b/libbuild2/test/script/parser.hxx @@ -4,6 +4,8 @@ #ifndef LIBBUILD2_TEST_SCRIPT_PARSER_HXX #define LIBBUILD2_TEST_SCRIPT_PARSER_HXX +#include <unordered_map> + #include <libbuild2/types.hxx> #include <libbuild2/forward.hxx> #include <libbuild2/utility.hxx> @@ -60,7 +62,13 @@ namespace build2 optional<description>&, lines* = nullptr, bool one = false, - bool if_line = false); + optional<line_type> flow_control_type = nullopt); + + bool + pre_parse_block_line (token&, token_type&, + line_type block_type, + optional<description>&, + lines&); bool pre_parse_if_else (token&, token_type&, @@ -77,6 +85,12 @@ namespace build2 optional<description>&, lines&); + bool + pre_parse_loop (token&, token_type&, + line_type, + optional<description>&, + lines&); + void pre_parse_directive (token&, token_type&); @@ -115,7 +129,7 @@ namespace build2 // protected: virtual lookup - lookup_variable (name&&, string&&, const location&) override; + lookup_variable (names&&, string&&, const location&) override; // Insert id into the id map checking for duplicates. // @@ -129,7 +143,7 @@ namespace build2 // Pre-parse state. // using id_map = std::unordered_map<string, location>; - using include_set = std::set<path>; + using include_set = set<path>; group* group_; id_map* id_map_; diff --git a/libbuild2/test/script/parser.test.cxx b/libbuild2/test/script/parser.test.cxx index 69583ae..6838e47 100644 --- a/libbuild2/test/script/parser.test.cxx +++ b/libbuild2/test/script/parser.test.cxx @@ -1,7 +1,6 @@ // file : libbuild2/test/script/parser.test.cxx -*- C++ -*- // license : MIT; see accompanying LICENSE file -#include <cassert> #include <iostream> #include <libbuild2/types.hxx> @@ -10,6 +9,7 @@ #include <libbuild2/target.hxx> #include <libbuild2/context.hxx> #include <libbuild2/scheduler.hxx> +#include <libbuild2/file-cache.hxx> #include <libbuild2/test/target.hxx> @@ -17,6 +17,9 @@ #include <libbuild2/test/script/parser.hxx> #include <libbuild2/test/script/runner.hxx> +#undef NDEBUG +#include <cassert> + using namespace std; namespace build2 @@ -30,8 +33,11 @@ namespace build2 class print_runner: public runner { public: - print_runner (bool scope, bool id, bool line) - : scope_ (scope), id_ (id), line_ (line) {} + print_runner (bool scope, bool id, bool line, bool iterations) + : scope_ (scope), + id_ (id), + line_ (line), + iterations_ (iterations) {} virtual bool test (scope&) const override @@ -39,6 +45,12 @@ namespace build2 return true; } + virtual pair<const process_path*, const strings*> + test_runner () override + { + return make_pair (nullptr, nullptr); + } + virtual void enter (scope& s, const location&) override { @@ -88,11 +100,32 @@ namespace build2 } virtual void - run (scope&, + run (scope& env, const command_expr& e, command_type t, - size_t i, - const location&) override + const iteration_index* ii, size_t i, + const function<command_function>& cf, + const location& ll) override { + // If the functions is specified, then just execute it with an empty + // stdin so it can perform the housekeeping (stop replaying tokens, + // increment line index, etc). + // + if (cf != nullptr) + { + assert (e.size () == 1 && !e[0].pipe.empty ()); + + const command& c (e[0].pipe.back ()); + + // Must be enforced by the caller. + // + assert (!c.out && !c.err && !c.exit); + + cf (env, c.arguments, + fdopen_null (), nullptr /* pipe */, + nullopt /* deadline */, + ll); + } + const char* s (nullptr); switch (t) @@ -104,22 +137,22 @@ namespace build2 cout << ind_ << s << e; - if (line_) - cout << " # " << i; + if (line_ || iterations_) + print_line_info (ii, i); cout << endl; } virtual bool - run_if (scope&, - const command_expr& e, - size_t i, - const location&) override + run_cond (scope&, + const command_expr& e, + const iteration_index* ii, size_t i, + const location&) override { cout << ind_ << "? " << e; - if (line_) - cout << " # " << i; + if (line_ || iterations_) + print_line_info (ii, i); cout << endl; @@ -137,13 +170,33 @@ namespace build2 } private: + void + print_line_info (const iteration_index* ii, size_t i) const + { + cout << " #"; + + if (line_) + cout << ' ' << i; + + if (iterations_ && ii != nullptr) + { + string s; + for (const iteration_index* i (ii); i != nullptr; i = i->prev) + s.insert (0, " i" + to_string (i->index)); + + cout << s; + } + } + + private: bool scope_; bool id_; bool line_; + bool iterations_; string ind_; }; - // Usage: argv[0] [-s] [-i] [-l] [<testscript-name>] + // Usage: argv[0] [-s] [-i] [-l] [-r] [<testscript-name>] // int main (int argc, char* argv[]) @@ -153,17 +206,19 @@ namespace build2 // Fake build system driver, default verbosity. // init_diag (1); - init (nullptr, argv[0]); + init (nullptr, argv[0], true); // Serial execution. // scheduler sched (1); global_mutexes mutexes (1); - context ctx (sched, mutexes); + file_cache fcache (true); + context ctx (sched, mutexes, fcache); bool scope (false); bool id (false); bool line (false); + bool iterations (false); path name; for (int i (1); i != argc; ++i) @@ -176,6 +231,8 @@ namespace build2 id = true; else if (a == "-l") line = true; + else if (a == "-r") + iterations = true; else { name = path (move (a)); @@ -208,7 +265,7 @@ namespace build2 tt.assign ( ctx.var_pool.rw ().insert<target_triplet> ("test.target"))); - v = cast<target_triplet> (ctx.global_scope["build.host"]); + v = *ctx.build_host; testscript& st ( ctx.targets.insert<testscript> (work, @@ -226,7 +283,7 @@ namespace build2 script s (tt, st, dir_path (work) /= "test-driver"); p.pre_parse (cin, s); - print_runner r (scope, id, line); + print_runner r (scope, id, line, iterations); p.execute (s, r); } catch (const failed&) diff --git a/libbuild2/test/script/runner.cxx b/libbuild2/test/script/runner.cxx index 03a1f0e..98d6868 100644 --- a/libbuild2/test/script/runner.cxx +++ b/libbuild2/test/script/runner.cxx @@ -3,6 +3,8 @@ #include <libbuild2/test/script/runner.hxx> +#include <libbuild2/filesystem.hxx> + #include <libbuild2/script/run.hxx> #include <libbuild2/test/common.hxx> @@ -21,6 +23,12 @@ namespace build2 return common_.test (s.root.test_target, s.id_path); } + pair<const process_path*, const strings*> default_runner:: + test_runner () + { + return make_pair (common_.runner_path, common_.runner_options); + } + void default_runner:: enter (scope& sp, const location&) { @@ -35,6 +43,18 @@ namespace build2 dr << info << "test id: " << sp.id_path.posix_string (); }); + // Note that we could probably keep the test programs sets fully + // independent across the scopes and check if the program is a test by + // traversing the scopes upwards recursively. Note though, that the + // parent scope's set cannot change during the nested scope execution + // and normally contains just a single entry. Thus, it seems more + // efficient to get rid of the recursion by copying the set from the + // parent now and potentially changing it later on the test variable + // assignment, etc. + // + if (sp.parent != nullptr) + sp.test_programs = sp.parent->test_programs; + // Scope working directory shall be empty (the script working // directory is cleaned up by the test rule prior the script // execution). @@ -122,7 +142,9 @@ namespace build2 void default_runner:: run (scope& sp, const command_expr& expr, command_type ct, - size_t li, const location& ll) + const iteration_index* ii, size_t li, + const function<command_function>& cf, + const location& ll) { // Noop for teardown commands if keeping tests output is requested. // @@ -144,40 +166,55 @@ namespace build2 text << ": " << c << expr; } - // Print test id once per test expression. + // Print test id once per test expression and only for the topmost + // one. // auto df = make_diag_frame ( - [&sp](const diag_record& dr) + [&sp, print = (sp.exec_level == 0)](const diag_record& dr) { - // Let's not depend on how the path representation can be improved - // for readability on printing. - // - dr << info << "test id: " << sp.id_path.posix_string (); + if (print) + { + // Let's not depend on how the path representation can be + // improved for readability on printing. + // + dr << info << "test id: " << sp.id_path.posix_string (); + } }); - build2::script::run (sp, expr, li, ll); + ++sp.exec_level; + build2::script::run (sp, expr, ii, li, ll, cf); + --sp.exec_level; } bool default_runner:: - run_if (scope& sp, - const command_expr& expr, - size_t li, const location& ll) + run_cond (scope& sp, + const command_expr& expr, + const iteration_index* ii, size_t li, + const location& ll) { if (verb >= 3) text << ": ?" << expr; - // Print test id once per test expression. + // Print test id once per test expression and only for the topmost + // one. // auto df = make_diag_frame ( - [&sp](const diag_record& dr) + [&sp, print = (sp.exec_level == 0)](const diag_record& dr) { - // Let's not depend on how the path representation can be improved - // for readability on printing. - // - dr << info << "test id: " << sp.id_path.posix_string (); + if (print) + { + // Let's not depend on how the path representation can be + // improved for readability on printing. + // + dr << info << "test id: " << sp.id_path.posix_string (); + } }); - return build2::script::run_if (sp, expr, li, ll); + ++sp.exec_level; + bool r (build2::script::run_cond (sp, expr, ii, li, ll)); + --sp.exec_level; + + return r; } } } diff --git a/libbuild2/test/script/runner.hxx b/libbuild2/test/script/runner.hxx index 22cae4e..687d991 100644 --- a/libbuild2/test/script/runner.hxx +++ b/libbuild2/test/script/runner.hxx @@ -29,6 +29,12 @@ namespace build2 virtual bool test (scope&) const = 0; + // Return the runner program path and options if the test commands + // must be run via the runner and the pair of NULLs otherwise. + // + virtual pair<const process_path*, const strings*> + test_runner () = 0; + // Location is the scope start location (for diagnostics, etc). // virtual void @@ -42,14 +48,21 @@ namespace build2 // Location is the start position of this command line in the // testscript. It can be used in diagnostics. // + // Optionally, execute the specified function instead of the last + // pipe command. + // virtual void run (scope&, const command_expr&, command_type, - size_t index, + const iteration_index*, size_t index, + const function<command_function>&, const location&) = 0; virtual bool - run_if (scope&, const command_expr&, size_t, const location&) = 0; + run_cond (scope&, + const command_expr&, + const iteration_index*, size_t, + const location&) = 0; // Location is the scope end location (for diagnostics, etc). // @@ -66,17 +79,27 @@ namespace build2 virtual bool test (scope& s) const override; + // Return the test.runner.{path,options} values, if config.test.runner + // is specified. + // + virtual pair<const process_path*, const strings*> + test_runner () override; + virtual void enter (scope&, const location&) override; virtual void run (scope&, const command_expr&, command_type, - size_t, + const iteration_index*, size_t, + const function<command_function>&, const location&) override; virtual bool - run_if (scope&, const command_expr&, size_t, const location&) override; + run_cond (scope&, + const command_expr&, + const iteration_index*, size_t, + const location&) override; virtual void leave (scope&, const location&) override; diff --git a/libbuild2/test/script/script.cxx b/libbuild2/test/script/script.cxx index 34d4723..f7827f6 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,12 +22,15 @@ namespace build2 { namespace script { + using build2::script::to_deadline; + using build2::script::to_timeout; + // scope_base // scope_base:: scope_base (script& s) : root (s), - vars (s.test_target.ctx, false /* global */) + vars (s.test_target.ctx, false /* shared */) // Note: managed. { vars.assign (root.wd_var) = dir_path (); } @@ -95,8 +102,20 @@ namespace build2 dir_path (*p->work_dir.path) /= id; } + bool scope:: + test_program (const path& p) + { + assert (!test_programs.empty ()); + + return find_if (test_programs.begin (), test_programs.end (), + [&p] (const path* tp) + { + return tp != nullptr ? *tp == p : false; + }) != test_programs.end (); + } + void scope:: - set_variable (string&& nm, + set_variable (string nm, names&& val, const string& attrs, const location& ll) @@ -149,6 +168,17 @@ namespace build2 token_type::assign, path_name ("<attributes>")); } + + if (root.test_command_var (var.name)) + reset_special (); + } + + const environment_vars& scope:: + exported_variables (environment_vars& storage) + { + return parent != nullptr + ? parent->merge_exported_variables (exported_vars, storage) + : exported_vars; } // script_base @@ -167,12 +197,12 @@ namespace build2 test_var (var_pool.insert<path> ("test")), options_var (var_pool.insert<strings> ("test.options")), arguments_var (var_pool.insert<strings> ("test.arguments")), - redirects_var (var_pool.insert<strings> ("test.redirects")), - cleanups_var (var_pool.insert<strings> ("test.cleanups")), + redirects_var (var_pool.insert<cmdline> ("test.redirects")), + cleanups_var (var_pool.insert<cmdline> ("test.cleanups")), wd_var (var_pool.insert<dir_path> ("~")), id_var (var_pool.insert<path> ("@")), - cmd_var (var_pool.insert<strings> ("*")), + cmd_var (var_pool.insert<cmdline> ("*")), cmdN_var { &var_pool.insert<path> ("0"), &var_pool.insert<string> ("1"), @@ -188,11 +218,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'). @@ -235,7 +268,7 @@ namespace build2 v = path (n->dir); else { - // Must be a target name. + // Must be a target name. Could be from src (e.g., a script). // // @@ OUT: what if this is a @-qualified pair of names? // @@ -277,11 +310,25 @@ namespace build2 } } + // Reserve the entry for the test program specified via the test + // variable. Note that the value will be assigned by the below + // reset_special() call. + // + test_programs.push_back (nullptr); + // Set the special $*, $N variables. // reset_special (); } + optional<deadline> script:: + effective_deadline () + { + return earlier (operation_deadline, group_deadline); + } + + // scope + // lookup scope:: lookup (const variable& var) const { @@ -308,7 +355,7 @@ namespace build2 // in parallel). Plus, if there is no such variable, then we cannot // possibly find any value. // - const variable* pvar (context.var_pool.find (n)); + const variable* pvar (root.target_scope.var_pool ().find (n)); if (pvar == nullptr) return lookup_type (); @@ -360,33 +407,51 @@ namespace build2 void scope:: reset_special () { - // First assemble the $* value. + // First assemble the $* value and save the test variable value into + // the test program set. // - strings s; + cmdline s; - auto append = [&s] (const strings& v) + auto append = [&s] (const strings& vs) { - s.insert (s.end (), v.begin (), v.end ()); + for (const string& v: vs) + s.push_back (name (v)); // Simple name. }; + // If the test variable can't be looked up for any reason (is NULL, + // etc), then keep $* empty. + // if (auto l = lookup (root.test_var)) - s.push_back (cast<path> (l).representation ()); + { + const path& p (cast<path> (l)); + s.push_back (name (p.representation ())); + + test_programs[0] = &p; - if (auto l = lookup (root.options_var)) - append (cast<strings> (l)); + if (auto l = lookup (root.options_var)) + append (cast<strings> (l)); - if (auto l = lookup (root.arguments_var)) - append (cast<strings> (l)); + if (auto l = lookup (root.arguments_var)) + append (cast<strings> (l)); + } + else + test_programs[0] = nullptr; // Keep redirects/cleanups out of $N. // size_t n (s.size ()); if (auto l = lookup (root.redirects_var)) - append (cast<strings> (l)); + { + const auto& v (cast<cmdline> (l)); + s.insert (s.end (), v.begin (), v.end ()); + } if (auto l = lookup (root.cleanups_var)) - append (cast<strings> (l)); + { + const auto& v (cast<cmdline> (l)); + s.insert (s.end (), v.begin (), v.end ()); + } // Set the $N values if present. // @@ -397,9 +462,9 @@ namespace build2 if (i < n) { if (i == 0) - v = path (s[i]); + v = path (s[i].value); else - v = s[i]; + v = s[i].value; } else v = nullptr; // Clear any old values. @@ -407,8 +472,174 @@ namespace build2 // Set $*. // + // We need to effective-quote the $test $test.options, $test.arguments + // part of it since they will be re-lexed. See the Testscript manual + // for details on quoting semantics. In particular, we cannot escape + // the special character (|<>&) so we have to rely on quoting. We can + // use single-quoting for everything except if the value contains a + // single quote. In which case we should probably just do separately- + // quoted regions (similar to shell), for example: + // + // <''> + // + // Can be quoted as: + // + // '<'"''"'>' + // + for (size_t i (0); i != n; ++i) + { + string& v (s[i].value); + + // Check if the quoting is required for this value. + // + if (!parser::need_cmdline_relex (v)) + continue; + + // If the value doesn't contain the single-quote character, then + // single-quote it. + // + size_t p (v.find ('\'')); + + if (p == string::npos) + { + v = '\'' + v + '\''; + continue; + } + + // Otherwise quote the regions. + // + // Note that we double-quote the single-quote character sequences + // and single-quote all the other regions. + // + string r; + char q (p == 0 ? '"' : '\''); // Current region quoting mode. + + r += q; // Open the first region. + + for (char c: v) + { + // If we are in the double-quoting mode, then switch to the + // single-quoting mode if a non-single-quote character is + // encountered. + // + if (q == '"') + { + if (c != '\'') + { + r += q; // Close the double-quoted region. + q = '\''; // Set the single-quoting mode. + r += q; // Open the single-quoted region. + } + } + // + // If we are in the single-quoting mode, then switch to the + // double-quoting mode if the single-quote character is + // encountered. + // + else + { + if (c == '\'') + { + r += q; // Close the single-quoted region. + q = '"'; // Set the double-quoting mode. + r += q; // Open the double-quoted region. + } + } + + r += c; + } + + r += q; // Close the last region. + + v = move (r); + } + 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"); + const char* pf ("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, pf, l), + success); + + if (p != t.size () - 1) + test_timeout = + to_timeout (parse_timeout (string (t, p + 1), tt, pf, l), + success); + } + else + group_deadline = to_deadline (parse_deadline (t, gt, pf, 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", "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..9409b01 100644 --- a/libbuild2/test/script/script.hxx +++ b/libbuild2/test/script/script.hxx @@ -4,8 +4,6 @@ #ifndef LIBBUILD2_TEST_SCRIPT_SCRIPT_HXX #define LIBBUILD2_TEST_SCRIPT_SCRIPT_HXX -#include <set> - #include <libbuild2/types.hxx> #include <libbuild2/forward.hxx> #include <libbuild2/utility.hxx> @@ -23,11 +21,19 @@ namespace build2 namespace script { using build2::script::line; + using build2::script::line_type; using build2::script::lines; using build2::script::redirect; using build2::script::redirect_type; - using build2::script::line_type; + using build2::script::command; + using build2::script::expr_term; using build2::script::command_expr; + using build2::script::iteration_index; + using build2::script::environment_vars; + using build2::script::deadline; + using build2::script::timeout; + using build2::script::pipe_command; + using build2::script::command_function; class parser; // Required by VC for 'friend class parser' declaration. @@ -91,18 +97,58 @@ namespace build2 scope_state state = scope_state::unknown; - void - set_variable (string&& name, + // The command expression execution nesting level. Can be maintained + // by the runner to, for example, only perform some housekeeping on + // the topmost level (add the test id to the diagnostics, etc). + // + // Note that the command expression execution can be nested, so that + // the outer expression execution is not completed before all the + // inner expressions are executed. As for example in: + // + // echo 'a b' | for x + // echo 'c d' | for y + // test $x $y + // end + // end + // + size_t exec_level = 0; + + // Test program paths. + // + // Currently always contains a single element (see test_program() for + // details). While in the future there can be more of them, the zero + // index will always refer to the test variable value and can + // potentially be NULL (see reset_special() for details). + // + small_vector<const path*, 1> test_programs; + + virtual void + set_variable (string name, names&&, const string& attrs, const location&) override; + // Merge the command execution environment variable (un)sets from this + // and outer scopes. + // + virtual const environment_vars& + exported_variables (environment_vars& storage) override; + // Noop since the temporary directory is a working directory and so // is created before the scope commands execution. // virtual void create_temp_dir () override {assert (false);}; + // Return true if this is a test program path. + // + // Note that currently the test program is only specified via the test + // variable ($0 effectively). In the future we may invent some other + // means of marking a program as a test (builtin, etc). + // + bool + test_program (const path&); + // Variables. // public: @@ -168,10 +214,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 +272,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: @@ -239,12 +327,27 @@ namespace build2 variable_pool var_pool; mutable shared_mutex var_pool_mutex; + // Used to compose a test command. + // + // Changing any of their values requires resetting the $* and $N + // special aliases. + // const variable& test_var; // test const variable& options_var; // test.options const variable& arguments_var; // test.arguments const variable& redirects_var; // test.redirects const variable& cleanups_var; // test.cleanups + bool + test_command_var (const string& name) const + { + return name == test_var.name || + name == options_var.name || + name == arguments_var.name || + name == redirects_var.name || + name == cleanups_var.name; + } + const variable& wd_var; // $~ const variable& id_var; // $@ const variable& cmd_var; // $* @@ -254,6 +357,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 +373,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: @@ -283,7 +399,7 @@ namespace build2 } }; - std::set<path_name_value, compare_paths> paths_; + set<path_name_value, compare_paths> paths_; }; } } diff --git a/libbuild2/test/target.cxx b/libbuild2/test/target.cxx index ce88baa..852abdf 100644 --- a/libbuild2/test/target.cxx +++ b/libbuild2/test/target.cxx @@ -56,7 +56,7 @@ namespace build2 &testscript_target_pattern, nullptr, &file_search, - false + target_type::flag::none }; } } diff --git a/libbuild2/test/target.hxx b/libbuild2/test/target.hxx index 1dd7307..e6c549f 100644 --- a/libbuild2/test/target.hxx +++ b/libbuild2/test/target.hxx @@ -18,11 +18,14 @@ namespace build2 class LIBBUILD2_SYMEXPORT testscript: public file { public: - using file::file; + testscript (context& c, dir_path d, dir_path o, string n) + : file (c, move (d), move (o), move (n)) + { + dynamic_type = &static_type; + } public: static const target_type static_type; - virtual const target_type& dynamic_type () const {return static_type;} }; } } |