xref: /netbsd-src/external/bsd/kyua-cli/dist/engine/kyuafile.cpp (revision 6b3a42af15b5e090c339512c790dd68f3d11a9d8)
1 // Copyright 2010 Google Inc.
2 // All rights reserved.
3 //
4 // Redistribution and use in source and binary forms, with or without
5 // modification, are permitted provided that the following conditions are
6 // met:
7 //
8 // * Redistributions of source code must retain the above copyright
9 //   notice, this list of conditions and the following disclaimer.
10 // * Redistributions in binary form must reproduce the above copyright
11 //   notice, this list of conditions and the following disclaimer in the
12 //   documentation and/or other materials provided with the distribution.
13 // * Neither the name of Google Inc. nor the names of its contributors
14 //   may be used to endorse or promote products derived from this software
15 //   without specific prior written permission.
16 //
17 // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
18 // "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
19 // LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
20 // A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
21 // OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
22 // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
23 // LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
24 // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
25 // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
26 // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
27 // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
28 
29 #include "engine/kyuafile.hpp"
30 
31 #include <algorithm>
32 #include <iterator>
33 #include <stdexcept>
34 
35 #include <lutok/exceptions.hpp>
36 #include <lutok/operations.hpp>
37 #include <lutok/stack_cleaner.hpp>
38 #include <lutok/state.ipp>
39 
40 #include "engine/exceptions.hpp"
41 #include "engine/test_program.hpp"
42 #include "engine/testers.hpp"
43 #include "utils/datetime.hpp"
44 #include "utils/format/macros.hpp"
45 #include "utils/fs/lua_module.hpp"
46 #include "utils/fs/operations.hpp"
47 #include "utils/logging/macros.hpp"
48 #include "utils/noncopyable.hpp"
49 #include "utils/optional.ipp"
50 #include "utils/sanity.hpp"
51 
52 namespace datetime = utils::datetime;
53 namespace fs = utils::fs;
54 
55 using utils::none;
56 using utils::optional;
57 
58 
59 // History of Kyuafile file versions:
60 //
61 // 2 - Changed the syntax() call to take only a version number, instead of the
62 //     word 'config' as the first argument and the version as the second one.
63 //     Files now start with syntax(2) instead of syntax('kyuafile', 1).
64 //
65 // 1 - Initial version.
66 
67 
68 namespace {
69 
70 
71 static int lua_atf_test_program(lutok::state&);
72 static int lua_current_kyuafile(lutok::state&);
73 static int lua_include(lutok::state&);
74 static int lua_plain_test_program(lutok::state&);
75 static int lua_syntax(lutok::state&);
76 static int lua_test_suite(lutok::state&);
77 
78 
79 /// Concatenates two paths while avoiding paths to start with './'.
80 ///
81 /// \param root Path to the directory containing the file.
82 /// \param file Path to concatenate to root.  Cannot be absolute.
83 ///
84 /// \return The concatenated path.
85 static fs::path
relativize(const fs::path & root,const fs::path & file)86 relativize(const fs::path& root, const fs::path& file)
87 {
88     PRE(!file.is_absolute());
89 
90     if (root == fs::path("."))
91         return file;
92     else
93         return root / file;
94 }
95 
96 
97 /// Implementation of a parser for Kyuafiles.
98 ///
99 /// The main purpose of having this as a class is to keep track of global state
100 /// within the Lua files and allowing the Lua callbacks to easily access such
101 /// data.
102 class parser : utils::noncopyable {
103     /// Lua state to parse a single Kyuafile file.
104     lutok::state _state;
105 
106     /// Root directory of the test suite represented by the Kyuafile.
107     const fs::path _source_root;
108 
109     /// Root directory of the test programs.
110     const fs::path _build_root;
111 
112     /// Name of the Kyuafile to load relative to _source_root.
113     const fs::path _relative_filename;
114 
115     /// Version of the Kyuafile file format requested by the parsed file.
116     ///
117     /// This is set once the Kyuafile invokes the syntax() call.
118     optional< int > _version;
119 
120     /// Name of the test suite defined by the Kyuafile.
121     ///
122     /// This is set once the Kyuafile invokes the test_suite() call.
123     optional< std::string > _test_suite;
124 
125     /// Collection of test programs defined by the Kyuafile.
126     ///
127     /// This acts as an accumulator for all the *_test_program() calls within
128     /// the Kyuafile.
129     engine::test_programs_vector _test_programs;
130 
131 public:
132     /// Initializes the parser and the Lua state.
133     ///
134     /// \param source_root_ The root directory of the test suite represented by
135     ///     the Kyuafile.
136     /// \param build_root_ The root directory of the test programs.
137     /// \param relative_filename_ Name of the Kyuafile to load relative to
138     ///     source_root_.
parser(const fs::path & source_root_,const fs::path & build_root_,const fs::path & relative_filename_)139     parser(const fs::path& source_root_, const fs::path& build_root_,
140            const fs::path& relative_filename_) :
141         _source_root(source_root_), _build_root(build_root_),
142         _relative_filename(relative_filename_)
143     {
144         lutok::stack_cleaner cleaner(_state);
145 
146         _state.push_cxx_function(lua_syntax);
147         _state.set_global("syntax");
148         *_state.new_userdata< parser* >() = this;
149         _state.set_global("_parser");
150 
151         _state.push_cxx_function(lua_atf_test_program);
152         _state.set_global("atf_test_program");
153         _state.push_cxx_function(lua_current_kyuafile);
154         _state.set_global("current_kyuafile");
155         _state.push_cxx_function(lua_include);
156         _state.set_global("include");
157         _state.push_cxx_function(lua_plain_test_program);
158         _state.set_global("plain_test_program");
159         _state.push_cxx_function(lua_test_suite);
160         _state.set_global("test_suite");
161 
162         _state.open_base();
163         _state.open_string();
164         _state.open_table();
165         fs::open_fs(_state);
166     }
167 
168     /// Destructor.
~parser(void)169     ~parser(void)
170     {
171     }
172 
173     /// Gets the parser object associated to a Lua state.
174     ///
175     /// \param state The Lua state from which to obtain the parser object.
176     ///
177     /// \return A pointer to the parser.
178     static parser*
get_from_state(lutok::state & state)179     get_from_state(lutok::state& state)
180     {
181         lutok::stack_cleaner cleaner(state);
182         state.get_global("_parser");
183         return *state.to_userdata< parser* >();
184     }
185 
186     /// Callback for the Kyuafile current_kyuafile() function.
187     ///
188     /// \return Returns the absolute path to the current Kyuafile.
189     fs::path
callback_current_kyuafile(void) const190     callback_current_kyuafile(void) const
191     {
192         const fs::path file = relativize(_source_root, _relative_filename);
193         if (file.is_absolute())
194             return file;
195         else
196             return file.to_absolute();
197     }
198 
199     /// Callback for the Kyuafile include() function.
200     ///
201     /// \post _test_programs is extended with the the test programs defined by
202     /// the included file.
203     ///
204     /// \param raw_file Path to the file to include.
205     void
callback_include(const fs::path & raw_file)206     callback_include(const fs::path& raw_file)
207     {
208         const fs::path file = relativize(_relative_filename.branch_path(),
209                                          raw_file);
210         const engine::test_programs_vector subtps =
211             parser(_source_root, _build_root, file).parse();
212 
213         std::copy(subtps.begin(), subtps.end(),
214                   std::back_inserter(_test_programs));
215     }
216 
217     /// Callback for the Kyuafile syntax() function.
218     ///
219     /// \post _version is set to the requested version.
220     ///
221     /// \param version Version of the Kyuafile syntax requested by the file.
222     ///
223     /// \throw std::runtime_error If the format or the version are invalid, or
224     /// if syntax() has already been called.
225     void
callback_syntax(const int version)226     callback_syntax(const int version)
227     {
228         if (_version)
229             throw std::runtime_error("Can only call syntax() once");
230 
231         if (version < 1 || version > 2)
232             throw std::runtime_error(F("Unsupported file version %s") %
233                                      version);
234 
235         _version = utils::make_optional(version);
236     }
237 
238     /// Callback for the various Kyuafile *_test_program() functions.
239     ///
240     /// \post _test_programs is extended to include the newly defined test
241     /// program.
242     ///
243     /// \param interface Name of the test program interface.
244     /// \param raw_path Path to the test program, relative to the Kyuafile.
245     ///     This has to be adjusted according to the relative location of this
246     ///     Kyuafile to _source_root.
247     /// \param test_suite_override Name of the test suite this test program
248     ///     belongs to, if explicitly defined at the test program level.
249     /// \param metadata Metadata variables passed to the test program.
250     ///
251     /// \throw std::runtime_error If the test program definition is invalid or
252     ///     if the test program does not exist.
253     void
callback_test_program(const std::string & interface,const fs::path & raw_path,const std::string & test_suite_override,const engine::metadata & metadata)254     callback_test_program(const std::string& interface,
255                           const fs::path& raw_path,
256                           const std::string& test_suite_override,
257                           const engine::metadata& metadata)
258     {
259         if (raw_path.is_absolute())
260             throw std::runtime_error(F("Got unexpected absolute path for test "
261                                        "program '%s'") % raw_path);
262         else if (raw_path.str() != raw_path.leaf_name())
263             throw std::runtime_error(F("Test program '%s' cannot contain path "
264                                        "components") % raw_path);
265 
266         const fs::path path = relativize(_relative_filename.branch_path(),
267                                          raw_path);
268 
269         if (!fs::exists(_build_root / path))
270             throw std::runtime_error(F("Non-existent test program '%s'") %
271                                      path);
272 
273         const std::string test_suite = test_suite_override.empty()
274             ? _test_suite.get() : test_suite_override;
275         _test_programs.push_back(engine::test_program_ptr(
276             new engine::test_program(interface, path, _build_root, test_suite,
277                                      metadata)));
278     }
279 
280     /// Callback for the Kyuafile test_suite() function.
281     ///
282     /// \post _version is set to the requested version.
283     ///
284     /// \param name Name of the test suite.
285     ///
286     /// \throw std::runtime_error If test_suite() has already been called.
287     void
callback_test_suite(const std::string & name)288     callback_test_suite(const std::string& name)
289     {
290         if (_test_suite)
291             throw std::runtime_error("Can only call test_suite() once");
292         _test_suite = utils::make_optional(name);
293     }
294 
295     /// Parses the Kyuafile.
296     ///
297     /// \pre Can only be invoked once.
298     ///
299     /// \return The collection of test programs defined by the Kyuafile.
300     ///
301     /// \throw load_error If there is any problem parsing the file.
302     const engine::test_programs_vector&
parse(void)303     parse(void)
304     {
305         PRE(_test_programs.empty());
306 
307         const fs::path load_path = relativize(_source_root, _relative_filename);
308         try {
309             lutok::do_file(_state, load_path.str());
310         } catch (const std::runtime_error& e) {
311             // It is tempting to think that all of our various auxiliary
312             // functions above could raise load_error by themselves thus making
313             // this exception rewriting here unnecessary.  Howver, that would
314             // not work because the helper functions above are executed within a
315             // Lua context, and we lose their type when they are propagated out
316             // of it.
317             throw engine::load_error(load_path, e.what());
318         }
319 
320         if (!_version)
321             throw engine::load_error(load_path, "syntax() never called");
322 
323         return _test_programs;
324     }
325 };
326 
327 
328 /// Gets a string field from a Lua table.
329 ///
330 /// \pre state(-1) contains a table.
331 ///
332 /// \param state The Lua state.
333 /// \param field The name of the field to query.
334 /// \param error The error message to raise when an error condition is
335 ///     encoutered.
336 ///
337 /// \return The string value from the table.
338 ///
339 /// \throw std::runtime_error If there is any problem accessing the table.
340 static inline std::string
get_table_string(lutok::state & state,const char * field,const std::string & error)341 get_table_string(lutok::state& state, const char* field,
342                  const std::string& error)
343 {
344     PRE(state.is_table());
345 
346     lutok::stack_cleaner cleaner(state);
347 
348     state.push_string(field);
349     state.get_table();
350     if (!state.is_string())
351         throw std::runtime_error(error);
352     return state.to_string();
353 }
354 
355 
356 /// Checks if the given interface name is valid.
357 ///
358 /// \param interface The name of the interface to validate.
359 ///
360 /// \throw std::runtime_error If the given interface is not supported.
361 static void
ensure_valid_interface(const std::string & interface)362 ensure_valid_interface(const std::string& interface)
363 {
364     try {
365         (void)engine::tester_path(interface);
366     } catch (const engine::error& e) {
367         throw std::runtime_error(F("Unsupported test interface '%s'") %
368                                  interface);
369     }
370 }
371 
372 
373 /// Glue to invoke parser::callback_test_program() from Lua.
374 ///
375 /// This is a helper function for the various *_test_program() calls, as they
376 /// only differ in the interface of the defined test program.
377 ///
378 /// \pre state(-1) A table with the arguments that define the test program.  The
379 /// special argument 'test_suite' provides an override to the global test suite
380 /// name.  The rest of the arguments are part of the test program metadata.
381 ///
382 /// \param state The Lua state that executed the function.
383 /// \param interface Name of the test program interface.
384 ///
385 /// \return Number of return values left on the Lua stack.
386 ///
387 /// \throw std::runtime_error If the arguments to the function are invalid.
388 static int
lua_generic_test_program(lutok::state & state,const std::string & interface)389 lua_generic_test_program(lutok::state& state, const std::string& interface)
390 {
391     if (!state.is_table())
392         throw std::runtime_error(
393             F("%s_test_program expects a table of properties as its single "
394               "argument") % interface);
395 
396     ensure_valid_interface(interface);
397 
398     lutok::stack_cleaner cleaner(state);
399 
400     state.push_string("name");
401     state.get_table();
402     if (!state.is_string())
403         throw std::runtime_error("Test program name not defined or not a "
404                                  "string");
405     const fs::path path(state.to_string());
406     state.pop(1);
407 
408     state.push_string("test_suite");
409     state.get_table();
410     std::string test_suite;
411     if (state.is_nil()) {
412         // Leave empty to use the global test-suite value.
413     } else if (state.is_string()) {
414         test_suite = state.to_string();
415     } else {
416         throw std::runtime_error(F("Found non-string value in the test_suite "
417                                    "property of test program '%s'") % path);
418     }
419     state.pop(1);
420 
421     engine::metadata_builder mdbuilder;
422     state.push_nil();
423     while (state.next()) {
424         if (!state.is_string(-2))
425             throw std::runtime_error(F("Found non-string metadata property "
426                                        "name in test program '%s'") %
427                                      path);
428         const std::string property = state.to_string(-2);
429 
430         if (property != "name" && property != "test_suite") {
431             if (!state.is_number(-1) && !state.is_string(-1))
432                 throw std::runtime_error(
433                     F("Metadata property '%s' in test program '%s' cannot be "
434                       "converted to a string") % property % path);
435             const std::string value = state.to_string(-1);
436 
437             mdbuilder.set_string(property, value);
438         }
439 
440         state.pop(1);
441     }
442 
443     parser::get_from_state(state)->callback_test_program(
444         interface, path, test_suite, mdbuilder.build());
445     return 0;
446 }
447 
448 
449 /// Specialization of lua_generic_test_program for ATF test programs.
450 ///
451 /// \param state The Lua state that executed the function.
452 ///
453 /// \return Number of return values left on the Lua stack.
454 static int
lua_atf_test_program(lutok::state & state)455 lua_atf_test_program(lutok::state& state)
456 {
457     return lua_generic_test_program(state, "atf");
458 }
459 
460 
461 /// Glue to invoke parser::callback_current_kyuafile() from Lua.
462 ///
463 /// \param state The Lua state that executed the function.
464 ///
465 /// \return Number of return values left on the Lua stack.
466 static int
lua_current_kyuafile(lutok::state & state)467 lua_current_kyuafile(lutok::state& state)
468 {
469     state.push_string(parser::get_from_state(state)->
470                       callback_current_kyuafile().str());
471     return 1;
472 }
473 
474 
475 /// Glue to invoke parser::callback_include() from Lua.
476 ///
477 /// \param state The Lua state that executed the function.
478 ///
479 /// \return Number of return values left on the Lua stack.
480 static int
lua_include(lutok::state & state)481 lua_include(lutok::state& state)
482 {
483     parser::get_from_state(state)->callback_include(
484         fs::path(state.to_string()));
485     return 0;
486 }
487 
488 
489 /// Specialization of lua_generic_test_program for plain test programs.
490 ///
491 /// \param state The Lua state that executed the function.
492 ///
493 /// \return Number of return values left on the Lua stack.
494 static int
lua_plain_test_program(lutok::state & state)495 lua_plain_test_program(lutok::state& state)
496 {
497     return lua_generic_test_program(state, "plain");
498 }
499 
500 
501 /// Glue to invoke parser::callback_syntax() from Lua.
502 ///
503 /// \pre state(-2) The syntax format name, if a v1 file.
504 /// \pre state(-1) The syntax format version.
505 ///
506 /// \param state The Lua state that executed the function.
507 ///
508 /// \return Number of return values left on the Lua stack.
509 static int
lua_syntax(lutok::state & state)510 lua_syntax(lutok::state& state)
511 {
512     if (!state.is_number(-1))
513         throw std::runtime_error("Last argument to syntax must be a number");
514     const int syntax_version = state.to_integer(-1);
515 
516     if (syntax_version == 1) {
517         if (state.get_top() != 2)
518             throw std::runtime_error("Version 1 files need two arguments to "
519                                      "syntax()");
520         if (!state.is_string(-2) || state.to_string(-2) != "kyuafile")
521             throw std::runtime_error("First argument to syntax must be "
522                                      "'kyuafile' for version 1 files");
523     } else {
524         if (state.get_top() != 1)
525             throw std::runtime_error("syntax() only takes one argument");
526     }
527 
528     parser::get_from_state(state)->callback_syntax(syntax_version);
529     return 0;
530 }
531 
532 
533 /// Glue to invoke parser::callback_test_suite() from Lua.
534 ///
535 /// \param state The Lua state that executed the function.
536 ///
537 /// \return Number of return values left on the Lua stack.
538 static int
lua_test_suite(lutok::state & state)539 lua_test_suite(lutok::state& state)
540 {
541     parser::get_from_state(state)->callback_test_suite(state.to_string());
542     return 0;
543 }
544 
545 
546 }  // anonymous namespace
547 
548 
549 /// Constructs a kyuafile form initialized data.
550 ///
551 /// Use load() to parse a test suite configuration file and construct a
552 /// kyuafile object.
553 ///
554 /// \param source_root_ The root directory for the test suite represented by the
555 ///     Kyuafile.  In other words, the directory containing the first Kyuafile
556 ///     processed.
557 /// \param build_root_ The root directory for the test programs themselves.  In
558 ///     general, this will be the same as source_root_.  If different, the
559 ///     specified directory must follow the exact same layout of source_root_.
560 /// \param tps_ Collection of test programs that belong to this test suite.
kyuafile(const fs::path & source_root_,const fs::path & build_root_,const test_programs_vector & tps_)561 engine::kyuafile::kyuafile(const fs::path& source_root_,
562                            const fs::path& build_root_,
563                            const test_programs_vector& tps_) :
564     _source_root(source_root_),
565     _build_root(build_root_),
566     _test_programs(tps_)
567 {
568 }
569 
570 
571 /// Destructor.
~kyuafile(void)572 engine::kyuafile::~kyuafile(void)
573 {
574 }
575 
576 
577 /// Parses a test suite configuration file.
578 ///
579 /// \param file The file to parse.
580 /// \param user_build_root If not none, specifies a path to a directory
581 ///     containing the test programs themselves.  The layout of the build root
582 ///     must match the layout of the source root (which is just the directory
583 ///     from which the Kyuafile is being read).
584 ///
585 /// \return High-level representation of the configuration file.
586 ///
587 /// \throw load_error If there is any problem loading the file.  This includes
588 ///     file access errors and syntax errors.
589 engine::kyuafile
load(const fs::path & file,const optional<fs::path> user_build_root)590 engine::kyuafile::load(const fs::path& file,
591                        const optional< fs::path > user_build_root)
592 {
593     const fs::path source_root_ = file.branch_path();
594     const fs::path build_root_ = user_build_root ?
595         user_build_root.get() : source_root_;
596 
597     return kyuafile(source_root_, build_root_,
598                     parser(source_root_, build_root_,
599                            fs::path(file.leaf_name())).parse());
600 }
601 
602 
603 /// Gets the root directory of the test suite.
604 ///
605 /// \return A path.
606 const fs::path&
source_root(void) const607 engine::kyuafile::source_root(void) const
608 {
609     return _source_root;
610 }
611 
612 
613 /// Gets the root directory of the test programs.
614 ///
615 /// \return A path.
616 const fs::path&
build_root(void) const617 engine::kyuafile::build_root(void) const
618 {
619     return _build_root;
620 }
621 
622 
623 /// Gets the collection of test programs that belong to this test suite.
624 ///
625 /// \return Collection of test program executable names.
626 const engine::test_programs_vector&
test_programs(void) const627 engine::kyuafile::test_programs(void) const
628 {
629     return _test_programs;
630 }
631