From 4230333bc5b32d30e35264b1104240bb5e2247ff Mon Sep 17 00:00:00 2001 From: Boris Kolpackov Date: Sun, 16 Oct 2016 13:29:57 +0200 Subject: Implement testscript $*, $NN, $~ special variables --- build2/test/script/lexer | 4 +- build2/test/script/lexer.cxx | 30 +++++++ build2/test/script/parser | 2 +- build2/test/script/parser.cxx | 125 ++++++++++++++++++++++++-- build2/test/script/runner.cxx | 2 +- build2/test/script/script | 15 +++- build2/test/script/script.cxx | 49 ++++++---- unit-tests/test/script/lexer/driver.cxx | 21 +++-- unit-tests/test/script/lexer/script-line.test | 10 +++ unit-tests/test/script/lexer/variable.test | 46 ++++++++++ 10 files changed, 266 insertions(+), 38 deletions(-) create mode 100644 unit-tests/test/script/lexer/variable.test diff --git a/build2/test/script/lexer b/build2/test/script/lexer index 99f87c0..be65018 100644 --- a/build2/test/script/lexer +++ b/build2/test/script/lexer @@ -62,8 +62,8 @@ namespace build2 token next_line (); - token - name_line (bool separated); + virtual token + name (bool) override; protected: bool quoted_ = false; diff --git a/build2/test/script/lexer.cxx b/build2/test/script/lexer.cxx index 324e577..4d57fe4 100644 --- a/build2/test/script/lexer.cxx +++ b/build2/test/script/lexer.cxx @@ -263,6 +263,36 @@ namespace build2 unget (c); return name (sep); } + + token lexer:: + name (bool sep) + { + // Customized implementation that handles special variable names ($*, + // $~, $NNN). + // + if (state_.top ().mode != lexer_mode::variable) + return base_lexer::name (sep); + + xchar c (peek ()); + + if (c != '*' && c != '~' && !digit (c)) + return base_lexer::name (sep); + + uint64_t ln (c.line), cn (c.column); + string lexeme; + + get (); + lexeme += c; + + if (digit (c)) + { + for (; digit (c = peek ()); get ()) + lexeme += c; + } + + state_.pop (); // Expire the variable mode. + return token (move (lexeme), sep, false, ln, cn); + } } } } diff --git a/build2/test/script/parser b/build2/test/script/parser index 05b4d47..daaa953 100644 --- a/build2/test/script/parser +++ b/build2/test/script/parser @@ -46,7 +46,7 @@ namespace build2 parse_script_line (token&, token_type&); void - parse_variable_line (token&, token_type&, string); + parse_variable_line (token&, token_type&, string, location); void parse_test_line (token&, token_type&, names, location); diff --git a/build2/test/script/parser.cxx b/build2/test/script/parser.cxx index 7d234eb..01dd010 100644 --- a/build2/test/script/parser.cxx +++ b/build2/test/script/parser.cxx @@ -98,15 +98,34 @@ namespace build2 if (ns.size () != 1 || !ns[0].simple () || ns[0].empty ()) fail (nl) << "variable name expected instead of '" << ns << "'"; - parse_variable_line (t, tt, move (ns[0].value)); + parse_variable_line (t, tt, move (ns[0].value), move (nl)); } else parse_test_line (t, tt, move (ns), move (nl)); } + // Return true if the string contains only digit characters (used to + // detect the special $NN variables). + // + static inline bool + digits (const string& s) + { + for (char c: s) + if (!digit (c)) + return false; + + return !s.empty (); + } + void parser:: - parse_variable_line (token& t, token_type& tt, string name) + parse_variable_line (token& t, token_type& tt, string name, location nl) { + // Check if we are trying to modify any of the special aliases ($*, + // $~, $N). + // + if (name == "*" || name == "~" || digits (name)) + fail (nl) << "attempt to set '" << name << "' variable directly"; + type kind (tt); // Assignment kind. const variable& var (script_->var_pool.insert (move (name))); @@ -125,6 +144,23 @@ namespace build2 // @@ Need to adjust to make strings the default type. // apply_value_attributes (&var, lhs, move (rhs), kind); + + // Handle the $*, $NN special aliases. + // + // The plan is as follows: in this function we detect modification of + // the source variables (test*), and (re)set $* to NULL on this scope + // (this is important to both invalidate any old values but also to + // "stake" the lookup position). This signals to the variable lookup + // function below that the $* and $NN values need to be recalculated + // from their sources. Note that we don't need to invalidate $NN since + // their lookup always checks $* first. + // + if (var.name == script_->test_var.name || + var.name == script_->opts_var.name || + var.name == script_->args_var.name) + { + scope_->assign (script_->cmd_var) = nullptr; + } } void parser:: @@ -599,13 +635,90 @@ namespace build2 } lookup parser:: - lookup_variable (name&& qual, string&& name, const location& l) + lookup_variable (name&& qual, string&& name, const location& loc) { if (!qual.empty ()) - fail (l) << "qualified variable name"; + fail (loc) << "qualified variable name"; - const variable& var (script_->var_pool.insert (move (name))); - return scope_->find (var); + if (name != "*" && !digits (name)) + return scope_->find (script_->var_pool.insert (move (name))); + + // Handle the $*, $NN special aliases. + // + // See the parse_variable_line() for the overall plan. + // + + // In both cases first thing we do is lookup $*. It should always be + // defined since we set it on the script's root scope. + // + lookup l (scope_->find (script_->cmd_var)); + assert (l.defined ()); + + // $* NULL value means it needs to be (re)calculated. + // + value& v (const_cast (*l)); + bool recalc (v.null); + + if (recalc) + { + strings s; + + auto append = [&s] (const strings& v) + { + s.insert (s.end (), v.begin (), v.end ()); + }; + + if (lookup l = scope_->find (script_->test_var)) + s.push_back (cast (l).string ()); + + if (lookup l = scope_->find (script_->opts_var)) + append (cast (l)); + + if (lookup l = scope_->find (script_->args_var)) + append (cast (l)); + + v = move (s); + } + + if (name == "*") + return l; + + // Use the string type for the $NN variables. + // + const variable& var (script_->var_pool.insert (move (name))); + + // We need to look for $NN in the same scope as where we found $*. + // + variable_map& vars (const_cast (*l.vars)); + + // If there is already a value and no need to recalculate it, then we + // are done. + // + if (!recalc && (l = vars[var]).defined ()) + return l; + + // Convert the variable name to index we can use on $*. + // + unsigned long i; + + try + { + i = stoul (var.name); + } + catch (const exception&) + { + fail (loc) << "invalid $* index " << var.name; + } + + const strings& s (cast (v)); + value& nv (vars.assign (var)); + + if (i < s.size ()) + nv = s[i]; + else + nv = nullptr; + + return lookup (nv, vars); } } } diff --git a/build2/test/script/runner.cxx b/build2/test/script/runner.cxx index 69b0213..a17b9bf 100644 --- a/build2/test/script/runner.cxx +++ b/build2/test/script/runner.cxx @@ -16,7 +16,7 @@ namespace build2 run (const test& t) { // @@ TODO - text << "run " << t.program; + text << "run " << t.program.string (); } } } diff --git a/build2/test/script/script b/build2/test/script/script index 005d6e5..b6a0f21 100644 --- a/build2/test/script/script +++ b/build2/test/script/script @@ -10,6 +10,8 @@ #include +#include + namespace build2 { class target; @@ -103,14 +105,21 @@ namespace build2 class script: public scope { public: - script (target& test_target, target& script_target); + script (target& test_target, testscript& script_target); public: - target& test_target; // Target we are testing. - target& script_target; // Target of the testscript file. + target& test_target; // Target we are testing. + testscript& script_target; // Target of the testscript file. public: variable_pool var_pool; + + const variable& test_var; // test + const variable& opts_var; // test.options + const variable& args_var; // test.arguments + + const variable& cmd_var; // $* + const variable& cwd_var; // $~ }; } } diff --git a/build2/test/script/script.cxx b/build2/test/script/script.cxx index b206e4f..2be023d 100644 --- a/build2/test/script/script.cxx +++ b/build2/test/script/script.cxx @@ -15,32 +15,45 @@ namespace build2 namespace script { script:: - script (target& tt, target& st) - : test_target (tt), script_target (st) + script (target& tt, testscript& st) + : test_target (tt), script_target (st), + + // Enter the test* variables with the same variable types as in + // buildfiles. + // + test_var (var_pool.insert ("test")), + opts_var (var_pool.insert ("test.options")), + args_var (var_pool.insert ("test.arguments")), + + cmd_var (var_pool.insert ("*")), + cwd_var (var_pool.insert ("~")) { // Unless we have the test variable set on the test or script target, // set it at the script level to the test target's path. // + if (!find (test_var)) { - // Note: use the same variable type as in buildfile. + value& v (assign (test_var)); + + // If this is a path-based target, then we use the path. If this + // is a directory (alias) target, then we use the directory path. + // Otherwise, we leave it NULL expecting the testscript to set it + // to something appropriate, if used. // - const variable& var (var_pool.insert ("test")); + if (auto* p = tt.is_a ()) + v = p->path (); + else if (tt.is_a ()) + v = path (tt.dir.string ()); // Strip trailing slash. + } - if (!find (var)) - { - value& v (assign (var)); + // Also add the NULL $* value that signals it needs to be recalculated + // on first access. + // + assign (cmd_var) = nullptr; - // If this is a path-based target, then we use the path. If this - // is a directory (alias) target, then we use the directory path. - // Otherwise, we leave it NULL expecting the testscript to set it - // to something appropriate, if used. - // - if (auto* p = tt.is_a ()) - v = p->path (); - else if (tt.is_a ()) - v = path (tt.dir.string ()); // Strip trailing slash. - } - } + // Set the script CWD ($~) which is the $out_base/. + // + assign (cwd_var) = dir_path (tt.out_dir ()) /= st.name; } lookup scope:: diff --git a/unit-tests/test/script/lexer/driver.cxx b/unit-tests/test/script/lexer/driver.cxx index 6e5167d..7e237ed 100644 --- a/unit-tests/test/script/lexer/driver.cxx +++ b/unit-tests/test/script/lexer/driver.cxx @@ -29,18 +29,25 @@ namespace build2 assert (argc == 2); string s (argv[1]); - if (s == "script") m = lexer_mode::script_line; - else if (s == "variable") m = lexer_mode::variable_line; - else if (s == "test") m = lexer_mode::test_line; - else if (s == "command") m = lexer_mode::command_line; - else if (s == "here") m = lexer_mode::here_line; - else assert (false); + if (s == "script-line") m = lexer_mode::script_line; + else if (s == "variable-line") m = lexer_mode::variable_line; + else if (s == "test-line") m = lexer_mode::test_line; + else if (s == "command-line") m = lexer_mode::command_line; + else if (s == "here-line") m = lexer_mode::here_line; + else if (s == "variable") m = lexer_mode::variable; + else assert (false); } try { cin.exceptions (istream::failbit | istream::badbit); - lexer l (cin, path ("stdin"), m); + + // The variable mode auto-expires so we need something underneath. + // + bool u (m == lexer_mode::variable); + lexer l (cin, path ("stdin"), u ? lexer_mode::script_line : m); + if (u) + l.mode (m); // No use printing eos since we will either get it or loop forever. // diff --git a/unit-tests/test/script/lexer/script-line.test b/unit-tests/test/script/lexer/script-line.test index 8fd77ce..f03c3bb 100644 --- a/unit-tests/test/script/lexer/script-line.test +++ b/unit-tests/test/script/lexer/script-line.test @@ -6,3 +6,13 @@ $foo fox = $foo-$cxx.target.class $fox $test +$~ +$* +$0 +test = xxx +$0 +$* +test.options += --foo +$1 +test.arguments += bar +$2 diff --git a/unit-tests/test/script/lexer/variable.test b/unit-tests/test/script/lexer/variable.test new file mode 100644 index 0000000..6478fea --- /dev/null +++ b/unit-tests/test/script/lexer/variable.test @@ -0,0 +1,46 @@ +# Test handling custom variable names ($*, $~, $NN). +# +test.arguments += variable + +$* <"*" >>EOO +'*' + +EOO + +$* <"*abc" >>EOO +'*' +'abc' + +EOO + +$* <"~" >>EOO +'~' + +EOO + +$* <"~123" >>EOO +'~' +'123' + +EOO + +$* <"0" >>EOO +'0' + +EOO + +$* <"10" >>EOO +'10' + +EOO + +$* <"101" >>EOO +'101' + +EOO + +$* <"1abc" >>EOO +'1' +'abc' + +EOO -- cgit v1.1