aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorKaren Arutyunov <karen@codesynthesis.com>2016-12-17 23:28:30 +0300
committerKaren Arutyunov <karen@codesynthesis.com>2017-01-05 15:30:41 +0300
commit3ecbf5d51b13e11a93ae5757408a27c21d804c9f (patch)
treebe46e3caa24574de106c2fbf1a05c43d32694e12
parenta63e1809afd9a837821d6e8376cb14a36e7fc26e (diff)
Add support for regex in runner
-rw-r--r--build2/test/script/parser24
-rw-r--r--build2/test/script/parser.cxx482
-rw-r--r--build2/test/script/regex36
-rw-r--r--build2/test/script/regex.cxx12
-rw-r--r--build2/test/script/runner.cxx204
-rw-r--r--build2/test/script/script.cxx42
-rw-r--r--doc/testscript.cli2
-rw-r--r--tests/function/path/testscript4
-rw-r--r--tests/test/script/builtin/cat.test8
-rw-r--r--tests/test/script/builtin/mkdir.test22
-rw-r--r--tests/test/script/builtin/rm.test8
-rw-r--r--tests/test/script/integration/testscript29
-rw-r--r--tests/test/script/runner/buildfile8
-rw-r--r--tests/test/script/runner/cleanup.test74
-rw-r--r--tests/test/script/runner/redirect.test322
-rw-r--r--tests/test/script/runner/status.test12
-rw-r--r--unit-tests/function/call.test30
-rw-r--r--unit-tests/test/script/parser/buildfile2
-rw-r--r--unit-tests/test/script/parser/here-document.test6
-rw-r--r--unit-tests/test/script/parser/regex.test188
-rw-r--r--unit-tests/test/script/parser/scope.test22
-rw-r--r--unit-tests/test/script/regex/driver.cxx21
22 files changed, 1266 insertions, 292 deletions
diff --git a/build2/test/script/parser b/build2/test/script/parser
index ee270d8..5fefe48 100644
--- a/build2/test/script/parser
+++ b/build2/test/script/parser
@@ -12,6 +12,7 @@
#include <build2/diagnostics>
#include <build2/test/script/token>
+#include <build2/test/script/regex>
#include <build2/test/script/script>
namespace build2
@@ -97,12 +98,20 @@ namespace build2
//
struct here_doc
{
- size_t expr; // Index in command_expr.
- size_t pipe; // Index in command_pipe.
- int fd; // Redirect fd (0 - in, 1 - out, 2 - err).
+ size_t expr; // Index in command_expr.
+ size_t pipe; // Index in command_pipe.
+ int fd; // Redirect fd (0 - in, 1 - out, 2 - err).
string end;
- bool literal; // Literal (single-quote).
+ bool literal; // Literal (single-quote).
string modifiers;
+
+ // Regex introducer ('\0' if not a regex, so can be used as bool).
+ //
+ char regex;
+
+ // Regex global flags. Meaningful if regex != '\0'.
+ //
+ regex::char_flags regex_flags;
};
using here_docs = vector<here_doc>;
@@ -115,10 +124,13 @@ namespace build2
void
parse_here_documents (token&, token_type&,
pair<command_expr, here_docs>&);
- string
+
+ pair<string, regex::line_regex>
parse_here_document (token&, token_type&,
const string&,
- const string&);
+ const string& mode,
+ char regex_introducer, // '\0' if not a regex.
+ regex::char_flags);
// Execute. Issue diagnostics and throw failed in case of an error.
//
diff --git a/build2/test/script/parser.cxx b/build2/test/script/parser.cxx
index dd5c5c7..9af85b1 100644
--- a/build2/test/script/parser.cxx
+++ b/build2/test/script/parser.cxx
@@ -2,6 +2,9 @@
// copyright : Copyright (c) 2014-2016 Code Synthesis Ltd
// license : MIT; see accompanying LICENSE file
+#include <sstream>
+#include <cstring> // strstr(), strchr()
+
#include <build2/test/script/parser>
#include <build2/scheduler>
@@ -11,6 +14,35 @@
using namespace std;
+namespace std
+{
+ // Print regex error description but only if it is meaningful (this is also
+ // why we have to print leading colon here).
+ //
+ // Currently libstdc++ just returns the name of the exception (bug #67361).
+ // So we check that the description contains at least one space character.
+ //
+ // While VC's description is meaningful, it has an undesired prefix that
+ // resembles the following: 'regex_error(error_badrepeat): '. So we skip it.
+ //
+ static ostream&
+ operator<< (ostream& os, const regex_error& e)
+ {
+ const char* d (e.what ());
+ if (strchr (d, ' ') != nullptr)
+ {
+#if defined(_MSC_VER) && _MSC_VER <= 1910
+ const char* s (strstr (d, "): "));
+ if (s != nullptr)
+ d = s + 3;
+#endif
+ os << ": " << d;
+ }
+
+ return os;
+ }
+}
+
namespace build2
{
namespace test
@@ -1277,8 +1309,69 @@ namespace build2
assert (tt == type::newline);
return move (p.first);
+ }
+
+ // Parse the regular expression representation (non-empty string value
+ // framed with introducer characters and optionally followed by flag
+ // characters from the {i} set, for example '/foo/i') into
+ // components. Also return end-of-parsing position if requested,
+ // otherwise treat any unparsed characters left as an error.
+ //
+ struct regex_parts
+ {
+ string value;
+ char introducer;
+ regex::char_flags flags; // {icase}
+
+ // Create a special empty object.
+ //
+ regex_parts ()
+ : introducer ('\0'), flags (regex::char_flags()) {}
+
+ regex_parts (string v, char i, regex::char_flags f)
+ : value (move (v)), introducer (i), flags (f) {}
};
+ static regex_parts
+ parse_regex (const string& s,
+ const location& l,
+ const char* what,
+ size_t* end = nullptr)
+ {
+ if (s.empty ())
+ fail (l) << "no introducer character in " << what;
+
+ size_t p (s.find (s[0], 1)); // Find terminating introducer.
+
+ if (p == string::npos)
+ fail (l) << "no closing introducer character in " << what;
+
+ size_t rn (p - 1); // Regex length.
+ if (rn == 0)
+ fail (l) << what << " is empty";
+
+ bool icase (s[++p] == 'i'); // Note: s[++p] can be '\0' (no flags).
+
+ if (icase)
+ ++p;
+
+ // If string end is not reached then report invalid flags, unless
+ // end-of-parsing position is requested (which means regex is just a
+ // prefix).
+ //
+ if (s[p] != '\0' && end == nullptr)
+ fail (l) << "junk at the end of " << what;
+
+ if (end != nullptr)
+ *end = p;
+
+ return regex_parts (string (s, 1, rn),
+ s[0],
+ icase
+ ? regex::char_regex::icase
+ : regex::char_flags ());
+ }
+
pair<command_expr, parser::here_docs> parser::
parse_command_expr (token& t, type& tt)
{
@@ -1310,11 +1403,15 @@ namespace build2
in_file,
out_merge,
out_string,
+ out_str_regex,
out_document,
+ out_doc_regex,
out_file,
err_merge,
err_string,
+ err_str_regex,
err_document,
+ err_doc_regex,
err_file,
clean
};
@@ -1351,6 +1448,50 @@ namespace build2
r.str = move (w);
};
+ auto add_here_str_regex = [&l, &mod, this] (
+ redirect& r, int fd, string&& w)
+ {
+ using namespace regex;
+
+ const char* what (nullptr);
+ switch (fd)
+ {
+ case 1: what = "stdout regex redirect"; break;
+ case 2: what = "stderr regex redirect"; break;
+ }
+
+ line_pool pool;
+ line_string s;
+
+ try
+ {
+ regex_parts re (parse_regex (w, l, what));
+ s += line_char (char_regex (re.value,
+ char_regex::ECMAScript | re.flags),
+ pool);
+ }
+ catch (const regex_error& e)
+ {
+ // Print regex_error description if meaningful.
+ //
+ fail (l) << "invalid " << what << e <<
+ info << "regex: " << w;
+ }
+
+ if (mod.find (':') == string::npos)
+ {
+ w += '\n';
+ s += line_char ("", pool);
+ }
+
+ r.regex.str = move (w);
+
+ // No special line-chars, so no way to try to create a malformed
+ // expression, and so can't throw.
+ //
+ r.regex.regex = line_regex (move (s), move (pool));
+ };
+
auto parse_path = [&l, this] (string&& w, const char* what) -> path
{
try
@@ -1399,11 +1540,24 @@ namespace build2
case pending::out_string: add_here_str (c.out, move (w)); break;
case pending::err_string: add_here_str (c.err, move (w)); break;
+ case pending::out_str_regex:
+ {
+ add_here_str_regex (c.out, 1, move (w));
+ break;
+ }
+ case pending::err_str_regex:
+ {
+ add_here_str_regex (c.err, 2, move (w));
+ break;
+ }
+
// These are handled specially below.
//
case pending::in_document:
case pending::out_document:
- case pending::err_document: assert (false); break;
+ case pending::err_document:
+ case pending::out_doc_regex:
+ case pending::err_doc_regex: assert (false); break;
case pending::in_file: add_file (c.in, 0, move (w)); break;
case pending::out_file: add_file (c.out, 1, move (w)); break;
@@ -1451,6 +1605,27 @@ namespace build2
case pending::err_document: what = "stderr here-document end"; break;
case pending::err_file: what = "stderr file"; break;
case pending::clean: what = "cleanup path"; break;
+
+ case pending::out_str_regex:
+ {
+ what = "stdout here-string regex";
+ break;
+ }
+ case pending::err_str_regex:
+ {
+ what = "stderr here-string regex";
+ break;
+ }
+ case pending::out_doc_regex:
+ {
+ what = "stdout here-document regex end";
+ break;
+ }
+ case pending::err_doc_regex:
+ {
+ what = "stderr here-document regex end";
+ break;
+ }
}
if (what != nullptr)
@@ -1523,25 +1698,47 @@ namespace build2
}
}
+ mod = move (t.value);
+
redirect_type rt (redirect_type::none);
switch (tt)
{
case type::in_pass:
- case type::out_pass: rt = redirect_type::pass; break;
+ case type::out_pass: rt = redirect_type::pass; break;
case type::in_null:
- case type::out_null: rt = redirect_type::null; break;
+ case type::out_null: rt = redirect_type::null; break;
- case type::out_merge: rt = redirect_type::merge; break;
+ case type::out_merge: rt = redirect_type::merge; break;
case type::in_str:
- case type::out_str: rt = redirect_type::here_str_literal; break;
+ case type::out_str:
+ {
+ bool re (mod.find ('~') != string::npos);
+ assert (tt == type::out_str || !re);
+
+ rt = re
+ ? redirect_type::here_str_regex
+ : redirect_type::here_str_literal;
+
+ break;
+ }
case type::in_doc:
- case type::out_doc: rt = redirect_type::here_doc_literal; break;
+ case type::out_doc:
+ {
+ bool re (mod.find ('~') != string::npos);
+ assert (tt == type::out_doc || !re);
+
+ rt = re
+ ? redirect_type::here_doc_regex
+ : redirect_type::here_doc_literal;
+
+ break;
+ }
case type::in_file:
- case type::out_file: rt = redirect_type::file; break;
+ case type::out_file: rt = redirect_type::file; break;
}
redirect& r (fd == 0 ? c.in : fd == 1 ? c.out : c.err);
@@ -1569,6 +1766,14 @@ namespace build2
case 2: p = pending::err_string; break;
}
break;
+ case redirect_type::here_str_regex:
+ switch (fd)
+ {
+ case 0: assert (false); break;
+ case 1: p = pending::out_str_regex; break;
+ case 2: p = pending::err_str_regex; break;
+ }
+ break;
case redirect_type::here_doc_literal:
switch (fd)
{
@@ -1577,10 +1782,14 @@ namespace build2
case 2: p = pending::err_document; break;
}
break;
-
- case redirect_type::here_str_regex: // @@ REGEX
- case redirect_type::here_doc_regex: assert (false); break;
-
+ case redirect_type::here_doc_regex:
+ switch (fd)
+ {
+ case 0: assert (false); break;
+ case 1: p = pending::out_doc_regex; break;
+ case 2: p = pending::err_doc_regex; break;
+ }
+ break;
case redirect_type::file:
switch (fd)
{
@@ -1590,8 +1799,6 @@ namespace build2
}
break;
}
-
- mod = move (t.value);
};
// Set pending cleanup type.
@@ -1674,9 +1881,9 @@ namespace build2
{
if (pre_parse_)
{
- // The only thing we need to handle here are the here-document
- // end markers since we need to know how many of them to pre-
- // parse after the command.
+ // The only things we need to handle here are the here-document
+ // and here-document regex end markers since we need to know
+ // how many of them to pre-parse after the command.
//
switch (tt)
{
@@ -1684,6 +1891,11 @@ namespace build2
case type::out_doc:
mod = move (t.value);
+ bool re (mod.find ('~') != string::npos);
+ const char* what (re
+ ? "here-document regex end marker"
+ : "here-document end marker");
+
// We require the end marker to be a literal, unquoted word.
// In particularm, we don't allow quoted because of cases
// like foo"$bar" (where we will see word 'foo').
@@ -1700,8 +1912,8 @@ namespace build2
// would be >>FOO$bar -- on reparse it will be expanded
// as a single word.
//
- if (tt != type::word)
- fail (t) << "expected here-document end marker";
+ if (tt != type::word || t.value.empty ())
+ fail (t) << "expected " << what;
peek ();
const token& p (peeked ());
@@ -1711,7 +1923,7 @@ namespace build2
{
case type::dollar:
case type::lparen:
- fail (p) << "here-document end marker must be literal";
+ fail (p) << what << " must be literal";
}
}
@@ -1727,15 +1939,25 @@ namespace build2
break;
// Fall through.
case quote_type::mixed:
- fail (t) << "partially-quoted here-document end marker";
+ fail (t) << "partially-quoted " << what;
+ }
+
+ regex_parts r;
+ string end (move (t.value));
+
+ if (re)
+ {
+ r = parse_regex (end, l, what);
+ end = move (r.value); // The "cleared" end marker.
}
hd.push_back (
here_doc {
0, 0, 0,
- move (t.value),
+ move (end),
qt == quote_type::single,
- move (mod)});
+ move (mod),
+ r.introducer, r.flags});
break;
}
@@ -1817,23 +2039,40 @@ namespace build2
int fd;
switch (p)
{
- case pending::in_document: fd = 0; break;
- case pending::out_document: fd = 1; break;
- case pending::err_document: fd = 2; break;
- default: fd = -1; break;
+ case pending::in_document: fd = 0; break;
+ case pending::out_document:
+ case pending::out_doc_regex: fd = 1; break;
+ case pending::err_document:
+ case pending::err_doc_regex: fd = 2; break;
+ default: fd = -1; break;
}
if (fd != -1)
{
+ string end (move (t.value));
+ regex_parts r;
+
+ if (p == pending::out_doc_regex ||
+ p == pending::err_doc_regex)
+ {
+ // We can't fail here as we already parsed all the end
+ // markers during pre-parsing stage, and so no need in the
+ // description.
+ //
+ r = parse_regex (end, l, "");
+ end = move (r.value); // The "cleared" end marker.
+ }
+
hd.push_back (
here_doc {
expr.size () - 1,
expr.back ().pipe.size (),
fd,
- move (t.value),
+ move (end),
(t.qtype == quote_type::unquoted ||
t.qtype == quote_type::single),
- move (mod)});
+ move (mod),
+ r.introducer, r.flags});
p = pending::none;
mod.clear ();
@@ -2130,30 +2369,54 @@ namespace build2
: lexer_mode::here_line_double);
next (t, tt);
- string v (parse_here_document (t, tt, h.end, h.modifiers));
+ pair<string, regex::line_regex> v (
+ parse_here_document (
+ t, tt, h.end, h.modifiers, h.regex, h.regex_flags));
if (!pre_parse_)
{
command& c (p.first[h.expr].pipe[h.pipe]);
redirect& r (h.fd == 0 ? c.in : h.fd == 1 ? c.out : c.err);
- r.str = move (v);
- r.end = move (h.end);
+ if (h.regex)
+ {
+ r.regex.str = move (v.first);
+ r.regex.regex = move (v.second);
+
+ // Restore the original end marker.
+ //
+ r.end = h.regex + h.end + h.regex;
+ if ((h.regex_flags & regex::char_regex::icase) != 0)
+ r.end += 'i';
+ }
+ else
+ {
+ r.str = move (v.first);
+ r.end = move (h.end);
+ }
}
expire_mode ();
}
}
- string parser::
+ pair<string, regex::line_regex> parser::
parse_here_document (token& t, type& tt,
const string& em,
- const string& mod)
+ const string& mod,
+ char re,
+ regex::char_flags refl)
{
// enter: first token on first line
// leave: newline (after end marker)
- string r;
+ using namespace regex;
+
+ string rs; // String or regex literal.
+
+ line_pool pool;
+ line_string ls;
+ line_regex rre;
// Here-documents can be indented. The leading whitespaces of the end
// marker line (called strip prefix) determine the indentation. Every
@@ -2173,8 +2436,17 @@ namespace build2
//
size_t ri (pre_parse_ ? replay_data_.size () - 1 : 0);
+ // We will use the location of the first token on the line for the
+ // regex diagnostics. At the end of the loop it will point to the
+ // beginning of the end marker which we use for diagnostics of the
+ // line_regex object creation.
+ //
+ location l;
+
while (tt != type::eos)
{
+ l = get_location (t);
+
// Check if this is the end marker. For starters, it should be a
// single, unquoted word followed by a newline.
//
@@ -2216,31 +2488,125 @@ namespace build2
if (!pre_parse_)
{
- if (!r.empty ()) // Add newline after previous line.
- r += '\n';
-
// What shall we do if the expansion results in multiple names?
// For, example if the line contains just the variable expansion
// and it is of type strings. Adding all the elements space-
// separated seems like the natural thing to do.
//
+ string s;
for (auto b (ns.begin ()), i (b); i != ns.end (); ++i)
{
- string s;
+ string n;
try
{
- s = value_traits<string>::convert (move (*i), nullptr);
+ n = value_traits<string>::convert (move (*i), nullptr);
}
catch (const invalid_argument&)
{
- fail (t) << "invalid string value '" << *i << "'";
+ fail (l) << "invalid string value '" << *i << "'";
+ }
+
+ if (i == b)
+ s = move (n);
+ else
+ {
+ s += ' ';
+ s += n;
}
+ }
+
+ // Add newline after previous line.
+ //
+ if (!rs.empty ())
+ rs += '\n';
+
+ rs += s;
+
+ if (re)
+ {
+ if (s[0] == re) // Line starts with the regex introducer.
+ {
+ size_t n (s.size ());
+
+ // Handle the empty line-regex characters.
+ //
+ if (n == 1)
+ fail (l) << "regex introducer without regex" <<
+ info << "consider changing regex introducer '" << re
+ << "' in here-document end marker";
+
+ // This is a char-regex, or a sequence of line-regex syntax
+ // characters or both (in this specific order). So we will add
+ // the char-regex first (if present), and then sequentially
+ // add the line-regex syntax characters (if present).
+ //
+ size_t p (s.find (re, 1));
+ if (p == string::npos)
+ {
+ // No char-regex, just a sequence of line-regex syntax
+ // characters. Prepare to parse them starting from the
+ // position right after the introducer.
+ //
+ p = 1;
+ }
+ else
+ {
+ // Add regex line-char, and then position to the end of the
+ // regex (that includes terminating introducer and the
+ // optional flags). This is the first line-regex syntax
+ // character position (if present).
+ //
+ line_char c;
+
+ // Empty regex is a special case repesenting the blank line.
+ //
+ if (p == 1)
+ {
+ c = line_char ("", pool);
+ ++p;
+ }
+ else
+ {
+ // Can't fail as all the pre-conditions verified (non-empty
+ // with both introducers in place), so no description
+ // required.
+ //
+ regex_parts re (parse_regex (s, l, "", &p));
- if (i != b)
- r += ' ';
+ try
+ {
+ c = line_char (
+ char_regex (re.value,
+ char_regex::ECMAScript | re.flags | refl),
+ pool);
+ }
+ catch (const regex_error& e)
+ {
+ // Print regex_error description if meaningful.
+ //
+ fail (l) << "invalid regex" << e;
+ }
+ }
+
+ ls += c;
+ }
- r += s;
+ while (p != n)
+ {
+ char c (s[p++]);
+ if (line_char::syntax (c))
+ ls += line_char (c);
+ else
+ fail (l) << "invalid line-regex syntax character '" << c
+ << "'";
+ }
+ }
+ else
+ // Line doesn't start with regex introducer. Add line-char
+ // literal (handles blank lines as well).
+ //
+ ls += line_char (move (s), pool);
}
}
@@ -2301,10 +2667,36 @@ namespace build2
// Add final newline unless suppressed.
//
if (mod.find (':') == string::npos)
- r += '\n';
+ {
+ rs += '\n';
+
+ if (re)
+ ls += line_char ("", pool);
+ }
+
+ // Parse line-regex.
+ //
+ if (re)
+ {
+ // Empty regex matches nothing, so not of much use.
+ //
+ if (ls.empty ())
+ fail (l) << "empty here-document regex";
+
+ try
+ {
+ rre = line_regex (move (ls), move (pool));
+ }
+ catch (const regex_error& e)
+ {
+ // Print regex_error description if meaningful.
+ //
+ fail (l) << "invalid here-document regex" << e;
+ }
+ }
}
- return r;
+ return make_pair (move (rs), move (rre));
}
//
diff --git a/build2/test/script/regex b/build2/test/script/regex
index cfc6031..ae31f59 100644
--- a/build2/test/script/regex
+++ b/build2/test/script/regex
@@ -24,6 +24,7 @@ namespace build2
{
using char_string = std::basic_string<char>;
using char_regex = std::basic_regex<char>;
+ using char_flags = char_regex::flag_type;
// Newlines are line separators and are not part of the line:
//
@@ -50,9 +51,9 @@ namespace build2
enum class line_type
{
special,
- literal,
- regex
- };
+ literal,
+ regex
+ };
struct line_char
{
@@ -127,6 +128,11 @@ namespace build2
return type == line_type::special ? special : '\a'; // BELL.
}
+ // Return true if the character is a syntax (special) one.
+ //
+ static bool
+ syntax (char);
+
// Provide basic_regex (such as from msvcrt) with the ability to
// explicitly cast line_chars to implementation-specific enums.
//
@@ -553,10 +559,10 @@ namespace std
// When used with libc++ the linker complains that it can't find
// __match_any_but_newline<line_char>::__exec() function. The problem is
- // that the function is only specialized for char and wchar_t. As line_char
- // has no notion of the newline character we specialize the class template
- // to behave as the __match_any<line_char> instantiation does (that luckily
- // has all the functions in place).
+ // that the function is only specialized for char and wchar_t
+ // (LLVM bug #31409). As line_char has no notion of the newline character we
+ // specialize the class template to behave as the __match_any<line_char>
+ // instantiation does (that luckily has all the functions in place).
//
#if defined(_LIBCPP_VERSION) && _LIBCPP_VERSION <= 4000
template <>
@@ -587,11 +593,6 @@ namespace build2
line_regex () = default;
- // Move constuctible-only type.
- //
- line_regex (line_regex&&) = default;
- line_regex (const line_regex&) = delete;
-
// Move string regex together with the pool used to create it.
//
line_regex (line_string&& s, line_pool&& p)
@@ -599,11 +600,18 @@ namespace build2
//
: base_type (s), pool (move (p)) {s.clear ();}
- line_regex& operator= (line_regex&&) = delete;
+ // Move constuctible/assignable-only type.
+ //
+ line_regex (line_regex&&) = default;
+ line_regex (const line_regex&) = delete;
+ line_regex& operator= (line_regex&&) = default;
line_regex& operator= (const line_regex&) = delete;
public:
- line_pool pool;
+ // Mutable since input line_char literals must go into the same
+ // pool (and thus is MT-unsafe).
+ //
+ mutable line_pool pool;
};
}
}
diff --git a/build2/test/script/regex.cxx b/build2/test/script/regex.cxx
index c6fba75..bd811e4 100644
--- a/build2/test/script/regex.cxx
+++ b/build2/test/script/regex.cxx
@@ -28,7 +28,6 @@ namespace build2
// @@ How can we allow anything for basic_regex but only subset
// for our own code?
//
- const char sp[] = "()|.*+?{\\}0123456789,=!";
const char ex[] = "pn\n\r";
assert (c == 0 || // Null character.
@@ -45,7 +44,7 @@ namespace build2
(c > 0 && c <= 255 && (
// Supported regex special characters.
//
- string::traits_type::find (sp, 23, c) != nullptr ||
+ syntax (c) ||
// libstdc++ look-ahead tokens, newline chars.
//
@@ -73,6 +72,13 @@ namespace build2
}
bool
+ line_char::syntax (char c)
+ {
+ return string::traits_type::find (
+ "()|.*+?{}\\0123456789,=!", 23, c) != nullptr;
+ }
+
+ bool
operator== (const line_char& l, const line_char& r)
{
if (l.type == r.type)
@@ -157,7 +163,7 @@ namespace std
if (n > 0 && d != s)
{
// If d < s then it can't be in [s, s + n) range and so using copy() is
- // safe. Otherwise d + n is out of (first, last] range and so using
+ // safe. Otherwise d + n is out of (s, s + n] range and so using
// copy_backward() is safe.
//
if (d < s)
diff --git a/build2/test/script/runner.cxx b/build2/test/script/runner.cxx
index 05b3b5c..08d358c 100644
--- a/build2/test/script/runner.cxx
+++ b/build2/test/script/runner.cxx
@@ -46,8 +46,7 @@ namespace build2
}
// Check if the test command output matches the expected result (redirect
- // value). Noop for redirect types other than none, here_string,
- // here_document.
+ // value). Noop for redirect types other than none, here_*.
//
static void
check_output (const path& pr,
@@ -64,6 +63,7 @@ namespace build2
d << info << "stdin: " << ip;
};
+ bool re;
if (rd.type == redirect_type::none)
{
assert (!op.empty ());
@@ -79,96 +79,173 @@ namespace build2
input_info (d);
}
}
- else if (rd.type == redirect_type::here_str_literal ||
+ 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)
{
assert (!op.empty ());
- path orp (op + ".orig");
+ // While the regex file is not used for output validation we still
+ // create it for troubleshooting.
+ //
+ path opp (op + (re ? ".regex" : ".orig"));
try
{
- ofdstream os (orp);
- sp.clean ({cleanup_type::always, orp}, true);
- os << rd.str;
+ ofdstream os (opp);
+ sp.clean ({cleanup_type::always, opp}, true);
+ os << (re ? rd.regex.str : rd.str);
os.close ();
}
catch (const io_error& e)
{
- fail (ll) << "unable to write " << orp << ": " << e.what ();
+ fail (ll) << "unable to write " << opp << ": " << e.what ();
}
- // Use diff utility to compare the output with the expected result.
- //
- path dp ("diff");
- process_path pp (run_search (dp, true));
+ 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";
+ };
- cstrings args {
- pp.recall_string (),
- "--strip-trailing-cr",
- "-u",
- orp.string ().c_str (),
- op.string ().c_str (),
- nullptr};
+ if (re)
+ {
+ // Match the output with the line_regex. That requires to parse the
+ // output into the line_string of literals first.
+ //
+ using namespace regex;
- if (verb >= 2)
- print_process (args);
+ line_string ls;
- try
+ 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.
+
+ while (!is.eof ())
+ {
+ 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
+ //
+ while (!s.empty () && s.back () == '\r')
+ s.pop_back ();
+
+ ls += line_char (move (s), rd.regex.regex.pool);
+ }
+ }
+ catch (const io_error& e)
+ {
+ fail (ll) << "unable to read " << op << ": " << e.what ();
+ }
+
+ if (regex_match (ls, rd.regex.regex)) // Doesn't throw.
+ return;
+
+ // Output doesn't match the regex.
+ //
+ diag_record d (error (ll));
+ d << pr << " " << what << " doesn't match the regex";
+
+ output_info (d, op);
+ output_info (d, opp, "", " regex");
+ input_info (d);
+
+ // Fall through.
+ //
+ }
+ else
{
- // 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.
+ // Use diff utility to compare the output with the expected result.
//
- process p (pp, args.data (), 0, 2);
+ path dp ("diff");
+ process_path pp (run_search (dp, true));
+
+ cstrings args {
+ pp.recall_string (),
+ "--strip-trailing-cr", // Is essential for cross-testing.
+ "-u",
+ opp.string ().c_str (),
+ op.string ().c_str (),
+ nullptr};
+
+ if (verb >= 2)
+ print_process (args);
try
{
- if (p.wait ())
- return;
-
- // Output doesn't match the expected result.
+ // 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.
//
- diag_record d (error (ll));
- d << pr << " " << what << " doesn't match the expected output";
+ process p (pp, args.data (), 0, 2);
- auto output_info =
- [&d, &what, &ll] (const path& p, const char* prefix)
+ try
{
- if (non_empty (p, ll))
- d << info << prefix << what << ": " << p;
- else
- d << info << prefix << what << " is empty";
- };
+ if (p.wait ())
+ return;
- output_info (op, "");
- output_info (orp, "expected ");
- input_info (d);
+ // Output doesn't match the expected result.
+ //
+ diag_record d (error (ll));
+ d << pr << " " << what << " doesn't match the expected output";
+
+ output_info (d, op);
+ output_info (d, opp, "expected ");
+ input_info (d);
+
+ // Fall through.
+ //
+ }
+ catch (const io_error&)
+ {
+ // Child exit status doesn't matter. Assume the child process
+ // issued diagnostics. Just wait for the process completion.
+ //
+ p.wait (); // Check throw.
+
+ error (ll) << "failed to compare " << what
+ << " with the expected output";
+ }
// Fall through.
//
}
- catch (const io_error&)
+ catch (const process_error& e)
{
- // Child exit status doesn't matter. Assume the child process
- // issued diagnostics. Just wait for the process completion.
- //
- p.wait (); // Check throw.
+ error (ll) << "unable to execute " << pp << ": " << e.what ();
- error (ll) << "failed to compare " << what
- << " with the expected output";
+ if (e.child ())
+ exit (1);
}
// Fall through.
//
}
- catch (const process_error& e)
- {
- error (ll) << "unable to execute " << pp << ": " << e.what ();
-
- if (e.child ())
- exit (1);
- }
throw failed ();
}
@@ -461,8 +538,8 @@ namespace build2
break;
}
- case redirect_type::merge: assert (false); break;
- case redirect_type::here_str_regex: // @@ REGEX
+ case redirect_type::merge:
+ case redirect_type::here_str_regex:
case redirect_type::here_doc_regex: assert (false); break;
}
@@ -482,10 +559,9 @@ namespace build2
// Open a file for command output redirect if requested explicitly
// (file redirect) or for the purpose of the output validation (none,
- // here_string, here_document), register the file for cleanup, return
- // the file descriptor. Return the specified, default or -2 file
- // descriptors for merge, pass or null redirects respectively not
- // opening a file.
+ // here_*), register the file for cleanup, return the file descriptor.
+ // 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,
@@ -549,13 +625,13 @@ namespace build2
case redirect_type::none:
case redirect_type::here_str_literal:
case redirect_type::here_doc_literal:
+ case redirect_type::here_str_regex:
+ case redirect_type::here_doc_regex:
{
p = std_path (what);
m |= fdopen_mode::truncate;
break;
}
- case redirect_type::here_str_regex: // @@ REGEX
- case redirect_type::here_doc_regex: assert (false); break;
}
try
diff --git a/build2/test/script/script.cxx b/build2/test/script/script.cxx
index 7941df6..c67e1b6 100644
--- a/build2/test/script/script.cxx
+++ b/build2/test/script/script.cxx
@@ -85,25 +85,36 @@ namespace build2
case redirect_type::merge: o << '&' << r.fd; break;
case redirect_type::here_str_literal:
+ case redirect_type::here_str_regex:
{
- const string& v (r.str);
+ bool re (r.type == redirect_type::here_str_regex);
+ const string& v (re ? r.regex.str : r.str);
bool nl (!v.empty () && v.back () == '\n');
if (!nl)
o << ':';
+ if (re)
+ o << '~';
+
to_stream_q (o, nl ? string (v, 0, v.size () - 1) : v);
break;
}
case redirect_type::here_doc_literal:
+ case redirect_type::here_doc_regex:
{
- const string& v (r.str);
+ bool re (r.type == redirect_type::here_doc_regex);
+ const string& v (re ? r.regex.str : r.str);
bool nl (!v.empty () && v.back () == '\n');
// Add another '>' or '<'. Note that here end marker never
// needs to be quoted.
//
o << d << (nl ? "" : ":");
+
+ if (re)
+ o << '~';
+
to_stream_q (o, r.end);
break;
}
@@ -115,16 +126,21 @@ namespace build2
print_path (r.file.path);
break;
}
- case redirect_type::here_str_regex: // @@ REGEX
- case redirect_type::here_doc_regex: assert (false); break;
}
};
auto print_doc = [&o] (const redirect& r)
{
- const string& v (r.str);
+ bool re (r.type == redirect_type::here_doc_regex);
+ const string& v (re ? r.regex.str : r.str);
bool nl (!v.empty () && v.back () == '\n');
- o << endl << v << (nl ? "" : "\n") << r.end;
+
+ // For the regex here-document the end marker contains introducer and
+ // flags characters, so need to remove them.
+ //
+ const string& e (r.end);
+ o << endl << v << (nl ? "" : "\n")
+ << (re ? string (e, 1, e.find (e[0], 1) - 1) : e);
};
if ((m & command_to_stream::header) == command_to_stream::header)
@@ -173,9 +189,17 @@ namespace build2
{
// Here-documents.
//
- if (c.in.type == redirect_type::here_doc_literal) print_doc (c.in);
- if (c.out.type == redirect_type::here_doc_literal) print_doc (c.out);
- if (c.err.type == redirect_type::here_doc_literal) print_doc (c.err);
+ if (c.in.type == redirect_type::here_doc_literal ||
+ c.in.type == redirect_type::here_doc_regex)
+ print_doc (c.in);
+
+ if (c.out.type == redirect_type::here_doc_literal ||
+ c.out.type == redirect_type::here_doc_regex)
+ print_doc (c.out);
+
+ if (c.err.type == redirect_type::here_doc_literal ||
+ c.err.type == redirect_type::here_doc_regex)
+ print_doc (c.err);
}
}
diff --git a/doc/testscript.cli b/doc/testscript.cli
index 3a7285d..4708745 100644
--- a/doc/testscript.cli
+++ b/doc/testscript.cli
@@ -1693,7 +1693,7 @@ the following more traditional form:
(A|B|C)+
\
-Only characters from the \c{.()|*+?{\}0123456789,=!} set are allowed as
+Only characters from the \c{.()|*+?{\}\\0123456789,=!} set are allowed as
syntax line-chars with presence of any other character being an error.
A blank line as well as the \c{//} sequence (assuming \c{/} is the introducer)
diff --git a/tests/function/path/testscript b/tests/function/path/testscript
index 828263f..a0d8422 100644
--- a/tests/function/path/testscript
+++ b/tests/function/path/testscript
@@ -32,9 +32,7 @@ s = ($cxx.target.class != windows ? '/' : '\')
if ($cxx.target.class == windows)
{
mkdir Foo;
- # @@ regex
- #$* <'print $path.normalize($out_base/foo, true)' >~'/.+\Foo/' # cross
- $* <'print $path.normalize($out_base/foo, true)' >"$~\\Foo"
+ $* <'print $path.normalize($out_base/foo, true)' >~'/.+\\Foo/'
}
}
diff --git a/tests/test/script/builtin/cat.test b/tests/test/script/builtin/cat.test
index edde422..5049ca9 100644
--- a/tests/test/script/builtin/cat.test
+++ b/tests/test/script/builtin/cat.test
@@ -49,7 +49,13 @@ EOO
: non-existent
:
-cat in 2>- != 0 # @@ REGEX
+: Note that there is an optional trailing blank line in the regex that matches
+: the newline added by msvcrt as a part of the error description.
+:
+cat in 2>>~%EOE% != 0
+%cat: unable to print '.+[/\\]test[/\\]cat[/\\]non-existent[/\\]in': .+%
+%%?
+EOE
: empty-path
:
diff --git a/tests/test/script/builtin/mkdir.test b/tests/test/script/builtin/mkdir.test
index a8857c1..dafcd35 100644
--- a/tests/test/script/builtin/mkdir.test
+++ b/tests/test/script/builtin/mkdir.test
@@ -37,14 +37,24 @@ mkdir '' 2>"mkdir: invalid path ''" == 1
: already-exists
:
-: Test creation of an existing directory. Note that error message is
-: platform-dependent so is not checked.
+: Test creation of an existing directory.
:
-mkdir a 2>- a == 1 # @@ REGEX
+: Note that there is an optional trailing blank line in the regex that matches
+: the newline added by msvcrt as a part of the error description.
+:
+mkdir a a 2>>~%EOE% == 1
+%mkdir: unable to create directory '.+[/\\]test[/\\]mkdir[/\\]already-exists[/\\]a': .+%
+%%?
+EOE
: not-exists
:
-: Test creation of a directory with non-existent parent. Note that error
-: message is platform-dependent so is not checked.
+: Test creation of a directory with non-existent parent.
+:
+: Note that there is an optional trailing blank line in the regex that matches
+: the newline added by msvcrt as a part of the error description.
:
-mkdir a/b 2>- == 1 # @@ REGEX
+mkdir a/b 2>>~%EOE% == 1
+%mkdir: unable to create directory '.+[/\\]test[/\\]mkdir[/\\]not-exists[/\\]a[/\\]b': .+%
+%%?
+EOE
diff --git a/tests/test/script/builtin/rm.test b/tests/test/script/builtin/rm.test
index cf5d2fa..20b2b24 100644
--- a/tests/test/script/builtin/rm.test
+++ b/tests/test/script/builtin/rm.test
@@ -33,7 +33,13 @@ rm a
:
: Removing non-existing file fails.
:
-rm a 2>- == 1 # @@ REGEX
+: Note that there is an optional trailing blank line in the regex that matches
+: the newline added by msvcrt as a part of the error description.
+:
+rm a 2>>~%EOE% == 1
+%rm: unable to remove '.+[/\\]test[/\\]rm[/\\]file-not-exists[/\\]a': .+%
+%%?
+EOE
: file-not-exists-force
:
diff --git a/tests/test/script/integration/testscript b/tests/test/script/integration/testscript
index 7cd8f0a..495c75a 100644
--- a/tests/test/script/integration/testscript
+++ b/tests/test/script/integration/testscript
@@ -8,28 +8,25 @@
.include ../../common.test
-cd = [string] $path.canonicalize(./)
-td = [string] $path.canonicalize(test/)
-
-: scrip-files
+: script-files
:
{
+touch testscript foo.test bar.test
: testscript-and-other
:
- $* <<EOI 2>>"EOE" != 0
+ $* <<EOI 2>>~%EOE% != 0
./: test{../testscript ../foo}
EOI
- error: both 'testscript' and other names specified for dir{$cd}
+ %error: both 'testscript' and other names specified for dir\{\.[/\\]\}%
EOE
: other-and-testscript
:
- $* <<EOI 2>>"EOE" != 0
+ $* <<EOI 2>>~%EOE% != 0
./: test{../foo ../testscript}
EOI
- error: both 'testscript' and other names specified for dir{$cd}
+ %error: both 'testscript' and other names specified for dir\{\.[/\\]\}%
EOE
: others
@@ -43,20 +40,20 @@ td = [string] $path.canonicalize(test/)
:
touch foo.test;
touch test;
-$* <<EOI 2>>"EOE" != 0
+$* <<EOI 2>>~%EOE% != 0
./: test{foo}
EOI
-error: working directory $td is a file/symlink
+%error: working directory test[/\\] is a file/symlink%
EOE
: wd-exists-before
:
touch foo.test;
mkdir test &!test/;
-$* <<EOI 2>>"EOE"
+$* <<EOI 2>>~%EOE%
./: test{foo}
EOI
-warning: working directory $td exists at the beginning of the test
+%warning: working directory test[/\\] exists at the beginning of the test%
EOE
: wd-not-empty-before
@@ -64,10 +61,10 @@ EOE
touch foo.test;
mkdir test &!test/;
touch test/dummy &!test/dummy;
-$* <<EOI 2>>"EOE"
+$* <<EOI 2>>~%EOE%
./: test{foo}
EOI
-warning: working directory $td exists and is not empty at the beginning of the test
+%warning: working directory test[/\\] exists and is not empty at the beginning of the test%
EOE
: wd-not-empty-after
@@ -81,8 +78,8 @@ EOE
cat <<EOI >>>foo.test;
touch ../../dummy
EOI
-$* <<EOI 2>>"EOE" &test/*** != 0
+$* <<EOI 2>>~%EOE% &test/*** != 0
./: test{foo}
EOI
-error: working directory $td is not empty at the end of the test
+%error: working directory test[/\\] is not empty at the end of the test%
EOE
diff --git a/tests/test/script/runner/buildfile b/tests/test/script/runner/buildfile
index 375be18..a7603e4 100644
--- a/tests/test/script/runner/buildfile
+++ b/tests/test/script/runner/buildfile
@@ -2,11 +2,9 @@
# copyright : Copyright (c) 2014-2016 Code Synthesis Ltd
# license : MIT; see accompanying LICENSE file
-./: test{cleanup status} exe{driver} $b
+./: test{cleanup redirect status} exe{driver} $b
-# @@ Do test{*} once redirect folds in.
-#
-test{cleanup}@./ test{status}@./: target = exe{driver}
+test{*}: target = exe{driver}
import libs = libbutl%lib{butl}
-exe{driver}: cxx{driver} $libs test{redirect}
+exe{driver}: cxx{driver} $libs
diff --git a/tests/test/script/runner/cleanup.test b/tests/test/script/runner/cleanup.test
index f14d02d..6f6bb95 100644
--- a/tests/test/script/runner/cleanup.test
+++ b/tests/test/script/runner/cleanup.test
@@ -6,8 +6,6 @@
b += --no-column
-td = [string] $path.canonicalize(test/)
-
# Valid cleanups.
#
# @@ TODO: $c <'$* -f a &a' && $b
@@ -93,8 +91,8 @@ $b
: Test cleanup of non-existing file.
:
$c <'$* &a';
-$b 2>>"EOE" != 0
-testscript:1: error: registered for cleanup file $path.canonicalize(test/1/a) does not exist
+$b 2>>~%EOE% != 0
+%testscript:1: error: registered for cleanup file test[/\\]1[/\\]a does not exist%
EOE
: file-out-wd
@@ -102,8 +100,8 @@ EOE
: Test explicit cleanup of a file out of the testscript working directory.
:
$c <'$* &../../a';
-$b 2>>"EOE" != 0
-testscript:1: error: file cleanup $path.canonicalize(../../a) is out of working directory $td
+$b 2>>~%EOE% != 0
+%testscript:1: error: file cleanup \.\.[/\\]\.\.[/\\]a is out of working directory test[/\\]%
EOE
: file-in-wd
@@ -112,24 +110,30 @@ EOE
: directory but inside the script working directory.
:
$c <'$* &../a';
-$b 2>>"EOE" != 0
-testscript:1: error: registered for cleanup file $path.canonicalize(test/a) does not exist
+$b 2>>~%EOE% != 0
+%testscript:1: error: registered for cleanup file test[/\\]a does not exist%
EOE
: not-file
:
: Test cleanup of a directory as a file.
:
+: Note that there is an optional trailing blank line in the regex that matches
+: the newline added by msvcrt as a part of the error description.
+:
$c <'$* -d a &a';
-$b 2>- != 0 # @@ REGEX
+$b 2>>~%EOE% != 0
+%error: unable to remove file test[/\\]1[/\\]a: .+%
+%%?
+EOE
: dir-not-exists
:
: Test cleanup of non-existing directory.
:
$c <'$* &a/';
-$b 2>>"EOE" != 0
-testscript:1: error: registered for cleanup directory ([string] $path.canonicalize(test/1/a/)) does not exist
+$b 2>>~%EOE% != 0
+%testscript:1: error: registered for cleanup directory test[/\\]1[/\\]a[/\\] does not exist%
EOE
: dir-out-wd
@@ -137,8 +141,8 @@ EOE
: Test cleanup of a directory out of the testscript working directory.
:
$c <'$* &../../a/';
-$b 2>>"EOE" != 0
-testscript:1: error: directory cleanup ([string] $path.canonicalize(../../a/)) is out of working directory $td
+$b 2>>~%EOE% != 0
+%testscript:1: error: directory cleanup \.\.[/\\]\.\.[/\\]a[/\\] is out of working directory test[/\\]%
EOE
: dir-in-wd
@@ -147,8 +151,8 @@ EOE
: working directory but inside the testscript working directory.
:
$c <'$* &../a/';
-$b 2>>"EOE" != 0
-testscript:1: error: registered for cleanup directory ([string] $path.canonicalize(test/a/)) does not exist
+$b 2>>~%EOE% != 0
+%testscript:1: error: registered for cleanup directory test[/\\]a[/\\] does not exist%
EOE
: dir-not-empty
@@ -156,24 +160,30 @@ EOE
: Test cleanup of a non-empty directory.
:
$c <'$* -d a -f a/b &a/';
-$b 2>>"EOE" != 0
-testscript:1: error: registered for cleanup directory ([string] $path.canonicalize(test/1/a/)) is not empty
+$b 2>>~%EOE% != 0
+%testscript:1: error: registered for cleanup directory test[/\\]1[/\\]a[/\\] is not empty%
EOE
: not-dir
:
: Test cleanup of a file as a directory.
:
+: Note that there is an optional trailing blank line in the regex that matches
+: the newline added by msvcrt as a part of the error description.
+:
$c <'$* -f a &a/';
-$b 2>- != 0 # @@ REGEX
+$b 2>>~%EOE% != 0
+%error: unable to remove directory test[/\\]1[/\\]a[/\\]: .+%
+%%?
+EOE
: wildcard-not-exists
:
: Test cleanup of a wildcard not matching any directory.
:
$c <'$* &a/***';
-$b 2>>"EOE" != 0
-testscript:1: error: registered for cleanup wildcard $path.canonicalize(test/1/a/***) doesn't match a directory
+$b 2>>~%EOE% != 0
+%testscript:1: error: registered for cleanup wildcard test[/\\]1[/\\]a[/\\]\*\*\* doesn't match a directory%
EOE
: wildcard-out-wd
@@ -181,8 +191,8 @@ EOE
: Test cleanup of a wildcard out of the testscript working directory.
:
$c <'$* &../../a/***';
-$b 2>>"EOE" != 0
-testscript:1: error: wildcard cleanup $path.canonicalize(../../a/***) is out of working directory $td
+$b 2>>~%EOE% != 0
+%testscript:1: error: wildcard cleanup \.\.[/\\]\.\.[/\\]a[/\\]\*\*\* is out of working directory test[/\\]%
EOE
: wildcard-in-wd
@@ -192,24 +202,30 @@ EOE
: directory.
:
$c <'$* &../a/***';
-$b 2>>"EOE" != 0
-testscript:1: error: registered for cleanup wildcard $path.canonicalize(test/a/***) doesn't match a directory
+$b 2>>~%EOE% != 0
+%testscript:1: error: registered for cleanup wildcard test[/\\]a[/\\]\*\*\* doesn't match a directory%
EOE
: wildcard-not-dir
:
: Test cleanup of a file as a wildcard.
:
+: Note that there is an optional trailing blank line in the regex that matches
+: the newline added by msvcrt as a part of the error description.
+:
$c <'$* -f a &a/***';
-$b 2>- != 0 # @@ REGEX
+$b 2>>~%EOE% != 0
+%error: unable to remove directory test[/\\]1[/\\]a[/\\]: .+%
+%%?
+EOE
: implicit-overwrite
:
: Test an implicit cleanup being overwritten with the explicit one,
:
$c <'$* -o foo >>>a &!a';
-$b 2>>"EOE" != 0
-testscript:1: error: registered for cleanup directory ([string] $path.canonicalize(test/1/)) is not empty
+$b 2>>~%EOE% != 0
+%testscript:1: error: registered for cleanup directory test[/\\]1[/\\] is not empty%
EOE
: explicit-overwrite
@@ -220,6 +236,6 @@ $c <<EOO;
$* &!a;
$* -o foo >>>a
EOO
-$b 2>>"EOE" != 0
-testscript:2: error: registered for cleanup directory ([string] $path.canonicalize(test/1/)) is not empty
+$b 2>>~%EOE% != 0
+%testscript:2: error: registered for cleanup directory test[/\\]1[/\\] is not empty%
EOE
diff --git a/tests/test/script/runner/redirect.test b/tests/test/script/runner/redirect.test
index e8d57c1..ec3c960 100644
--- a/tests/test/script/runner/redirect.test
+++ b/tests/test/script/runner/redirect.test
@@ -2,150 +2,291 @@
# copyright : Copyright (c) 2014-2016 Code Synthesis Ltd
# license : MIT; see accompanying LICENSE file
-# @@ Needs conversion to cat & build (then adjust buildfile).
+.include ../common.test
-$* -o foo >- : out-null
-$* -e foo 2>- : err-null
-$* -i 0 <foo : in-str
-$* -o foo >foo : out-str
-$* -e foo 2>foo : err-str
-$* -i 1 <foo >foo : inout-str
-$* -i 2 <foo 2>foo : inerr-str
-$* -i 1 -e bar <foo 1>foo 2>bar : inout-err-str
+b += --no-column
+# null-redirect tests.
+#
+: out-null
+:
+$c <'$* -o foo >-';
+$b
+
+: err-null
+:
+$c <'$* -e foo 2>-';
+$b
+
+# Here-string tests.
+#
+: in-str
+:
+$c <'$* -i 0 <foo';
+$b
+
+: out-str
+:
+$c <'$* -o foo >foo';
+$b
+
+: err-str
+:
+$c <'$* -e foo 2>foo';
+$b
+
+: inout-str
+:
+$c <'$* -i 1 <foo >foo';
+$b
+
+: inout-str-fail
+:
+$c <'$* -i 1 <foo >bar';
+$b 2>>~%EOE% != 0
+%.{3}
+-bar
++foo
+%testscript:1: error: \.\.[/\\]\.\.[/\\]\.\.[/\\]driver(\.exe)? stdout doesn't match the expected output%
+% info: stdout: test[/\\]1[/\\]stdout%
+% info: expected stdout: test[/\\]1[/\\]stdout\.orig%
+% info: stdin: test[/\\]1[/\\]stdin%
+EOE
+
+: inerr-str
+:
+$c <'$* -i 2 <foo 2>foo';
+$b
+
+: inout-err-str
+:
+$c <'$* -i 1 -e bar <foo 1>foo 2>bar';
+$b
+
+# Here-document tests.
+#
: in-doc
:
+$c <<EOI;
$* -i 0 <<EOO
foo
bar
EOO
+EOI
+$b
: out-doc
:
+$c <<EOI;
$* -o foo -o bar >>EOO
foo
bar
EOO
+EOI
+$b
: err-doc
:
+$c <<EOI;
$* -e foo -e bar 2>>EOO
foo
bar
EOO
+EOI
+$b
: inout-doc
:
-$* -i 1 <<EOI >>EOO
+$c <<EOI;
+$* -i 1 <<EOF >>EOO
foo
bar
-EOI
+EOF
foo
bar
EOO
+EOI
+$b
: inerr-doc
:
-$* -i 2 <<EOI 2>>EOE
+$c <<EOI;
+$* -i 2 <<EOF 2>>EOE
foo
bar
-EOI
+EOF
foo
bar
EOE
+EOI
+$b
: empty-str-doc
:
-$* -i 1 -e "" <<EOI >>EOO 2>""
-EOI
+$c <<EOI;
+$* -i 1 -e "" <<EOF >>EOO 2>""
+EOF
EOO
+EOI
+$b
: nl-containing-doc
:
-$* -i 1 <<EOI >>EOO
+$c <<EOI;
+$* -i 1 <<EOF >>EOO
-EOI
+EOF
EOO
+EOI
+$b
# No-newline tests.
#
-# @@ TMP Need does not compare test.
-#
: no-newline-str
:
-$* -i 1 <:"foo" >:"foo"
+$c <'$* -i 1 <:"foo" >:"foo"';
+$b
+
+: no-newline-empty-str
+:
+$c <'$* -i 1 <:"" >:""';
+$b
+
+: no-newline-str-fail1
+:
+$c <'$* -i 1 <:"foo" >"foo"';
+$b 2>>~/EOE/ != 0
+/.{3}
+-foo
++foo
+\ No newline at end of file
+/testscript:1: error: .+driver(\.exe)? stdout doesn't match the expected output/
+/.{3}
+EOE
-#\
-$* -i 1 <:"foo" >!"foo" # no-newline-str-fail1
-$* -i 1 <"foo" >:!"foo" # no-newline-str-fail2
-#\
+: no-newline-str-fail2
+:
+$c <'$* -i 1 <"foo" >:"foo"';
+$b 2>>~/EOE/ != 0
+/.{3}
+-foo
+\ No newline at end of file
++foo
+/testscript:1: error: .+driver(\.exe)? stdout doesn't match the expected output/
+/.{3}
+EOE
: no-newline-doc
:
-$* -i 1 <<:EOI >>:EOO
+$c <<EOI;
+$* -i 1 <<:EOF >>:EOO
foo
-EOI
+EOF
foo
EOO
+EOI
+$b
-#\
-$* -i 1 <<:EOI >>!EOO # no-newline-doc-fail1
+: no-newline-doc-fail1
+:
+$c <<EOI;
+$* -i 1 <<:EOF >>EOO
foo
-EOI
+EOF
foo
EOO
+EOI
+$b 2>>~/EOE/ != 0
+/.{3}
+-foo
++foo
+\ No newline at end of file
+/testscript:1: error: .+driver(\.exe)? stdout doesn't match the expected output/
+/.{3}
+EOE
-$* -i 1 <<EOI >>:!EOO # no-newline-doc-fail2
+: no-newline-doc-fail2
+:
+$c <<EOI;
+$* -i 1 <<EOF >>:EOO
foo
-EOI
+EOF
foo
EOO
-#\
+EOI
+$b 2>>~/EOE/ != 0
+/.{3}
+-foo
+\ No newline at end of file
++foo
+/testscript:1: error: .+driver(\.exe)? stdout doesn't match the expected output/
+/.{3}
+EOE
: no-newline-empty-str-doc
:
-$* -i 1 <<:EOI >>:EOO 2>:""
-EOI
+$c <<EOI;
+$* -i 1 <<:EOF >>:EOO 2>:""
+EOF
EOO
+EOI
+$b
: no-newline-nl-cont-doc
:
-$* -i 1 <<:EOI >>:EOO
-
-EOI
+$c <<EOI;
+$* -i 1 <<:EOF >>:EOO
-EOO
+EOF
-: file
-:
-$* -o foo >>>out;
-$* -e bar 2>>>&out;
-$* -i 1 <<<out >>EOO
-foo
-bar
EOO
+EOI
+$b
+# Merge tests.
+#
: merge-str
:
+$c <<EOI;
$* -o foo -e bar 2>>EOE 1>&2
foo
bar
EOE
+EOI
+$b
: merge-doc
:
-$* -i 1 <<EOI -e baz >>EOO 2>&1
+$c <<EOI;
+$* -i 1 <<EOF -e baz >>EOO 2>&1
foo
bar
-EOI
+EOF
foo
bar
baz
EOO
+EOI
+$b
+
+# File tests.
+#
+: file
+:
+$c <<EOI;
+$* -o foo >>>out;
+$* -e bar 2>>>&out;
+$* -i 1 <<<out >>EOO
+foo
+bar
+EOO
+EOI
+$b
: merge-file
:
+$c <<EOI;
$* -o foo -e bar 2>&1 >>>out;
$* -e baz -o biz 1>&2 2>>>&out;
$* -i 1 <<<out >>EOO
@@ -154,6 +295,52 @@ bar
baz
biz
EOO
+EOI
+$b
+
+# Regex tests.
+#
+: out-str-regex
+:
+{
+ : match
+ :
+ $c <'$* -o foo >~/Foo?/i';
+ $b
+
+ : fail
+ :
+ $c <'$* -o fooo >~/Foo?/i';
+ $b 2>>~%EOE% != 0
+ %testscript:1: error: \.\.[/\\]\.\.[/\\]\.\.[/\\]\.\.[/\\]driver(\.exe)? stdout doesn't match the regex%
+ % info: stdout: test[/\\]1[/\\]stdout%
+ % info: stdout regex: test[/\\]1[/\\]stdout\.regex%
+ EOE
+}
+
+: out-doc-regex
+:
+{
+ : match
+ :
+ $c <<EOI;
+ $* -o foo -o foo -o bar >>~/EOO/i
+ /FO*/*
+ bar
+ /*
+ EOO
+ EOI
+ $b
+
+ : match-empty
+ :
+ $c <<EOI;
+ $* >>:~/EOO/i
+ /.{0}
+ EOO
+ EOI
+ $b
+}
# Builtins redirects.
#
@@ -163,11 +350,38 @@ EOO
: builtins
:
{
- echo "abc" >- : out-null
- echo "abc" 1>&2 2>- : err-null
- echo <foo 1>- : in-str
- echo "foo" >foo : out-str
- echo "foo" 2>foo 1>&2 : err-str
- cat <foo >foo : inout-str
- cat <foo 2>foo 1>&2 : inerr-str
+ : out-null
+ :
+ $c <'echo "abc" >-';
+ $b
+
+ : err-null
+ :
+ $c <'echo "abc" 1>&2 2>-';
+ $b
+
+ : in-str
+ :
+ $c <'echo <foo 1>-';
+ $b
+
+ : out-str
+ :
+ $c <'echo "foo" >foo';
+ $b
+
+ : err-str
+ :
+ $c <'echo "foo" 2>foo 1>&2';
+ $b
+
+ : inout-str
+ :
+ $c <'cat <foo >foo';
+ $b
+
+ : inerr-str
+ :
+ $c <'cat <foo 2>foo 1>&2';
+ $b
}
diff --git a/tests/test/script/runner/status.test b/tests/test/script/runner/status.test
index e115f13..6a2c06c 100644
--- a/tests/test/script/runner/status.test
+++ b/tests/test/script/runner/status.test
@@ -6,10 +6,6 @@
b += --no-column
-if ($cxx.target.class == "windows")
- ext = .exe
-end
-
# Successfull tests.
#
: eq-true
@@ -27,13 +23,13 @@ $b
: eq-false
:
$c <'$* -s 1 == 0';
-$b 2>>"EOE" != 0
-testscript:1: error: $path.canonicalize(../../../driver$ext) exit status 1 != 0
+$b 2>>~%EOE% != 0
+%testscript:1: error: \.\.[/\\]\.\.[/\\]\.\.[/\\]driver(.exe)? exit status 1 != 0%
EOE
: ne-false
:
$c <'$* -s 1 != 1';
-$b 2>>"EOE" != 0
-testscript:1: error: $path.canonicalize(../../../driver$ext) exit status 1 == 1
+$b 2>>~%EOE% != 0
+%testscript:1: error: \.\.[/\\]\.\.[/\\]\.\.[/\\]driver(.exe)? exit status 1 == 1%
EOE
diff --git a/unit-tests/function/call.test b/unit-tests/function/call.test
index 9129785..5584c15 100644
--- a/unit-tests/function/call.test
+++ b/unit-tests/function/call.test
@@ -63,11 +63,16 @@ EOE
: ambig
:
-$* <'$ambig(abc)' 2>- != 0 # @@ REGEX
-#buildfile:1:2: error: ambiguous call to ambig(<untyped>)
-# info: candidate: ambig(<untyped> [, uint64]), qualified name dummy.ambig
-# info: candidate: ambig(<untyped> [, string]), qualified name dummy.ambig
-#EOE
+$* <'$ambig(abc)' 2>>~/EOE/ != 0
+buildfile:1:2: error: ambiguous call to ambig(<untyped>)
+/((
+ info: candidate: ambig(<untyped> [, uint64]), qualified name dummy.ambig
+ info: candidate: ambig(<untyped> [, string]), qualified name dummy.ambig
+/)|(
+ info: candidate: ambig(<untyped> [, string]), qualified name dummy.ambig
+ info: candidate: ambig(<untyped> [, uint64]), qualified name dummy.ambig
+/))
+EOE
: optional-absent
:
@@ -115,11 +120,16 @@ EOE
: print-fovl
:
-$* <'$ambig([bool] true)' 2>- != 0 # @@ REGEX
-#buildfile:1:2: error: unmatched call to ambig(bool)
-# info: candidate: ambig(<untyped> [, uint64]), qualified name dummy.ambig
-# info: candidate: ambig(<untyped> [, string]), qualified name dummy.ambig
-#EOE
+$* <'$ambig([bool] true)' 2>>~/EOE/ != 0
+buildfile:1:2: error: unmatched call to ambig(bool)
+/((
+ info: candidate: ambig(<untyped> [, uint64]), qualified name dummy.ambig
+ info: candidate: ambig(<untyped> [, string]), qualified name dummy.ambig
+/)|(
+ info: candidate: ambig(<untyped> [, string]), qualified name dummy.ambig
+ info: candidate: ambig(<untyped> [, uint64]), qualified name dummy.ambig
+/))
+EOE
: print-fovl-variadic
:
diff --git a/unit-tests/test/script/parser/buildfile b/unit-tests/test/script/parser/buildfile
index 41fe34d..34b6d9e 100644
--- a/unit-tests/test/script/parser/buildfile
+++ b/unit-tests/test/script/parser/buildfile
@@ -17,6 +17,6 @@ scheduler
exe{driver}: cxx{driver} ../../../../build2/cxx{$src} $libs \
test{cleanup command-if command-re-parse description directive exit \
expansion here-document here-string include pipe-expr pre-parse \
- redirect scope scope-if setup-teardown}
+ redirect regex scope scope-if setup-teardown}
include ../../../../build2/
diff --git a/unit-tests/test/script/parser/here-document.test b/unit-tests/test/script/parser/here-document.test
index 7cb9474..4afcf68 100644
--- a/unit-tests/test/script/parser/here-document.test
+++ b/unit-tests/test/script/parser/here-document.test
@@ -13,6 +13,12 @@
testscript:1:8: error: expected here-document end marker
EOE
+ : missing-empty
+ :
+ $* <'cmd <<""' 2>>EOE != 0
+ testscript:1:7: error: expected here-document end marker
+ EOE
+
: unseparated-expansion
:
$* <'cmd <<FOO$foo' 2>>EOE != 0
diff --git a/unit-tests/test/script/parser/regex.test b/unit-tests/test/script/parser/regex.test
new file mode 100644
index 0000000..d28a1a4
--- /dev/null
+++ b/unit-tests/test/script/parser/regex.test
@@ -0,0 +1,188 @@
+# file : unit-tests/test/script/parser/regex.test
+# copyright : Copyright (c) 2014-2016 Code Synthesis Ltd
+# license : MIT; see accompanying LICENSE file
+
+: here-string
+:
+{
+ : stdout
+ :
+ {
+ : missing
+ :
+ $* <'cmd >~' 2>>EOE != 0
+ testscript:1:7: error: missing stdout here-string regex
+ EOE
+
+ : no-introducer
+ :
+ $* <'cmd >~""' 2>>EOE != 0
+ testscript:1:7: error: no introducer character in stdout regex redirect
+ EOE
+
+ : no-term-introducer
+ :
+ $* <'cmd >~/' 2>>EOE != 0
+ testscript:1:7: error: no closing introducer character in stdout regex redirect
+ EOE
+
+ : empty
+ :
+ $* <'cmd >~//' 2>>EOE != 0
+ testscript:1:7: error: stdout regex redirect is empty
+ EOE
+
+ : invalid-flags1
+ :
+ $* <'cmd >~/foo/z' 2>>EOE != 0
+ testscript:1:7: error: junk at the end of stdout regex redirect
+ EOE
+
+ : invalid-flags2
+ :
+ $* <'cmd >~/foo/iz' 2>>EOE != 0
+ testscript:1:7: error: junk at the end of stdout regex redirect
+ EOE
+
+ : malformed
+ :
+ $* <'cmd >~/*foo/' 2>>~/EOE/ != 0
+ /testscript:1:7: error: invalid stdout regex redirect.*/
+ info: regex: /*foo/
+ EOE
+
+ : without-flags
+ :
+ $* <'cmd >~/fo*/' >'cmd >~/fo*/'
+
+ : with-flags
+ :
+ $* <'cmd >~/fo*/i' >'cmd >~/fo*/i'
+
+ : no-newline
+ :
+ $* <'cmd >:~/fo*/' >'cmd >:~/fo*/'
+ }
+
+ : stderr
+ :
+ {
+ : missing
+ :
+ $* <'cmd 2>~' 2>>EOE != 0
+ testscript:1:8: error: missing stderr here-string regex
+ EOE
+
+ : no-introducer
+ :
+ : Note that there is no need to reproduce all the errors as for stdout.
+ : All we need is to make sure that the proper description is passed to
+ : the parse_regex() function.
+ :
+ $* <'cmd 2>~""' 2>>EOE != 0
+ testscript:1:8: error: no introducer character in stderr regex redirect
+ EOE
+ }
+}
+
+: here-doc
+:
+{
+ : stdout
+ :
+ {
+ : missing
+ :
+ $* <'cmd >>~' 2>>EOE != 0
+ testscript:1:8: error: expected here-document regex end marker
+ EOE
+
+ : unterminated-line-char
+ :
+ $* <<EOI 2>>EOE != 0
+ cmd >>~/EOO/
+ /
+ EOO
+ EOI
+ testscript:2:1: error: regex introducer without regex
+ info: consider changing regex introducer '/' in here-document end marker
+ EOE
+
+ : invalid-syntax-char
+ :
+ $* <<EOI 2>>EOE != 0
+ cmd >>~/EOO/
+ /x
+ EOO
+ EOI
+ testscript:2:1: error: invalid line-regex syntax character 'x'
+ EOE
+
+ : invalid-char-regex
+ :
+ $* <<EOI 2>>~/EOE/ != 0
+ cmd >>~/EOO/
+ /?foo/
+ EOO
+ EOI
+ /testscript:2:1: error: invalid regex.*/
+ EOE
+
+ : invalid-line-regex
+ :
+ $* <<EOI 2>>~/EOE/ != 0
+ cmd >>~/EOO/
+ /*
+ EOO
+ EOI
+ /testscript:3:1: error: invalid here-document regex.*/
+ EOE
+
+ : empty
+ :
+ $* <<EOI 2>>EOE != 0
+ cmd >>:~/EOO/
+ EOO
+ EOI
+ testscript:2:1: error: empty here-document regex
+ EOE
+
+ : valid
+ :
+ $* <<EOI >>EOO
+ cmd 2>>~/EOE/
+ foo
+ /?
+ /foo/
+ /foo/*
+ /foo/i
+ /foo/i*
+
+ //
+ //*
+ EOE
+ EOI
+ cmd 2>>~/EOE/
+ foo
+ /?
+ /foo/
+ /foo/*
+ /foo/i
+ /foo/i*
+
+ //
+ //*
+ EOE
+ EOO
+ }
+
+ : stderr
+ :
+ {
+ : missing
+ :
+ $* <'cmd 2>>~' 2>>EOE != 0
+ testscript:1:9: error: expected here-document regex end marker
+ EOE
+ }
+}
diff --git a/unit-tests/test/script/parser/scope.test b/unit-tests/test/script/parser/scope.test
index 24b4a42..7d78aca 100644
--- a/unit-tests/test/script/parser/scope.test
+++ b/unit-tests/test/script/parser/scope.test
@@ -1,23 +1,13 @@
-# @@ REGEX Note that the script can still fail on POSIX for space-containing
-# paths.
-#
-if ($cxx.target.class == "windows")
- q = "'"
-end
-
$* testscript <'cmd $@' >"cmd 1" # id-testscript
$* foo.test <'cmd $@' >"cmd foo/1" # id
-wd = [dir_path] $~;
-wd += test-driver;
-wd += 1;
-$* testscript <'cmd "$~"' >"cmd $q$wd$q" # wd-testscript
+: wd-testscript
+:
+$* testscript <'cmd "$~"' >~"%cmd '?.+[/\\\\]test-driver[/\\\\]1'?%"
-wd = [dir_path] $~;
-wd += test-driver;
-wd += foo;
-wd += 1;
-$* foo.test <'cmd "$~"' >"cmd $q$wd$q" # wd
+: wd
+:
+$* foo.test <'cmd "$~"' >~"%cmd '?.+[/\\\\]test-driver[/\\\\]foo[/\\\\]1'?%"
$* -s <<EOI # group-empty
{
diff --git a/unit-tests/test/script/regex/driver.cxx b/unit-tests/test/script/regex/driver.cxx
index ca09048..5b3d648 100644
--- a/unit-tests/test/script/regex/driver.cxx
+++ b/unit-tests/test/script/regex/driver.cxx
@@ -129,6 +129,27 @@ main ()
assert (v1 == vc ({'1', '2', '2'}));
}
+ // Test line_string.
+ //
+ // @@ Add more tests.
+ //
+ // Note that the following code crashes if compiled with libc++ (LLVM bug
+ // #31454).
+ //
+ // @@ Probably we can overcome it by providing our own allocator for
+ // basic_string instantiation. The function allocate() could allocate some
+ // more elements that would be enough not to corrupt the memory (which
+ // push_back() does).
+ // @@ But maybe doesn't worth to bother as the bug seems to get assigned.
+ // @@ Heavily affects MacOS where clang++/libc++ is the default setup.
+ //
+ {
+ line_string s;
+ s.push_back (line_char ('0'));
+ s.push_back (line_char ('1'));
+ s.push_back (line_char ('2'));
+ }
+
// Test line_char_locale and ctype<line_char> (only non-trivial functions).
//
{