diff options
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). // { |