From 31e16a7413813293e3cccb6799eaa08b7af5af4e Mon Sep 17 00:00:00 2001 From: Boris Kolpackov Date: Sat, 22 Oct 2016 11:59:20 +0200 Subject: Implement support for compound tests --- build2/test/script/lexer.cxx | 26 +++-- build2/test/script/parser | 8 +- build2/test/script/parser.cxx | 146 ++++++++++++++++++------ build2/test/script/script | 10 +- build2/test/script/token | 4 +- build2/test/script/token.cxx | 2 + doc/testscript.cli | 12 +- unit-tests/test/script/lexer/assign-line.test | 8 ++ unit-tests/test/script/lexer/buildfile | 3 +- unit-tests/test/script/lexer/driver.cxx | 7 +- unit-tests/test/script/lexer/script-line.test | 18 +++ unit-tests/test/script/lexer/variable-line.test | 18 +++ unit-tests/test/script/parser/driver.cxx | 54 +++++++-- unit-tests/test/script/parser/scope.test | 107 +++++++++++++++-- 14 files changed, 347 insertions(+), 76 deletions(-) create mode 100644 unit-tests/test/script/lexer/assign-line.test create mode 100644 unit-tests/test/script/lexer/script-line.test create mode 100644 unit-tests/test/script/lexer/variable-line.test diff --git a/build2/test/script/lexer.cxx b/build2/test/script/lexer.cxx index 5061a1d..2192f35 100644 --- a/build2/test/script/lexer.cxx +++ b/build2/test/script/lexer.cxx @@ -26,24 +26,24 @@ namespace build2 { case lexer_mode::script_line: { - s1 = "=!|&<> $(#\t\n"; - s2 = "== "; + s1 = ";=!|&<> $(#\t\n"; + s2 = " == "; break; } case lexer_mode::assign_line: { // As script_line but with variable assignments. // - s1 = "=+!|&<> $(#\t\n"; - s2 = " == "; + s1 = ";=+!|&<> $(#\t\n"; + s2 = " == "; break; } case lexer_mode::variable_line: { - // Like value except we don't recognize {. + // Like value except we recognize ';' and don't recognize '{'. // - s1 = " $([]#\t\n"; - s2 = " "; + s1 = "; $([]#\t\n"; + s2 = " "; break; } @@ -159,6 +159,18 @@ namespace build2 } } + // Line separators. + // + if (m == lexer_mode::script_line || + m == lexer_mode::assign_line || + m == lexer_mode::variable_line) + { + switch (c) + { + case ';': return make_token (type::semi); + } + } + // Command line operator/separators. // if (m == lexer_mode::script_line || m == lexer_mode::assign_line) diff --git a/build2/test/script/parser b/build2/test/script/parser index df79cf6..39ec4f8 100644 --- a/build2/test/script/parser +++ b/build2/test/script/parser @@ -54,16 +54,16 @@ namespace build2 void parse_script (); - line_type + pair pre_parse_script_line (token&, token_type&); void parse_script_line (token&, token_type&, line_type, size_t); - void + bool parse_variable_line (token&, token_type&); - void + bool parse_test_line (token&, token_type&, size_t); command_exit @@ -100,7 +100,7 @@ namespace build2 // Pre-parse state. // group* group_; - test* test_; + lines* lines_; // Parse state. // diff --git a/build2/test/script/parser.cxx b/build2/test/script/parser.cxx index 1448e7a..510b8cc 100644 --- a/build2/test/script/parser.cxx +++ b/build2/test/script/parser.cxx @@ -22,6 +22,8 @@ namespace build2 { path_ = &p; + pre_parse_ = true; + lexer l (is, *path_, lexer_mode::script_line); lexer_ = &l; base_parser::lexer_ = &l; @@ -30,10 +32,9 @@ namespace build2 runner_ = nullptr; group_ = script_; - test_ = nullptr; - scope_ = nullptr; + lines_ = nullptr; - pre_parse_ = true; + scope_ = nullptr; pre_parse_script (); } @@ -43,6 +44,8 @@ namespace build2 { path_ = &p; + pre_parse_ = false; + lexer_ = nullptr; base_parser::lexer_ = nullptr; @@ -50,10 +53,9 @@ namespace build2 runner_ = &r; group_ = nullptr; - test_ = nullptr; - scope_ = ≻ + lines_ = nullptr; - pre_parse_ = false; + scope_ = ≻ parse_script (); } @@ -81,47 +83,98 @@ namespace build2 if (tt == type::eos) { + // Check that we don't expect more lines. + // + // @@ Also for explicit scope close. + // + if (lines_ != nullptr) + fail (t) << "expected another line after semicolon"; + group_->end_loc_ = get_location (t); replay_stop (); // Discard replay of eos. break; } const location ll (get_location (t)); - line_type lt (pre_parse_script_line (t, tt)); + pair lt (pre_parse_script_line (t, tt)); assert (tt == type::newline); // Stop saving and get the tokens. // - line l {lt, replay_data ()}; + line l {lt.first, replay_data ()}; // Decide where it goes. // lines* ls (nullptr); - switch (lt) + + // If lines_ is not NULL then the previous command ended with a + // semicolon and we should add this one to the same place. + // + if (lines_ != nullptr) + ls = lines_; + else { - case line_type::variable: + switch (lt.first) { - ls = &group_->setup_; - break; - } - case line_type::test: - { - // Create implicit test scope. Use line number as the scope id. - // - group_->scopes.push_back ( - unique_ptr ( - (test_ = new test (to_string (ll.line), *group_)))); + case line_type::variable: + { + // If there is a semicolon after the variable then we assume + // it is part of a test (there is no reason to use semicolons + // after variables in the group scope). + // + if (!lt.second) + { + // If we don't have any nested scopes or teardown commands, + // then we assume this is a setup, otherwise -- teardown. + // + ls = group_->scopes.empty () && group_->tdown_.empty () + ? &group_->setup_ + : &group_->tdown_; + + break; + } - test_->start_loc_ = ll; - test_->end_loc_ = get_location (t); + // Fall through. + } + case line_type::test: + { + // First check that we don't have any teardown commands yet. + // This will detect things like variable assignments between + // tests. + // + // @@ Can the teardown line be from a different file? + // + if (!group_->tdown_.empty ()) + { + location tl ( + get_location ( + group_->tdown_.back ().tokens.front ().token)); + + fail (ll) << "test after teardown" << + info (tl) << "last teardown line appears here"; + } - ls = &test_->tests_; + // Create implicit test scope. Use line number as the scope id. + // + unique_ptr p (new test (to_string (ll.line), *group_)); + + p->start_loc_ = ll; + p->end_loc_ = get_location (t); - test_ = nullptr; + ls = &p->tests_; + + group_->scopes.push_back (move (p)); + break; + } } } ls->push_back (move (l)); + + // If this command ended with a semicolon, then the next one should + // go to the same place. + // + lines_ = lt.second ? ls : nullptr; } } @@ -144,6 +197,9 @@ namespace build2 // next (t, tt); + // @@ The index counts variable assignment lines. This is probably + // not what we want. + // parse_script_line (t, tt, l.type, n == 1 ? 0 : i + 1); assert (tt == type::newline); @@ -153,14 +209,14 @@ namespace build2 runner_->enter (*scope_, scope_->start_loc_); - play (scope_->setup_); - if (test* t = dynamic_cast (scope_)) { play (t->tests_); } else if (group* g = dynamic_cast (scope_)) { + play (g->setup_); + for (const unique_ptr& s: g->scopes) { // Hand it off to a sub-parser potentially in another thread. But @@ -174,16 +230,16 @@ namespace build2 parser p; p.parse (*s, *path_, *script_, *runner_); } + + play (g->tdown_); } else assert (false); - play (scope_->tdown_); - runner_->leave (*scope_, scope_->end_loc_); } - line_type parser:: + pair parser:: pre_parse_script_line (token& t, type& tt) { // Decide whether this is a variable assignment or a command. It is a @@ -202,13 +258,12 @@ namespace build2 if (p == type::assign || p == type::prepend || p == type::append) { - parse_variable_line (t, tt); - return line_type::variable; + return make_pair (line_type::variable, + parse_variable_line (t, tt)); } } - parse_test_line (t, tt, 0); - return line_type::test; + return make_pair (line_type::test, parse_test_line (t, tt, 0)); } void parser:: @@ -234,7 +289,7 @@ namespace build2 return !s.empty (); } - void parser:: + bool parser:: parse_variable_line (token& t, type& tt) { string name (move (t.value)); @@ -262,12 +317,17 @@ namespace build2 // attributes_push (t, tt, true); - value rhs (tt != type::newline && tt != type::eos + value rhs (tt != type::newline && tt != type::semi ? parse_names_value (t, tt, "variable value", nullptr) : value (names ())); + bool semi (tt == type::semi); + + if (semi) + next (t, tt); // Get newline. + if (tt != type::newline) - fail (t) << "unexpected " << t; + fail (t) << "expected newline instead of " << t; if (!pre_parse_) { @@ -298,9 +358,11 @@ namespace build2 scope_->assign (script_->cmd_var) = nullptr; } } + + return semi; } - void parser:: + bool parser:: parse_test_line (token& t, type& tt, size_t li) { command c; @@ -506,6 +568,7 @@ namespace build2 { case type::equal: case type::not_equal: + case type::semi: case type::newline: { done = true; @@ -724,8 +787,13 @@ namespace build2 if (tt == type::equal || tt == type::not_equal) c.exit = parse_command_exit (t, tt); + bool semi (tt == type::semi); + + if (semi) + next (t, tt); // Get newline. + if (tt != type::newline) - fail (t) << "unexpected " << t; + fail (t) << "expected newline instead of " << t; // Parse here-document fragments in the order they were mentioned on // the command line. @@ -754,6 +822,8 @@ namespace build2 // if (!pre_parse_) runner_->run (*scope_, c, li, ll); + + return semi; } command_exit parser:: diff --git a/build2/test/script/script b/build2/test/script/script index ff9fa46..a7bdd7b 100644 --- a/build2/test/script/script +++ b/build2/test/script/script @@ -161,8 +161,6 @@ namespace build2 friend class parser; location start_loc_; - lines setup_; - lines tdown_; location end_loc_; }; @@ -176,6 +174,14 @@ namespace build2 protected: group (const string& id): scope (id, nullptr) {} // For root. + + // Pre-parse data. + // + private: + friend class parser; + + lines setup_; + lines tdown_; }; class test: public scope diff --git a/build2/test/script/token b/build2/test/script/token index c39811a..da468af 100644 --- a/build2/test/script/token +++ b/build2/test/script/token @@ -24,7 +24,9 @@ namespace build2 { // NOTE: remember to update token_printer()! - pipe = base_type::value_next, // | + semi = base_type::value_next, // ; + + pipe, // | clean, // & log_and, // && log_or, // || diff --git a/build2/test/script/token.cxx b/build2/test/script/token.cxx index 27e86d6..6ed0443 100644 --- a/build2/test/script/token.cxx +++ b/build2/test/script/token.cxx @@ -21,6 +21,8 @@ namespace build2 switch (t.type) { + case token_type::semi: os << q << ';' << q; break; + case token_type::pipe: os << q << '|' << q; break; case token_type::clean: os << q << '&' << q; break; case token_type::log_and: os << q << "&&" << q; break; diff --git a/doc/testscript.cli b/doc/testscript.cli index 027fb64..b12ca98 100644 --- a/doc/testscript.cli +++ b/doc/testscript.cli @@ -713,12 +713,14 @@ variable-line: ('='|'+='|'=+') value-attributes? value-attributes: '[' ']' test-line: - command command-exit? - *here-document + *((variable-line|command-line) ';') + command-line -command-exit: ('=='|'!=') +command-line: + command -command: (' '+(|stdin|stdout|stderr))* +command: (' '+(|stdin|stdout|stderr))* command-exit? + *here-document stdin: '0'?('|\ @@ -732,6 +734,8 @@ stderr: '2'('>!'|\ '>' |\ '>>' ) +command-exit: ('=='|'!=') + here-document: * diff --git a/unit-tests/test/script/lexer/assign-line.test b/unit-tests/test/script/lexer/assign-line.test new file mode 100644 index 0000000..ce3e8a1 --- /dev/null +++ b/unit-tests/test/script/lexer/assign-line.test @@ -0,0 +1,8 @@ +# Note: this mode auto-expires after each token. +# +test.arguments += assign-line + +$* <";" >>EOO # semi-only +; + +EOO diff --git a/unit-tests/test/script/lexer/buildfile b/unit-tests/test/script/lexer/buildfile index a9f6be9..70be793 100644 --- a/unit-tests/test/script/lexer/buildfile +++ b/unit-tests/test/script/lexer/buildfile @@ -7,6 +7,7 @@ import libs = libbutl%lib{butl} src = token lexer diagnostics utility variable name test/script/{token lexer} -exe{driver}: cxx{driver} ../../../../build2/cxx{$src} $libs test{variable.test} +exe{driver}: cxx{driver} ../../../../build2/cxx{$src} $libs \ +test{script-line assign-line variable-line variable} include ../../../../build2/ diff --git a/unit-tests/test/script/lexer/driver.cxx b/unit-tests/test/script/lexer/driver.cxx index cd7110f..e37e29d 100644 --- a/unit-tests/test/script/lexer/driver.cxx +++ b/unit-tests/test/script/lexer/driver.cxx @@ -42,9 +42,12 @@ namespace build2 { cin.exceptions (istream::failbit | istream::badbit); - // The variable mode auto-expires so we need something underneath. + // Some modes auto-expire so we need something underneath. // - bool u (m == lexer_mode::variable); + bool u (m == lexer_mode::assign_line || + m == lexer_mode::variable_line || + m == lexer_mode::variable); + lexer l (cin, path ("stdin"), u ? lexer_mode::script_line : m); if (u) l.mode (m); diff --git a/unit-tests/test/script/lexer/script-line.test b/unit-tests/test/script/lexer/script-line.test new file mode 100644 index 0000000..b4fe3ef --- /dev/null +++ b/unit-tests/test/script/lexer/script-line.test @@ -0,0 +1,18 @@ +test.arguments += script-line + +$* <"cmd;" >>EOO # semi +'cmd' +; + +EOO + +$* <"cmd ;" >>EOO # semi-separated +'cmd' +; + +EOO + +$* <";" >>EOO # semi-only +; + +EOO diff --git a/unit-tests/test/script/lexer/variable-line.test b/unit-tests/test/script/lexer/variable-line.test new file mode 100644 index 0000000..543c6f9 --- /dev/null +++ b/unit-tests/test/script/lexer/variable-line.test @@ -0,0 +1,18 @@ +test.arguments += variable-line + +$* <"cmd;" >>EOO # semi +'cmd' +; + +EOO + +$* <"cmd ;" >>EOO # semi-separated +'cmd' +; + +EOO + +$* <";" >>EOO # semi-only +; + +EOO diff --git a/unit-tests/test/script/parser/driver.cxx b/unit-tests/test/script/parser/driver.cxx index 4d71082..09fd6f5 100644 --- a/unit-tests/test/script/parser/driver.cxx +++ b/unit-tests/test/script/parser/driver.cxx @@ -25,25 +25,45 @@ namespace build2 { namespace script { + // Here we assume we are running serially. + // class print_runner: public runner { public: + print_runner (bool scope): scope_ (scope) {} + virtual void - enter (scope&, const location&) override {} + enter (scope&, const location&) override + { + if (scope_) + { + cout << ind_ << "{" << endl; + ind_ += " "; + } + } virtual void run (scope&, const command& t, size_t, const location&) override { - // Here we assume we are running serially. - // - cout << t << endl; + cout << ind_ << t << endl; } virtual void - leave (scope&, const location&) override {} + leave (scope&, const location&) override + { + if (scope_) + { + ind_.resize (ind_.size () - 2); + cout << ind_ << "}" << endl; + } + } + + private: + bool scope_; + string ind_; }; - // Usage: argv[0] [] + // Usage: argv[0] [-s] [] // int main (int argc, char* argv[]) @@ -53,9 +73,27 @@ namespace build2 init (1); // Default verbosity. reset (strings ()); // No command line variables. + bool scope (false); + path name; + + for (int i (1); i != argc; ++i) + { + string a (argv[i]); + + if (a == "-s") + scope = true; + else + { + name = path (move (a)); + break; + } + } + + if (name.empty ()) + name = path ("testscript"); + try { - path name (argc > 1 ? argv[1] : "testscript"); cin.exceptions (istream::failbit | istream::badbit); // Enter mock targets. Use fixed names and paths so that we can use @@ -83,7 +121,7 @@ namespace build2 // Parse and run. // script s (tt, st); - print_runner r; + print_runner r (scope); parser p; p.pre_parse (cin, name, s); diff --git a/unit-tests/test/script/parser/scope.test b/unit-tests/test/script/parser/scope.test index a2c6d9f..7517022 100644 --- a/unit-tests/test/script/parser/scope.test +++ b/unit-tests/test/script/parser/scope.test @@ -1,15 +1,104 @@ $* testscript <'cmd $@' >"cmd 1" # id-testscript $* foo.test <'cmd $@' >"cmd foo/1" # id -wd = [dir_path] $build.work -wd += test -wd += 1 +wd = [dir_path] $~; +wd += test; +wd += 1; $* testscript <'cmd $~' >"cmd $wd" # wd-testscript -# @@ TMP wd1 +wd = [dir_path] $~; +wd += test; +wd += foo; +wd += 1; +$* foo.test <'cmd $~' >"cmd $wd" # wd + +$* -s <>EOO # compound-2 +cmd1; +cmd2 +EOI +{ + { + cmd1 + cmd2 + } +} +EOO + +$* -s <>EOO # compound-3 +cmd1; +cmd2; +cmd3 +EOI +{ + { + cmd1 + cmd2 + cmd3 + } +} +EOO + +$* -s <>EOO # compound-var +cmd1; +x = abc; +cmd2 \$x +EOI +{ + { + cmd1 + cmd2 abc + } +} +EOO + +$* -s <>EOO # compound-var-first +x = abc; +cmd \$x +EOI +{ + { + cmd abc + } +} +EOO + +$* -s <>EOO # var-setup-tdown +x = abc +cmd \$x +y = 123 +EOI +{ + { + cmd abc + } +} +EOO + +$* <>EOE != 0 # test-after-tdown +cmd1 +x = abc +cmd2 +EOI +testscript:3:1: error: test after teardown + testscript:2:1: info: last teardown line appears here +EOE + +$* <>EOE != 0 # expected-line +cmd; +EOI +testscript:2:1: error: expected another line after semicolon +EOE + +# @@ Need newline-less support. # -wd1 = [dir_path] $build.work -wd1 += test -wd1 += foo -wd1 += 1 -$* foo.test <'cmd $~' >"cmd $wd1" # wd +#$* <>EOE != 0 # expected-newline-cmd +#cmd ;\ +#EOI +#testscript:2:1: error: expected newline instead of +#EOE + +#$* <>EOE != 0 # expected-newline-var +#x =abc;\ +#EOI +#testscript:2:1: error: expected newline instead of +#EOE -- cgit v1.1