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