From 9bf93c1ab73ee3cd2b763285fc5fc5456e972854 Mon Sep 17 00:00:00 2001 From: Boris Kolpackov Date: Wed, 11 Jan 2017 10:14:23 +0200 Subject: Implement support for narrowing down tests (config.test) --- build/bootstrap.build | 2 +- build2/bin/init.cxx | 46 ++--- build2/buildfile | 2 + build2/cc/module.cxx | 14 +- build2/config/utility | 31 ++-- build2/config/utility.cxx | 12 +- build2/config/utility.txx | 4 +- build2/dist/init.cxx | 16 +- build2/install/init.cxx | 12 +- build2/pkgconfig/init.cxx | 4 +- build2/test/common | 44 +++++ build2/test/common.cxx | 214 ++++++++++++++++++++++ build2/test/init.cxx | 61 +++++-- build2/test/module | 26 +++ build2/test/rule | 20 ++- build2/test/rule.cxx | 299 ++++++++++++++++++++----------- build2/test/script/parser | 8 +- build2/test/script/parser.cxx | 12 +- build2/test/script/runner | 16 ++ build2/test/script/runner.cxx | 32 ++-- build2/test/script/script.cxx | 22 +-- doc/manual.cli | 42 +++++ tests/test/buildfile | 2 +- tests/test/common.test | 10 +- tests/test/config-test/buildfile | 10 ++ tests/test/config-test/driver.cxx | 19 ++ tests/test/config-test/testscript | 200 +++++++++++++++++++++ unit-tests/test/script/parser/driver.cxx | 17 +- 28 files changed, 960 insertions(+), 237 deletions(-) create mode 100644 build2/test/common create mode 100644 build2/test/common.cxx create mode 100644 build2/test/module create mode 100644 tests/test/config-test/buildfile create mode 100644 tests/test/config-test/driver.cxx create mode 100644 tests/test/config-test/testscript diff --git a/build/bootstrap.build b/build/bootstrap.build index 2120151..688df96 100644 --- a/build/bootstrap.build +++ b/build/bootstrap.build @@ -15,6 +15,6 @@ if ($revision != 0) dist.package += +$revision using config -using dist using test +using dist using install diff --git a/build2/bin/init.cxx b/build2/bin/init.cxx index e1b812b..eaded2a 100644 --- a/build2/bin/init.cxx +++ b/build2/bin/init.cxx @@ -112,7 +112,7 @@ namespace build2 { value& v (b.assign ("bin.lib")); if (!v) - v = required (r, "config.bin.lib", "both").first; + v = *required (r, "config.bin.lib", "both").first; } // config.bin.exe.lib @@ -120,7 +120,7 @@ namespace build2 { value& v (b.assign ("bin.exe.lib")); if (!v) - v = required (r, "config.bin.exe.lib", exe_lib).first; + v = *required (r, "config.bin.exe.lib", exe_lib).first; } // config.bin.liba.lib @@ -128,7 +128,7 @@ namespace build2 { value& v (b.assign ("bin.liba.lib")); if (!v) - v = required (r, "config.bin.liba.lib", liba_lib).first; + v = *required (r, "config.bin.liba.lib", liba_lib).first; } // config.bin.libs.lib @@ -136,7 +136,7 @@ namespace build2 { value& v (b.assign ("bin.libs.lib")); if (!v) - v = required (r, "config.bin.libs.lib", libs_lib).first; + v = *required (r, "config.bin.libs.lib", libs_lib).first; } // config.bin.rpath @@ -154,16 +154,16 @@ namespace build2 // that might have been specified before loading the module. // { - const value* p (omitted (r, "config.bin.prefix").first); - const value* s (omitted (r, "config.bin.suffix").first); + lookup p (omitted (r, "config.bin.prefix").first); + lookup s (omitted (r, "config.bin.suffix").first); - auto set = [&r, &b] (const char* bv, const char* cv, const value* v) + auto set = [&r, &b] (const char* bv, const char* cv, lookup l) { - if (const value* o = omitted (r, cv).first) - v = o; + if (lookup o = omitted (r, cv).first) + l = o; - if (v != nullptr) - b.assign (bv) = *v; + if (l) + b.assign (bv) = *l; }; set ("bin.lib.prefix", "config.bin.lib.prefix", p); @@ -186,21 +186,21 @@ namespace build2 // mechanism. // auto p (omitted (r, var)); - const value* v (p.first); + lookup l (p.first); // Then see if there is a config hint (e.g., from the C++ module). // bool hint (false); - if (v == nullptr) + if (!l) { - if (auto l = hints[var]) + if (auto hl = hints[var]) { - v = l.value; + l = hl; hint = true; } } - if (v == nullptr) + if (!l) fail (loc) << "unable to determine binutils target" << info << "consider specifying it with " << var << info << "or first load a module that can provide it as a hint, " @@ -208,7 +208,7 @@ namespace build2 // Split/canonicalize the target. // - string s (cast (*v)); + string s (cast (l)); // Did the user ask us to use config.sub? If this is a hinted value, // then we assume it has already been passed through config.sub. @@ -262,22 +262,22 @@ namespace build2 // mechanism. // auto p (omitted (r, var)); - const value* v (p.first); + lookup l (p.first); // Then see if there is a config hint (e.g., from the C++ module). // - if (v == nullptr) + if (!l) { - if (auto l = hints[var]) - v = l.value; + if (auto hl = hints[var]) + l = hl; } // For ease of use enter it as bin.pattern (since it can come from // different places). // - if (v != nullptr) + if (l) { - const string& s (cast (*v)); + const string& s (cast (l)); if (s.empty () || (!path::traits::is_separator (s.back ()) && diff --git a/build2/buildfile b/build2/buildfile index 83f29f8..6d497ca 100644 --- a/build2/buildfile +++ b/build2/buildfile @@ -79,7 +79,9 @@ exe{b}: \ install/{hxx cxx}{ operation } \ install/{hxx cxx}{ rule } \ install/{hxx }{ utility } \ + test/{hxx cxx}{ common } \ test/{hxx cxx}{ init } \ + test/{hxx }{ module } \ test/{hxx cxx}{ operation } \ test/{hxx cxx}{ rule } \ test/{hxx cxx}{ target } \ diff --git a/build2/cc/module.cxx b/build2/cc/module.cxx index 40bd454..e63f740 100644 --- a/build2/cc/module.cxx +++ b/build2/cc/module.cxx @@ -55,7 +55,7 @@ namespace build2 // auto p (config::omitted (rs, config_x)); - if (p.first == nullptr) + if (!p.first) { // If someone already loaded cc.core.config then use its toolchain // id and (optional) pattern to guess an appropriate default (e.g., @@ -71,13 +71,11 @@ namespace build2 // user changes the source of the pattern, this one will get updated // as well. // - auto p1 (config::required (rs, - config_x, - d, - false, - cc_loaded ? config::save_commented : 0)); - p.first = &p1.first.get (); - p.second = p1.second; + p = config::required (rs, + config_x, + d, + false, + cc_loaded ? config::save_commented : 0); } // Figure out which compiler we are dealing with, its target, etc. diff --git a/build2/config/utility b/build2/config/utility index 1f1fba2..6c18715 100644 --- a/build2/config/utility +++ b/build2/config/utility @@ -28,14 +28,16 @@ namespace build2 // the value is "new", that is, it was set to the default value (inherited // or not, including overrides). We also treat command line overrides // (inherited or not) as new. This flag is usually used to test that the - // new value is valid, print report, etc. + // new value is valid, print report, etc. We return the value as lookup + // (always defined) to pass alone its location (could be used to detect + // inheritance, etc). // // Note also that if save_flags has save_commented, then a default value // is never considered "new" since for such variables absence of a value // means the default value. // template - pair, bool> + pair required (scope& root, const variable&, const T& default_value, @@ -43,7 +45,7 @@ namespace build2 uint64_t save_flags = 0); template - inline pair, bool> + inline pair required (scope& root, const string& name, const T& default_value, @@ -54,7 +56,7 @@ namespace build2 root, var_pool[name], default_value, override, save_flags); } - inline pair, bool> + inline pair required (scope& root, const string& name, const char* default_value, @@ -65,32 +67,31 @@ namespace build2 root, name, string (default_value), override, save_flags); } - // As above, but leave the unspecified value as undefined (and return - // NULL pointer) rather than setting it to the default value. + // As above, but leave the unspecified value as undefined rather than + // setting it to the default value. // // This can be useful when we don't have a default value but may figure // out some fallback. See config.bin.target for an example. // - pair + pair omitted (scope& root, const variable&); - inline pair + inline pair omitted (scope& root, const string& name) { return omitted (root, var_pool[name]); } - // Set, if necessary, an optional config.* variable. In particular, - // an unspecified variable is set to NULL which is used to distinguish - // between the "configured as unspecified" and "not yet configured" - // cases. + // Set, if necessary, an optional config.* variable. In particular, an + // unspecified variable is set to NULL which is used to distinguish + // between the "configured as unspecified" and "not yet configured" cases. // - // Return the value, which can be NULL. + // Return the value (as always defined lookup), which can be NULL. // - const value& + lookup optional (scope& root, const variable&); - inline const value& + inline lookup optional (scope& root, const string& var) { return optional (root, var_pool[var]); diff --git a/build2/config/utility.cxx b/build2/config/utility.cxx index dac11c2..7a3fa9c 100644 --- a/build2/config/utility.cxx +++ b/build2/config/utility.cxx @@ -14,7 +14,7 @@ namespace build2 { namespace config { - pair + pair omitted (scope& r, const variable& var) { // This is a stripped-down version of the required() twisted @@ -44,12 +44,12 @@ namespace build2 } if (l.defined () && current_mif->id == configure_id) - save_variable (r, var); + save_variable (r, var); - return pair (l.value, n); + return pair (l, n); } - const value& + lookup optional (scope& r, const variable& var) { if (current_mif->id == configure_id) @@ -57,8 +57,8 @@ namespace build2 auto l (r[var]); return l.defined () - ? *l - : r.assign (var); // NULL. + ? l + : lookup (r.assign (var), r); // NULL. } bool diff --git a/build2/config/utility.txx b/build2/config/utility.txx index b6859d0..1434406 100644 --- a/build2/config/utility.txx +++ b/build2/config/utility.txx @@ -10,7 +10,7 @@ namespace build2 namespace config { template - pair, bool> + pair required (scope& root, const variable& var, const T& def_val, @@ -60,7 +60,7 @@ namespace build2 } } - return pair, bool> (*l, n); + return pair (l, n); } } } diff --git a/build2/dist/init.cxx b/build2/dist/init.cxx index 14e1887..f185145 100644 --- a/build2/dist/init.cxx +++ b/build2/dist/init.cxx @@ -112,8 +112,8 @@ namespace build2 if (s) { - if (const value& cv = config::optional (r, "config.dist.root")) - v = cast (cv); // Strip abs_dir_path. + if (lookup l = config::optional (r, "config.dist.root")) + v = cast (l); // Strip abs_dir_path. } } @@ -124,10 +124,10 @@ namespace build2 if (s) { - if (const value& cv = config::required (r, - "config.dist.cmd", - path ("install")).first) - v = run_search (cast (cv), true); + if (lookup l = config::required (r, + "config.dist.cmd", + path ("install")).first) + v = run_search (cast (l), true); } } @@ -138,8 +138,8 @@ namespace build2 if (s) { - if (const value& cv = config::optional (r, "config.dist.archives")) - v = cv; + if (lookup l = config::optional (r, "config.dist.archives")) + v = *l; } } diff --git a/build2/install/init.cxx b/build2/install/init.cxx index bb54bcd..5281d38 100644 --- a/build2/install/init.cxx +++ b/build2/install/init.cxx @@ -46,7 +46,7 @@ namespace build2 bool override = false) { string vn; - const value* cv (nullptr); + lookup l; bool global (*name == '\0'); @@ -61,10 +61,10 @@ namespace build2 vn += var; const variable& vr (var_pool.insert (move (vn), true)); - cv = dv != nullptr - ? &config::required (r, vr, *dv, override).first.get () + l = dv != nullptr + ? config::required (r, vr, *dv, override).first : (global - ? &config::optional (r, vr) + ? config::optional (r, vr) : config::omitted (r, vr).first); } @@ -80,8 +80,8 @@ namespace build2 if (spec) { - if (cv != nullptr && *cv) - v = cast (*cv); // Strip CT to T. + if (l) + v = cast (l); // Strip CT to T. } else { diff --git a/build2/pkgconfig/init.cxx b/build2/pkgconfig/init.cxx index 8d50def..ff386bb 100644 --- a/build2/pkgconfig/init.cxx +++ b/build2/pkgconfig/init.cxx @@ -59,9 +59,9 @@ namespace build2 auto p (config::omitted (rs, c_x)); - if (const value* v = p.first) + if (p.first) { - const path& x (cast (*v)); + const path& x (cast (p.first)); try { diff --git a/build2/test/common b/build2/test/common new file mode 100644 index 0000000..e9213b6 --- /dev/null +++ b/build2/test/common @@ -0,0 +1,44 @@ +// file : build2/test/common -*- C++ -*- +// copyright : Copyright (c) 2014-2017 Code Synthesis Ltd +// license : MIT; see accompanying LICENSE file + +#ifndef BUILD2_TEST_COMMON +#define BUILD2_TEST_COMMON + +#include +#include + +#include + +namespace build2 +{ + namespace test + { + struct common + { + // The config.test query interface. + // + const names* test_ = nullptr; // The config.test value if any. + scope* root_ = nullptr; // The root scope for target resolution. + + // Return true if the specified alias target should pass-through to it + // prerequisites. + // + bool + pass (target& alias_target) const; + + // Return true if the specified target should be tested. + // + bool + test (target& test_target) const; + + // Return true if the specified target should be tested with the + // specified testscript test (or group). + // + bool + test (target& test_target, const path& id_path) const; + }; + } +} + +#endif // BUILD2_TEST_COMMON diff --git a/build2/test/common.cxx b/build2/test/common.cxx new file mode 100644 index 0000000..afd52e6 --- /dev/null +++ b/build2/test/common.cxx @@ -0,0 +1,214 @@ +// file : build2/test/common.cxx -*- C++ -*- +// copyright : Copyright (c) 2014-2017 Code Synthesis Ltd +// license : MIT; see accompanying LICENSE file + +#include + +#include +#include + +using namespace std; + +namespace build2 +{ + namespace test + { + // Determine if we have the target (first), id path (second), or both (in + // which case we also advance the iterator). + // + static pair + sense (names::const_iterator& i) + { + const name* tn (nullptr); + const name* pn (nullptr); + + if (i->pair) + { + tn = &*i++; + pn = &*i; + } + else + { + // If it has a type (exe{hello}) or a directory (basics/), then + // we assume it is a target. + // + (i->typed () || !i->dir.empty () ? tn : pn) = &*i; + } + + // Validate the target. + // + if (tn != nullptr) + { + if (tn->qualified ()) + fail << "project-qualified target '" << *tn << " in config.test"; + } + + // Validate the id path. + // + if (pn != nullptr) + { + if (!pn->simple () || pn->empty ()) + fail << "invalid id path '" << *pn << " in config.test"; + } + + return make_pair (tn, pn); + } + + bool common:: + pass (target& a) const + { + if (test_ == nullptr) + return true; + + // We need to "enable" aliases that "lead up" to the targets we are + // interested in. So see if any target is in a subdirectory of this + // alias. + // + // If we don't see any targets (e.g., only id paths), then we assume all + // targets match and therefore we always pass. + // + bool r (true); + + // Directory part from root to this alias (the same in src and out). + // + const dir_path d (a.out_dir ().leaf (root_->out_path ())); + + for (auto i (test_->begin ()); i != test_->end (); ++i) + { + if (const name* n = sense (i).first) + { + // Reset result to false if no match (but we have seen a target). + // + r = n->dir.sub (d); + + // See test() below for details on this special case. + // + if (!r && !n->typed ()) + r = d.sub (n->dir); + + if (r) + break; + } + } + + return r; + } + + bool common:: + test (target& t) const + { + if (test_ == nullptr) + return true; + + // If we don't see any targets (e.g., only id paths), then we assume + // all of them match. + // + bool r (true); + + // Directory part from root to this alias (the same in src and out). + // + const dir_path d (t.out_dir ().leaf (root_->out_path ())); + const target_type& tt (t.type ()); + + for (auto i (test_->begin ()); i != test_->end (); ++i) + { + if (const name* n = sense (i).first) + { + // Reset result to false if no match (but we have seen a target). + // + + // When specifying a directory, for example, config.tests=tests/, + // one would intuitively expect that all the tests under it will + // run. But that's not what will happen with the below test: while + // the dir{tests/} itself will match, any target underneath won't. + // So we are going to handle this type if a target specially by + // making it match any target in or under it. + // + // Note that we only do this for tests/, not dir{tests/} since it is + // not always the semantics that one wants. Sometimes one may want + // to run tests (scripts) just for the tests/ target but not for any + // of its prerequisites. So dir{tests/} is a way to disable this + // special logic. + // + // Note: the same code as in test() below. + // + if (!n->typed ()) + r = d.sub (n->dir); + else + // First quickly and cheaply weed out names that cannot possibly + // match. Only then search for a target as if it was a prerequisite, + // which can be expensive. + // + r = + t.name == n->value && // Name matches. + tt.name == n->type && // Target type matches. + d == n->dir && // Directory matches. + &search (*n, *root_) == &t; + + if (r) + break; + } + } + + return r; + } + + bool common:: + test (target& t, const path& id) const + { + if (test_ == nullptr) + return true; + + // If we don't see any id paths (e.g., only targets), then we assume + // all of them match. + // + bool r (true); + + // Directory part from root to this alias (the same in src and out). + // + const dir_path d (t.out_dir ().leaf (root_->out_path ())); + const target_type& tt (t.type ()); + + for (auto i (test_->begin ()); i != test_->end (); ++i) + { + auto p (sense (i)); + + if (const name* n = p.second) + { + // If there is a target, check that it matches ours. + // + if (const name* n = p.first) + { + // Note: the same code as in test() above. + // + bool r; + + if (!n->typed ()) + r = d.sub (n->dir); + else + r = + t.name == n->value && + tt.name == n->type && + d == n->dir && + &search (*n, *root_) == &t; + + if (!r) + continue; // Not our target. + } + + // If the id (group) "leads up" to what we want to run or we + // (group) lead up to the id, then match. + // + const path p (n->value); + + // Reset result to false if no match (but we have seen an id path). + // + if ((r = p.sub (id) || id.sub (p))) + break; + } + } + + return r; + } + } +} diff --git a/build2/test/init.cxx b/build2/test/init.cxx index f592568..4338231 100644 --- a/build2/test/init.cxx +++ b/build2/test/init.cxx @@ -9,7 +9,9 @@ #include #include -#include +#include + +#include #include #include @@ -20,9 +22,6 @@ namespace build2 { namespace test { - static rule rule_; - static alias_rule alias_rule_; - void boot (scope& rs, const location&, unique_ptr&) { @@ -39,6 +38,16 @@ namespace build2 // auto& vp (var_pool); + // Tests to execute. + // + // Specified as @ pairs with both sides being optional. + // The variable is untyped (we want a list of name-pairs), overridable, + // and inheritable. The target is relative (in essence a prerequisite) + // which is resolved from the (root) scope where the config.test value + // is defined. + // + vp.insert ("config.test", true); + // Note: none are overridable. // // The test variable is a name which can be a path (with the @@ -76,7 +85,7 @@ namespace build2 init (scope& rs, scope&, const location& l, - unique_ptr&, + unique_ptr& mod, bool first, bool, const variable_map& config_hints) @@ -92,16 +101,39 @@ namespace build2 const dir_path& out_root (rs.out_path ()); l5 ([&]{trace << "for " << out_root;}); - assert (config_hints.empty ()); // We don't known any hints. + assert (mod == nullptr); + mod.reset (new module ()); + module& m (static_cast (*mod)); - //@@ TODO: Need ability to specify extra diff options (e.g., - // --strip-trailing-cr, now hardcoded). + // Configure. + // + assert (config_hints.empty ()); // We don't known any hints. // Adjust module priority so that the config.test.* values are saved at // the end of config.build. // - // if (s) - // config::save_module (r, "test", INT32_MAX); + config::save_module (rs, "test", INT32_MAX); + + // config.test + // + if (lookup l = config::omitted (rs, "config.test").first) + { + // Figure out which root scope it came from. + // + scope* s (&rs); + for (; + s != nullptr && !l.belongs (*s); + s = s->parent_scope ()->root_scope ()) + assert (s != nullptr); + + m.test_ = &cast (l); + m.root_ = s; + } + + //@@ TODO: Need ability to specify extra diff options (e.g., + // --strip-trailing-cr, now hardcoded). + // + //@@ TODO: Pring report. // Register target types. // @@ -114,18 +146,19 @@ namespace build2 // Register rules. // { - auto& r (rs.rules); + rule& r (m); + alias_rule& ar (m); // Register our test running rule. // - r.insert (perform_test_id, "test", rule_); - r.insert (perform_test_id, "test", alias_rule_); + rs.rules.insert (perform_test_id, "test", r); + rs.rules.insert (perform_test_id, "test", ar); // Register our rule for the dist meta-operation. We need to do this // because we may have ad hoc prerequisites (test input/output files) // that need to be entered into the target list. // - r.insert (dist_id, test_id, "test", rule_); + rs.rules.insert (dist_id, test_id, "test", r); } return true; diff --git a/build2/test/module b/build2/test/module new file mode 100644 index 0000000..49b3031 --- /dev/null +++ b/build2/test/module @@ -0,0 +1,26 @@ +// file : build2/test/module -*- C++ -*- +// copyright : Copyright (c) 2014-2017 Code Synthesis Ltd +// license : MIT; see accompanying LICENSE file + +#ifndef BUILD2_TEST_MODULE +#define BUILD2_TEST_MODULE + +#include +#include + +#include + +#include +#include + +namespace build2 +{ + namespace test + { + struct module: module_base, virtual common, rule, alias_rule + { + }; + } +} + +#endif // BUILD2_TEST_MODULE diff --git a/build2/test/rule b/build2/test/rule index e3fc864..20ad905 100644 --- a/build2/test/rule +++ b/build2/test/rule @@ -11,34 +11,40 @@ #include #include +#include + namespace build2 { namespace test { - class rule: public build2::rule + class rule_common: public build2::rule, protected virtual common { public: virtual match_result match (action, target&, const string&) const override; + target_state + perform_script (action, target&) const; + }; + + class rule: public rule_common + { + public: virtual recipe apply (action, target&) const override; static target_state - perform_script (action, target&); - - static target_state perform_test (action, target&); }; - class alias_rule: public rule + class alias_rule: public rule_common { public: virtual recipe apply (action, target&) const override; - static target_state - perform_test (action, target&); + target_state + perform_test (action, target&) const; }; } } diff --git a/build2/test/rule.cxx b/build2/test/rule.cxx index 4201f37..c50f035 100644 --- a/build2/test/rule.cxx +++ b/build2/test/rule.cxx @@ -25,14 +25,16 @@ namespace build2 { struct match_data { - bool test = false; - bool script = false; + bool pass; // Pass-through to prerequsites (for alias only). + bool test; + + bool script; }; static_assert (sizeof (match_data) <= target::data_size, "insufficient space"); - match_result rule:: + match_result rule_common:: match (action a, target& t, const string&) const { // The (admittedly twisted) logic of this rule tries to achieve the @@ -47,64 +49,69 @@ namespace build2 // (which, if not testable, it will noop). // // And to add a bit more complexity, we want to handle aliases slightly - // differently: we don't want to ignore their prerequisites if the alias - // is not testable since their prerequisites could be. + // differently: we may not want to ignore their prerequisites if the + // alias is not testable since their prerequisites could be. - match_data md; + match_data md {t.is_a () && pass (t), false, false}; - // We have two very different cases: testscript and simple test (plus it - // may not be a testable target at all). So as the first step determine - // which case this is. - // - // If we have any prerequisites of the test{} type, then this is the - // testscript case. - // - for (prerequisite_member p: group_prerequisite_members (a, t)) + if (test (t)) { - if (p.is_a ()) + // We have two very different cases: testscript and simple test (plus + // it may not be a testable target at all). So as the first step + // determine which case this is. + // + // If we have any prerequisites of the test{} type, then this is the + // testscript case. + // + for (prerequisite_member p: group_prerequisite_members (a, t)) { - md.script = true; + if (p.is_a ()) + { + md.script = true; - // We treat this target as testable unless the test variable is - // explicitly set to false. - // - const name* n (cast_null (t["test"])); - md.test = n == nullptr || !n->simple () || n->value != "false"; - break; + // We treat this target as testable unless the test variable is + // explicitly set to false. + // + const name* n (cast_null (t["test"])); + md.test = n == nullptr || !n->simple () || n->value != "false"; + break; + } } - } - // If this is not a script, then determine if it is a simple test. - // Ignore aliases and testscripts files themselves at the outset. - // - if (!md.script && !t.is_a () && !t.is_a ()) - { - // For the simple case whether this is a test is controlled by the - // test variable. Also, it feels redundant to specify, say, "test = - // true" and "test.output = test.out" -- the latter already says this - // is a test. + // If this is not a script, then determine if it is a simple test. + // Ignore aliases and testscripts files themselves at the outset. // + if (!md.script && !t.is_a () && !t.is_a ()) + { + // For the simple case whether this is a test is controlled by the + // test variable. Also, it feels redundant to specify, say, "test = + // true" and "test.output = test.out" -- the latter already says this + // is a test. + // - // Use lookup depths to figure out who "overrides" whom. - // - auto p (t.find ("test")); - const name* n (cast_null (p.first)); + // Use lookup depths to figure out who "overrides" whom. + // + auto p (t.find ("test")); + const name* n (cast_null (p.first)); - if (n != nullptr && n->simple () && n->value != "false") - md.test = true; - else - { - auto test = [&t, &p] (const char* var) + // Note that test can be set to an "override" target. + // + if (n != nullptr && (!n->simple () || n->value != "false")) + md.test = true; + else { - return t.find (var).second < p.second; - }; - - md.test = - test ("test.input") || - test ("test.output") || - test ("test.roundtrip") || - test ("test.options") || - test ("test.arguments"); + auto test = [&t, &p] (const char* var) + { + return t.find (var).second < p.second; + }; + + md.test = + test ("test.input") || + test ("test.output") || + test ("test.roundtrip") || + test ("test.options") || + test ("test.arguments"); + } } } @@ -137,7 +144,7 @@ namespace build2 // Change the recipe action to (update, 0) (i.e., "unconditional // update") to make sure we won't match any prerequisites. // - if (md.test && a.operation () == update_id) + if (a.operation () == update_id && (md.pass || md.test)) mr.recipe_action = action (a.meta_operation (), update_id); // Note that we match even if this target is not testable so that we can @@ -157,6 +164,9 @@ namespace build2 // assert (!md.test || md.script); + if (!md.pass && !md.test) + return noop_recipe; + // If this is the update pre-operation then simply redirect to the // standard alias rule. // @@ -175,7 +185,9 @@ namespace build2 // If not a test then also redirect to the alias rule. // - return md.test ? perform_test : default_recipe; + return md.test + ? [this] (action a, target& t) {return perform_test (a, t);} + : default_recipe; } recipe rule:: @@ -206,7 +218,7 @@ namespace build2 t.prerequisite_targets.push_back (&p.search ()); } - return &perform_script; + return [this] (action a, target& t) {return perform_script (a, t);}; } else { @@ -350,29 +362,33 @@ namespace build2 } } - target_state rule:: - perform_script (action, target& t) + target_state rule_common:: + perform_script (action, target& t) const { // Figure out whether the testscript file is called 'testscript', in // which case it should be the only one. // - optional one; - for (target* pt: t.prerequisite_targets) + bool one; { - // In case we are using the alias rule's list (see above). - // - if (testscript* ts = pt->is_a ()) + optional o; + for (target* pt: t.prerequisite_targets) { - bool r (ts->name == "testscript"); + // In case we are using the alias rule's list (see above). + // + if (testscript* ts = pt->is_a ()) + { + bool r (ts->name == "testscript"); - if ((r && one) || (!r && one && *one)) - fail << "both 'testscript' and other names specified for " << t; + if ((r && o) || (!r && o && *o)) + fail << "both 'testscript' and other names specified for " << t; - one = r; + o = r; + } } - } - assert (one); // We should have a testscript or we wouldn't be here. + assert (o); // We should have a testscript or we wouldn't be here. + one = *o; + } // Calculate root working directory. It is in the out_base of the target // and is called just test for dir{} targets and test- for @@ -402,45 +418,58 @@ namespace build2 if (exists (wd)) { - bool e (empty (wd)); - warn << "working directory " << wd << " exists " - << (e ? "" : "and is not empty ") << "at the beginning " + << (empty (wd) ? "" : "and is not empty ") << "at the beginning " << "of the test"; - if (!e) - build2::rmdir_r (wd, false, 2); + // Remove the directory itself not to confuse the runner which tries + // to detect when tests stomp on each others feet. + // + build2::rmdir_r (wd, true, 2); } - else if (!*one) - mkdir (wd, 2); + + // Delay actually creating the directory in case all the tests are + // ignored (via config.test). + // + bool mk (!one); // Run all the testscripts. // - auto run = [&t, &wd] (testscript& ts) + for (target* pt: t.prerequisite_targets) { - if (verb) + if (testscript* ts = pt->is_a ()) { - const auto& tt (cast (t["test.target"])); - text << "test " << t << " with " << ts << " on " << tt; - } + // If this is just the testscript, then its id path is empty (and + // it can only be ignored by ignoring the test target, which makes + // sense since it's the only testscript file). + // + if (one || test (t, path (ts->name))) + { + if (mk) + { + mkdir (wd, 2); + mk = false; + } - script::parser p; - script::script s (t, ts, wd); - p.pre_parse (s); + if (verb) + { + const auto& tt (cast (t["test.target"])); + text << "test " << t << " with " << *ts << " on " << tt; + } - script::default_runner r; - p.execute (s, r); - }; + script::parser p; + script::script s (t, *ts, wd); + p.pre_parse (s); - for (target* pt: t.prerequisite_targets) - { - if (testscript* ts = pt->is_a ()) - run (*ts); + script::default_runner r (*this); + p.execute (s, r); + } + } } // Cleanup. // - if (!*one) + if (!one && !mk) { if (!empty (wd)) fail << "working directory " << wd << " is not empty at the " @@ -471,10 +500,9 @@ namespace build2 for (next++; *next != nullptr; next++) ; next++; - // Redirect stdout to a pipe unless we are last, in which - // case redirect it to stderr. + // Redirect stdout to a pipe unless we are last. // - int out (*next == nullptr ? 2 : -1); + int out (*next != nullptr ? -1 : 1); bool pr, wr; try @@ -519,28 +547,89 @@ namespace build2 } target_state rule:: - perform_test (action, target& t) + perform_test (action, target& tt) { // @@ Would be nice to print what signal/core was dumped. // - // @@ Doesn't have to be a file target if we have test.cmd (or - // just use test which is now path). + + // See if we have the test executable override. // + path p; + { + // Note that the test variable's visibility is target. + // + lookup l (tt["test"]); + + // Note that we have similar code for scripted tests. + // + target* t (nullptr); + + if (l.defined ()) + { + const name* n (cast_null (l)); + + if (n == nullptr) + fail << "invalid test executable override: null value"; + else if (n->empty ()) + fail << "invalid test executable override: empty value"; + else if (n->simple ()) + { + // Ignore the special 'true' value. + // + if (n->value != "true") + p = path (n->value); + else + t = &tt; + } + else if (n->directory ()) + fail << "invalid test executable override: '" << *n << "'"; + else + { + // Must be a target name. + // + // @@ OUT: what if this is a @-qualified pair or names? + // + t = &search (*n, tt.base_scope ()); + } + } + else + // By default we set it to the test target's path. + // + t = &tt; + + if (t != nullptr) + { + if (auto* pt = t->is_a ()) + { + // Do some sanity checks: the target better be up-to-date with + // an assigned path. + // + p = pt->path (); - file& ft (static_cast (t)); - assert (!ft.path ().empty ()); // Should have been assigned by update. + if (p.empty ()) + fail << "target " << *pt << " specified in the test variable " + << "is out of date" << + info << "consider specifying it as a prerequisite of " << tt; + } + else + fail << "target " << *t << (t != &tt + ? " specified in the test variable " + : " requested to be tested ") + << "is not path-based"; + } + } - process_path fpp (run_search (ft.path (), true)); - cstrings args {fpp.recall_string ()}; + process_path pp (run_search (p, true)); + cstrings args {pp.recall_string ()}; // Do we have options? // - if (auto l = t["test.options"]) + if (auto l = tt["test.options"]) append_options (args, cast (l)); // Do we have input? // - auto& pts (t.prerequisite_targets); + auto& pts (tt.prerequisite_targets); if (pts.size () != 0 && pts[0] != nullptr) { file& it (static_cast (*pts[0])); @@ -551,7 +640,7 @@ namespace build2 // else { - if (auto l = t["test.arguments"]) + if (auto l = tt["test.arguments"]) append_options (args, cast (l)); } @@ -581,10 +670,10 @@ namespace build2 if (verb >= 2) print_process (args); else if (verb) - text << "test " << t; + text << "test " << tt; diag_record dr; - if (!run_test (t, dr, args.data ())) + if (!run_test (tt, dr, args.data ())) { dr << info << "test command line: "; print_process (dr, args); @@ -595,7 +684,7 @@ namespace build2 } target_state alias_rule:: - perform_test (action a, target& t) + perform_test (action a, target& t) const { // Run the alias recipe first then the test. // diff --git a/build2/test/script/parser b/build2/test/script/parser index 8f30a8c..9ad5fe9 100644 --- a/build2/test/script/parser +++ b/build2/test/script/parser @@ -136,7 +136,11 @@ namespace build2 // public: void - execute (script& s, runner& r) {if (!s.empty ()) execute (s, s, r);} + execute (script& s, runner& r) + { + if (!s.empty ()) + execute (s, s, r); + } void execute (scope&, script&, runner&); @@ -195,7 +199,7 @@ namespace build2 lexer* lexer_; string id_prefix_; // Auto-derived id prefix. - // Parse state. + // Execute state. // runner* runner_; scope* scope_; diff --git a/build2/test/script/parser.cxx b/build2/test/script/parser.cxx index da072dc..f250d27 100644 --- a/build2/test/script/parser.cxx +++ b/build2/test/script/parser.cxx @@ -2,11 +2,11 @@ // copyright : Copyright (c) 2014-2017 Code Synthesis Ltd // license : MIT; see accompanying LICENSE file +#include + #include #include // strstr() -#include - #include #include @@ -2743,6 +2743,14 @@ namespace build2 for (unique_ptr& chain: g->scopes) { + // Check if this scope is ignored (e.g., via config.test). + // + if (!runner_->test (*chain)) + { + chain.reset (); + continue; + } + // Pick a scope from the if-else chain. // // In fact, we are going to drop all but the selected (if any) diff --git a/build2/test/script/runner b/build2/test/script/runner index 5e05255..7b932b9 100644 --- a/build2/test/script/runner +++ b/build2/test/script/runner @@ -16,11 +16,18 @@ namespace build2 { namespace test { + class common; + namespace script { class runner { public: + // Return false if this test/group should be skipped. + // + virtual bool + test (scope&) const = 0; + // Location is the scope start location (for diagnostics, etc). // virtual void @@ -49,6 +56,12 @@ namespace build2 class default_runner: public runner { public: + explicit + default_runner (const common& c): common_ (c) {} + + virtual bool + test (scope& s) const override; + virtual void enter (scope&, const location&) override; @@ -60,6 +73,9 @@ namespace build2 virtual void leave (scope&, const location&) override; + + private: + const common& common_; }; } } diff --git a/build2/test/script/runner.cxx b/build2/test/script/runner.cxx index 64c6e87..522dedd 100644 --- a/build2/test/script/runner.cxx +++ b/build2/test/script/runner.cxx @@ -10,6 +10,9 @@ #include // fdopen_mode, fdnull(), fddup() #include + +#include + #include using namespace std; @@ -310,21 +313,26 @@ namespace build2 } } + bool default_runner:: + test (scope& s) const + { + return common_.test (s.root->test_target, s.id_path); + } + void default_runner:: enter (scope& sp, const location&) { - if (!exists (sp.wd_path)) - // @@ Shouldn't we add an optional location parameter to mkdir() and - // alike utility functions so the failure message can contain - // location info? - // - mkdir (sp.wd_path, 2); - else - // Scope working directory shall be empty (the script working - // directory is cleaned up by the test rule prior the script - // execution). - // - assert (empty (sp.wd_path)); + // Scope working directory shall be empty (the script working + // directory is cleaned up by the test rule prior the script + // execution). + // + // @@ Shouldn't we add an optional location parameter to mkdir() and + // alike utility functions so the failure message can contain + // location info? + // + if (mkdir (sp.wd_path, 2) == mkdir_status::already_exists) + fail << "working directory " << sp.wd_path << " already exists" << + info << "are tests stomping on each other's feet?"; // We don't change the current directory here but indicate that the // scope test commands will be executed in that directory. diff --git a/build2/test/script/script.cxx b/build2/test/script/script.cxx index 661ec7f..2a34f66 100644 --- a/build2/test/script/script.cxx +++ b/build2/test/script/script.cxx @@ -424,24 +424,11 @@ namespace build2 // script // - static inline string - script_id (const path& p) - { - string r (p.leaf ().string ()); - - if (r == "testscript") - return string (); - - size_t n (path::traits::find_extension (r)); - assert (n != string::npos); - r.resize (n); - return r; - } - script:: script (target& tt, testscript& st, const dir_path& rwd) - : group (script_id (st.path ())), - test_target (tt), script_target (st) + : group (st.name == "testscript" ? string () : st.name), + test_target (tt), + script_target (st) { // Set the script working dir ($~) to $out_base/test/ (id_path // for root is just the id which is empty if st is 'testscript'). @@ -458,7 +445,10 @@ namespace build2 // lookup l (find_in_buildfile ("test", false)); + // Note that we have similar code for simple tests. + // target* t (nullptr); + if (l.defined ()) { const name* n (cast_null (l)); diff --git a/doc/manual.cli b/doc/manual.cli index 0263bc1..2fa666e 100644 --- a/doc/manual.cli +++ b/doc/manual.cli @@ -38,4 +38,46 @@ precedence. A qualified name cannot be combined with any other operator in the \c{eval-value} production shall contain single value only (no commas). +\h1#module-test|Test Module| + +The targets to be tested as well as the tests/groups from testscripts to be +run can be narrowed down using the \c{config.test} variable. While this +value is normally specified as a command line override (for example, to +quickly re-run a previously failed test), it can also be persisted in +\c{config.build} in order to create a configuration that will only run a +subset of tests by default. For example: + +\ +b test config.test=foo/exe{driver} # Only test foo/exe{driver} target. +b test config.test=bar/baz # Only run bar/baz testscript test. +\ + +The \c{config.test} variable contains a list of \c{@}-separated pairs with the +left hand side being the target and the right hand side being the testscript +id path. Either can be omitted (along with \c{@}). If the value contains a +target type or ends with a directory separator, then it is treated as a target +name. Otherwise \- an id path. The targets are resolved relative to the root +scope where the \c{config.test} value is set. For example: + +\ +b test config.test=foo/exe{driver}@bar +\ + +To specify multiple id paths for the same target we can use the pair +generation syntax: + +\ +b test config.test=foo/exe{driver}@{bar baz} +\ + +If no targets are specified (only id paths), then all the targets are tested +(with the testscript tests to be run limited to the specified id paths). If no +id paths are specified (only targets), then all the testscript tests are run +(with the targets to be tested limited to the specified targets). An id path +without a target applies to all the targets being considered. + +A directory target without an explicit target type (for example, \c{foo/}) is +treated specially. It enables all the tests at and under its directory. This +special treatment can be inhibited by specifying the target type explicitly +(for example, \c{dir{foo/\}}). " diff --git a/tests/test/buildfile b/tests/test/buildfile index 9c92c93..c0ea682 100644 --- a/tests/test/buildfile +++ b/tests/test/buildfile @@ -2,6 +2,6 @@ # copyright : Copyright (c) 2014-2017 Code Synthesis Ltd # license : MIT; see accompanying LICENSE file -d = script/ +d = config-test/ script/ ./: $d file{common.test} include $d diff --git a/tests/test/common.test b/tests/test/common.test index da366ce..0f63bd3 100644 --- a/tests/test/common.test +++ b/tests/test/common.test @@ -14,10 +14,16 @@ amalgamation = using test EOI -test.options += --jobs 1 --quiet --buildfile - +# By default read buildfile from stdin. +# +if ($null($test.options)) + test.options = --buildfile - +end + +test.options += --jobs 1 --quiet # By default perform test. # -if ($empty($test.arguments)) +if ($null($test.arguments)) test.arguments = test end diff --git a/tests/test/config-test/buildfile b/tests/test/config-test/buildfile new file mode 100644 index 0000000..cca33fa --- /dev/null +++ b/tests/test/config-test/buildfile @@ -0,0 +1,10 @@ +# file : tests/test/config-build/buildfile +# copyright : Copyright (c) 2014-2017 Code Synthesis Ltd +# license : MIT; see accompanying LICENSE file + +# Test config.test. +# + +./: test{testscript} exe{driver} $b + +exe{driver}: cxx{driver} diff --git a/tests/test/config-test/driver.cxx b/tests/test/config-test/driver.cxx new file mode 100644 index 0000000..1da7c9d --- /dev/null +++ b/tests/test/config-test/driver.cxx @@ -0,0 +1,19 @@ +// file : tests/test/config-test/driver.cxx -*- C++ -*- +// copyright : Copyright (c) 2014-2017 Code Synthesis Ltd +// license : MIT; see accompanying LICENSE file + +#include + +using namespace std; + +int +main (int argc, char* argv[]) +{ + if (argc != 2) + { + cerr << "usage: " << argv[0] << " " << endl; + return 1; + } + + cout << argv[1] << endl; +} diff --git a/tests/test/config-test/testscript b/tests/test/config-test/testscript new file mode 100644 index 0000000..be342ef --- /dev/null +++ b/tests/test/config-test/testscript @@ -0,0 +1,200 @@ +# file : tests/test/config-build/testscript +# copyright : Copyright (c) 2014-2017 Code Synthesis Ltd +# license : MIT; see accompanying LICENSE file + +# Setup a realistic test project that we will then exercise. +# + +test.options = --jobs 1 --quiet +test.arguments = 'test(../proj/@./)' # Test out-of-src (for parallel). +#test.cleanups = &**/ # Cleanup out directory structure. +test.cleanups = &!./ #@@ TMP + ++mkdir proj ++mkdir proj/build ++cat <>>proj/build/bootstrap.build +project = proj +amalgamation = + +using test +EOI + ++cat <>>proj/buildfile +d = tests/ units/ +./: $d +include $d +EOI + +# tests/ - as a subproject +# ++mkdir proj/tests ++mkdir proj/tests/build ++cat <>>proj/tests/build/bootstrap.build +project = + +using test +EOI + ++cat <>>proj/tests/buildfile +d = script/ +./: $d +include $d +EOI + +# tests/script - scripted test +# ++mkdir proj/tests/script ++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 + +: baz +{ + echo 'tests/script/basics/baz/foo' >+ : foo + echo 'tests/script/basics/baz/bar' >+ : bar +} +EOI + +# units/ - as a subdirectory +# ++mkdir proj/units + +# This one is "dual": test and sub-test alias. +# ++cat <>>proj/units/buildfile +d = simple/ script/ +./: $d test{testscript} +include $d +EOI ++cat <>>proj/units/testscript +echo 'units' >+ +EOI + +# units/simple - simple (non-scripted) test +# +# This one is a bit tricky since we need an executable to run. We don't want +# to be building anything as part of our test project so what we do is test +# a dummy file target with an overridden test target. +# ++mkdir proj/units/simple ++touch proj/units/simple/driver ++cat <>>proj/units/simple/buildfile +driver = $src_root/../../exe{driver} +#@@ TMP file{driver}@./: $driver +./: file{driver} $driver +file{driver}@./: test = $driver +file{driver}@./: test.arguments = units/simple +EOI + +# units/script - scripted test +# ++mkdir proj/units/script ++cat <>>proj/units/script/buildfile +./: test{testscript} +EOI ++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). +# + +: all +: +$* >>EOO +tests/script/basics/foo +tests/script/basics/bar +tests/script/basics/baz/foo +tests/script/basics/baz/bar +units/simple +units/script/foo +units/script/bar +units +EOO + +: alias-pass +: Test lead-up alias pass-through (but not test) +: +$* config.test=units/simple/file{driver} >>EOO +units/simple +EOO + +: alias-test +: Test lead-up alias test (but not pass-through) +: +$* config.test=dir{units/} >>EOO +units +EOO + +: alias-pass-test +: Test lead-up alias pass-through and test +: +$* config.test=units/ >>EOO +units/simple +units/script/foo +units/script/bar +units +EOO + +: alias-pass-id-only +: Test lead-up alias pass-through (ids only) +: +$* config.test=bogus >>EOO +units/simple +EOO + +: target-simple +: +$* config.test=units/simple/file{driver} >>EOO +units/simple +EOO + +: target-script +: +$* config.test=dir{units/script/} >>EOO +units/script/foo +units/script/bar +EOO + +: id +: +$* config.test=foo >>EOO +units/simple +units/script/foo +EOO + +: target-id +: +$* config.test=dir{units/script/}@foo >>EOO +units/script/foo +EOO + +: target-ids +: +$* 'config.test=dir{units/script/}@{foo bar}' >>EOO +units/script/foo +units/script/bar +EOO + +: id-group +: +$* config.test=tests/@{basics/baz} >>EOO +tests/script/basics/baz/foo +tests/script/basics/baz/bar +EOO + +: id-in-group +: +$* config.test=tests/@{basics/baz/bar} >>EOO +tests/script/basics/baz/bar +EOO + +# @@ TMP HACK +# +-rm -r all/ alias-pass/ alias-test/ alias-pass-test/ alias-pass-id-only/ \ + target-simple/ target-script/ id/ target-id/ target-ids/ id-group/ \ + id-in-group/ diff --git a/unit-tests/test/script/parser/driver.cxx b/unit-tests/test/script/parser/driver.cxx index 0fc0585..e167505 100644 --- a/unit-tests/test/script/parser/driver.cxx +++ b/unit-tests/test/script/parser/driver.cxx @@ -34,6 +34,12 @@ namespace build2 print_runner (bool scope, bool id, bool line) : scope_ (scope), id_ (id), line_ (line) {} + virtual bool + test (scope&) const override + { + return true; + } + virtual void enter (scope& s, const location&) override { @@ -184,11 +190,12 @@ namespace build2 trace)); testscript& st ( - targets.insert (work, - dir_path (), - "testscript", - &extension_pool.find (""), - trace)); + targets.insert ( + work, + dir_path (), + name.leaf ().base ().string (), + &extension_pool.find (name.leaf ().extension ()), + trace)); tt.path (path ("driver")); st.path (name); -- cgit v1.1