aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorKaren Arutyunov <karen@codesynthesis.com>2017-01-20 20:25:59 +0300
committerKaren Arutyunov <karen@codesynthesis.com>2017-01-24 14:53:00 +0300
commit28106f96de8ae5cdb3a0ee0e3a8a8185551e3b00 (patch)
tree8b912a7c9a2bd2ba1263695428d8343d022953a5
parenteeed734583c2f553e71ae0bd78a4b6e7e4d9cc2b (diff)
Add support for comparison of test command output to a file
-rw-r--r--build2/test/script/builtin.cxx2
-rw-r--r--build2/test/script/lexer.cxx14
-rw-r--r--build2/test/script/parser.cxx30
-rw-r--r--build2/test/script/runner.cxx703
-rw-r--r--build2/test/script/script11
-rw-r--r--build2/test/script/script.cxx12
-rw-r--r--build2/test/script/token8
-rw-r--r--build2/test/script/token.cxx8
-rw-r--r--tests/common.test2
-rw-r--r--tests/search/dir/testscript4
-rw-r--r--tests/test/common.test2
-rw-r--r--tests/test/config-test/testscript36
-rw-r--r--tests/test/script-integration/testscript2
-rw-r--r--tests/test/script/builtin/cat.test2
-rw-r--r--tests/test/script/builtin/touch.test2
-rw-r--r--tests/test/script/common.test6
-rw-r--r--tests/test/script/runner/cleanup.test6
-rw-r--r--tests/test/script/runner/redirect.test50
-rw-r--r--unit-tests/test/script/lexer/command-expansion.test24
-rw-r--r--unit-tests/test/script/lexer/command-line.test24
-rw-r--r--unit-tests/test/script/parser/directive.test2
-rw-r--r--unit-tests/test/script/parser/include.test12
-rw-r--r--unit-tests/test/script/parser/redirect.test34
23 files changed, 545 insertions, 451 deletions
diff --git a/build2/test/script/builtin.cxx b/build2/test/script/builtin.cxx
index f911b60..6a8ae16 100644
--- a/build2/test/script/builtin.cxx
+++ b/build2/test/script/builtin.cxx
@@ -90,7 +90,7 @@ namespace build2
// this. That would require to fstat() file descriptors and complicate
// the code a bit. Was able to reproduce on a big file (should be
// bigger than the stream buffer size) with the test
- // 'cat file >>>&file'.
+ // 'cat file >+file'.
//
// Note: must be executed asynchronously.
//
diff --git a/build2/test/script/lexer.cxx b/build2/test/script/lexer.cxx
index ba3f1ae..d0d59aa 100644
--- a/build2/test/script/lexer.cxx
+++ b/build2/test/script/lexer.cxx
@@ -354,13 +354,13 @@ namespace build2
type r (type::in_str);
xchar p (peek ());
- if (p == '+' || p == '-' || p == '<')
+ if (p == '|' || p == '-' || p == '<')
{
get ();
switch (p)
{
- case '+': return make_token (type::in_pass);
+ case '|': return make_token (type::in_pass);
case '-': return make_token (type::in_null);
case '<':
{
@@ -395,15 +395,18 @@ namespace build2
type r (type::out_str);
xchar p (peek ());
- if (p == '+' || p == '-' || p == '&' || p == '>')
+ if (p == '|' || p == '-' || p == '&' ||
+ p == '=' || p == '+' || p == '>')
{
get ();
switch (p)
{
- case '+': return make_token (type::out_pass);
+ case '|': return make_token (type::out_pass);
case '-': return make_token (type::out_null);
case '&': return make_token (type::out_merge);
+ case '=': return make_token (type::out_file_ovr);
+ case '+': return make_token (type::out_file_app);
case '>':
{
r = type::out_doc;
@@ -412,7 +415,7 @@ namespace build2
if (p == '>')
{
get ();
- r = type::out_file;
+ r = type::out_file_cmp;
}
break;
}
@@ -427,7 +430,6 @@ namespace build2
{
case type::out_str:
case type::out_doc: mods = ":/~"; stop = "~"; break;
- case type::out_file: mods = "&"; break;
}
return make_token_with_modifiers (r, mods, stop);
diff --git a/build2/test/script/parser.cxx b/build2/test/script/parser.cxx
index 4b1c777..c3dc233 100644
--- a/build2/test/script/parser.cxx
+++ b/build2/test/script/parser.cxx
@@ -1518,7 +1518,6 @@ namespace build2
}
r.file.path = parse_path (move (w), what);
- r.file.append = r.modifiers.find ('&') != string::npos;
};
switch (p)
@@ -1684,7 +1683,9 @@ namespace build2
case type::out_merge:
case type::out_str:
case type::out_doc:
- case type::out_file:
+ case type::out_file_cmp:
+ case type::out_file_ovr:
+ case type::out_file_app:
{
if ((fd = fd == 3 ? 1 : fd) == 0)
fail (l) << "invalid out redirect file descriptor " << fd;
@@ -1733,7 +1734,9 @@ namespace build2
}
case type::in_file:
- case type::out_file: rt = redirect_type::file; break;
+ case type::out_file_cmp:
+ case type::out_file_ovr:
+ case type::out_file_app: rt = redirect_type::file; break;
}
redirect& r (fd == 0 ? c.in : fd == 1 ? c.out : c.err);
@@ -1797,6 +1800,15 @@ namespace build2
case 1: p = pending::out_file; break;
case 2: p = pending::err_file; break;
}
+
+ // Also sets for stdin, but this is harmless.
+ //
+ r.file.mode = tt == type::out_file_ovr
+ ? redirect_fmode::overwrite
+ : (tt == type::out_file_app
+ ? redirect_fmode::append
+ : redirect_fmode::compare);
+
break;
}
};
@@ -1875,7 +1887,9 @@ namespace build2
case type::out_doc:
case type::in_file:
- case type::out_file:
+ case type::out_file_cmp:
+ case type::out_file_ovr:
+ case type::out_file_app:
case type::clean:
{
@@ -2013,7 +2027,9 @@ namespace build2
case type::out_doc:
case type::in_file:
- case type::out_file:
+ case type::out_file_cmp:
+ case type::out_file_ovr:
+ case type::out_file_app:
{
parse_redirect (t, l);
break;
@@ -2263,7 +2279,9 @@ namespace build2
case type::out_str:
case type::in_file:
- case type::out_file:
+ case type::out_file_cmp:
+ case type::out_file_ovr:
+ case type::out_file_app:
{
parse_redirect (t, l);
break;
diff --git a/build2/test/script/runner.cxx b/build2/test/script/runner.cxx
index 8e31cf8..41527cf 100644
--- a/build2/test/script/runner.cxx
+++ b/build2/test/script/runner.cxx
@@ -60,6 +60,26 @@ namespace build2
{
namespace script
{
+ // Normalize a path. Also make the relative path absolute using the
+ // scope's working directory unless it is already absolute.
+ //
+ static path
+ normalize (path p, const scope& sp, const location& l)
+ {
+ path r (p.absolute () ? move (p) : sp.wd_path / move (p));
+
+ try
+ {
+ r.normalize ();
+ }
+ catch (const invalid_path& e)
+ {
+ fail (l) << "invalid file path " << e.path;
+ }
+
+ return r;
+ }
+
// Check if a path is not empty, the referenced file exists and is not
// empty.
//
@@ -213,7 +233,17 @@ namespace build2
d << info << "stdin: " << ip;
};
- bool re;
+ auto output_info = [&what, &ll] (diag_record& d,
+ const path& p,
+ const char* prefix = "",
+ const char* suffix = "")
+ {
+ if (non_empty (p, ll))
+ d << info << prefix << what << suffix << ": " << p;
+ else
+ d << info << prefix << what << suffix << " is empty";
+ };
+
if (rd.type == redirect_type::none)
{
assert (!op.empty ());
@@ -229,382 +259,373 @@ namespace build2
input_info (d);
}
}
- else if ((re = (rd.type == redirect_type::here_str_regex ||
- rd.type == redirect_type::here_doc_regex)) ||
- rd.type == redirect_type::here_str_literal ||
- rd.type == redirect_type::here_doc_literal)
+ else if (rd.type == redirect_type::here_str_literal ||
+ rd.type == redirect_type::here_doc_literal ||
+ (rd.type == redirect_type::file &&
+ rd.file.mode == redirect_fmode::compare))
{
assert (!op.empty ());
- auto output_info = [&what, &ll] (diag_record& d,
- const path& p,
- const char* prefix = "",
- const char* suffix = "")
- {
- if (non_empty (p, ll))
- d << info << prefix << what << suffix << ": " << p;
- else
- d << info << prefix << what << suffix << " is empty";
- };
+ // The expected output is provided as a file or as a string. Save the
+ // string to a file in the later case.
+ //
+ path eop;
- if (re) // Match the output with the regex.
+ if (rd.type == redirect_type::file)
+ eop = normalize (rd.file.path, sp, ll);
+ else
{
- // The overall plan is:
- //
- // 1. Create regex line string. While creating it's line characters
- // transform regex lines according to the redirect modifiers.
- //
- // 2. Create line regex using the line string. If creation fails
- // then save the (transformed) regex redirect to a file for
- // troubleshooting.
- //
- // 3. Parse the output into the literal line string.
- //
- // 4. Match the output line string with the line regex.
- //
- // 5. If match fails save the (transformed) regex redirect to a
- // file for troubleshooting.
- //
- using namespace regex;
-
- // Create regex line string.
- //
- line_pool pool;
- line_string rls;
- const regex_lines rl (rd.regex);
+ eop = path (op + ".orig");
+ save (eop, transform (rd.str, false, rd.modifiers, *sp.root), ll);
+ sp.clean ({cleanup_type::always, eop}, true);
+ }
- // Parse regex flags.
- //
- // When add support for new flags don't forget to update
- // parse_regex().
- //
- auto parse_flags = [] (const string& f) -> char_flags
- {
- char_flags r (char_flags::none);
+ // Use diff utility for the comparison.
+ //
+ path dp ("diff");
+ process_path pp (run_search (dp, true));
- for (char c: f)
- {
- switch (c)
- {
- case 'd': r |= char_flags::idot; break;
- case 'i': r |= char_flags::icase; break;
- default: assert (false); // Error so should have been checked.
- }
- }
+ cstrings args {
+ pp.recall_string (),
+ "--strip-trailing-cr", // Is essential for cross-testing.
+ "-u",
+ eop.string ().c_str (),
+ op.string ().c_str (),
+ nullptr};
- return r;
- };
+ if (verb >= 2)
+ print_process (args);
- // Return original regex line with the transformation applied.
+ try
+ {
+ // Save diff's stdout to a file for troubleshooting and for the
+ // optional (if not too large) printing (at the end of
+ // diagnostics).
//
- auto line = [&rl, &rd, &sp] (const regex_line& l) -> string
- {
- string r;
- if (l.regex) // Regex (possibly empty),
- {
- r += rl.intro;
- r += transform (l.value, true, rd.modifiers, *sp.root);
- r += rl.intro;
- r += l.flags;
- }
- else if (!l.special.empty ()) // Special literal.
- r += rl.intro;
- else // Textual literal.
- r += transform (l.value, false, rd.modifiers, *sp.root);
+ path ep (op + ".diff");
+ auto_fd efd;
- r += l.special;
- return r;
- };
-
- // Return regex line location.
- //
- // Note that we rely on the fact that the command and regex lines
- // are always belong to the same testscript file.
- //
- auto loc = [&ll] (uint64_t line, uint64_t column) -> location
+ try
{
- location r (ll);
- r.line = line;
- r.column = column;
- return r;
- };
-
- // Save the regex to file for troubleshooting, return the file path
- // it have been saved to.
- //
- // Note that we save the regex on line regex creation failure or if
- // the program output doesn't match.
- //
- auto save_regex = [&op, &rl, &rd, &ll, &line] () -> path
+ efd = fdopen (ep, fdopen_mode::out | fdopen_mode::create);
+ sp.clean ({cleanup_type::always, ep}, true);
+ }
+ catch (const io_error& e)
{
- path rp (op + ".regex");
+ fail (ll) << "unable to write " << ep << ": " << e;
+ }
- // Encode here-document regex global flags if present as a file
- // name suffix. For example if icase and idot flags are specified
- // the name will look like:
- //
- // test/1/stdout.regex~di
- //
- if (rd.type == redirect_type::here_doc_regex &&
- !rl.flags.empty ())
- rp += "~" + rl.flags;
-
- // Note that if would be more efficient to directly write chunks
- // to file rather than to compose a string first. Hower we don't
- // bother (about performance) for the sake of the code as we
- // already failed.
- //
- string s;
- for (const auto& l: rl.lines)
- {
- if (!s.empty ()) s += '\n';
- s += line (l);
- }
+ // Diff utility prints the differences to stdout. But for the
+ // user it is a part of the test failure diagnostics so let's
+ // redirect stdout to stderr.
+ //
+ process p (pp, args.data (), 0, 2, efd.get ());
+ efd.reset ();
- save (rp, s, ll);
- return rp;
- };
+ if (p.wait ())
+ return;
- // Finally create regex line string.
+ // Output doesn't match the expected result.
//
- // Note that diagnostics doesn't refer to the program path as it is
- // irrelevant to failures at this stage.
+ diag_record d (error (ll));
+ d << pr << " " << what << " doesn't match the expected output";
+
+ output_info (d, op);
+ output_info (d, eop, "expected ");
+ output_info (d, ep, "", " diff");
+ input_info (d);
+
+ print_file (d, ep, ll);
+
+ // Fall through.
//
- char_flags gf (parse_flags (rl.flags)); // Regex global flags.
+ }
+ catch (const process_error& e)
+ {
+ error (ll) << "unable to execute " << pp << ": " << e;
- for (const auto& l: rl.lines)
- {
- if (l.regex) // Regex (with optional special characters).
- {
- line_char c;
+ if (e.child ())
+ exit (1);
+ }
- // Empty regex is a special case repesenting the blank line.
- //
- if (l.value.empty ())
- c = line_char ("", pool);
- else
- {
- try
- {
- string s (
- transform (l.value, true, rd.modifiers, *sp.root));
-
- c = line_char (
- char_regex (s, gf | parse_flags (l.flags)), pool);
- }
- catch (const regex_error& e)
- {
- // Print regex_error description if meaningful.
- //
- diag_record d (fail (loc (l.line, l.column)));
-
- if (rd.type == redirect_type::here_str_regex)
- d << "invalid " << what << " regex redirect" << e <<
- info << "regex: '" << line (l) << "'";
- else
- d << "invalid char-regex in " << what
- << " regex redirect" << e <<
- info << "regex line: '" << line (l) << "'";
- }
- }
+ throw failed ();
+ }
+ else if (rd.type == redirect_type::here_str_regex ||
+ rd.type == redirect_type::here_doc_regex)
+ {
+ assert (!op.empty ());
- rls += c; // Append blank literal or regex line char.
- }
- else if (!l.special.empty ()) // Special literal.
- {
- // Literal can not be followed by special characters in the
- // same line.
- //
- assert (l.value.empty ());
- }
- else // Textual literal.
- {
- // Append literal line char.
- //
- rls += line_char (
- transform (l.value, false, rd.modifiers, *sp.root), pool);
- }
+ // The overall plan is:
+ //
+ // 1. Create regex line string. While creating it's line characters
+ // transform regex lines according to the redirect modifiers.
+ //
+ // 2. Create line regex using the line string. If creation fails
+ // then save the (transformed) regex redirect to a file for
+ // troubleshooting.
+ //
+ // 3. Parse the output into the literal line string.
+ //
+ // 4. Match the output line string with the line regex.
+ //
+ // 5. If match fails save the (transformed) regex redirect to a file
+ // for troubleshooting.
+ //
+ using namespace regex;
+
+ // Create regex line string.
+ //
+ line_pool pool;
+ line_string rls;
+ const regex_lines rl (rd.regex);
- for (char c: l.special)
+ // Parse regex flags.
+ //
+ // When add support for new flags don't forget to update
+ // parse_regex().
+ //
+ auto parse_flags = [] (const string& f) -> char_flags
+ {
+ char_flags r (char_flags::none);
+
+ for (char c: f)
+ {
+ switch (c)
{
- if (line_char::syntax (c))
- rls += line_char (c); // Append special line char.
- else
- fail (loc (l.line, l.column))
- << "invalid syntax character '" << c << "' in " << what
- << " regex redirect" <<
- info << "regex line: '" << line (l) << "'";
+ case 'd': r |= char_flags::idot; break;
+ case 'i': r |= char_flags::icase; break;
+ default: assert (false); // Error so should have been checked.
}
}
- // Create line regex.
- //
- line_regex regex;
+ return r;
+ };
- try
+ // Return original regex line with the transformation applied.
+ //
+ auto line = [&rl, &rd, &sp] (const regex_line& l) -> string
+ {
+ string r;
+ if (l.regex) // Regex (possibly empty),
{
- regex = line_regex (move (rls), move (pool));
+ r += rl.intro;
+ r += transform (l.value, true, rd.modifiers, *sp.root);
+ r += rl.intro;
+ r += l.flags;
}
- catch (const regex_error& e)
- {
- // Note that line regex creation can not fail for here-string
- // redirect as it doesn't have syntax line chars. That in
- // particular means that end_line and end_column are meaningful.
- //
- assert (rd.type == redirect_type::here_doc_regex);
+ else if (!l.special.empty ()) // Special literal.
+ r += rl.intro;
+ else // Textual literal.
+ r += transform (l.value, false, rd.modifiers, *sp.root);
- diag_record d (fail (loc (rd.end_line, rd.end_column)));
+ r += l.special;
+ return r;
+ };
- // Print regex_error description if meaningful.
- //
- d << "invalid " << what << " regex redirect" << e;
+ // Return regex line location.
+ //
+ // Note that we rely on the fact that the command and regex lines
+ // are always belong to the same testscript file.
+ //
+ auto loc = [&ll] (uint64_t line, uint64_t column) -> location
+ {
+ location r (ll);
+ r.line = line;
+ r.column = column;
+ return r;
+ };
- output_info (d, save_regex (), "", " regex");
- }
+ // Save the regex to file for troubleshooting, return the file path
+ // it have been saved to.
+ //
+ // Note that we save the regex on line regex creation failure or if
+ // the program output doesn't match.
+ //
+ auto save_regex = [&op, &rl, &rd, &ll, &line] () -> path
+ {
+ path rp (op + ".regex");
- // Parse the output into the literal line string.
+ // Encode here-document regex global flags if present as a file
+ // name suffix. For example if icase and idot flags are specified
+ // the name will look like:
//
- line_string ls;
+ // test/1/stdout.regex~di
+ //
+ if (rd.type == redirect_type::here_doc_regex &&
+ !rl.flags.empty ())
+ rp += "~" + rl.flags;
+
+ // Note that if would be more efficient to directly write chunks
+ // to file rather than to compose a string first. Hower we don't
+ // bother (about performance) for the sake of the code as we
+ // already failed.
+ //
+ string s;
+ for (const auto& l: rl.lines)
+ {
+ if (!s.empty ()) s += '\n';
+ s += line (l);
+ }
- try
+ save (rp, s, ll);
+ return rp;
+ };
+
+ // Finally create regex line string.
+ //
+ // Note that diagnostics doesn't refer to the program path as it is
+ // irrelevant to failures at this stage.
+ //
+ char_flags gf (parse_flags (rl.flags)); // Regex global flags.
+
+ for (const auto& l: rl.lines)
+ {
+ if (l.regex) // Regex (with optional special characters).
{
- // Do not throw when eofbit is set (end of stream reached), and
- // when failbit is set (getline() failed to extract any
- // character).
- //
- // Note that newlines are treated as line-chars separators. That
- // in particular means that the trailing newline produces a blank
- // line-char (empty literal). Empty output produces the
- // zero-length line-string.
- //
- // Also note that we strip the trailing CR characters (otherwise
- // can mismatch when cross-test).
- //
- ifdstream is (op, ifdstream::in, ifdstream::badbit);
- is.peek (); // Sets eofbit for an empty stream.
+ line_char c;
- while (!is.eof ())
+ // Empty regex is a special case repesenting the blank line.
+ //
+ if (l.value.empty ())
+ c = line_char ("", pool);
+ else
{
- string s;
- getline (is, s);
-
- // It is safer to strip CRs in cycle, as msvcrt unexplainably
- // adds too much trailing junk to the system_error
- // descriptions, and so it can appear in programs output. For
- // example:
- //
- // ...: Invalid data.\r\r\n
- //
- // Note that our custom operator<<(ostream&, const exception&)
- // removes this junk.
- //
- while (!s.empty () && s.back () == '\r')
- s.pop_back ();
+ try
+ {
+ string s (
+ transform (l.value, true, rd.modifiers, *sp.root));
- ls += line_char (move (s), regex.pool);
+ c = line_char (
+ char_regex (s, gf | parse_flags (l.flags)), pool);
+ }
+ catch (const regex_error& e)
+ {
+ // Print regex_error description if meaningful.
+ //
+ diag_record d (fail (loc (l.line, l.column)));
+
+ if (rd.type == redirect_type::here_str_regex)
+ d << "invalid " << what << " regex redirect" << e <<
+ info << "regex: '" << line (l) << "'";
+ else
+ d << "invalid char-regex in " << what << " regex redirect"
+ << e <<
+ info << "regex line: '" << line (l) << "'";
+ }
}
+
+ rls += c; // Append blank literal or regex line char.
}
- catch (const io_error& e)
+ else if (!l.special.empty ()) // Special literal.
{
- fail (ll) << "unable to read " << op << ": " << e;
+ // Literal can not be followed by special characters in the same
+ // line.
+ //
+ assert (l.value.empty ());
+ }
+ else // Textual literal.
+ {
+ // Append literal line char.
+ //
+ rls += line_char (
+ transform (l.value, false, rd.modifiers, *sp.root), pool);
}
- // Match the output with the regex.
- //
- if (regex_match (ls, regex)) // Doesn't throw.
- return;
-
- // Output doesn't match the regex.
- //
- diag_record d (error (ll));
- d << pr << " " << what << " doesn't match the regex";
+ for (char c: l.special)
+ {
+ if (line_char::syntax (c))
+ rls += line_char (c); // Append special line char.
+ else
+ fail (loc (l.line, l.column))
+ << "invalid syntax character '" << c << "' in " << what
+ << " regex redirect" <<
+ info << "regex line: '" << line (l) << "'";
+ }
+ }
- output_info (d, op);
- output_info (d, save_regex (), "", " regex");
- input_info (d);
+ // Create line regex.
+ //
+ line_regex regex;
- // Fall through.
- //
+ try
+ {
+ regex = line_regex (move (rls), move (pool));
}
- else // Compare the output with the expected result.
+ catch (const regex_error& e)
{
- // Use diff utility for the comparison.
+ // Note that line regex creation can not fail for here-string
+ // redirect as it doesn't have syntax line chars. That in
+ // particular means that end_line and end_column are meaningful.
//
- path eop (op + ".orig");
- save (eop, transform (rd.str, false, rd.modifiers, *sp.root), ll);
- sp.clean ({cleanup_type::always, eop}, true);
-
- path dp ("diff");
- process_path pp (run_search (dp, true));
+ assert (rd.type == redirect_type::here_doc_regex);
- cstrings args {
- pp.recall_string (),
- "--strip-trailing-cr", // Is essential for cross-testing.
- "-u",
- eop.string ().c_str (),
- op.string ().c_str (),
- nullptr};
+ diag_record d (fail (loc (rd.end_line, rd.end_column)));
- if (verb >= 2)
- print_process (args);
+ // Print regex_error description if meaningful.
+ //
+ d << "invalid " << what << " regex redirect" << e;
- try
- {
- // Save diff's stdout to a file for troubleshooting and for the
- // optional (if not too large) printing (at the end of
- // diagnostics).
- //
- path ep (op + ".diff");
- auto_fd efd;
+ output_info (d, save_regex (), "", " regex");
+ }
- try
- {
- efd = fdopen (ep, fdopen_mode::out | fdopen_mode::create);
- sp.clean ({cleanup_type::always, ep}, true);
- }
- catch (const io_error& e)
- {
- fail (ll) << "unable to write " << ep << ": " << e;
- }
+ // Parse the output into the literal line string.
+ //
+ line_string ls;
- // Diff utility prints the differences to stdout. But for the
- // user it is a part of the test failure diagnostics so let's
- // redirect stdout to stderr.
- //
- process p (pp, args.data (), 0, 2, efd.get ());
- efd.reset ();
+ try
+ {
+ // Do not throw when eofbit is set (end of stream reached), and
+ // when failbit is set (getline() failed to extract any character).
+ //
+ // Note that newlines are treated as line-chars separators. That
+ // in particular means that the trailing newline produces a blank
+ // line-char (empty literal). Empty output produces the zero-length
+ // line-string.
+ //
+ // Also note that we strip the trailing CR characters (otherwise
+ // can mismatch when cross-test).
+ //
+ ifdstream is (op, ifdstream::in, ifdstream::badbit);
+ is.peek (); // Sets eofbit for an empty stream.
- if (p.wait ())
- return;
+ while (!is.eof ())
+ {
+ string s;
+ getline (is, s);
- // Output doesn't match the expected result.
+ // It is safer to strip CRs in cycle, as msvcrt unexplainably
+ // adds too much trailing junk to the system_error descriptions,
+ // and so it can appear in programs output. For example:
//
- diag_record d (error (ll));
- d << pr << " " << what << " doesn't match the expected output";
-
- output_info (d, op);
- output_info (d, eop, "expected ");
- output_info (d, ep, "", " diff");
- input_info (d);
-
- print_file (d, ep, ll);
-
- // Fall through.
+ // ...: Invalid data.\r\r\n
//
- }
- catch (const process_error& e)
- {
- error (ll) << "unable to execute " << pp << ": " << e;
+ // Note that our custom operator<<(ostream&, const exception&)
+ // removes this junk.
+ //
+ while (!s.empty () && s.back () == '\r')
+ s.pop_back ();
- if (e.child ())
- exit (1);
+ ls += line_char (move (s), regex.pool);
}
-
- // Fall through.
- //
+ }
+ catch (const io_error& e)
+ {
+ fail (ll) << "unable to read " << op << ": " << e;
}
- throw failed ();
+ // Match the output with the regex.
+ //
+ if (regex_match (ls, regex)) // Doesn't throw.
+ return;
+
+ // Output doesn't match the regex.
+ //
+ diag_record d (fail (ll));
+ d << pr << " " << what << " doesn't match the regex";
+
+ output_info (d, op);
+ output_info (d, save_regex (), "", " regex");
+ input_info (d);
}
}
@@ -732,25 +753,6 @@ namespace build2
if (verb >= 3)
text << c;
- // Normalize a path. Also make the relative path absolute using the
- // scope's working directory unless it is already absolute.
- //
- auto normalize = [&sp, &ll] (path p) -> path
- {
- path r (p.absolute () ? move (p) : sp.wd_path / move (p));
-
- try
- {
- r.normalize ();
- }
- catch (const invalid_path& e)
- {
- fail (ll) << "invalid file path " << e.path;
- }
-
- return r;
- };
-
// Register the command explicit cleanups. Verify that the path being
// cleaned up is a sub-path of the testscript working directory. Fail
// if this is not the case.
@@ -758,7 +760,7 @@ namespace build2
for (const auto& cl: c.cleanups)
{
const path& p (cl.path);
- path np (normalize (p));
+ path np (normalize (p, sp, ll));
bool wc (np.leaf ().string () == "***");
const path& cp (wc ? np.directory () : np);
@@ -778,7 +780,7 @@ namespace build2
// Create a unique path for a command standard stream cache file.
//
- auto std_path = [&li, &normalize] (const char* n) -> path
+ auto std_path = [&li, &sp, &ll] (const char* n) -> path
{
path p (n);
@@ -788,7 +790,7 @@ namespace build2
if (li > 0)
p += "-" + to_string (li);
- return normalize (move (p));
+ return normalize (move (p), sp, ll);
};
// Assign file descriptors to pass as a builtin or a process standard
@@ -870,7 +872,7 @@ namespace build2
case redirect_type::file:
{
- isp = normalize (c.in.file.path);
+ isp = normalize (c.in.file.path, sp, ll);
open_stdin ();
break;
@@ -917,11 +919,11 @@ namespace build2
// Return the specified, default or -2 file descriptors for merge, pass
// or null redirects respectively not opening a file.
//
- auto open = [&sp, &ll, &std_path, &normalize] (const redirect& r,
- int dfd,
- path& p,
- auto_fd& fd) -> int
- {
+ auto open = [&sp, &ll, &std_path] (const redirect& r,
+ int dfd,
+ path& p,
+ auto_fd& fd) -> int
+ {
assert (dfd == 1 || dfd == 2);
const char* what (dfd == 1 ? "stdout" : "stderr");
@@ -970,8 +972,19 @@ namespace build2
case redirect_type::file:
{
- p = normalize (r.file.path);
- m |= r.file.append ? fdopen_mode::at_end : fdopen_mode::truncate;
+ // For the cmp mode the user-provided path refers a content to
+ // match against, rather than a content to be produced (as for
+ // overwrite and append modes). And so for cmp mode we redirect
+ // the process output to a temporary file.
+ //
+ p = r.file.mode == redirect_fmode::compare
+ ? std_path (what)
+ : normalize (r.file.path, sp, ll);
+
+ m |= r.file.mode == redirect_fmode::append
+ ? fdopen_mode::at_end
+ : fdopen_mode::truncate;
+
break;
}
diff --git a/build2/test/script/script b/build2/test/script/script
index bb9b074..fbf3dd5 100644
--- a/build2/test/script/script
+++ b/build2/test/script/script
@@ -131,6 +131,15 @@ namespace build2
small_vector<regex_line, 8> lines;
};
+ // Output file redirect mode.
+ //
+ enum class redirect_fmode
+ {
+ compare,
+ overwrite,
+ append
+ };
+
struct redirect
{
redirect_type type;
@@ -139,7 +148,7 @@ namespace build2
{
using path_type = build2::path;
path_type path;
- bool append = false;
+ redirect_fmode mode; // Meaningless for input redirect.
};
union
diff --git a/build2/test/script/script.cxx b/build2/test/script/script.cxx
index 6f56661..f5cec44 100644
--- a/build2/test/script/script.cxx
+++ b/build2/test/script/script.cxx
@@ -80,7 +80,7 @@ namespace build2
switch (r.type)
{
case redirect_type::none: assert (false); break;
- case redirect_type::pass: o << '+'; break;
+ case redirect_type::pass: o << '|'; break;
case redirect_type::null: o << '-'; break;
case redirect_type::merge: o << '&' << r.fd; break;
@@ -141,9 +141,15 @@ namespace build2
case redirect_type::file:
{
- // Add '>>' or '<<' (and so make it '<<<' or '>>>').
+ // For stdin or stdout-comparison redirect add '>>' or '<<' (and
+ // so make it '<<<' or '>>>'). Otherwise add '+' or '=' (and so
+ // make it '>+' or '>=').
//
- o << d << d << r.modifiers;
+ if (d == '<' || r.file.mode == redirect_fmode::compare)
+ o << d << d;
+ else
+ o << (r.file.mode == redirect_fmode::append ? '+' : '=');
+
print_path (r.file.path);
break;
}
diff --git a/build2/test/script/token b/build2/test/script/token
index b5f8b6b..6dc446f 100644
--- a/build2/test/script/token
+++ b/build2/test/script/token
@@ -34,18 +34,20 @@ namespace build2
pipe, // |
clean, // &{?!} (modifiers in value)
- in_pass, // <+
+ in_pass, // <|
in_null, // <-
in_str, // <{:} (modifiers in value)
in_doc, // <<{:} (modifiers in value)
in_file, // <<<
- out_pass, // >+
+ out_pass, // >|
out_null, // >-
out_merge, // >&
out_str, // >{:~} (modifiers in value)
out_doc, // >>{:~} (modifiers in value)
- out_file // >>>{&} (modifiers in value)
+ out_file_cmp, // >>>
+ out_file_ovr, // >=
+ out_file_app // >+
};
token_type () = default;
diff --git a/build2/test/script/token.cxx b/build2/test/script/token.cxx
index 6086a63..a4d658a 100644
--- a/build2/test/script/token.cxx
+++ b/build2/test/script/token.cxx
@@ -33,18 +33,20 @@ namespace build2
case token_type::clean: os << q << '&' << v << q; break;
case token_type::pipe: os << q << '|' << q; break;
- case token_type::in_pass: os << q << "<+" << q; break;
+ case token_type::in_pass: os << q << "<|" << q; break;
case token_type::in_null: os << q << "<-" << q; break;
case token_type::in_str: os << q << '<' << v << q; break;
case token_type::in_doc: os << q << "<<" << v << q; break;
case token_type::in_file: os << q << "<<<" << q; break;
- case token_type::out_pass: os << q << ">+" << q; break;
+ case token_type::out_pass: os << q << ">|" << q; break;
case token_type::out_null: os << q << ">-" << q; break;
case token_type::out_merge: os << q << ">&" << q; break;
case token_type::out_str: os << q << '>' << v << q; break;
case token_type::out_doc: os << q << ">>" << v << q; break;
- case token_type::out_file: os << q << ">>>" << v << q; break;
+ case token_type::out_file_cmp: os << q << ">>>" << v << q; break;
+ case token_type::out_file_ovr: os << q << ">=" << v << q; break;
+ case token_type::out_file_app: os << q << ">+" << v << q; break;
default: build2::token_printer (os, t, d);
}
diff --git a/tests/common.test b/tests/common.test
index d495da0..250948e 100644
--- a/tests/common.test
+++ b/tests/common.test
@@ -6,7 +6,7 @@
#
+mkdir build
-+cat <<EOI >>>build/bootstrap.build
++cat <<EOI >=build/bootstrap.build
project = test
amalgamation =
EOI
diff --git a/tests/search/dir/testscript b/tests/search/dir/testscript
index 4c427b2..49c964e 100644
--- a/tests/search/dir/testscript
+++ b/tests/search/dir/testscript
@@ -11,11 +11,11 @@ test.arguments = 'update(../)'
# baz/ has invalid buildfile
#
+mkdir foo bar baz
-+cat <<EOI >>>bar/buildfile
++cat <<EOI >=bar/buildfile
print bar
./:
EOI
-+cat <'assert false' >>>baz/buildfile
++cat <'assert false' >=baz/buildfile
: no-buildfile
:
diff --git a/tests/test/common.test b/tests/test/common.test
index 0f63bd3..7e3aa67 100644
--- a/tests/test/common.test
+++ b/tests/test/common.test
@@ -7,7 +7,7 @@
#
+mkdir build
-+cat <<EOI >>>build/bootstrap.build
++cat <<EOI >=build/bootstrap.build
project = test
amalgamation =
diff --git a/tests/test/config-test/testscript b/tests/test/config-test/testscript
index be342ef..0851da7 100644
--- a/tests/test/config-test/testscript
+++ b/tests/test/config-test/testscript
@@ -12,14 +12,14 @@ test.cleanups = &!./ #@@ TMP
+mkdir proj
+mkdir proj/build
-+cat <<EOI >>>proj/build/bootstrap.build
++cat <<EOI >=proj/build/bootstrap.build
project = proj
amalgamation =
using test
EOI
-+cat <<EOI >>>proj/buildfile
++cat <<EOI >=proj/buildfile
d = tests/ units/
./: $d
include $d
@@ -29,13 +29,13 @@ EOI
#
+mkdir proj/tests
+mkdir proj/tests/build
-+cat <<EOI >>>proj/tests/build/bootstrap.build
++cat <<EOI >=proj/tests/build/bootstrap.build
project =
using test
EOI
-+cat <<EOI >>>proj/tests/buildfile
++cat <<EOI >=proj/tests/buildfile
d = script/
./: $d
include $d
@@ -44,17 +44,17 @@ EOI
# tests/script - scripted test
#
+mkdir proj/tests/script
-+cat <<EOI >>>proj/tests/script/buildfile
++cat <<EOI >=proj/tests/script/buildfile
./: test{basics.test}
EOI
-+cat <<EOI >>>proj/tests/script/basics.test
-echo 'tests/script/basics/foo' >+ : foo
-echo 'tests/script/basics/bar' >+ : bar
++cat <<EOI >=proj/tests/script/basics.test
+echo 'tests/script/basics/foo' >| : foo
+echo 'tests/script/basics/bar' >| : bar
: baz
{
- echo 'tests/script/basics/baz/foo' >+ : foo
- echo 'tests/script/basics/baz/bar' >+ : bar
+ echo 'tests/script/basics/baz/foo' >| : foo
+ echo 'tests/script/basics/baz/bar' >| : bar
}
EOI
@@ -64,13 +64,13 @@ EOI
# This one is "dual": test and sub-test alias.
#
-+cat <<EOI >>>proj/units/buildfile
++cat <<EOI >=proj/units/buildfile
d = simple/ script/
./: $d test{testscript}
include $d
EOI
-+cat <<EOI >>>proj/units/testscript
-echo 'units' >+
++cat <<EOI >=proj/units/testscript
+echo 'units' >|
EOI
# units/simple - simple (non-scripted) test
@@ -81,7 +81,7 @@ EOI
#
+mkdir proj/units/simple
+touch proj/units/simple/driver
-+cat <<EOI >>>proj/units/simple/buildfile
++cat <<EOI >=proj/units/simple/buildfile
driver = $src_root/../../exe{driver}
#@@ TMP file{driver}@./: $driver
./: file{driver} $driver
@@ -92,12 +92,12 @@ EOI
# units/script - scripted test
#
+mkdir proj/units/script
-+cat <<EOI >>>proj/units/script/buildfile
++cat <<EOI >=proj/units/script/buildfile
./: test{testscript}
EOI
-+cat <<EOI >>>proj/units/script/testscript
-echo 'units/script/foo' >+ : foo
-echo 'units/script/bar' >+ : bar
++cat <<EOI >=proj/units/script/testscript
+echo 'units/script/foo' >| : foo
+echo 'units/script/bar' >| : bar
EOI
# Now the tests. Should all be top-level, no groups (or set test.arguments).
diff --git a/tests/test/script-integration/testscript b/tests/test/script-integration/testscript
index f102f5b..faade22 100644
--- a/tests/test/script-integration/testscript
+++ b/tests/test/script-integration/testscript
@@ -74,7 +74,7 @@ EOE
: scope. Also note that we still have to remove everything after detecting the
: failure.
:
-cat <<EOI >>>foo.test;
+cat <<EOI >=foo.test;
touch ../../dummy
EOI
$* <<EOI 2>>/EOE &test/*** != 0
diff --git a/tests/test/script/builtin/cat.test b/tests/test/script/builtin/cat.test
index f5041fc..442ef00 100644
--- a/tests/test/script/builtin/cat.test
+++ b/tests/test/script/builtin/cat.test
@@ -33,7 +33,7 @@ $b
: file
:
$c <<EOI;
-cat <<EOF >>>out;
+cat <<EOF >=out;
foo
bar
EOF
diff --git a/tests/test/script/builtin/touch.test b/tests/test/script/builtin/touch.test
index a80a05c..b3a043e 100644
--- a/tests/test/script/builtin/touch.test
+++ b/tests/test/script/builtin/touch.test
@@ -24,7 +24,7 @@ $b
: Test that existing file touch doesn't fail.
:
$c <<EOI;
-cat <"" >>>a;
+cat <"" >=a;
touch a
EOI
$b
diff --git a/tests/test/script/common.test b/tests/test/script/common.test
index 177fa75..781be30 100644
--- a/tests/test/script/common.test
+++ b/tests/test/script/common.test
@@ -7,7 +7,7 @@
#
+mkdir build
-+cat <<"EOI" >>>build/bootstrap.build
++cat <<"EOI" >=build/bootstrap.build
project = test
amalgamation =
@@ -18,7 +18,7 @@ EOI
# levels up from our working directory.
#
+if! $empty($target)
- cat <<"EOI" >>>build/root.build
+ cat <<"EOI" >=build/root.build
target = \$src_root/../../$string([name] $target)
test{*}: test = \$target
EOI
@@ -31,6 +31,6 @@ end
# Note that the buildfile is clever hack that relies on the first target
# automatically becoming dir{./}'s prerequisite.
#
-c = cat >>>testscript
+c = cat >=testscript
b = $0 --jobs 1 --quiet --buildfile - test <"'test{testscript}: \$target'" \
&?test/***
diff --git a/tests/test/script/runner/cleanup.test b/tests/test/script/runner/cleanup.test
index ed724ee..1c3ccae 100644
--- a/tests/test/script/runner/cleanup.test
+++ b/tests/test/script/runner/cleanup.test
@@ -46,7 +46,7 @@ b += --no-column
:
$c <<EOI;
touch a &!a;
- $* -o foo >>>&a;
+ $* -o foo >+a;
rm a
EOI
$b
@@ -224,7 +224,7 @@ $b
:
: Test an implicit cleanup being overwritten with the explicit one,
:
-$c <'$* -o foo >>>a &!a';
+$c <'$* -o foo >=a &!a';
$b 2>>/EOE != 0
testscript:1: error: registered for cleanup directory test/1/ is not empty
EOE
@@ -235,7 +235,7 @@ EOE
:
$c <<EOO;
$* &!a;
-$* -o foo >>>a
+$* -o foo >=a
EOO
$b 2>>/EOE != 0
testscript:2: error: registered for cleanup directory test/1/ is not empty
diff --git a/tests/test/script/runner/redirect.test b/tests/test/script/runner/redirect.test
index e2d765c..f32492a 100644
--- a/tests/test/script/runner/redirect.test
+++ b/tests/test/script/runner/redirect.test
@@ -465,31 +465,53 @@ psr = ($cxx.target.class != 'windows' ? '/' : '\\') # Path separator in regex.
: file
:
{
- : in-out
+ : in
:
$c <<EOI;
- $* -o foo >>>out;
+ $* -o foo >=out;
$* -i 1 <<<out >foo
EOI
$b
- : append
+ : out
:
- $c <<EOI;
- $* -o foo >>>out;
- $* -e bar 2>>>&out;
- $* -i 1 <<<out >>EOO
- foo
- bar
- EOO
- EOI
- $b
+ {
+ : match
+ :
+ $c <<EOI;
+ $* -o foo >=out;
+ $* -e bar 2>+out;
+ $* -i 1 <<EOF >>>out
+ foo
+ bar
+ EOF
+ EOI
+ $b
+
+ : mismatch
+ :
+ $c <<EOI;
+ $* -o foo >=out;
+ $* -o bar >>>out
+ EOI
+ $b 2>>/~%EOE%d != 0
+ %testscript:2: error: ../../../../../driver(.exe)? stdout doesn't match the expected output%
+ info: stdout: test/1/stdout-2
+ info: expected stdout: test/1/out
+ info: stdout diff: test/1/stdout-2.diff
+ %--- \.*%
+ %\+\+\+ \.*%
+ %@@ \.*%
+ -foo
+ +bar
+ EOE
+ }
: merge
:
$c <<EOI;
- $* -o foo -e bar 2>&1 >>>out;
- $* -e baz -o biz 1>&2 2>>>&out;
+ $* -o foo -e bar 2>&1 >=out;
+ $* -e baz -o biz 1>&2 2>+out;
$* -i 1 <<<out >>EOO
foo
bar
diff --git a/unit-tests/test/script/lexer/command-expansion.test b/unit-tests/test/script/lexer/command-expansion.test
index faae29d..d405de6 100644
--- a/unit-tests/test/script/lexer/command-expansion.test
+++ b/unit-tests/test/script/lexer/command-expansion.test
@@ -9,30 +9,30 @@ test.arguments = command-expansion
{
: in
:
- $* <:"0<+" >>EOO
+ $* <:"0<|" >>EOO
'0'
- <+
+ <|
EOO
: arg-in
:
- $* <:"0 <+" >>EOO
+ $* <:"0 <|" >>EOO
'0 '
- <+
+ <|
EOO
: out
:
- $* <:"1>+" >>EOO
+ $* <:"1>|" >>EOO
'1'
- >+
+ >|
EOO
: arg-out
:
- $* <:"1 >+" >>EOO
+ $* <:"1 >|" >>EOO
'1 '
- >+
+ >|
EOO
}
@@ -189,17 +189,17 @@ test.arguments = command-expansion
: out
:
- $* <:"1>>>a b" >>EOO
+ $* <:"1>=a b" >>EOO
'1'
- >>>
+ >=
'a b'
EOO
: out-app
:
- $* <:"1>>>&a b" >>EOO
+ $* <:"1>+a b" >>EOO
'1'
- >>>&
+ >+
'a b'
EOO
}
diff --git a/unit-tests/test/script/lexer/command-line.test b/unit-tests/test/script/lexer/command-line.test
index 8a73b3d..57fcfdf 100644
--- a/unit-tests/test/script/lexer/command-line.test
+++ b/unit-tests/test/script/lexer/command-line.test
@@ -64,11 +64,11 @@ test.arguments = command-line
{
: pass
:
- $* <"cmd <+ 1>+" >>EOO
+ $* <"cmd <| 1>|" >>EOO
'cmd'
- <+
+ <|
'1'
- >+
+ >|
<newline>
EOO
@@ -140,16 +140,28 @@ test.arguments = command-line
<newline>
EOO
- : file
+ : file-cmp
:
- $* <"cmd <<<in >>>out 2>>>&err" >>EOO
+ $* <"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
diff --git a/unit-tests/test/script/parser/directive.test b/unit-tests/test/script/parser/directive.test
index d426d9c..d735fb4 100644
--- a/unit-tests/test/script/parser/directive.test
+++ b/unit-tests/test/script/parser/directive.test
@@ -46,7 +46,7 @@ EOI
: var-expansion
:
-cat <<EOI >>>"foo-$(build.version).test";
+cat <<EOI >="foo-$(build.version).test";
cmd
EOI
$* <<EOI >>EOO
diff --git a/unit-tests/test/script/parser/include.test b/unit-tests/test/script/parser/include.test
index c810036..0559716 100644
--- a/unit-tests/test/script/parser/include.test
+++ b/unit-tests/test/script/parser/include.test
@@ -19,7 +19,7 @@ EOI
: one
:
-cat <"cmd" >>>foo.test;
+cat <"cmd" >=foo.test;
$* <<EOI >>EOO
.include foo.test
EOI
@@ -28,8 +28,8 @@ EOO
: multiple
:
-cat <"cmd foo" >>>foo.test;
-cat <"cmd bar" >>>bar.test;
+cat <"cmd foo" >=foo.test;
+cat <"cmd bar" >=bar.test;
$* <<EOI >>EOO
.include foo.test bar.test
EOI
@@ -39,7 +39,7 @@ EOO
: once
:
-cat <"cmd" >>>foo.test;
+cat <"cmd" >=foo.test;
$* <<EOI >>EOO
.include foo.test
x
@@ -56,7 +56,7 @@ EOO
: group-id
:
-cat <<EOI >>>foo.test;
+cat <<EOI >=foo.test;
{
x = b
}
@@ -73,7 +73,7 @@ EOO
: test-id
:
-cat <<EOI >>>foo.test;
+cat <<EOI >=foo.test;
cmd
EOI
$* -s -i <<EOI >>EOO
diff --git a/unit-tests/test/script/parser/redirect.test b/unit-tests/test/script/parser/redirect.test
index 3db684a..b1c1209 100644
--- a/unit-tests/test/script/parser/redirect.test
+++ b/unit-tests/test/script/parser/redirect.test
@@ -85,20 +85,28 @@
: file
:
{
- : not-quote
+ : cmp
:
$* <<EOI >>EOO
- cmd 0<<<a 1>>>b 2>>>&c
+ cmd 0<<<a 1>>>b 2>>>c
EOI
- cmd <<<a >>>b 2>>>&c
+ cmd <<<a >>>b 2>>>c
+ EOO
+
+ : write
+ :
+ $* <<EOI >>EOO
+ cmd 1>=b 2>+c
+ EOI
+ cmd >=b 2>+c
EOO
: quote
:
$* <<EOI >>EOO
- cmd 0<<<"a f" 1>>>"b f" 2>>>&"c f"
+ cmd 0<<<"a f" 1>="b f" 2>+"c f"
EOI
- cmd <<<'a f' >>>'b f' 2>>>&'c f'
+ cmd <<<'a f' >='b f' 2>+'c f'
EOO
: in
@@ -127,17 +135,17 @@
: missed
:
$* <<EOI 2>>EOE !=0
- cmd >>>
+ cmd >=
EOI
- testscript:1:8: error: missing stdout file
+ testscript:1:7: error: missing stdout file
EOE
: empty
:
$* <<EOI 2>>EOE !=0
- cmd >>>""
+ cmd >=""
EOI
- testscript:1:8: error: empty stdout redirect path
+ testscript:1:7: error: empty stdout redirect path
EOE
}
@@ -147,17 +155,17 @@
: missed
:
$* <<EOI 2>>EOE !=0
- cmd 2>>>
+ cmd 2>=
EOI
- testscript:1:9: error: missing stderr file
+ testscript:1:8: error: missing stderr file
EOE
: empty
:
$* <<EOI 2>>EOE !=0
- cmd 2>>>""
+ cmd 2>=""
EOI
- testscript:1:9: error: empty stderr redirect path
+ testscript:1:8: error: empty stderr redirect path
EOE
}
}