aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorKaren Arutyunov <karen@codesynthesis.com>2017-01-11 01:43:09 +0300
committerKaren Arutyunov <karen@codesynthesis.com>2017-01-19 17:56:07 +0300
commita83f3866667bca073c4d4c5d80b4deb5ac05906c (patch)
tree479464203f6be4535c8f165a20d21322a88a2751
parentba99b60aeb8ccdeffc777589b99728395cd28f95 (diff)
Add support for portable path modifer and dot character escaping inversion
-rw-r--r--build2/buildfile2
-rw-r--r--build2/test/script/parser26
-rw-r--r--build2/test/script/parser.cxx386
-rw-r--r--build2/test/script/regex35
-rw-r--r--build2/test/script/regex.cxx81
-rw-r--r--build2/test/script/regex.ixx35
-rw-r--r--build2/test/script/runner2
-rw-r--r--build2/test/script/runner.cxx376
-rw-r--r--build2/test/script/script74
-rw-r--r--build2/test/script/script.cxx115
-rw-r--r--tests/function/path/testscript6
-rw-r--r--tests/test/script-integration/testscript23
-rw-r--r--tests/test/script/builtin/cat.test4
-rw-r--r--tests/test/script/builtin/mkdir.test8
-rw-r--r--tests/test/script/builtin/rm.test8
-rw-r--r--tests/test/script/runner/buildfile2
-rw-r--r--tests/test/script/runner/cleanup.test60
-rw-r--r--tests/test/script/runner/driver.cxx1
-rw-r--r--tests/test/script/runner/redirect.test684
-rw-r--r--tests/test/script/runner/regex.test321
-rw-r--r--tests/test/script/runner/status.test18
-rw-r--r--tests/value/reverse.test2
-rw-r--r--unit-tests/test/script/parser/driver.cxx7
-rw-r--r--unit-tests/test/script/parser/redirect.test76
-rw-r--r--unit-tests/test/script/parser/regex.test115
-rw-r--r--unit-tests/test/script/regex/driver.cxx63
26 files changed, 1813 insertions, 717 deletions
diff --git a/build2/buildfile b/build2/buildfile
index 6d497ca..84e2f82 100644
--- a/build2/buildfile
+++ b/build2/buildfile
@@ -88,7 +88,7 @@ exe{b}: \
test/script/{hxx cxx}{ builtin } \
test/script/{hxx cxx}{ lexer } \
test/script/{hxx cxx}{ parser } \
-test/script/{hxx cxx}{ regex } \
+test/script/{hxx ixx cxx}{ regex } \
test/script/{hxx cxx}{ runner } \
test/script/{hxx ixx cxx}{ script } \
test/script/{hxx cxx}{ token } \
diff --git a/build2/test/script/parser b/build2/test/script/parser
index 9ad5fe9..edd64a3 100644
--- a/build2/test/script/parser
+++ b/build2/test/script/parser
@@ -12,7 +12,6 @@
#include <build2/diagnostics>
#include <build2/test/script/token>
-#include <build2/test/script/regex>
#include <build2/test/script/script>
namespace build2
@@ -111,7 +110,7 @@ namespace build2
// Regex global flags. Meaningful if regex != '\0'.
//
- regex::char_flags regex_flags;
+ string regex_flags;
};
using here_docs = vector<here_doc>;
@@ -125,12 +124,29 @@ namespace build2
parse_here_documents (token&, token_type&,
pair<command_expr, here_docs>&);
- pair<string, regex::line_regex>
+ struct parsed_doc
+ {
+ union
+ {
+ string str; // Here-document literal.
+ regex_lines regex; // Here-document regex.
+ };
+
+ bool re; // True if regex.
+ uint64_t end_line; // Here-document end marker location.
+ uint64_t end_column;
+
+ parsed_doc (string, uint64_t line, uint64_t column);
+ parsed_doc (regex_lines, uint64_t line, uint64_t column);
+ parsed_doc (parsed_doc&&); // Note: move constuctible-only type.
+ ~parsed_doc ();
+ };
+
+ parsed_doc
parse_here_document (token&, token_type&,
const string&,
const string& mode,
- char regex_introducer, // '\0' if not a regex.
- regex::char_flags);
+ char re_intro); // '\0' if not a regex.
// 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 f381118..4b1c777 100644
--- a/build2/test/script/parser.cxx
+++ b/build2/test/script/parser.cxx
@@ -5,7 +5,6 @@
#include <build2/test/script/parser>
#include <sstream>
-#include <cstring> // strstr()
#include <build2/scheduler>
@@ -14,39 +13,6 @@
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& o, const regex_error& e)
- {
- const char* d (e.what ());
-
-#if defined(_MSC_VER) && _MSC_VER <= 1910
- const char* rd (strstr (d, "): "));
- if (rd != nullptr)
- d = rd + 3;
-#endif
-
- ostringstream os;
- os << runtime_error (d); // Sanitize the description.
-
- string s (os.str ());
- if (s.find (' ') != string::npos)
- o << ": " << s;
-
- return o;
- }
-}
-
namespace build2
{
namespace test
@@ -1340,23 +1306,22 @@ namespace build2
// 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
+ // characters from the {di} set, for example '/foo/id') 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}
+ char intro;
+ string flags; // Combination of characters from {di} set.
// Create a special empty object.
//
- regex_parts ()
- : introducer ('\0'), flags (regex::char_flags ()) {}
+ regex_parts (): intro ('\0') {}
- regex_parts (string v, char i, regex::char_flags f)
- : value (move (v)), introducer (i), flags (f) {}
+ regex_parts (string v, char i, string f)
+ : value (move (v)), intro (i), flags (move (f)) {}
};
static regex_parts
@@ -1377,10 +1342,10 @@ namespace build2
if (rn == 0)
fail (l) << what << " is empty";
- bool icase (s[++p] == 'i'); // Note: s[++p] can be '\0' (no flags).
-
- if (icase)
- ++p;
+ // Find end-of-flags position.
+ //
+ size_t fp (++p); // Save flags starting position.
+ for (char c; (c = s[p]) == 'd' || c == 'i'; ++p) ;
// If string end is not reached then report invalid flags, unless
// end-of-parsing position is requested (which means regex is just a
@@ -1392,11 +1357,7 @@ namespace build2
if (end != nullptr)
*end = p;
- return regex_parts (string (s, 1, rn),
- s[0],
- icase
- ? regex::char_regex::icase
- : regex::char_flags ());
+ return regex_parts (string (s, 1, rn), s[0], string (s, fp, p - fp));
}
pair<command_expr, parser::here_docs> parser::
@@ -1419,6 +1380,27 @@ namespace build2
fail (l) << "stdout and stderr redirected to each other";
};
+ // Check that the introducer character differs from '/' if the
+ // portable path modifier is specified. Must be called before
+ // parse_regex() (see below) to make sure its diagnostics is
+ // meaningful.
+ //
+ // Note that the portable path modifier assumes '/' to be a valid
+ // regex character and so makes it indistinguishable from the
+ // terminating introducer.
+ //
+ auto check_regex_mod = [this] (const string& mod,
+ const string& re,
+ const location& l,
+ const char* what)
+ {
+ // Handles empty regex properly.
+ //
+ if (mod.find ('/') != string::npos && re[0] == '/')
+ fail (l) << "portable path modifier and '/' introducer in "
+ << what;
+ };
+
// Pending positions where the next word should go.
//
enum class pending
@@ -1449,7 +1431,8 @@ namespace build2
// Add the next word to either one of the pending positions or to
// program arguments by default.
//
- auto add_word = [&c, &p, &mod, this] (string&& w, const location& l)
+ auto add_word = [&c, &p, &mod, &check_regex_mod, this] (
+ string&& w, const location& l)
{
auto add_merge = [&l, this] (redirect& r, const string& w, int fd)
{
@@ -1468,18 +1451,16 @@ namespace build2
<< "file descriptor must be " << fd;
};
- auto add_here_str = [&mod] (redirect& r, string&& w)
+ auto add_here_str = [] (redirect& r, string&& w)
{
- if (mod.find (':') == string::npos)
+ if (r.modifiers.find (':') == string::npos)
w += '\n';
r.str = move (w);
};
- auto add_here_str_regex = [&l, &mod, this] (
+ auto add_here_str_regex = [&l, &check_regex_mod, this] (
redirect& r, int fd, string&& w)
{
- using namespace regex;
-
const char* what (nullptr);
switch (fd)
{
@@ -1487,36 +1468,23 @@ namespace build2
case 2: what = "stderr regex redirect"; break;
}
- line_pool pool;
- line_string s;
+ check_regex_mod (r.modifiers, w, l, what);
- 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;
- }
+ regex_parts rp (parse_regex (w, l, what));
- if (mod.find (':') == string::npos)
- {
- w += '\n';
- s += line_char ("", pool);
- }
+ regex_lines& re (r.regex);
+ re.intro = rp.intro;
- r.regex.str = move (w);
+ re.lines.emplace_back (
+ l.line, l.column, move (rp.value), move (rp.flags));
- // No special line-chars, so no way to try to create a malformed
- // expression, and so can't throw.
+ // Add final blank line unless suppressed.
//
- r.regex.regex = line_regex (move (s), move (pool));
+ // Note that the position is synthetic, but that's ok as we don't
+ // expect any diagnostics to refer this line.
+ //
+ if (r.modifiers.find (':') == string::npos)
+ re.lines.emplace_back (l.line, l.column, string (), false);
};
auto parse_path = [&l, this] (string&& w, const char* what) -> path
@@ -1539,7 +1507,7 @@ namespace build2
}
};
- auto add_file = [&mod, &parse_path] (redirect& r, int fd, string&& w)
+ auto add_file = [&parse_path] (redirect& r, int fd, string&& w)
{
const char* what (nullptr);
switch (fd)
@@ -1550,7 +1518,7 @@ namespace build2
}
r.file.path = parse_path (move (w), what);
- r.file.append = mod.find ('&') != string::npos;
+ r.file.append = r.modifiers.find ('&') != string::npos;
};
switch (p)
@@ -1771,6 +1739,11 @@ namespace build2
redirect& r (fd == 0 ? c.in : fd == 1 ? c.out : c.err);
r = redirect (rt);
+ // Don't move as still may be used for pending here-document end
+ // marker processing.
+ //
+ r.modifiers = mod;
+
switch (rt)
{
case redirect_type::none:
@@ -1974,6 +1947,8 @@ namespace build2
if (re)
{
+ check_regex_mod (mod, end, l, what);
+
r = parse_regex (end, l, what);
end = move (r.value); // The "cleared" end marker.
}
@@ -1984,7 +1959,7 @@ namespace build2
move (end),
qt == quote_type::single,
move (mod),
- r.introducer, r.flags});
+ r.intro, move (r.flags)});
break;
}
@@ -2099,7 +2074,7 @@ namespace build2
(t.qtype == quote_type::unquoted ||
t.qtype == quote_type::single),
move (mod),
- r.introducer, r.flags});
+ r.intro, move (r.flags)});
p = pending::none;
mod.clear ();
@@ -2396,54 +2371,43 @@ namespace build2
: lexer_mode::here_line_double);
next (t, tt);
- pair<string, regex::line_regex> v (
- parse_here_document (
- t, tt, h.end, h.modifiers, h.regex, h.regex_flags));
+ parsed_doc v (
+ parse_here_document (t, tt, h.end, h.modifiers, h.regex));
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);
- if (h.regex)
+ if (v.re)
{
- 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';
+ r.regex = move (v.regex);
+ r.regex.flags = move (h.regex_flags);
}
else
- {
- r.str = move (v.first);
- r.end = move (h.end);
- }
+ r.str = move (v.str);
+
+ r.end = move (h.end);
+ r.end_line = v.end_line;
+ r.end_column = v.end_column;
}
expire_mode ();
}
}
- pair<string, regex::line_regex> parser::
+ parser::parsed_doc parser::
parse_here_document (token& t, type& tt,
const string& em,
const string& mod,
- char re,
- regex::char_flags refl)
+ char re)
{
// enter: first token on first line
// leave: newline (after end marker)
- using namespace regex;
-
- string rs; // String or regex literal.
+ string rs; // String literal.
- line_pool pool;
- line_string ls;
- line_regex rre;
+ regex_lines rre;
// Here-documents can be indented. The leading whitespaces of the end
// marker line (called strip prefix) determine the indentation. Every
@@ -2465,8 +2429,7 @@ namespace build2
// 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.
+ // beginning of the end marker.
//
location l;
@@ -2543,97 +2506,93 @@ namespace build2
}
}
- // Add newline after previous line.
- //
- if (!rs.empty ())
- rs += '\n';
-
- rs += s;
+ if (!re)
+ {
+ // Add newline after previous line.
+ //
+ if (!rs.empty ())
+ rs += '\n';
- if (re)
+ rs += s;
+ }
+ else
{
- if (s[0] == re) // Line starts with the regex introducer.
+ // Due to expansion we can end up with multiple lines. If empty
+ // then will add a blank textual literal.
+ //
+ for (size_t p (0); p != string::npos; )
{
- size_t n (s.size ());
+ string ln;
+ size_t np (s.find ('\n', p));
- // 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)
+ if (np != 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;
+ ln = string (s, p, np - p);
+ p = np + 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;
+ ln = string (s, p);
+ p = np;
+ }
- // Empty regex is a special case repesenting the blank line.
+ if (ln[0] != re) // Line doesn't start with regex introducer.
+ {
+ // This is a line-char literal (covers blank lines as well).
//
- if (p == 1)
+ // Append textual literal.
+ //
+ rre.lines.emplace_back (l.line, l.column, move (ln), false);
+ }
+ else // Line starts with the regex introducer.
+ {
+ // This is a char-regex, or a sequence of line-regex syntax
+ // characters or both (in this specific order). So we will
+ // add regex (with optional special characters) or special
+ // literal.
+ //
+ size_t p (ln.find (re, 1));
+ if (p == string::npos)
{
- c = line_char ("", pool);
- ++p;
+ // No regex, just a sequence of syntax characters.
+ //
+ string spec (ln, 1);
+ if (spec.empty ())
+ fail (l) << "no syntax line characters";
+
+ // Append special literal.
+ //
+ rre.lines.emplace_back (
+ l.line, l.column, move (spec), true);
}
else
{
- // Can't fail as all the pre-conditions verified (non-empty
- // with both introducers in place), so no description
- // required.
+ // Regex (probably with syntax characters).
//
- regex_parts re (parse_regex (s, l, "", &p));
+ regex_parts re;
- 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.
+ // Empty regex is a special case repesenting a blank line.
+ //
+ if (p == 1)
+ // Position to optional specal characters of an empty
+ // regex.
//
- fail (l) << "invalid regex" << e;
- }
- }
-
- ls += c;
- }
+ ++p;
+ else
+ // Can't fail as all the pre-conditions verified
+ // (non-empty with both introducers in place), so no
+ // description required.
+ //
+ re = parse_regex (ln, l, "", &p);
- 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
- << "'";
+ // Append regex with optional special characters.
+ //
+ rre.lines.emplace_back (l.line, l.column,
+ move (re.value), move (re.flags),
+ string (ln, p));
+ }
}
}
- else
- // Line doesn't start with regex introducer. Add line-char
- // literal (handles blank lines as well).
- //
- ls += line_char (move (s), pool);
}
}
@@ -2695,35 +2654,31 @@ namespace build2
//
if (mod.find (':') == string::npos)
{
- rs += '\n';
-
if (re)
- ls += line_char ("", pool);
+ // Note that the position is synthetic, but that's ok as we don't
+ // expect any diagnostics to refer this line.
+ //
+ rre.lines.emplace_back (l.line, l.column, string (), false);
+ else
+ rs += '\n';
}
- // Parse line-regex.
+ // Finalize regex lines.
//
if (re)
{
// Empty regex matches nothing, so not of much use.
//
- if (ls.empty ())
+ if (rre.lines.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;
- }
+ rre.intro = re;
}
}
- return make_pair (move (rs), move (rre));
+ return re
+ ? parsed_doc (move (rre), l.line, l.column)
+ : parsed_doc (move (rs), l.line, l.column);
}
//
@@ -3184,6 +3139,39 @@ namespace build2
lexer_ = l;
base_parser::lexer_ = l;
}
+
+ // parser::parsed_doc
+ //
+ parser::parsed_doc::
+ parsed_doc (string s, uint64_t l, uint64_t c)
+ : str (move (s)), re (false), end_line (l), end_column (c)
+ {
+ }
+
+ parser::parsed_doc::
+ parsed_doc (regex_lines r, uint64_t l, uint64_t c)
+ : regex (move (r)), re (true), end_line (l), end_column (c)
+ {
+ }
+
+ parser::parsed_doc::
+ parsed_doc (parsed_doc&& d)
+ : re (d.re), end_line (d.end_line), end_column (d.end_column)
+ {
+ if (re)
+ new (&regex) regex_lines (move (d.regex));
+ else
+ new (&str) string (move (d.str));
+ }
+
+ parser::parsed_doc::
+ ~parsed_doc ()
+ {
+ if (re)
+ regex.~regex_lines ();
+ else
+ str.~string ();
+ }
}
}
}
diff --git a/build2/test/script/regex b/build2/test/script/regex
index 7708410..b25c1f1 100644
--- a/build2/test/script/regex
+++ b/build2/test/script/regex
@@ -24,8 +24,25 @@ namespace build2
namespace regex
{
using char_string = std::basic_string<char>;
- using char_regex = std::basic_regex<char>;
- using char_flags = char_regex::flag_type;
+
+ enum class char_flags: std::uint16_t
+ {
+ icase = 0x1, // Case-insensitive match.
+ idot = 0x2, // Invert '.' escaping.
+
+ none = 0
+ };
+
+ // Restricts valid standard flags to just {icase}, extends with custom
+ // flags {idot}.
+ //
+ class char_regex: public std::basic_regex<char>
+ {
+ public:
+ using base_type = std::basic_regex<char>;
+
+ char_regex (const char_string&, char_flags = char_flags::none);
+ };
// Newlines are line separators and are not part of the line:
//
@@ -110,7 +127,7 @@ namespace build2
//
// 0 (nul character)
// -1 (EOF)
- // [()|.*+?{\}0123456789,=!] (excluding [])
+ // [()|.*+?{}\0123456789,=!] (excluding [])
//
// Note that the constructor is implicit to allow basic_regex to
// implicitly construct line_chars from special char literals (in
@@ -252,9 +269,8 @@ namespace build2
template <typename T>
struct line_char_cmp
: public std::enable_if<std::is_integral<T>::value ||
- std::is_enum<T>::value>
- {
- };
+ (std::is_enum<T>::value &&
+ !std::is_same<T, char_flags>::value)> {};
template <typename T, typename = typename line_char_cmp<T>::type>
bool
@@ -655,14 +671,13 @@ namespace build2
line_regex& operator= (const line_regex&) = delete;
public:
- // Mutable since input line_char literals must go into the same
- // pool (and thus is MT-unsafe).
- //
- mutable line_pool pool;
+ line_pool pool;
};
}
}
}
}
+#include <build2/test/script/regex.ixx>
+
#endif // BUILD2_TEST_SCRIPT_REGEX
diff --git a/build2/test/script/regex.cxx b/build2/test/script/regex.cxx
index bbf3f00..48e1eeb 100644
--- a/build2/test/script/regex.cxx
+++ b/build2/test/script/regex.cxx
@@ -171,6 +171,87 @@ namespace build2
new std::ctype<line_char> ()) // Hidden by ctype bitmask.
{
}
+
+ // char_regex
+ //
+ // Transform regex according to the extended flags {idot}. If regex is
+ // malformed then keep transforming, so the resulting string is
+ // malformed the same way. We expect the error to be reported by the
+ // char_regex ctor.
+ //
+ static string
+ transform (const string& s, char_flags f)
+ {
+ assert ((f & char_flags::idot) != char_flags::none);
+
+ string r;
+ bool escape (false);
+ bool cclass (false);
+
+ for (char c: s)
+ {
+ // Inverse escaping for a dot which is out of the char class
+ // brackets.
+ //
+ bool inverse (c == '.' && !cclass);
+
+ // Handle the escape case. Note that we delay adding the backslash
+ // since we may have to inverse things.
+ //
+ if (escape)
+ {
+ if (!inverse)
+ r += '\\';
+
+ r += c;
+ escape = false;
+
+ continue;
+ }
+ else if (c == '\\')
+ {
+ escape = true;
+ continue;
+ }
+
+ // Keep track of being inside the char class brackets, escape if
+ // inversion. Note that we never inverse square brackets.
+ //
+ if (c == '[' && !cclass)
+ cclass = true;
+ else if (c == ']' && cclass)
+ cclass = false;
+ else if (inverse)
+ r += '\\';
+
+ r += c;
+ }
+
+ if (escape) // Regex is malformed but that's not our problem.
+ r += '\\';
+
+ return r;
+ }
+
+ static char_regex::flag_type
+ to_std_flags (char_flags f)
+ {
+ // Note that ECMAScript flag is implied in the absense of a grammar
+ // flag.
+ //
+ return (f & char_flags::icase) != char_flags::none
+ ? char_regex::icase
+ : char_regex::flag_type ();
+ }
+
+ char_regex::
+ char_regex (const char_string& s, char_flags f)
+ : base_type ((f & char_flags::idot) != char_flags::none
+ ? transform (s, f)
+ : s,
+ to_std_flags (f))
+ {
+ }
}
}
}
diff --git a/build2/test/script/regex.ixx b/build2/test/script/regex.ixx
new file mode 100644
index 0000000..4073312
--- /dev/null
+++ b/build2/test/script/regex.ixx
@@ -0,0 +1,35 @@
+// file : build2/test/script/regex.ixx -*- C++ -*-
+// copyright : Copyright (c) 2014-2017 Code Synthesis Ltd
+// license : MIT; see accompanying LICENSE file
+
+namespace build2
+{
+ namespace test
+ {
+ namespace script
+ {
+ namespace regex
+ {
+ inline char_flags
+ operator&= (char_flags& x, char_flags y)
+ {
+ return x = static_cast<char_flags> (
+ static_cast<uint16_t> (x) & static_cast<uint16_t> (y));
+ }
+
+ inline char_flags
+ operator|= (char_flags& x, char_flags y)
+ {
+ return x = static_cast<char_flags> (
+ static_cast<uint16_t> (x) | static_cast<uint16_t> (y));
+ }
+
+ inline char_flags
+ operator& (char_flags x, char_flags y) {return x &= y;}
+
+ inline char_flags
+ operator| (char_flags x, char_flags y) {return x |= y;}
+ }
+ }
+ }
+}
diff --git a/build2/test/script/runner b/build2/test/script/runner
index 7b932b9..56ea834 100644
--- a/build2/test/script/runner
+++ b/build2/test/script/runner
@@ -16,7 +16,7 @@ namespace build2
{
namespace test
{
- class common;
+ struct common;
namespace script
{
diff --git a/build2/test/script/runner.cxx b/build2/test/script/runner.cxx
index 522dedd..8e31cf8 100644
--- a/build2/test/script/runner.cxx
+++ b/build2/test/script/runner.cxx
@@ -5,7 +5,9 @@
#include <build2/test/script/runner>
#include <set>
-#include <ios> // streamsize
+#include <ios> // streamsize
+#include <cstring> // strstr()
+#include <sstream>
#include <butl/fdstream> // fdopen_mode, fdnull(), fddup()
@@ -13,11 +15,45 @@
#include <build2/test/common>
+#include <build2/test/script/regex>
#include <build2/test/script/builtin>
using namespace std;
using namespace butl;
+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& o, const regex_error& e)
+ {
+ const char* d (e.what ());
+
+#if defined(_MSC_VER) && _MSC_VER <= 1910
+ const char* rd (strstr (d, "): "));
+ if (rd != nullptr)
+ d = rd + 3;
+#endif
+
+ ostringstream os;
+ os << runtime_error (d); // Sanitize the description.
+
+ string s (os.str ());
+ if (s.find (' ') != string::npos)
+ o << ": " << s;
+
+ return o;
+ }
+}
+
namespace build2
{
namespace test
@@ -99,6 +135,66 @@ namespace build2
}
}
+ // Save a string to the file. Fail if exception is thrown by underlying
+ // operations.
+ //
+ static void
+ save (const path& p, const string& s, const location& ll)
+ {
+ try
+ {
+ ofdstream os (p);
+ os << s;
+ os.close ();
+ }
+ catch (const io_error& e)
+ {
+ fail (ll) << "unable to write " << p << ": " << e;
+ }
+ }
+
+ // Transform string according to here-* redirect modifiers from the {/}
+ // set.
+ //
+ static string
+ transform (const string& s,
+ bool regex,
+ const string& modifiers,
+ const script& scr)
+ {
+ if (modifiers.find ('/') == string::npos)
+ return s;
+
+ // For targets other than Windows leave the string intact.
+ //
+ if (cast<target_triplet> (scr.test_target["test.target"]).class_ !=
+ "windows")
+ return s;
+
+ // Convert forward slashes to Windows path separators (escape for
+ // regex).
+ //
+ string r;
+ for (size_t p (0);;)
+ {
+ size_t sp (s.find ('/', p));
+
+ if (sp != string::npos)
+ {
+ r.append (s, p, sp - p);
+ r.append (regex ? "\\\\" : "\\");
+ p = sp + 1;
+ }
+ else
+ {
+ r.append (s, p, sp);
+ break;
+ }
+ }
+
+ return r;
+ }
+
// Check if the test command output matches the expected result (redirect
// value). Noop for redirect types other than none, here_*.
//
@@ -140,23 +236,6 @@ namespace build2
{
assert (!op.empty ());
- // 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 (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 " << opp << ": " << e;
- }
-
auto output_info = [&what, &ll] (diag_record& d,
const path& p,
const char* prefix = "",
@@ -168,13 +247,223 @@ namespace build2
d << info << prefix << what << suffix << " is empty";
};
- if (re)
+ if (re) // Match the output with the regex.
{
- // Match the output with the line_regex. That requires to parse the
- // output into the line_string of literals first.
+ // 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);
+
+ // 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)
+ {
+ case 'd': r |= char_flags::idot; break;
+ case 'i': r |= char_flags::icase; break;
+ default: assert (false); // Error so should have been checked.
+ }
+ }
+
+ return r;
+ };
+
+ // 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),
+ {
+ 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);
+
+ 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
+ {
+ 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
+ {
+ path rp (op + ".regex");
+
+ // 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);
+ }
+
+ 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).
+ {
+ line_char c;
+
+ // 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) << "'";
+ }
+ }
+
+ 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);
+ }
+
+ 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) << "'";
+ }
+ }
+
+ // Create line regex.
+ //
+ line_regex regex;
+
+ try
+ {
+ regex = line_regex (move (rls), move (pool));
+ }
+ 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);
+
+ diag_record d (fail (loc (rd.end_line, rd.end_column)));
+
+ // Print regex_error description if meaningful.
+ //
+ d << "invalid " << what << " regex redirect" << e;
+
+ output_info (d, save_regex (), "", " regex");
+ }
+
+ // Parse the output into the literal line string.
+ //
line_string ls;
try
@@ -212,7 +501,7 @@ namespace build2
while (!s.empty () && s.back () == '\r')
s.pop_back ();
- ls += line_char (move (s), rd.regex.regex.pool);
+ ls += line_char (move (s), regex.pool);
}
}
catch (const io_error& e)
@@ -220,7 +509,9 @@ namespace build2
fail (ll) << "unable to read " << op << ": " << e;
}
- if (regex_match (ls, rd.regex.regex)) // Doesn't throw.
+ // Match the output with the regex.
+ //
+ if (regex_match (ls, regex)) // Doesn't throw.
return;
// Output doesn't match the regex.
@@ -229,16 +520,20 @@ namespace build2
d << pr << " " << what << " doesn't match the regex";
output_info (d, op);
- output_info (d, opp, "", " regex");
+ output_info (d, save_regex (), "", " regex");
input_info (d);
// Fall through.
//
}
- else
+ else // Compare the output with the expected result.
{
- // Use diff utility to compare the output with the expected result.
+ // Use diff utility for the comparison.
//
+ 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));
@@ -246,7 +541,7 @@ namespace build2
pp.recall_string (),
"--strip-trailing-cr", // Is essential for cross-testing.
"-u",
- opp.string ().c_str (),
+ eop.string ().c_str (),
op.string ().c_str (),
nullptr};
@@ -288,7 +583,7 @@ namespace build2
d << pr << " " << what << " doesn't match the expected output";
output_info (d, op);
- output_info (d, opp, "expected ");
+ output_info (d, eop, "expected ");
output_info (d, ep, "", " diff");
input_info (d);
@@ -589,17 +884,9 @@ namespace build2
//
isp = std_path ("stdin");
- try
- {
- ofdstream os (isp);
- sp.clean ({cleanup_type::always, isp}, true);
- os << c.in.str;
- os.close ();
- }
- catch (const io_error& e)
- {
- fail (ll) << "unable to write " << isp << ": " << e;
- }
+ const redirect& r (c.in);
+ save (isp, transform (r.str, false, r.modifiers, *sp.root), ll);
+ sp.clean ({cleanup_type::always, isp}, true);
open_stdin ();
break;
@@ -767,12 +1054,7 @@ namespace build2
{
// Execute the process.
//
- // Pre-search the program path so it is reflected in the failure
- // diagnostics. The user can see the original path running the test
- // operation with the verbosity level > 2.
- //
- process_path pp (run_search (c.program, true));
- cstrings args {pp.recall_string ()};
+ cstrings args {c.program.string ().c_str ()};
for (const auto& a: c.arguments)
args.push_back (a.c_str ());
@@ -781,6 +1063,8 @@ namespace build2
try
{
+ process_path pp (process::path_search (args[0]));
+
if (verb >= 2)
print_process (args);
@@ -798,7 +1082,7 @@ namespace build2
}
catch (const process_error& e)
{
- error (ll) << "unable to execute " << pp << ": " << e;
+ error (ll) << "unable to execute " << args[0] << ": " << e;
if (e.child ())
std::exit (1);
diff --git a/build2/test/script/script b/build2/test/script/script
index 0144af7..bb9b074 100644
--- a/build2/test/script/script
+++ b/build2/test/script/script
@@ -15,7 +15,6 @@
#include <build2/test/target>
#include <build2/test/script/token> // replay_tokens
-#include <build2/test/script/regex>
namespace build2
{
@@ -78,16 +77,64 @@ namespace build2
file
};
+ // Pre-parsed (but not instantiated) regex lines. The idea here is that
+ // we should be able to re-create their (more or less) exact text
+ // representation for diagnostics but also instantiate without any
+ // re-parsing.
+ //
+ struct regex_line
+ {
+ // If regex is true, then value is the regex expression. Otherwise, it
+ // is a literal. Note that special characters can be present in both
+ // cases. For example, //+ is a regex, while /+ is a literal, both
+ // with '+' as a special character. Flags are only valid for regex.
+ // Literals falls apart into textual (has no special characters) and
+ // special (has just special characters instead) ones. For example
+ // foo is a textual literal, while /.+ is a special one. Note that
+ // literal must not have value and special both non-empty.
+ //
+ bool regex;
+
+ string value;
+ string flags;
+ string special;
+
+ uint64_t line;
+ uint64_t column;
+
+ // Create regex with optional special characters.
+ //
+ regex_line (uint64_t l, uint64_t c,
+ string v, string f, string s = string ())
+ : regex (true),
+ value (move (v)),
+ flags (move (f)),
+ special (move (s)),
+ line (l),
+ column (c) {}
+
+ // Create a literal, either text or special.
+ //
+ regex_line (uint64_t l, uint64_t c, string v, bool s)
+ : regex (false),
+ value (s ? string () : move (v)),
+ special (s ? move (v) : string ()),
+ line (l),
+ column (c) {}
+ };
+
+ struct regex_lines
+ {
+ char intro; // Introducer character.
+ string flags; // Global flags (here-document).
+
+ small_vector<regex_line, 8> lines;
+ };
+
struct redirect
{
redirect_type type;
- struct regex_type
- {
- regex::line_regex regex;
- string str; // String representation for printing.
- };
-
struct file_type
{
using path_type = build2::path;
@@ -97,13 +144,16 @@ namespace build2
union
{
- int fd; // Merge-to descriptor.
- string str; // Note: includes trailing newline, if requested.
- regex_type regex; // Note: includes trailing blank, if requested.
- file_type file;
+ int fd; // Merge-to descriptor.
+ string str; // Note: includes trailing newline, if requested.
+ regex_lines regex; // Note: includes trailing blank, if requested.
+ file_type file;
};
- string end; // Here-document end marker for printing.
+ string modifiers; // Redirect modifiers.
+ string end; // Here-document end marker (no regex intro/flags).
+ uint64_t end_line; // Here-document end marker location.
+ uint64_t end_column;
explicit
redirect (redirect_type = redirect_type::none);
diff --git a/build2/test/script/script.cxx b/build2/test/script/script.cxx
index 2a34f66..6f56661 100644
--- a/build2/test/script/script.cxx
+++ b/build2/test/script/script.cxx
@@ -85,44 +85,65 @@ namespace build2
case redirect_type::merge: o << '&' << r.fd; break;
case redirect_type::here_str_literal:
- case redirect_type::here_str_regex:
+ case redirect_type::here_doc_literal:
{
- 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');
+ bool doc (r.type == redirect_type::here_doc_literal);
- if (!nl)
- o << ':';
+ // For here-document add another '>' or '<'. Note that here end
+ // marker never needs to be quoted.
+ //
+ if (doc)
+ o << d;
- if (re)
- o << '~';
+ o << r.modifiers;
+
+ if (doc)
+ o << r.end;
+ else
+ {
+ const string& v (r.str);
+ to_stream_q (o,
+ r.modifiers.find (':') == string::npos
+ ? string (v, 0, v.size () - 1) // Strip newline.
+ : v);
+ }
- to_stream_q (o, nl ? string (v, 0, v.size () - 1) : v);
break;
}
- case redirect_type::here_doc_literal:
+
+ case redirect_type::here_str_regex:
case redirect_type::here_doc_regex:
{
- 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');
+ bool doc (r.type == redirect_type::here_doc_regex);
- // Add another '>' or '<'. Note that here end marker never
- // needs to be quoted.
+ // For here-document add another '>' or '<'. Note that here end
+ // marker never needs to be quoted.
//
- o << d << (nl ? "" : ":");
+ if (doc)
+ o << d;
+
+ o << r.modifiers;
+
+ const regex_lines& re (r.regex);
+
+ if (doc)
+ o << re.intro + r.end + re.intro + re.flags;
+ else
+ {
+ assert (!re.lines.empty ()); // Regex can't be empty.
- if (re)
- o << '~';
+ regex_line l (re.lines[0]);
+ to_stream_q (o, re.intro + l.value + re.intro + l.flags);
+ }
- to_stream_q (o, r.end);
break;
}
+
case redirect_type::file:
{
// Add '>>' or '<<' (and so make it '<<<' or '>>>').
//
- o << d << d << (r.file.append ? "&" : "");
+ o << d << d << r.modifiers;
print_path (r.file.path);
break;
}
@@ -131,16 +152,36 @@ namespace build2
auto print_doc = [&o] (const redirect& r)
{
- 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;
- // 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 (r.type == redirect_type::here_doc_literal)
+ o << r.str;
+ else
+ {
+ assert (r.type == redirect_type::here_doc_regex);
+
+ const regex_lines& rl (r.regex);
+
+ for (auto b (rl.lines.cbegin ()), i (b), e (rl.lines.cend ());
+ i != e; ++i)
+ {
+ if (i != b)
+ o << endl;
+
+ const regex_line& l (*i);
+
+ if (l.regex) // Regex (possibly empty),
+ o << rl.intro << l.value << rl.intro << l.flags;
+ else if (!l.special.empty ()) // Special literal.
+ o << rl.intro;
+ else // Textual literal.
+ o << l.value;
+
+ o << l.special;
+ }
+ }
+
+ o << (r.modifiers.find (':') == string::npos ? "" : "\n") << r.end;
};
if ((m & command_to_stream::header) == command_to_stream::header)
@@ -268,7 +309,11 @@ namespace build2
case redirect_type::here_doc_literal: new (&str) string (); break;
case redirect_type::here_str_regex:
- case redirect_type::here_doc_regex: new (&regex) regex_type (); break;
+ case redirect_type::here_doc_regex:
+ {
+ new (&regex) regex_lines ();
+ break;
+ }
case redirect_type::file: new (&file) file_type (); break;
}
@@ -276,7 +321,11 @@ namespace build2
redirect::
redirect (redirect&& r)
- : type (r.type), end (move (r.end))
+ : type (r.type),
+ modifiers (move (r.modifiers)),
+ end (move (r.end)),
+ end_line (r.end_line),
+ end_column (r.end_column)
{
switch (type)
{
@@ -295,7 +344,7 @@ namespace build2
case redirect_type::here_str_regex:
case redirect_type::here_doc_regex:
{
- new (&regex) regex_type (move (r.regex));
+ new (&regex) regex_lines (move (r.regex));
break;
}
case redirect_type::file:
@@ -320,7 +369,7 @@ namespace build2
case redirect_type::here_doc_literal: str.~string (); break;
case redirect_type::here_str_regex:
- case redirect_type::here_doc_regex: regex.~regex_type (); break;
+ case redirect_type::here_doc_regex: regex.~regex_lines (); break;
case redirect_type::file: file.~file_type (); break;
}
diff --git a/tests/function/path/testscript b/tests/function/path/testscript
index 4621ba4..bfbc895 100644
--- a/tests/function/path/testscript
+++ b/tests/function/path/testscript
@@ -4,7 +4,7 @@
.include ../../common.test
-s = ($cxx.target.class != windows ? '/' : '\')
+s = ($cxx.target.class != 'windows' ? '/' : '\')
: canonicalize
:
@@ -29,7 +29,7 @@ s = ($cxx.target.class != windows ? '/' : '\')
: actualize
:
- if ($cxx.target.class == windows)
+ if ($cxx.target.class == 'windows')
{
mkdir Foo;
$* <'print $path.normalize($out_base/foo, true)' >~'/.+\\Foo/'
@@ -38,7 +38,7 @@ s = ($cxx.target.class != windows ? '/' : '\')
: invalid-path
:
-p = ($cxx.target.class != windows ? /../foo : 'c:/../foo');
+p = ($cxx.target.class != 'windows' ? /../foo : 'c:/../foo');
$* <"\$path.normalize\('$p')" 2>>"EOE" != 0
error: invalid path: '$p'
EOE
diff --git a/tests/test/script-integration/testscript b/tests/test/script-integration/testscript
index 914ff30..f102f5b 100644
--- a/tests/test/script-integration/testscript
+++ b/tests/test/script-integration/testscript
@@ -11,18 +11,18 @@
: testscript-and-other
:
- $* <<EOI 2>>~%EOE% != 0
+ $* <<EOI 2>>/EOE != 0
./: test{../testscript ../foo}
EOI
- %error: both 'testscript' and other names specified for dir\{\.[/\\]\}%
+ 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\{\.[/\\]\}%
+ error: both 'testscript' and other names specified for dir{./}
EOE
: others
@@ -34,6 +34,9 @@
: wd-is-file
:
+: Note that here we can not use portable path modifier as not all slashes are
+: path separators.
+:
touch foo.test;
touch test;
$* <<EOI 2>>~%EOE% != 0
@@ -46,10 +49,10 @@ EOE
:
touch foo.test;
mkdir test &!test/;
-$* <<EOI 2>>~%EOE%
+$* <<EOI 2>>/EOE
./: test{foo}
EOI
-%warning: working directory test[/\\] exists at the beginning of the test%
+warning: working directory test/ exists at the beginning of the test
EOE
: wd-not-empty-before
@@ -57,10 +60,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 test[/\\] 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
@@ -74,8 +77,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 test[/\\] 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/builtin/cat.test b/tests/test/script/builtin/cat.test
index 89301cd..f5041fc 100644
--- a/tests/test/script/builtin/cat.test
+++ b/tests/test/script/builtin/cat.test
@@ -60,8 +60,8 @@ $b
: non-existent
:
$c <<EOI;
-cat in 2>>~%EOE% != 0
-%cat: unable to print '.+[/\\]test[/\\]cat[/\\]non-existent[/\\]test[/\\]1[/\\]in': .+%
+cat in 2>>/~%EOE% != 0
+%cat: unable to print '.+/test/cat/non-existent/test/1/in': .+%
EOE
EOI
$b
diff --git a/tests/test/script/builtin/mkdir.test b/tests/test/script/builtin/mkdir.test
index 8ad2714..ace4012 100644
--- a/tests/test/script/builtin/mkdir.test
+++ b/tests/test/script/builtin/mkdir.test
@@ -56,8 +56,8 @@ $b
: Test creation of an existing directory.
:
$c <<EOI;
-mkdir a a 2>>~%EOE% == 1
-%mkdir: unable to create directory '.+[/\\]test[/\\]mkdir[/\\]already-exists[/\\]test[/\\]1[/\\]a': .+%
+mkdir a a 2>>/~%EOE% == 1
+%mkdir: unable to create directory '.+/test/mkdir/already-exists/test/1/a': .+%
EOE
EOI
$b
@@ -67,8 +67,8 @@ $b
: Test creation of a directory with non-existent parent.
:
$c <<EOI;
-mkdir a/b 2>>~%EOE% == 1
-%mkdir: unable to create directory '.+[/\\]test[/\\]mkdir[/\\]not-exists[/\\]test[/\\]1[/\\]a[/\\]b': .+%
+mkdir a/b 2>>/~%EOE% == 1
+%mkdir: unable to create directory '.+/test/mkdir/not-exists/test/1/a/b': .+%
EOE
EOI
$b
diff --git a/tests/test/script/builtin/rm.test b/tests/test/script/builtin/rm.test
index cd4a922..a6de003 100644
--- a/tests/test/script/builtin/rm.test
+++ b/tests/test/script/builtin/rm.test
@@ -40,8 +40,8 @@
: Removing non-existing file fails.
:
$c <<EOI;
- rm a 2>>~%EOE% == 1
- %rm: unable to remove '.+[/\\]test[/\\]rm[/\\]file[/\\]not-exists[/\\]test[/\\]1[/\\]a': .+%
+ rm a 2>>/~%EOE% == 1
+ %rm: unable to remove '.+/test/rm/file/not-exists/test/1/a': .+%
EOE
EOI
$b
@@ -106,8 +106,8 @@
:
:
$c <<EOI;
- rm ../../a/b/c 2>>~%EOE% == 1
- %rm: '.+[/\\]path[/\\]outside-scope[/\\]a[/\\]b[/\\]c' is out of working directory '.+[/\\]outside-scope[/\\]test'%
+ rm ../../a/b/c 2>>/~%EOE% == 1
+ %rm: '.+/path/outside-scope/a/b/c' is out of working directory '.+/outside-scope/test'%
EOE
EOI
$b
diff --git a/tests/test/script/runner/buildfile b/tests/test/script/runner/buildfile
index 1ea8643..86c18e2 100644
--- a/tests/test/script/runner/buildfile
+++ b/tests/test/script/runner/buildfile
@@ -2,7 +2,7 @@
# copyright : Copyright (c) 2014-2017 Code Synthesis Ltd
# license : MIT; see accompanying LICENSE file
-./: test{cleanup redirect status} exe{driver} $b
+./: test{cleanup redirect regex status} exe{driver} $b
test{*}: target = exe{driver}
diff --git a/tests/test/script/runner/cleanup.test b/tests/test/script/runner/cleanup.test
index 3dfd73e..ed724ee 100644
--- a/tests/test/script/runner/cleanup.test
+++ b/tests/test/script/runner/cleanup.test
@@ -56,8 +56,8 @@ b += --no-column
: Test cleanup of non-existing file.
:
$c <'$* &a';
- $b 2>>~%EOE% != 0
- %testscript:1: error: registered for cleanup file test[/\\]1[/\\]a does not exist%
+ $b 2>>/EOE != 0
+ testscript:1: error: registered for cleanup file test/1/a does not exist
EOE
: out-wd
@@ -65,8 +65,8 @@ b += --no-column
: Test explicit cleanup of a file out of the testscript working directory.
:
$c <'$* &../../a';
- $b 2>>~%EOE% != 0
- %testscript:1: error: file cleanup \.\.[/\\]\.\.[/\\]a is out of working directory test[/\\]%
+ $b 2>>/EOE != 0
+ testscript:1: error: file cleanup ../../a is out of working directory test/
EOE
: in-wd
@@ -75,8 +75,8 @@ b += --no-column
: directory but inside the script working directory.
:
$c <'$* &../a';
- $b 2>>~%EOE% != 0
- %testscript:1: error: registered for cleanup file test[/\\]a does not exist%
+ $b 2>>/EOE != 0
+ testscript:1: error: registered for cleanup file test/a does not exist
EOE
: not-file
@@ -84,8 +84,8 @@ b += --no-column
: Test cleanup of a directory as a file.
:
$c <'$* -d a &a';
- $b 2>>~%EOE% != 0
- %error: unable to remove file test[/\\]1[/\\]a: .+%
+ $b 2>>/~%EOE% != 0
+ %error: unable to remove file test/1/a: .+%
EOE
}
@@ -119,8 +119,8 @@ b += --no-column
: Test cleanup of non-existing directory.
:
$c <'$* &a/';
- $b 2>>~%EOE% != 0
- %testscript:1: error: registered for cleanup directory test[/\\]1[/\\]a[/\\] does not exist%
+ $b 2>>/EOE != 0
+ testscript:1: error: registered for cleanup directory test/1/a/ does not exist
EOE
: out-wd
@@ -128,8 +128,8 @@ b += --no-column
: Test cleanup of a directory out of the testscript working directory.
:
$c <'$* &../../a/';
- $b 2>>~%EOE% != 0
- %testscript:1: error: directory cleanup \.\.[/\\]\.\.[/\\]a[/\\] is out of working directory test[/\\]%
+ $b 2>>/EOE != 0
+ testscript:1: error: directory cleanup ../../a/ is out of working directory test/
EOE
: in-wd
@@ -138,8 +138,8 @@ b += --no-column
: working directory but inside the testscript working directory.
:
$c <'$* &../a/';
- $b 2>>~%EOE% != 0
- %testscript:1: error: registered for cleanup directory test[/\\]a[/\\] does not exist%
+ $b 2>>/EOE != 0
+ testscript:1: error: registered for cleanup directory test/a/ does not exist
EOE
: not-empty
@@ -147,8 +147,8 @@ b += --no-column
: 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 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
@@ -156,8 +156,8 @@ b += --no-column
: Test cleanup of a file as a directory.
:
$c <'$* -f a &a/';
- $b 2>>~%EOE% != 0
- %error: unable to remove directory test[/\\]1[/\\]a[/\\]: .+%
+ $b 2>>/~%EOE% != 0
+ %error: unable to remove directory test/1/a/: .+%
EOE
}
@@ -179,8 +179,8 @@ b += --no-column
: Test cleanup of a wildcard not matching any directory.
:
$c <'$* &a/***';
- $b 2>>~%EOE% != 0
- %testscript:1: error: registered for cleanup wildcard 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
: out-wd
@@ -188,8 +188,8 @@ b += --no-column
: Test cleanup of a wildcard out of the testscript working directory.
:
$c <'$* &../../a/***';
- $b 2>>~%EOE% != 0
- %testscript:1: error: wildcard cleanup \.\.[/\\]\.\.[/\\]a[/\\]\*\*\* is out of working directory test[/\\]%
+ $b 2>>/EOE != 0
+ testscript:1: error: wildcard cleanup ../../a/*** is out of working directory test/
EOE
: in-wd
@@ -199,8 +199,8 @@ b += --no-column
: directory.
:
$c <'$* &../a/***';
- $b 2>>~%EOE% != 0
- %testscript:1: error: registered for cleanup wildcard 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
: not-dir
@@ -208,8 +208,8 @@ b += --no-column
: Test cleanup of a file as a wildcard.
:
$c <'$* -f a &a/***';
- $b 2>>~%EOE% != 0
- %error: unable to remove directory test[/\\]1[/\\]a[/\\]: .+%
+ $b 2>>/~%EOE% != 0
+ %error: unable to remove directory test/1/a/: .+%
EOE
}
@@ -225,8 +225,8 @@ $b
: 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 test[/\\]1[/\\] is not empty%
+$b 2>>/EOE != 0
+testscript:1: error: registered for cleanup directory test/1/ is not empty
EOE
: explicit-overwrite
@@ -237,6 +237,6 @@ $c <<EOO;
$* &!a;
$* -o foo >>>a
EOO
-$b 2>>~%EOE% != 0
-%testscript:2: error: registered for cleanup directory 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/driver.cxx b/tests/test/script/runner/driver.cxx
index 6921e4c..ff91926 100644
--- a/tests/test/script/runner/driver.cxx
+++ b/tests/test/script/runner/driver.cxx
@@ -72,6 +72,7 @@ main (int argc, char* argv[])
optional<int> status;
char aterm ('\0'); // Abnormal termination method.
+ cin.exceptions (ostream::failbit | ostream::badbit);
cout.exceptions (ostream::failbit | ostream::badbit);
cerr.exceptions (ostream::failbit | ostream::badbit);
diff --git a/tests/test/script/runner/redirect.test b/tests/test/script/runner/redirect.test
index 72e49b2..e2d765c 100644
--- a/tests/test/script/runner/redirect.test
+++ b/tests/test/script/runner/redirect.test
@@ -6,6 +6,9 @@
b += --no-column
+ps = ($cxx.target.class != 'windows' ? '/' : '\') # Path separator.
+psr = ($cxx.target.class != 'windows' ? '/' : '\\') # Path separator in regex.
+
: null
:
{
@@ -23,279 +26,440 @@ b += --no-column
: str
:
{
- : in
+ : literal
:
- $c <'$* -i 0 <foo';
- $b
+ {
+ : in
+ :
+ $c <'$* -i 0 <foo';
+ $b
- : out
- :
- $c <'$* -o foo >foo';
- $b
+ : out
+ :
+ $c <'$* -o foo >foo';
+ $b
- : err
- :
- $c <'$* -e foo 2>foo';
- $b
+ : err
+ :
+ $c <'$* -e foo 2>foo';
+ $b
- : inout
- :
- $c <'$* -i 1 <foo >foo';
- $b
+ : inout
+ :
+ $c <'$* -i 1 <foo >foo';
+ $b
- : inout-fail
- :
- $c <'$* -i 1 <foo >bar';
- $b 2>>~%EOE% != 0
- %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: stdout diff: test[/\\]1[/\\]stdout\.diff%
- % info: stdin: test[/\\]1[/\\]stdin%
- %--- .*%
- %\+\+\+ .*%
- %@@ .*%
- -bar
- +foo
- EOE
-
- : inerr
- :
- $c <'$* -i 2 <foo 2>foo';
- $b
+ : inout-fail
+ :
+ $c <'$* -i 1 <foo >bar';
+ $b 2>>/~%EOE%d != 0
+ %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: stdout diff: test/1/stdout.diff
+ info: stdin: test/1/stdin
+ %--- \.*%
+ %\+\+\+ \.*%
+ %@@ \.*%
+ -bar
+ +foo
+ EOE
- : inout-err
- :
- $c <'$* -i 1 -e bar <foo 1>foo 2>bar';
- $b
+ : inerr
+ :
+ $c <'$* -i 2 <foo 2>foo';
+ $b
- : empty
- :
- $c <'$* -o "" >""';
- $b
+ : inout-err
+ :
+ $c <'$* -i 1 -e bar <foo 1>foo 2>bar';
+ $b
- : no-newline
- :
- $c <'$* -i 1 <:"foo" >:"foo"';
- $b
+ : empty
+ :
+ $c <'$* -o "" >""';
+ $b
- : no-newline-empty
- :
- $c <'$* -i 1 <:"" >:""';
- $b
+ : no-newline
+ :
+ $c <'$* -i 1 <:"foo" >:"foo"';
+ $b
- : no-newline-fail1
+ : no-newline-empty
+ :
+ $c <'$* -i 1 <:"" >:""';
+ $b
+
+ : no-newline-fail1
+ :
+ $c <'$* -i 1 <:"foo" >"foo"';
+ $b 2>>~/EOE/ != 0
+ /testscript:1: error: .+driver(\.exe)? stdout doesn't match the expected output/
+ /.{7}
+ -foo
+ +foo
+ \ No newline at end of file
+ EOE
+
+ : no-newline-fail2
+ :
+ $c <'$* -i 1 <"foo" >:"foo"';
+ $b 2>>~/EOE/ != 0
+ /testscript:1: error: .+driver(\.exe)? stdout doesn't match the expected output/
+ /.{7}
+ -foo
+ \ No newline at end of file
+ +foo
+ EOE
+
+ : merge
+ :
+ $c <<EOI;
+ $* -o foo -e bar 2>>EOE 1>&2
+ foo
+ bar
+ EOE
+ EOI
+ $b
+
+ : portable-path
+ :
+ {
+ : in
+ :
+ $c <"\$* -i 1 </'foo/' >'foo$ps'";
+ $b
+
+ : out
+ :
+ $c <"\$* -i 1 <'foo$ps' >/'foo/'";
+ $b
+
+ : err
+ :
+ $c <"\$* -i 2 <'foo$ps' 2>/'foo/'";
+ $b
+ }
+ }
+
+ : regex
:
- $c <'$* -i 1 <:"foo" >"foo"';
- $b 2>>~/EOE/ != 0
- /testscript:1: error: .+driver(\.exe)? stdout doesn't match the expected output/
- /.{7}
- -foo
- +foo
- \ No newline at end of file
- EOE
-
- : no-newline-fail2
+ : Test regex matching. Note that tests that check regex parsing are located
+ : in regex.test testscript.
:
- $c <'$* -i 1 <"foo" >:"foo"';
- $b 2>>~/EOE/ != 0
- /testscript:1: error: .+driver(\.exe)? stdout doesn't match the expected output/
- /.{7}
- -foo
- \ No newline at end of file
- +foo
- EOE
+ {
+ : match
+ :
+ $c <'$* -o foo >~/Foo?/i';
+ $b
- : merge
- :
- $c <<EOI;
- $* -o foo -e bar 2>>EOE 1>&2
- foo
- bar
- EOE
- EOI
- $b
+ : mismatch
+ :
+ $c <'$* -o fooo >~/Foo?/i';
+ $b 2>>/~%EOE%d != 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
+
+ : portable-path-failure
+ :
+ : Note that we check not only build2 diagnostics being produced, but also
+ : the correctness of regex being saved to file (for troubleshooting).
+ :
+ {
+ : newline
+ :
+ $c <"\$* -i 1 <'foo' >/~%bar/%";
+ $b 2>>/~%EOE%d != 0;
+ %testscript:1: error: ../../../../../../driver(.exe)? stdout doesn't match the regex%
+ info: stdout: test/1/stdout
+ info: stdout regex: test/1/stdout.regex
+ info: stdin: test/1/stdin
+ EOE
+ cat test/1/stdout.regex >"%bar$psr%"
+
+ : no-newline
+ :
+ $c <"\$* -i 1 <'foo' >:/~%bar/%";
+ $b 2>>/~%EOE%d != 0;
+ %testscript:1: error: ../../../../../../driver(.exe)? stdout doesn't match the regex%
+ info: stdout: test/1/stdout
+ info: stdout regex: test/1/stdout.regex
+ info: stdin: test/1/stdin
+ EOE
+ cat test/1/stdout.regex >:"%bar$psr%"
+ }
+ }
}
: doc
:
{
- : in
+ : literal
:
- $c <<EOI;
- $* -i 0 <<EOO
- foo
- bar
- EOO
- EOI
- $b
+ {
+ : in
+ :
+ $c <<EOI;
+ $* -i 0 <<EOO
+ foo
+ bar
+ EOO
+ EOI
+ $b
- : out
- :
- $c <<EOI;
- $* -o foo -o bar >>EOO
- foo
- bar
- EOO
- EOI
- $b
+ : out
+ :
+ $c <<EOI;
+ $* -o foo -o bar >>EOO
+ foo
+ bar
+ EOO
+ EOI
+ $b
- : err
- :
- $c <<EOI;
- $* -e foo -e bar 2>>EOO
- foo
- bar
- EOO
- EOI
- $b
+ : err
+ :
+ $c <<EOI;
+ $* -e foo -e bar 2>>EOO
+ foo
+ bar
+ EOO
+ EOI
+ $b
- : inout
- :
- $c <<EOI;
- $* -i 1 <<EOF >>EOO
- foo
- bar
- EOF
- foo
- bar
- EOO
- EOI
- $b
+ : inout
+ :
+ $c <<EOI;
+ $* -i 1 <<EOF >>EOO
+ foo
+ bar
+ EOF
+ foo
+ bar
+ EOO
+ EOI
+ $b
- : inerr
- :
- $c <<EOI;
- $* -i 2 <<EOF 2>>EOE
- foo
- bar
- EOF
- foo
- bar
- EOE
- EOI
- $b
+ : inerr
+ :
+ $c <<EOI;
+ $* -i 2 <<EOF 2>>EOE
+ foo
+ bar
+ EOF
+ foo
+ bar
+ EOE
+ EOI
+ $b
- : empty
- :
- $c <<EOI;
- $* -i 1 <<EOF >>EOO
- EOF
- EOO
- EOI
- $b
+ : empty
+ :
+ $c <<EOI;
+ $* -i 1 <<EOF >>EOO
+ EOF
+ EOO
+ EOI
+ $b
- : extra-newline
- :
- $c <<EOI;
- $* -i 1 <<EOF >>EOO
+ : extra-newline
+ :
+ $c <<EOI;
+ $* -i 1 <<EOF >>EOO
- EOF
+ EOF
- EOO
- EOI
- $b
+ EOO
+ EOI
+ $b
- : no-newline
- :
- $c <<EOI;
- $* -i 1 <<:EOF >>:EOO
- foo
- EOF
- foo
- EOO
- EOI
- $b
+ : no-newline
+ :
+ $c <<EOI;
+ $* -i 1 <<:EOF >>:EOO
+ foo
+ EOF
+ foo
+ EOO
+ EOI
+ $b
- : no-newline-fail1
- :
- $c <<EOI;
- $* -i 1 <<:EOF >>EOO
- foo
- EOF
- foo
- EOO
- EOI
- $b 2>>~/EOE/ != 0
- /testscript:1: error: .+driver(\.exe)? stdout doesn't match the expected output/
- /.{7}
- -foo
- +foo
- \ No newline at end of file
- EOE
-
- : no-newline-fail2
- :
- $c <<EOI;
- $* -i 1 <<EOF >>:EOO
- foo
- EOF
- foo
- EOO
- EOI
- $b 2>>~/EOE/ != 0
- /testscript:1: error: .+driver(\.exe)? stdout doesn't match the expected output/
- /.{7}
- -foo
- \ No newline at end of file
- +foo
- EOE
-
- : no-newline-empty
- :
- $c <<EOI;
- $* -i 1 <<:EOF >>:EOO
- EOF
- EOO
- EOI
- $b
+ : no-newline-fail1
+ :
+ $c <<EOI;
+ $* -i 1 <<:EOF >>EOO
+ foo
+ EOF
+ foo
+ EOO
+ EOI
+ $b 2>>~/EOE/ != 0
+ /testscript:1: error: .+driver(\.exe)? stdout doesn't match the expected output/
+ /.{7}
+ -foo
+ +foo
+ \ No newline at end of file
+ EOE
- : no-newline-extra-newline
- :
- $c <<EOI;
- $* -i 1 <<:EOF >>:EOO
+ : no-newline-fail2
+ :
+ $c <<EOI;
+ $* -i 1 <<EOF >>:EOO
+ foo
+ EOF
+ foo
+ EOO
+ EOI
+ $b 2>>~/EOE/ != 0
+ /testscript:1: error: .+driver(\.exe)? stdout doesn't match the expected output/
+ /.{7}
+ -foo
+ \ No newline at end of file
+ +foo
+ EOE
- EOF
+ : no-newline-empty
+ :
+ $c <<EOI;
+ $* -i 1 <<:EOF >>:EOO
+ EOF
+ EOO
+ EOI
+ $b
- EOO
- EOI
- $b
+ : no-newline-extra-newline
+ :
+ $c <<EOI;
+ $* -i 1 <<:EOF >>:EOO
- : merge
- :
- $c <<EOI;
- $* -i 1 <<EOF -e baz >>EOO 2>&1
- foo
- bar
- EOF
- foo
- bar
- baz
- EOO
- EOI
- $b
+ EOF
+
+ EOO
+ EOI
+ $b
+
+ : merge
+ :
+ $c <<EOI;
+ $* -i 1 <<EOF -e baz >>EOO 2>&1
+ foo
+ bar
+ EOF
+ foo
+ bar
+ baz
+ EOO
+ EOI
+ $b
- : large-diff
+ : large-diff
+ :
+ : Make sure that the large (>4KB) expected/real output difference is not
+ : printed as a part of the diagnostics.
+ :
+ $c <<EOI;
+ s="------------------------------------------------------------------------";
+ s="$s$s$s$s$s$s$s$s$s$s$s$s$s$s$s$s$s$s$s$s$s$s$s$s$s$s$s$s$s$s$s$s$s$s$s$s";
+ $* -i 1 <<"EOF" >>"EOO"
+ $s
+ EOF
+ x$s
+ EOO
+ EOI
+ $b 2>>/~%EOE%d != 0
+ %testscript:3: error: ../../../../../driver(.exe)? stdout doesn't match the expected output%
+ info: stdout: test/1/stdout
+ info: expected stdout: test/1/stdout.orig
+ info: stdout diff: test/1/stdout.diff
+ info: stdin: test/1/stdin
+ EOE
+
+ : portable-path
+ :
+ {
+ : in
+ :
+ $c <<"EOI";
+ \$* -i 1 <</EOF >'foo$ps'
+ foo/
+ EOF
+ EOI
+ $b
+
+ : out
+ :
+ $c <<"EOI";
+ \$* -i 1 <'foo$ps' >>/EOO
+ foo/
+ EOO
+ EOI
+ $b
+
+ : err
+ :
+ $c <<"EOI";
+ \$* -i 2 <'foo$ps' 2>>/EOE
+ foo/
+ EOE
+ EOI
+ $b
+ }
+ }
+
+ : regex
:
- : Make sure that the large (>4KB) expected/real output difference is not
- : printed as a part of the diagnostics.
+ : Test regex matching. Note that tests that check regex parsing are located
+ : in regex.test testscript.
:
- $c <<EOI;
- s="------------------------------------------------------------------------";
- s="$s$s$s$s$s$s$s$s$s$s$s$s$s$s$s$s$s$s$s$s$s$s$s$s$s$s$s$s$s$s$s$s$s$s$s$s";
- $* -i 1 <<"EOF" >>"EOO"
- $s
- EOF
- x$s
- EOO
- EOI
- $b 2>>~%EOE% != 0
- %testscript:3: error: \.\.[/\\]\.\.[/\\]\.\.[/\\]\.\.[/\\]driver(\.exe)? stdout doesn't match the expected output%
- % info: stdout: test[/\\]1[/\\]stdout%
- % info: expected stdout: test[/\\]1[/\\]stdout\.orig%
- % info: stdout diff: test[/\\]1[/\\]stdout\.diff%
- % info: stdin: test[/\\]1[/\\]stdin%
- EOE
+ {
+ : match
+ :
+ $c <<EOI;
+ $* -o foo -o foo -o bar >>~/EOO/i
+ /FO*/*
+ bar
+ /*
+ EOO
+ EOI
+ $b
+
+ : match-empty
+ :
+ $c <<EOI;
+ $* >>:~/EOO/
+ /.{0}
+ EOO
+ EOI
+ $b
+
+ : mismatch
+ :
+ $c <<EOI;
+ $* -o foo >>~/EOO/
+ bar
+ EOO
+ EOI
+ $b 2>>/~%EOE%d != 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
+
+ : mismatch-icase
+ :
+ $c <<EOI;
+ $* -o foo >>~/EOO/i
+ bar
+ EOO
+ EOI
+ $b 2>>/~%EOE%d != 0
+ %testscript:1: error: ../../../../../driver(.exe)? stdout doesn't match the regex%
+ info: stdout: test/1/stdout
+ info: stdout regex: test/1/stdout.regex~i
+ EOE
+ }
}
: file
@@ -336,52 +500,6 @@ b += --no-column
$b
}
-: regex
-:
-{
- : str
- :
- {
- : match
- :
- $c <'$* -o foo >~/Foo?/i';
- $b
-
- : mismatch
- :
- $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
- }
-
- : doc
- :
- {
- : 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
- }
-}
-
# @@ That will probably become redundant when builtins and process obtain file
# descriptors uniformly.
#
diff --git a/tests/test/script/runner/regex.test b/tests/test/script/runner/regex.test
new file mode 100644
index 0000000..f4863b1
--- /dev/null
+++ b/tests/test/script/runner/regex.test
@@ -0,0 +1,321 @@
+# file : tests/test/script/runner/regex.test
+# copyright : Copyright (c) 2014-2017 Code Synthesis Ltd
+# license : MIT; see accompanying LICENSE file
+
+# Here we test that regex objects are properly created by the parser/runner
+# venture. The unit test approach is not of much use as regex object is not
+# serializable back to string. The only way we can test their proper creation
+# is via matching.
+#
+# Note that such a tests are separated from ones that check regex matching
+# specifically (in particular matching failures), The latest ones are located
+# in redirect.test testscript.
+#
+# Also note that the following tests are grouped by features: basic
+# functionality, flags, portable-path modifier.
+#
+
+.include ../common.test
+
+: basic
+:
+{
+ : str
+ :
+ {
+ : out
+ :
+ $c <'cat <foo >~/fo./';
+ $b
+
+ : err
+ :
+ $c <'cat <foo 1>&2 2>~/fo./';
+ $b
+
+ : no-newline
+ :
+ $c <'cat <:foo >:~/fo./';
+ $b
+
+ : malformed
+ :
+ : Note that old versions of libc++ (for example 1.1) do not detect some
+ : regex errors. For example '*' is parsed successfully.
+ :
+ $c <'$* -o foo >~/foo[/';
+ $b 2>>~/EOE/ != 0
+ /testscript:1:13: error: invalid stdout regex redirect.*/
+ info: regex: '/foo[/'
+ EOE
+ }
+
+ : doc
+ :
+ {
+ : out
+ :
+ $c <<EOI;
+ cat <foo >>~/EOO/
+ /foo/
+ EOO
+ EOI
+ $b
+
+ : err
+ :
+ $c <<EOI;
+ cat <foo 1>&2 2>>~/EOO/
+ /fo./
+ EOO
+ EOI
+ $b
+
+ : no-newline
+ :
+ $c <<EOI;
+ cat <:foo >>:~/EOO/
+ /fo./
+ EOO
+ EOI
+ $b
+
+ : line-char
+ :
+ $c <<EOI;
+ cat <<EOF >>~/EOO/
+ foo
+ bar
+ baz
+ baz
+ Biz
+ Fox
+ fox
+
+
+
+
+ EOF
+ foo
+ /?
+ /bar/
+ /baz/+
+ /biz/i
+ /fox/i+
+
+ //
+ //{2}
+ EOO
+ EOI
+ $b
+
+ : expansion
+ :
+ $c <<EOI;
+ s="O*/i
+ bar
+ ";
+ cat <<EOF >>~"/EOO/"
+ foo
+ bar
+
+ baz
+ EOF
+ /f$(s)
+ baz
+ EOO
+ EOI
+ $b
+
+ : invalid-syntax-char
+ :
+ $c <<EOI;
+ $* -o foo >>~/EOO/
+ /x
+ EOO
+ EOI
+ $b 2>>EOE != 0
+ testscript:2:3: error: invalid syntax character 'x' in stdout regex redirect
+ info: regex line: '/x'
+ EOE
+
+ : invalid-char-regex
+ :
+ $c <<EOI;
+ $* -o foo >>~/EOO/
+ /foo[/
+ EOO
+ EOI
+ $b 2>>~/EOE/ != 0
+ /testscript:2:3: error: invalid char-regex in stdout regex redirect.*/
+ info: regex line: '/foo[/'
+ EOE
+
+ : invalid-line-regex
+ :
+ $c <<EOI;
+ $* -o foo >>~/EOO/
+ a
+ /{
+ EOO
+ EOI
+ $b 2>>/~%EOE% != 0
+ %testscript:4:3: error: invalid stdout regex redirect.*%
+ info: stdout regex: test/1/stdout.regex
+ EOE
+ }
+}
+
+:flags
+:
+{
+ : str
+ :
+ {
+ : i
+ :
+ $c <'cat <Foo >~/foo/i';
+ $b
+
+ : d
+ :
+ {
+ : escaped-dot
+ :
+ : Escaped dot becomes syntax dot and matches any character ('i' in our
+ : case).
+ :
+ $c <'cat <fio >~/f\\.o/d';
+ $b
+
+ : syntax-dot
+ :
+ : Syntax dot becomes escaped dot and matches only '.' and so we fail.
+ :
+ $c <'cat <fio >~/f.o/d';
+ $b 2>>~/EOE/ != 0
+ testscript:1:1: error: cat stdout doesn't match the regex
+ /.+
+ EOE
+ }
+ }
+
+ : doc
+ {
+ : i
+ :
+ $c <<EOI;
+ cat <Foo >>~/EOO/
+ /foo/i
+ EOO
+ EOI
+ $b
+
+ : d
+ :
+ : All the reasonings for the /flags/str/d test group are valid for the
+ : current one.
+ :
+ {
+ : escaped-dot
+ :
+ $c <<EOI;
+ cat <fio >>~/EOO/
+ /f\.o/d
+ EOO
+ EOI
+ $b
+
+ : syntax-dot
+ :
+ $c <<EOI;
+ cat <fio >>~/EOO/
+ /f.o/d
+ EOO
+ EOI
+ $b 2>>~/EOE/ != 0
+ testscript:1:1: error: cat stdout doesn't match the regex
+ /.+
+ EOE
+ }
+
+ : global
+ :
+ {
+ : i
+ :
+ $c <<EOI;
+ cat <Foo >>~/EOO/i
+ /foo/
+ EOO
+ EOI
+ $b
+
+ : d
+ :
+ {
+ : escaped-dot
+ :
+ $c <<EOI;
+ cat <fio >>~/EOO/d
+ /f\.o/
+ EOO
+ EOI
+ $b
+
+ : syntax-dot
+ :
+ $c <<EOI;
+ cat <fio >>~/EOO/d
+ /f.o/
+ EOO
+ EOI
+ $b 2>>~/EOE/ != 0
+ testscript:1:1: error: cat stdout doesn't match the regex
+ /.+
+ EOE
+ }
+ }
+ }
+}
+
+: portable-path
+:
+{
+ ps = ($cxx.target.class != 'windows' ? '/' : '\')
+
+ : str
+ :
+ {
+ : out
+ :
+ $c <"cat <'foo$ps' >/~%foo/%";
+ $b
+
+ : err
+ :
+ $c <"cat <'foo$ps' >/~%foo/%";
+ $b
+ }
+
+ : doc
+ {
+ : out
+ :
+ $c <<"EOI";
+ cat <'foo$ps' >>/~%EOO%
+ foo/
+ EOO
+ EOI
+ $b
+
+ : err
+ :
+ $c <<"EOI";
+ cat <'foo$ps' >>/~%EOO%
+ foo/
+ EOO
+ EOI
+ $b
+ }
+}
diff --git a/tests/test/script/runner/status.test b/tests/test/script/runner/status.test
index d7a0d0d..c1df3e4 100644
--- a/tests/test/script/runner/status.test
+++ b/tests/test/script/runner/status.test
@@ -17,8 +17,8 @@ b += --no-column
: false
:
$c <'$* -s 1 == 0';
- $b 2>>~%EOE% != 0
- %testscript:1: error: \.\.[/\\]\.\.[/\\]\.\.[/\\]\.\.[/\\]driver(\.exe)? exit status 1 != 0%
+ $b 2>>/~%EOE%d != 0
+ %testscript:1: error: ../../../../driver(.exe)? exit status 1 != 0%
EOE
}
@@ -33,17 +33,17 @@ b += --no-column
: false
:
$c <'$* -s 1 != 1';
- $b 2>>~%EOE% != 0
- %testscript:1: error: \.\.[/\\]\.\.[/\\]\.\.[/\\]\.\.[/\\]driver(\.exe)? exit status 1 == 1%
+ $b 2>>/~%EOE% != 0
+ %testscript:1: error: ../../../../driver(.exe)? exit status 1 == 1%
EOE
}
: error
:
$c <'$* -s 1 -e "Error"';
-$b 2>>~%EOE% != 0
-%testscript:1: error: \.\.[/\\]\.\.[/\\]\.\.[/\\]driver(\.exe)? exit status 1 != 0%
-% info: stderr: test[/\\]1[/\\]stderr%
+$b 2>>/~%EOE% != 0
+%testscript:1: error: ../../../driver(.exe)? exit status 1 != 0%
+ info: stderr: test/1/stderr
Error
EOE
@@ -54,9 +54,9 @@ EOE
: under Wine some extra info is printed to STDOUT.
:
$c <'$* -t m';
-$b 2>>~%EOE% != 0
+$b 2>>/~%EOE% != 0
%wine: .+%?
-%testscript:1: error: \.\.[/\\]\.\.[/\\]\.\.[/\\]driver(\.exe)? terminated abnormally%
+%testscript:1: error: ../../../driver(.exe)? terminated abnormally%d
% info: .+%
% info: stdout: test\\1\\stdout%?
EOE
diff --git a/tests/value/reverse.test b/tests/value/reverse.test
index d667a9d..a323935 100644
--- a/tests/value/reverse.test
+++ b/tests/value/reverse.test
@@ -66,7 +66,7 @@
: root
:
- if ($cxx.target.class != windows)
+ if ($cxx.target.class != 'windows')
{
$* <<EOI >>EOO
x = [dir_path] /
diff --git a/unit-tests/test/script/parser/driver.cxx b/unit-tests/test/script/parser/driver.cxx
index 271fd2c..f641dc4 100644
--- a/unit-tests/test/script/parser/driver.cxx
+++ b/unit-tests/test/script/parser/driver.cxx
@@ -189,6 +189,13 @@ namespace build2
string (),
trace));
+ value& v (
+ tt.assign (
+ var_pool.insert<target_triplet> (
+ "test.target", variable_visibility::project)));
+
+ v = cast<target_triplet> ((*global_scope)["build.host"]);
+
testscript& st (
targets.insert<testscript> (work,
dir_path (),
diff --git a/unit-tests/test/script/parser/redirect.test b/unit-tests/test/script/parser/redirect.test
index 5a91354..3db684a 100644
--- a/unit-tests/test/script/parser/redirect.test
+++ b/unit-tests/test/script/parser/redirect.test
@@ -6,6 +6,82 @@
# @@ Does it make sense to split into separate files - one per redirect type?
#
+: str
+:
+{
+ : literal
+ :
+ {
+ : portable-path
+ :
+ $* <<EOI >>EOO
+ cmd </foo >/bar 2>/baz
+ EOI
+ cmd </foo >/bar 2>/baz
+ EOO
+ }
+
+ : regex
+ :
+ {
+ : portable-path
+ :
+ $* <<EOI >>EOO
+ cmd >/~%foo% 2>/~%bar%
+ EOI
+ cmd >/~%foo% 2>/~%bar%
+ EOO
+ }
+}
+
+: doc
+:
+{
+ : literal
+ :
+ {
+ : portable-path
+ :
+ $* <<EOI >>EOO
+ cmd <</EOI_ >/EOO_ 2>/EOE_
+ foo
+ EOI_
+ bar
+ EOO_
+ baz
+ EOE_
+ EOI
+ cmd <</EOI_ >/EOO_ 2>/EOE_
+ foo
+ EOI_
+ bar
+ EOO_
+ baz
+ EOE_
+ EOO
+ }
+
+ : regex
+ :
+ {
+ : portable-path
+ :
+ $* <<EOI >>EOO
+ cmd >/~%EOF% 2>/~%EOE%
+ foo
+ EOF
+ bar
+ EOE
+ EOI
+ cmd >/~%EOF% 2>/~%EOE%
+ foo
+ EOF
+ bar
+ EOE
+ EOO
+ }
+}
+
: file
:
{
diff --git a/unit-tests/test/script/parser/regex.test b/unit-tests/test/script/parser/regex.test
index f9101c9..058a5bc 100644
--- a/unit-tests/test/script/parser/regex.test
+++ b/unit-tests/test/script/parser/regex.test
@@ -26,12 +26,30 @@
testscript:1:7: error: no closing introducer character in stdout regex redirect
EOE
+ : portable-path-introducer
+ :
+ $* <'cmd >/~/foo/' 2>>EOE != 0
+ testscript:1:8: error: portable path modifier and '/' introducer in stdout regex redirect
+ EOE
+
: empty
:
$* <'cmd >~//' 2>>EOE != 0
testscript:1:7: error: stdout regex redirect is empty
EOE
+ : no-flags
+ :
+ $* <'cmd >~/fo*/' >'cmd >~/fo*/'
+
+ : idot
+ :
+ $* <'cmd >~/fo*/d' >'cmd >~/fo*/d'
+
+ : icase
+ :
+ $* <'cmd >~/fo*/i' >'cmd >~/fo*/i'
+
: invalid-flags1
:
$* <'cmd >~/foo/z' 2>>EOE != 0
@@ -44,24 +62,6 @@
testscript:1:7: error: junk at the end of stdout regex redirect
EOE
- : malformed
- :
- : Note that old versions of libc++ (for example 1.1) do not detect some
- : regex errors. For example '*' is parsed successfully.
- :
- $* <'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*/'
@@ -106,46 +106,24 @@
testscript:1:8: error: expected here-document regex end marker
EOE
- : unterminated-line-char
+ : portable-path-introducer
:
$* <<EOI 2>>EOE != 0
- cmd >>~/EOO/
- /
+ cmd >>/~/EOO/
+ foo
EOO
EOI
- testscript:2:1: error: regex introducer without regex
- info: consider changing regex introducer '/' in here-document end marker
+ testscript:1:5: error: portable path modifier and '/' introducer in here-document regex end marker
EOE
- : invalid-syntax-char
+ : unterminated-line-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/
- a
- /{
+ /
EOO
EOI
- /testscript:4:1: error: invalid here-document regex.*/
+ testscript:2:1: error: no syntax line characters
EOE
: empty
@@ -157,7 +135,7 @@
testscript:2:1: error: empty here-document regex
EOE
- : valid
+ : no-flags
:
$* <<EOI >>EOO
cmd 2>>~/EOE/
@@ -184,6 +162,47 @@
//*
EOE
EOO
+
+ : no-newline
+ :
+ $* <'cmd >:~/fo*/' >'cmd >:~/fo*/'
+ $* <<EOI >>EOO
+ cmd 2>>:~/EOE/
+ foo
+ EOE
+ EOI
+ cmd 2>>:~/EOE/
+ foo
+ EOE
+ EOO
+
+ : end-marker-restore
+ :
+ {
+ : idot
+ :
+ $* <<EOI >>EOO
+ cmd 2>>~/EOE/d
+ foo
+ EOE
+ EOI
+ cmd 2>>~/EOE/d
+ foo
+ EOE
+ EOO
+
+ : icase
+ :
+ $* <<EOI >>EOO
+ cmd 2>>~/EOE/i
+ foo
+ EOE
+ EOI
+ cmd 2>>~/EOE/i
+ foo
+ EOE
+ EOO
+ }
}
: stderr
diff --git a/unit-tests/test/script/regex/driver.cxx b/unit-tests/test/script/regex/driver.cxx
index 6ff4d75..2680672 100644
--- a/unit-tests/test/script/regex/driver.cxx
+++ b/unit-tests/test/script/regex/driver.cxx
@@ -16,6 +16,8 @@ main ()
using lc = line_char;
using ls = line_string;
using lr = line_regex;
+ using cf = char_flags;
+ using cr = char_regex;
// Test line_char.
//
@@ -54,7 +56,7 @@ main ()
assert (lc ('0') != '1');
assert (lc ('n') != mp);
assert (lc ('0') != lc ("0", p));
- assert (lc ('0') != lc (regex ("0"), p));
+ assert (lc ('0') != lc (cr ("0"), p));
assert (lc ('0') < lc ('1'));
assert (lc ('0') < '1');
@@ -77,24 +79,54 @@ main ()
assert (char (lc ("a", p)) == '\a');
assert (lc ("a", p) != lc ("b", p));
- assert (!(lc ("a", p) != lc (regex ("a"), p))); // Matches.
- assert (lc ("a", p) != lc (regex ("b"), p));
+ assert (!(lc ("a", p) != lc (cr ("a"), p)));
+ assert (lc ("a", p) != lc (cr ("b"), p));
assert (lc ("a", p) < lc ("b", p));
- assert (!(lc ("a", p) < lc (regex ("a"), p))); // Matches.
+ assert (!(lc ("a", p) < lc (cr ("a"), p)));
assert (lc ("a", p) <= lc ("b", p));
- assert (lc ("a", p) <= lc (regex ("a"), p));
- assert (lc ("a", p) < lc (regex ("c"), p));
+ assert (lc ("a", p) <= lc (cr ("a"), p));
+ assert (lc ("a", p) < lc (cr ("c"), p));
// Regex roundtrip.
//
- assert (regex_match ("abc", *lc (regex ("abc"), p).regex ()));
+ assert (regex_match ("abc", *lc (cr ("abc"), p).regex ()));
+
+ // Regex flags.
+ //
+ // icase
+ //
+ assert (regex_match ("ABC", cr ("abc", cf::icase)));
+
+ // idot
+ //
+ assert (!regex_match ("a", cr ("[.]", cf::idot)));
+ assert (!regex_match ("a", cr ("[\\.]", cf::idot)));
+
+ assert (regex_match ("a", cr (".")));
+ assert (!regex_match ("a", cr (".", cf::idot)));
+ assert (regex_match ("a", cr ("\\.", cf::idot)));
+ assert (!regex_match ("a", cr ("\\.")));
+
+ // regex::transform()
+ //
+ // The function is static and we can't test it directly. So we will test
+ // it indirectly via regex matches.
+ //
+ // @@ Would be nice to somehow address the inability to test internals (not
+ // exposed via headers). As a part of utility library support?
+ //
+ assert (regex_match (".a[.", cr (".\\.\\[[.]", cf::idot)));
+ assert (regex_match (".a[.", cr (".\\.\\[[\\.]", cf::idot)));
+ assert (!regex_match ("ba[.", cr (".\\.\\[[.]", cf::idot)));
+ assert (!regex_match (".a[b", cr (".\\.\\[[.]", cf::idot)));
+ assert (!regex_match (".a[b", cr (".\\.\\[[\\.]", cf::idot)));
// Regex comparison.
//
- assert (lc ("a", p) == lc (regex ("a|b"), p));
- assert (lc (regex ("a|b"), p) == lc ("a", p));
+ assert (lc ("a", p) == lc (cr ("a|b"), p));
+ assert (lc (cr ("a|b"), p) == lc ("a", p));
}
// Test char_traits<line_char>.
@@ -159,7 +191,7 @@ main ()
const ct& t (use_facet<ct> (l));
line_pool p;
- assert (t.is (ct::digit, '0'));
+ assert (t.is (ct::digit, '0'));
assert (!t.is (ct::digit, '?'));
assert (!t.is (ct::digit, lc ("0", p)));
@@ -216,7 +248,7 @@ main ()
//
{
line_pool p;
- lr r1 ({lc ("foo", p), lc (regex ("ba(r|z)"), p)}, move (p));
+ lr r1 ({lc ("foo", p), lc (cr ("ba(r|z)"), p)}, move (p));
lr r2 (move (r1));
assert (regex_match (ls ({lc ("foo", r2.pool), lc ("bar", r2.pool)}), r2));
@@ -239,11 +271,12 @@ main ()
assert (regex_match (ls ({bar, foo}),
lr ({'(', foo, '|', bar, ')', '+'})));
- assert (regex_match (ls ({foo, foo}), lr ({'(', foo, ')', '\\', '1'})));
+ assert (regex_match (ls ({foo, foo, bar}),
+ lr ({'(', foo, ')', '\\', '1', bar})));
- assert (regex_match (ls ({foo}), lr ({lc (regex ("fo+"), p)})));
- assert (regex_match (ls ({foo}), lr ({lc (regex (".*"), p)})));
- assert (regex_match (ls ({blank}), lr ({lc (regex (".*"), p)})));
+ assert (regex_match (ls ({foo}), lr ({lc (cr ("fo+"), p)})));
+ assert (regex_match (ls ({foo}), lr ({lc (cr (".*"), p)})));
+ assert (regex_match (ls ({blank}), lr ({lc (cr (".*"), p)})));
assert (regex_match (ls ({blank, blank, foo}),
lr ({blank, '*', foo, blank, '*'})));