aboutsummaryrefslogtreecommitdiff
path: root/libbuild2/test
diff options
context:
space:
mode:
Diffstat (limited to 'libbuild2/test')
-rw-r--r--libbuild2/test/common.cxx6
-rw-r--r--libbuild2/test/init.cxx50
-rw-r--r--libbuild2/test/operation.cxx15
-rw-r--r--libbuild2/test/rule.cxx514
-rw-r--r--libbuild2/test/rule.hxx8
-rw-r--r--libbuild2/test/script/lexer+for-loop.test.testscript231
-rw-r--r--libbuild2/test/script/lexer.cxx60
-rw-r--r--libbuild2/test/script/lexer.hxx13
-rw-r--r--libbuild2/test/script/lexer.test.cxx5
-rw-r--r--libbuild2/test/script/parser+command-if.test.testscript6
-rw-r--r--libbuild2/test/script/parser+command-re-parse.test.testscript2
-rw-r--r--libbuild2/test/script/parser+description.test.testscript4
-rw-r--r--libbuild2/test/script/parser+expansion.test.testscript2
-rw-r--r--libbuild2/test/script/parser+for.test.testscript1029
-rw-r--r--libbuild2/test/script/parser+while.test.testscript265
-rw-r--r--libbuild2/test/script/parser.cxx549
-rw-r--r--libbuild2/test/script/parser.hxx18
-rw-r--r--libbuild2/test/script/parser.test.cxx89
-rw-r--r--libbuild2/test/script/runner.cxx55
-rw-r--r--libbuild2/test/script/runner.hxx19
-rw-r--r--libbuild2/test/script/script.cxx119
-rw-r--r--libbuild2/test/script/script.hxx33
-rw-r--r--libbuild2/test/target.cxx2
-rw-r--r--libbuild2/test/target.hxx7
24 files changed, 2707 insertions, 394 deletions
diff --git a/libbuild2/test/common.cxx b/libbuild2/test/common.cxx
index 7fdb347..89f3dd6 100644
--- a/libbuild2/test/common.cxx
+++ b/libbuild2/test/common.cxx
@@ -150,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;
@@ -198,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.
diff --git a/libbuild2/test/init.cxx b/libbuild2/test/init.cxx
index dadbd20..32548f4 100644
--- a/libbuild2/test/init.cxx
+++ b/libbuild2/test/init.cxx
@@ -23,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)
{
@@ -30,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 {
@@ -69,7 +70,7 @@ namespace build2
// 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"),
@@ -111,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.
@@ -125,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)));
}
@@ -278,6 +284,12 @@ namespace build2
//
//@@ 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.
//
{
@@ -290,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/operation.cxx b/libbuild2/test/operation.cxx
index 0a65bed..2535adb 100644
--- a/libbuild2/test/operation.cxx
+++ b/libbuild2/test/operation.cxx
@@ -17,11 +17,8 @@ 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;
@@ -67,7 +64,9 @@ namespace build2
"has nothing to test", // We cannot "be tested".
execution_mode::first,
1 /* concurrency */,
- &test_pre,
+ &pre_test,
+ nullptr,
+ nullptr,
nullptr,
nullptr,
&adhoc_apply
@@ -85,8 +84,10 @@ 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 06fb12f..28eb35b 100644
--- a/libbuild2/test/rule.cxx
+++ b/libbuild2/test/rule.cxx
@@ -30,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()).
@@ -66,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.
//
@@ -540,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
@@ -555,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.
@@ -641,25 +649,50 @@ namespace build2
// Stack-allocated linked list of information about the running pipeline
// processes.
//
+ // Note: constructed incrementally.
+ //
struct pipe_process
{
- process& proc;
- const char* prog; // Only for diagnostics.
+ // 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;
- pipe_process* prev; // NULL for the left-most program.
+ // 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 (process& p, const char* g, pipe_process* r)
- : proc (p), prog (g), prev (r) {}
+ 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 bool
+ static void
run_test (const target& t,
- diag_record& dr,
char const** args,
+ int ofd,
const optional<timestamp>& deadline,
pipe_process* prev = nullptr)
{
@@ -669,14 +702,28 @@ 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;
+ int out (last ? ofd : -1);
- // Absent if the process misses the deadline.
+ // Propagate the pointer to the left-most program.
//
- optional<process_exit> pe;
+ // 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
{
@@ -707,11 +754,11 @@ namespace build2
{
try
{
- p->proc.term ();
+ p->proc->term ();
}
catch (const process_error& e)
{
- dr << fail << "unable to terminate " << p->prog << ": " << e;
+ dr << fail << "unable to terminate " << p->args[0] << ": " << e;
}
p->terminated = true;
@@ -724,7 +771,7 @@ namespace build2
for (pipe_process* p (pp); p != nullptr; p = p->prev)
{
- process& pr (p->proc);
+ process& pr (*p->proc);
try
{
@@ -736,26 +783,310 @@ namespace build2
}
catch (const process_error& e)
{
- dr << fail << "unable to wait/kill " << p->prog << ": " << e;
+ dr << fail << "unable to wait/kill " << p->args[0] << ": " << e;
+ }
+ }
+ };
+
+ // 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);
+ }
+
+ 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 (prev == nullptr
- ? process (args, 0, out) // First process.
- : process (args, prev->proc, out)); // Next process.
+ 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.
+ }
- pipe_process pp (p, args[0], prev);
+ pp.proc = &p;
- // If the deadline is specified, then make sure we don't miss it
- // waiting indefinitely in the process destructor on the right-hand
- // part of the pipe failure.
+ // 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 ([&deadline, &pp, &term_pipe] ()
+ auto g (make_exception_guard ([&pp, &term_pipe] ()
{
- if (deadline)
+ 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&)
@@ -764,25 +1095,17 @@ namespace build2
}
}));
- pr = *next == nullptr || run_test (t, dr, next, deadline, &pp);
-
- if (!deadline)
- p.wait ();
- else if (!timed_wait (p, *deadline))
- term_pipe (&pp);
+ if (!last)
+ run_test (t, next, ofd, deadline, &pp);
- assert (p.exit);
-
-#ifndef _WIN32
- if (!(pp.terminated &&
- !p.exit->normal () &&
- p.exit->signal () == SIGTERM))
-#else
- if (!(pp.terminated &&
- !p.exit->normal () &&
- p.exit->status == DBG_TERMINATE_PROCESS))
-#endif
- pe = *p.exit;
+ // Complete the pipeline execution, if not done yet.
+ //
+ if (pp.proc != nullptr)
+ {
+ read_pipe ();
+ wait_pipe ();
+ complete_pipe ();
+ }
}
catch (const process_error& e)
{
@@ -793,24 +1116,6 @@ namespace build2
throw failed ();
}
-
- bool wr (pe && 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);
-
- if (pe)
- dr << " " << *pe;
- else
- dr << " terminated: execution timeout expired";
- }
-
- return pr && wr;
}
target_state rule::
@@ -856,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?
//
@@ -986,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.
@@ -1035,25 +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;
- pipe_process pp (cat, "cat", nullptr);
-
- if (!run_test (tt,
- dr,
- args.data () + (sin ? 3 : 0), // Skip cat.
- test_deadline (tt),
- sin ? &pp : 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+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 0a90f24..b712c21 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, &lt, &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;
- }
- }
-
- if (tt != type::newline)
- fail (t) << "expected newline instead of " << t;
+ parse_command_tail ();
+ parse_here_documents (t, tt, r);
- 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,18 @@ 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);
- command_expr r (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.
@@ -1402,9 +1624,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.
@@ -1424,8 +1643,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.
@@ -1437,19 +1657,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);
@@ -1459,16 +1695,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)
{
@@ -1526,7 +1763,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)
{
@@ -1593,24 +1831,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 ();
}
}
@@ -1637,8 +1875,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);
@@ -1652,7 +1890,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
@@ -1661,7 +1900,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 f8c3f21..6fe46e2 100644
--- a/libbuild2/test/script/parser.hxx
+++ b/libbuild2/test/script/parser.hxx
@@ -62,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&,
@@ -79,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&);
@@ -117,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.
//
@@ -131,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 dbdeb57..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
@@ -94,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)
@@ -110,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;
@@ -143,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[])
@@ -159,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)
@@ -182,6 +231,8 @@ namespace build2
id = true;
else if (a == "-l")
line = true;
+ else if (a == "-r")
+ iterations = true;
else
{
name = path (move (a));
@@ -214,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,
@@ -232,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 af5f30a..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>
@@ -140,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.
//
@@ -162,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 b6a038d..687d991 100644
--- a/libbuild2/test/script/runner.hxx
+++ b/libbuild2/test/script/runner.hxx
@@ -48,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).
//
@@ -84,11 +91,15 @@ namespace build2
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 3a8ceac..f7827f6 100644
--- a/libbuild2/test/script/script.cxx
+++ b/libbuild2/test/script/script.cxx
@@ -30,7 +30,7 @@ namespace build2
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 ();
}
@@ -115,7 +115,7 @@ namespace build2
}
void scope::
- set_variable (string&& nm,
+ set_variable (string nm,
names&& val,
const string& attrs,
const location& ll)
@@ -197,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"),
@@ -268,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?
//
@@ -355,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 ();
@@ -410,11 +410,12 @@ namespace build2
// 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,
@@ -423,7 +424,7 @@ namespace build2
if (auto l = lookup (root.test_var))
{
const path& p (cast<path> (l));
- s.push_back (p.representation ());
+ s.push_back (name (p.representation ()));
test_programs[0] = &p;
@@ -441,10 +442,16 @@ namespace build2
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.
//
@@ -455,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.
@@ -465,6 +472,88 @@ 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);
}
diff --git a/libbuild2/test/script/script.hxx b/libbuild2/test/script/script.hxx
index 7dae78c..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,16 +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_expr;
- using build2::script::expr_term;
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.
@@ -96,6 +97,22 @@ namespace build2
scope_state state = scope_state::unknown;
+ // 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
@@ -105,8 +122,8 @@ namespace build2
//
small_vector<const path*, 1> test_programs;
- void
- set_variable (string&& name,
+ virtual void
+ set_variable (string name,
names&&,
const string& attrs,
const location&) override;
@@ -382,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;}
};
}
}