From dd973d03bf5f3f439dcdacbb22470105e66e698a Mon Sep 17 00:00:00 2001 From: Karen Arutyunov Date: Wed, 29 Mar 2017 00:45:30 +0300 Subject: Implement manifests and build_config --- INSTALL | 6 + LICENSE | 20 + NEWS | 3 + README | 25 + bbot/build-config | 54 +++ bbot/build-config.cxx | 121 +++++ bbot/buildfile | 35 ++ bbot/export | 41 ++ bbot/manifest | 215 +++++++++ bbot/manifest.cxx | 931 +++++++++++++++++++++++++++++++++++++ bbot/variable | 49 ++ bbot/variable.cxx | 94 ++++ bbot/version | 37 ++ build/.gitignore | 1 + build/bootstrap.build | 26 ++ build/export.build | 10 + build/root.build | 20 + buildfile | 12 + manifest | 15 + tests/.gitignore | 1 + tests/buildfile | 5 + tests/buildtab/buildfile | 9 + tests/buildtab/driver.cxx | 48 ++ tests/buildtab/testscript | 63 +++ tests/manifest/buildfile | 9 + tests/manifest/driver.cxx | 71 +++ tests/manifest/machine.test | 157 +++++++ tests/manifest/result-request.test | 92 ++++ tests/manifest/result.test | 230 +++++++++ tests/manifest/task-request.test | 96 ++++ tests/manifest/task-response.test | 114 +++++ tests/manifest/task.test | 260 +++++++++++ tests/variable/buildfile | 9 + tests/variable/driver.cxx | 61 +++ tests/variable/testscript | 55 +++ version | 1 + 36 files changed, 2996 insertions(+) create mode 100644 INSTALL create mode 100644 LICENSE create mode 100644 NEWS create mode 100644 README create mode 100644 bbot/build-config create mode 100644 bbot/build-config.cxx create mode 100644 bbot/buildfile create mode 100644 bbot/export create mode 100644 bbot/manifest create mode 100644 bbot/manifest.cxx create mode 100644 bbot/variable create mode 100644 bbot/variable.cxx create mode 100644 bbot/version create mode 100644 build/.gitignore create mode 100644 build/bootstrap.build create mode 100644 build/export.build create mode 100644 build/root.build create mode 100644 buildfile create mode 100644 manifest create mode 100644 tests/.gitignore create mode 100644 tests/buildfile create mode 100644 tests/buildtab/buildfile create mode 100644 tests/buildtab/driver.cxx create mode 100644 tests/buildtab/testscript create mode 100644 tests/manifest/buildfile create mode 100644 tests/manifest/driver.cxx create mode 100644 tests/manifest/machine.test create mode 100644 tests/manifest/result-request.test create mode 100644 tests/manifest/result.test create mode 100644 tests/manifest/task-request.test create mode 100644 tests/manifest/task-response.test create mode 100644 tests/manifest/task.test create mode 100644 tests/variable/buildfile create mode 100644 tests/variable/driver.cxx create mode 100644 tests/variable/testscript create mode 100644 version diff --git a/INSTALL b/INSTALL new file mode 100644 index 0000000..7942d83 --- /dev/null +++ b/INSTALL @@ -0,0 +1,6 @@ +The easiest way to build this package is with the bpkg package manager: + +$ bpkg build libbbot + +But if you don't want to use the package manager, then you can also build it +manually using the standard build2 build system. diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..91c0877 --- /dev/null +++ b/LICENSE @@ -0,0 +1,20 @@ +Copyright (c) 2014-2017 Code Synthesis Ltd + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be included +in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/NEWS b/NEWS new file mode 100644 index 0000000..b96445c --- /dev/null +++ b/NEWS @@ -0,0 +1,3 @@ +Version 0.5.0 + + * First public release. diff --git a/README b/README new file mode 100644 index 0000000..34cf937 --- /dev/null +++ b/README @@ -0,0 +1,25 @@ +This package contains the build2 build bot library. + +build2 is an open source, cross-platform toolchain for building and packaging +C++ code. Its aim is a modern build system and package manager for the C++ +language that provide a consistent, out of the box interface across multiple +platforms and compilers. For more information see: + +https://build2.org/ + +This library defines the types and utilities for working with build2 build +tasks. In particular, it provides C++ classes as well as the parser and +serializer implementations that can be used to read, manipulate, and write +machine, task, result, task request/response and result request manifests. + +See the NEWS file for the user-visible changes from the previous release. + +See the LICENSE file for the distribution conditions. + +See the INSTALL file for the prerequisites and installation instructions. + +See the doc/ directory for documentation. + +Send questions, bug reports, or any other feedback to the users@build2.org +mailing list. You can post without subscribing. See https://lists.build2.org +for searchable archives, posting guidelines, etc. diff --git a/bbot/build-config b/bbot/build-config new file mode 100644 index 0000000..0a3ecb3 --- /dev/null +++ b/bbot/build-config @@ -0,0 +1,54 @@ +// file : bbot/build-config -*- C++ -*- +// copyright : Copyright (c) 2014-2017 Code Synthesis Ltd +// license : MIT; see accompanying LICENSE file + +#ifndef BBOT_BUILD_CONFIG +#define BBOT_BUILD_CONFIG + +#include +#include +#include + +#include +#include +#include +#include + +#include + +#include + +namespace bbot +{ + // Build configuration matching specific machine names. Used by bbot + // controllers. + // + struct build_config + { + std::string machine_pattern; // Machine name pattern. + std::string name; // Configuration name. + + butl::optional target; + + variables vars; + }; + + using build_configs = std::vector; + + // Parse buildtab stream or file. Throw tab_parsing on parsing error, + // ios::failure on the underlying OS error. + // + // buildtab consists of lines in the following format: + // + // [] [] + // + using butl::tab_parsing; + + LIBBBOT_EXPORT build_configs + parse_buildtab (std::istream&, const std::string& name); + + LIBBBOT_EXPORT build_configs + parse_buildtab (const butl::path&); +} + +#endif // BBOT_BUILD_CONFIG diff --git a/bbot/build-config.cxx b/bbot/build-config.cxx new file mode 100644 index 0000000..226ead3 --- /dev/null +++ b/bbot/build-config.cxx @@ -0,0 +1,121 @@ +// file : bbot/build-config.cxx -*- C++ -*- +// copyright : Copyright (c) 2014-2017 Code Synthesis Ltd +// license : MIT; see accompanying LICENSE file + +#include + +#include +#include // size_t +#include // move() +#include // invalid_argument + +#include +#include +#include + +using namespace std; +using namespace butl; + +namespace bbot +{ + LIBBBOT_EXPORT build_configs + parse_buildtab (istream& is, const string& name) + { + build_configs r; + tab_parser parser (is, name); + + tab_fields tl; + while (!(tl = parser.next ()).empty ()) + { + size_t n (tl.size ()); // Fields count. + size_t i (0); // The field currently being processed. + + // Throw tab_parsing for the field currently being processed. If i == n + // then we refer to the end-of-line column (presumably reporting a missed + // field). + // + auto bad_line = [&name, &tl, &i, n] (const string& d, size_t offset = 0) + { + // Offset beyond the end-of-line is meaningless. + // + assert (i < n || (i == n && offset == 0)); + + throw tab_parsing (name, + tl.line, + i == n + ? tl.end_column + : tl[i].column + offset, + d); + }; + + build_config config; + config.machine_pattern = move (tl[i++].value); + + // Configuration name field is a required one. + // + if (i == n) + bad_line ("no configuration name found"); + + config.name = move (tl[i].value); + + // Make sure the name is unique. + // + for (const auto& c: r) + if (c.name == config.name) + bad_line ("duplicate configuration name"); + + // If there is no target nor configuration variables then save the + // configuration and proceed with the next line. + // + if (++i == n) + { + r.emplace_back (move (config)); + continue; + } + + // If the third field doesn't contain '=' character, then we will treat + // it as a target. + // + if (tl[i].value.find ('=') == string::npos) + { + try + { + config.target = target_triplet (tl[i].value); + } + catch (const invalid_argument& e) + { + bad_line (e.what ()); + } + + ++i; + } + + try + { + for (; i < n; ++i) + config.vars.emplace_back (variable (move (tl[i].value))); + } + catch (const invalid_variable& e) + { + bad_line (e.what (), e.pos); // Note that tl[i].value is moved from, + // but happily we don't use it + } + + // Save the configuration. + // + r.emplace_back (move (config)); + } + + return r; + } + + build_configs + parse_buildtab (const path& p) + { + ifdstream ifs (p); + build_configs r (parse_buildtab (ifs, p.string ())); + + ifs.close (); // Throws on failure. + return r; + } +} diff --git a/bbot/buildfile b/bbot/buildfile new file mode 100644 index 0000000..1bb608f --- /dev/null +++ b/bbot/buildfile @@ -0,0 +1,35 @@ +# file : bbot/buildfile +# copyright : Copyright (c) 2014-2017 Code Synthesis Ltd +# license : MIT; see accompanying LICENSE file + +import int_libs = libbutl%lib{butl} libbpkg%lib{bpkg} + +lib{bbot}: \ +{hxx cxx}{ build-config } \ +{hxx }{ export } \ +{hxx cxx}{ manifest } \ +{hxx cxx}{ variable } \ +{hxx }{ version } \ + $int_libs + +# For pre-releases use the complete version to make sure they cannot be used +# in place of another pre-release or the final version. +# +if $abi_prerelease + lib{bbot}: bin.lib.version = @-$version +else + lib{bbot}: bin.lib.version = @-$abi_major.$abi_minor + +cxx.poptions =+ "-I$src_root" +obja{*}: cxx.poptions += -DLIBBBOT_STATIC_BUILD +objs{*}: cxx.poptions += -DLIBBBOT_SHARED_BUILD + +lib{bbot}: cxx.export.poptions = "-I$src_root" +liba{bbot}: cxx.export.poptions += -DLIBBBOT_STATIC +libs{bbot}: cxx.export.poptions += -DLIBBBOT_SHARED + +lib{bbot}: cxx.export.libs = $int_libs + +# Install into the bbot/ subdirectory of, say, /usr/include/. +# +install.include = $install.include/bbot/ diff --git a/bbot/export b/bbot/export new file mode 100644 index 0000000..6947752 --- /dev/null +++ b/bbot/export @@ -0,0 +1,41 @@ +// file : bbot/export -*- C++ -*- +// copyright : Copyright (c) 2014-2017 Code Synthesis Ltd +// license : MIT; see accompanying LICENSE file + +#ifndef BBOT_EXPORT +#define BBOT_EXPORT + +// Normally we don't export class templates (but do complete specializations), +// inline functions, and classes with only inline member functions. Exporting +// classes that inherit from non-exported/imported bases (e.g., std::string) +// will end up badly. The only known workarounds are to not inherit or to not +// export. Also, MinGW GCC doesn't like seeing non-exported function being +// used before their inline definition. The workaround is to reorder code. In +// the end it's all trial and error. + +#if defined(LIBBBOT_STATIC) // Using static. +# define LIBBBOT_EXPORT +#elif defined(LIBBBOT_STATIC_BUILD) // Building static. +# define LIBBBOT_EXPORT +#elif defined(LIBBBOT_SHARED) // Using shared. +# ifdef _WIN32 +# define LIBBBOT_EXPORT __declspec(dllimport) +# else +# define LIBBBOT_EXPORT +# endif +#elif defined(LIBBBOT_SHARED_BUILD) // Building shared. +# ifdef _WIN32 +# define LIBBBOT_EXPORT __declspec(dllexport) +# else +# define LIBBBOT_EXPORT +# endif +#else +// If none of the above macros are defined, then we assume we are being used +// by some third-party build system that cannot/doesn't signal the library +// type. Note that this fallback works for both static and shared but in case +// of shared will be sub-optimal compared to having dllimport. +// +# define LIBBBOT_EXPORT // Using static or shared. +#endif + +#endif // BBOT_EXPORT diff --git a/bbot/manifest b/bbot/manifest new file mode 100644 index 0000000..5122dc6 --- /dev/null +++ b/bbot/manifest @@ -0,0 +1,215 @@ +// file : bbot/manifest -*- C++ -*- +// copyright : Copyright (c) 2014-2017 Code Synthesis Ltd +// license : MIT; see accompanying LICENSE file + +#ifndef BBOT_MANIFEST +#define BBOT_MANIFEST + +#include +#include +#include + +#include +#include +#include +#include + +#include // version, repository_location + +#include + +#include + +namespace bbot +{ + enum class machine_type {vm, container}; + + class LIBBBOT_EXPORT machine_manifest + { + public: + std::string id; + std::string name; + + // Absent if inside task_request_manifest. + // + butl::optional type; + + std::string summary; + + public: + machine_manifest () = default; // VC export. + machine_manifest (butl::manifest_parser&, bool ignore_unknown = false); + machine_manifest (butl::manifest_parser&, + butl::manifest_name_value start, + bool ignore_unknown = false); + + void + serialize (butl::manifest_serializer&) const; + + private: + machine_manifest (butl::manifest_parser&, + butl::manifest_name_value start, + bool in_list, + bool ignore_unknown); + }; + + class LIBBBOT_EXPORT task_request_manifest + { + public: + std::string agent; + + // Agent's public key SHA256 fingerprint. + // + // @@ How the fingerpring for openssl public key will be produced? Seems + // there is no "standard" for it. Possibly we will use the following + // command result (plain SHA256). + // + // $ cat key.pub | openssl sha256 + // + std::string fingerprint; + + std::vector machines; + + public: + task_request_manifest () = default; // VC export. + task_request_manifest (butl::manifest_parser&, + bool ignore_unknown = false); + + void + serialize (butl::manifest_serializer&) const; + }; + + class LIBBBOT_EXPORT task_manifest + { + public: + // Package to build. + // + std::string name; + bpkg::version version; + bpkg::repository_location repository; // Remote or absolute. + + // Build machine to use for building the package. + // + std::string machine; + + // Default for the machine if absent. + // + butl::optional target; + + // Build system configuration variables (in addition to build environment + // configuration variables). + // + variables config; + + public: + task_manifest () = default; // VC export. + task_manifest (butl::manifest_parser&, bool ignore_unknown = false); + task_manifest (butl::manifest_parser&, + butl::manifest_name_value start, + bool ignore_unknown = false); + + void + serialize (butl::manifest_serializer&) const; + }; + + class LIBBBOT_EXPORT task_response_manifest + { + public: + // If empty then no task available. + // + std::string session; + + // Challenge and task are absent if session is empty. + // + butl::optional challenge; + butl::optional task; + + public: + task_response_manifest () = default; // VC export. + task_response_manifest (butl::manifest_parser&, + bool ignore_unknown = false); + + void + serialize (butl::manifest_serializer&) const; + }; + + // Build task or operation result status. + // + enum class result_status: std::uint8_t + { + // The order of the enumerators is arranged so that their integral values + // indicate whether one "overrides" the other in the "merge" operator| + // (see below). + // + success, + warning, + error, + abort, + abnormal + }; + + std::ostream& + operator<< (std::ostream&, result_status); + + inline result_status& + operator |= (result_status& l, result_status r) + { + if (static_cast (r) > static_cast (l)) + l = r; + return l; + } + + struct operation_result + { + std::string operation; // "configure", "update", "test", etc. + result_status status; + std::string log; + }; + + using operation_results = butl::small_vector; + + class LIBBBOT_EXPORT result_manifest + { + public: + // Built package. + // + std::string name; + bpkg::version version; + + result_status status; + + // Ordered (ascending) by operation value. May not contain all the + // operations if the task failed in the middle, but should have no gaps + // (operation can not start unless all previous ones succeeded). + // + operation_results results; + + result_manifest () = default; // VC export. + result_manifest (butl::manifest_parser&, bool ignore_unknown = false); + result_manifest (butl::manifest_parser&, + butl::manifest_name_value start, + bool ignore_unknown = false); + + void + serialize (butl::manifest_serializer&) const; + }; + + class LIBBBOT_EXPORT result_request_manifest + { + public: + std::string session; // The task response session. + std::string challenge; // The answer to challenge in the task response. + + result_manifest result; + + public: + result_request_manifest () = default; // VC export. + result_request_manifest (butl::manifest_parser&, + bool ignore_unknown = false); + + void + serialize (butl::manifest_serializer&) const; + }; +} + +#endif // BBOT_MANIFEST diff --git a/bbot/manifest.cxx b/bbot/manifest.cxx new file mode 100644 index 0000000..6c9fe4d --- /dev/null +++ b/bbot/manifest.cxx @@ -0,0 +1,931 @@ +// file : bbot/manifest.cxx -*- C++ -*- +// copyright : Copyright (c) 2014-2017 Code Synthesis Ltd +// license : MIT; see accompanying LICENSE file + +#include + +#include +#include +#include +#include +#include // size_t +#include // move() +#include // uint64_t +#include // find() + +#include // digit() +#include +#include +#include + +using namespace std; +using namespace butl; +using namespace bpkg; + +namespace bbot +{ + using parser = manifest_parser; + using parsing = manifest_parsing; + using serializer = manifest_serializer; + using serialization = manifest_serialization; + using name_value = manifest_name_value; + + using strings = vector; + + static const strings machine_type_names ({"vm", "container"}); + + static const strings result_status_names ({ + "success", + "warning", + "error", + "abort", + "abnormal"}); + + ostream& + operator<< (ostream& o, result_status s) + { + size_t i (static_cast (s)); + assert (i < result_status_names.size ()); + o << result_status_names[i]; + return o; + } + + inline static bool + valid_sha256 (const string& s) noexcept + { + if (s.size () != 64) + return false; + + for (const auto& c: s) + { + if ((c < 'a' || c > 'f' ) && !digit (c)) + return false; + } + + return true; + } + + // machine_manifest + // + machine_manifest:: + machine_manifest (parser& p, bool iu) + : machine_manifest (p, p.next (), false, iu) + { + // Make sure this is the end. + // + name_value nv (p.next ()); + if (!nv.empty ()) + throw parsing (p.name (), nv.name_line, nv.name_column, + "single machine manifest expected"); + } + + machine_manifest:: + machine_manifest (parser& p, name_value nv, bool iu) + : machine_manifest (p, nv, true, iu) + { + } + + machine_manifest:: + machine_manifest (parser& p, name_value nv, bool il, bool iu) + { + auto bad_name = [&p, &nv] (const string& d) + { + throw parsing (p.name (), nv.name_line, nv.name_column, d); + }; + + auto bad_value = [&p, &nv] (const string& d) + { + throw parsing (p.name (), nv.value_line, nv.value_column, d); + }; + + // Make sure this is the start and we support the version. + // + if (!nv.name.empty ()) + bad_name ("start of machine manifest expected"); + + if (nv.value != "1") + bad_value ("unsupported format version"); + + for (nv = p.next (); !nv.empty (); nv = p.next ()) + { + string& n (nv.name); + string& v (nv.value); + + if (n == "id") + { + if (!id.empty ()) + bad_name ("machine id redefinition"); + + if (v.empty ()) + bad_value ("empty machine id"); + + id = move (v); + } + else if (n == "name") + { + if (!name.empty ()) + bad_name ("machine name redefinition"); + + if (v.empty ()) + bad_value ("empty machine name"); + + name = move (v); + } + else if (n == "type") + { + if (il) + bad_name ("machine type not allowed"); + + if (type) + bad_name ("machine type redefinition"); + + auto b (machine_type_names.cbegin ()); + auto e (machine_type_names.cend ()); + auto i (find (b, e, v)); + + if (i == e) + bad_value ("invalid machine type"); + + type = static_cast (i - b); + } + else if (n == "summary") + { + if (!summary.empty ()) + bad_name ("machine summary redefinition"); + + if (v.empty ()) + bad_value ("empty machine summary"); + + summary = move (v); + } + else if (!iu) + bad_name ("unknown name '" + n + "' in machine manifest"); + } + + // Verify all non-optional values were specified. + // + if (id.empty ()) + bad_value ("no machine id specified"); + + if (name.empty ()) + bad_value ("no machine name specified"); + + if (!il && !type) + bad_value ("no machine type specified"); + + if (summary.empty ()) + bad_value ("no machine summary specified"); + } + + void machine_manifest:: + serialize (serializer& s) const + { + // @@ Should we check that all non-optional values are specified and all + // values are valid? + // + s.next ("", "1"); // Start of manifest. + s.next ("id", id); + s.next ("name", name); + + if (type) + { + size_t v (static_cast (*type)); + assert (v < machine_type_names.size ()); + s.next ("type", machine_type_names[v]); + } + + s.next ("summary", summary); + s.next ("", ""); // End of manifest. + } + + // task_request_manifest + // + task_request_manifest:: + task_request_manifest (parser& p, bool iu) + { + name_value nv (p.next ()); + + auto bad_name = [&p, &nv] (const string& d) + { + throw parsing (p.name (), nv.name_line, nv.name_column, d); + }; + + auto bad_value = [&p, &nv] (const string& d) + { + throw parsing (p.name (), nv.value_line, nv.value_column, d); + }; + + // Make sure this is the start and we support the version. + // + if (!nv.name.empty ()) + bad_name ("start of task request manifest expected"); + + if (nv.value != "1") + bad_value ("unsupported format version"); + + // Parse the task request manifest. + // + for (nv = p.next (); !nv.empty (); nv = p.next ()) + { + string& n (nv.name); + string& v (nv.value); + + if (n == "agent") + { + if (!agent.empty ()) + bad_name ("task request agent redefinition"); + + if (v.empty ()) + bad_value ("empty task request agent"); + + agent = move (v); + } + else if (n == "fingerprint") + { + if (!fingerprint.empty ()) + bad_name ("task request fingerprint redefinition"); + + if (!valid_sha256 (v)) + bad_value ("invalid task request fingerprint"); + + fingerprint = move (v); + } + else if (!iu) + bad_name ("unknown name '" + n + "' in task request manifest"); + } + + // Verify all non-optional values were specified. + // + if (agent.empty ()) + bad_value ("no task request agent specified"); + + if (fingerprint.empty ()) + bad_value ("no task request fingerprint specified"); + + // Parse machine manifests. + // + for (nv = p.next (); !nv.empty (); nv = p.next ()) + machines.emplace_back (machine_manifest (p, nv, iu)); + + if (machines.empty ()) + bad_value ("no task request machines specified"); + } + + void task_request_manifest:: + serialize (serializer& s) const + { + // @@ Should we check that all non-optional values are specified and all + // values are valid? + // + s.next ("", "1"); // Start of manifest. + s.next ("agent", agent); + s.next ("fingerprint", fingerprint); + s.next ("", ""); // End of manifest. + + for (const machine_manifest& m: machines) + { + if (m.type) + throw serialization (s.name (), "machine type is forbidden"); + + m.serialize (s); + } + + s.next ("", ""); // End of stream. + } + + // task_manifest + // + task_manifest:: + task_manifest (parser& p, bool iu) + : task_manifest (p, p.next (), iu) + { + // Make sure this is the end. + // + name_value nv (p.next ()); + if (!nv.empty ()) + throw parsing (p.name (), nv.name_line, nv.name_column, + "single task manifest expected"); + } + + task_manifest:: + task_manifest (parser& p, name_value nv, bool iu) + { + auto bad_name = [&p, &nv] (const string& d) + { + throw parsing (p.name (), nv.name_line, nv.name_column, d); + }; + + // Offsets are used to tie an error to the specific position inside a + // manifest value (possibly a multiline one). + // + auto bad_value = [&p, &nv] ( + const string& d, uint64_t column_offset = 0, uint64_t line_offset = 0) + { + throw parsing (p.name (), + nv.value_line + line_offset, + (line_offset == 0 ? nv.value_column : 1) + column_offset, + d); + }; + + // Make sure this is the start and we support the version. + // + if (!nv.name.empty ()) + bad_name ("start of task manifest expected"); + + if (nv.value != "1") + bad_value ("unsupported format version"); + + // Parse the task manifest. + // + for (nv = p.next (); !nv.empty (); nv = p.next ()) + { + string& n (nv.name); + string& v (nv.value); + + if (n == "name") + { + if (!name.empty ()) + bad_name ("task package name redefinition"); + + if (v.empty ()) + bad_value ("empty task package name"); + + name = move (v); + } + else if (n == "version") + { + if (!version.empty ()) + bad_name ("task package version redefinition"); + + try + { + version = bpkg::version (move (v)); + } + catch (const invalid_argument& e) + { + bad_value (string ("invalid task package version: ") + e.what ()); + } + + // Versions like 1.2.3- are forbidden in manifest as intended to be + // used for version constrains rather than actual releases. + // + if (version.release && version.release->empty ()) + bad_value ("invalid task package version release"); + } + else if (n == "repository") + { + if (!repository.empty ()) + bad_name ("task repository redefinition"); + + if (v.empty ()) + bad_value ("empty task repository"); + + try + { + // Call remote/absolute repository location constructor (throws + // invalid_argument for relative location). + // + repository = repository_location (move (v)); + } + catch (const invalid_argument& e) + { + bad_value (string ("invalid task repository: ") + e.what ()); + } + } + else if (n == "machine") + { + if (!machine.empty ()) + bad_name ("task machine redefinition"); + + if (v.empty ()) + bad_value ("empty task machine"); + + machine = move (v); + } + else if (n == "target") + { + if (target) + bad_name ("task target redefinition"); + + try + { + target = target_triplet (v); + } + catch (const invalid_argument& e) + { + bad_value (string ("invalid task target: ") + e.what ()); + } + } + else if (n == "config") + { + if (!config.empty ()) + bad_name ("task configuration redefinition"); + + // Note that when reporting errors we combine the manifest value + // position with the respective field and error positions. + // + try + { + istringstream is (v); + tab_parser parser (is, ""); + + // Here we naturally support multiline config manifest. + // + tab_fields tl; + while (!(tl = parser.next ()).empty ()) + { + for (auto& tf: tl) + { + try + { + config.emplace_back (variable (move (tf.value))); + } + catch (const invalid_variable& e) + { + bad_value (string ("invalid task configuration: ") + e.what (), + tf.column - 1 + e.pos, + tl.line - 1); + } + } + } + } + catch (const tab_parsing& e) + { + bad_value ("invalid task configuration: " + e.description, + e.column - 1, + e.line - 1); + } + + if (config.empty ()) + bad_value ("empty task configuration"); + } + else if (!iu) + bad_name ("unknown name '" + n + "' in task manifest"); + } + + // Verify all non-optional values were specified. + // + if (name.empty ()) + bad_value ("no task package name specified"); + + if (version.empty ()) + bad_value ("no task package version specified"); + + if (repository.empty ()) + bad_value ("no task repository specified"); + + if (machine.empty ()) + bad_value ("no task machine specified"); + } + + void task_manifest:: + serialize (serializer& s) const + { + // @@ Should we check that all non-optional values are specified and all + // values are valid? + // + s.next ("", "1"); // Start of manifest. + s.next ("name", name); + s.next ("version", version.string ()); + s.next ("repository", repository.string ()); + s.next ("machine", machine); + + if (target) + s.next ("target", target->string ()); + + // Recompose config string as a space-separated variable list, + // + if (!config.empty ()) + { + string v; + for (auto b (config.cbegin ()), i (b), e (config.cend ()); i != e; ++i) + { + if (i != b) + v += ' '; + + v += *i; + } + + s.next ("config", v); + } + + s.next ("", ""); // End of manifest. + } + + // task_response_manifest + // + task_response_manifest:: + task_response_manifest (parser& p, bool iu) + { + name_value nv (p.next ()); + + auto bad_name = [&p, &nv] (const string& d) + { + throw parsing (p.name (), nv.name_line, nv.name_column, d); + }; + + auto bad_value = [&p, &nv] (const string& d) + { + throw parsing (p.name (), nv.value_line, nv.value_column, d); + }; + + // Make sure this is the start and we support the version. + // + if (!nv.name.empty ()) + bad_name ("start of task response manifest expected"); + + if (nv.value != "1") + bad_value ("unsupported format version"); + + // Parse the task response manifest. + // + // Note that we need to distinguish an empty and absent session. + // + optional sess; + for (nv = p.next (); !nv.empty (); nv = p.next ()) + { + string& n (nv.name); + string& v (nv.value); + + if (n == "session") + { + if (sess) + bad_name ("task response session redefinition"); + + sess = move (v); + } + else if (n == "challenge") + { + if (challenge) + bad_name ("task response challenge redefinition"); + + if (v.empty ()) + bad_value ("empty task response challenge"); + + challenge = move (v); + } + else if (!iu) + bad_name ("unknown name '" + n + "' in task response manifest"); + } + + // Verify all non-optional values were specified, and all values are + // expected. + // + if (!sess) + bad_value ("no task response session specified"); + + session = move (*sess); + + // If session is not empty then the challenge must present, otherwise it + // shouldn't. + // + if (!session.empty ()) + { + if (!challenge) + bad_value ("no task response challenge specified"); + } + else if (challenge) + bad_value ("unexpected task response challenge"); + + // If session is not empty then the task manifest must follow, otherwise it + // shouldn't. + // + nv = p.next (); + + if (!session.empty ()) + { + if (nv.empty ()) + bad_value ("task manifest expected"); + + task = task_manifest (p, nv, iu); + + nv = p.next (); + } + + // Make sure this is the end. + // + if (!nv.empty ()) + throw parsing (p.name (), nv.name_line, nv.name_column, + "single task response manifest expected"); + } + + void task_response_manifest:: + serialize (serializer& s) const + { + // @@ Should we check that all non-optional values are specified and all + // values are valid? + // + s.next ("", "1"); // Start of manifest. + s.next ("session", session); + + if (challenge) + s.next ("challenge", *challenge); + + s.next ("", ""); // End of manifest. + + if (task) + task->serialize (s); + + s.next ("", ""); // End of stream. + } + + // result_manifest + // + result_manifest:: + result_manifest (parser& p, bool iu) + : result_manifest (p, p.next (), iu) + { + // Make sure this is the end. + // + name_value nv (p.next ()); + if (!nv.empty ()) + throw parsing (p.name (), nv.name_line, nv.name_column, + "single result manifest expected"); + } + + result_manifest:: + result_manifest (parser& p, name_value nv, bool iu) + { + auto bad_name = [&p, &nv] (const string& d) + { + throw parsing (p.name (), nv.name_line, nv.name_column, d); + }; + + auto bad_value = [&p, &nv] (const string& d, size_t offset = 0) + { + throw parsing (p.name (), nv.value_line, nv.value_column + offset, d); + }; + + auto result_stat = + [&bad_value] (const string& v, const string& what) -> result_status + { + auto b (result_status_names.cbegin ()); + auto e (result_status_names.cend ()); + auto i (find (b, e, v)); + + if (i == e) + bad_value ("invalid " + what); + + return static_cast (i - b); + }; + + // Make sure this is the start and we support the version. + // + if (!nv.name.empty ()) + bad_name ("start of result manifest expected"); + + if (nv.value != "1") + bad_value ("unsupported format version"); + + // Parse the result manifest. + // + optional stat; + + // Number of parsed *-log values. Also denotes the next expected log type. + // + size_t nlog (0); + + for (nv = p.next (); !nv.empty (); nv = p.next ()) + { + string& n (nv.name); + string& v (nv.value); + + if (n == "name") + { + if (!name.empty ()) + bad_name ("result package name redefinition"); + + if (v.empty ()) + bad_value ("empty result package name"); + + name = move (v); + } + else if (n == "version") + { + if (!version.empty ()) + bad_name ("result package version redefinition"); + + try + { + version = bpkg::version (move (v)); + } + catch (const invalid_argument& e) + { + bad_value (string ("invalid result package version: ") + e.what ()); + } + + // Versions like 1.2.3- are forbidden in manifest as intended to be + // used for version constrains rather than actual releases. + // + if (version.release && version.release->empty ()) + bad_value ("invalid result package version release"); + } + else if (n == "status") + { + if (stat) + bad_name ("result status redefinition"); + + stat = result_stat (v, "result status"); + } + else + { + size_t nn (n.size ()); // Name length. + + // Note: returns false if nothing preceeds a suffix. + // + auto suffix = [&n, nn] (const char* s, size_t sn) -> bool + { + return nn > sn && n.compare (nn - sn, sn, s) == 0; + }; + + size_t sn; + if (suffix ("-status", sn = 7)) + { + if (!stat) + bad_name ("result status must appear first"); + + if (nlog > 0) // Some logs have already been parsed. + bad_name (n + " after operations logs"); + + string op (n, 0, nn - sn); + + // Make sure the operation result status is not redefined. + // + for (const auto& r: results) + { + if (r.operation == op) + bad_name ("result " + n + " redefinition"); + } + + // Add the operation result (log will come later). + // + results.push_back ({move (op), result_stat (v, n), string ()}); + } + else if (suffix ("-log", sn = 4)) + { + string op (n, 0, nn - sn); + + // Check that specifically this operation log is expected. + // + if (nlog >= results.size ()) + bad_name ("unexpected " + n); + + if (results[nlog].operation != op) + bad_name (results[nlog].operation + "-log is expected"); + + // Save operation log. + // + results[nlog++].log = move (v); + } + else if (!iu) + bad_name ("unknown name '" + n + "' in result manifest"); + } + } + + // Verify all non-optional values were specified. + // + if (name.empty ()) + bad_value ("no result package name specified"); + + if (version.empty ()) + bad_value ("no result package version specified"); + + if (!stat) + bad_value ("no result status specified"); + + // @@ Checking that the result status is consistent with operations + // statuses is a bit hairy, so let's postpone for now. + // + status = move (*stat); + + // Check that we have log for every operation result status. + // + if (nlog < results.size ()) + bad_name ("no result " + results[nlog].operation + "-log specified"); + } + + void result_manifest:: + serialize (serializer& s) const + { + // @@ Should we check that all non-optional values are specified and all + // values are valid? + // + s.next ("", "1"); // Start of manifest. + s.next ("name", name); + s.next ("version", version.string ()); + + size_t v (static_cast (status)); + assert (v < result_status_names.size ()); + s.next ("status", result_status_names[v]); + + // Serialize *-status values. + // + for (const auto& r: results) + { + size_t rs (static_cast (r.status)); + assert (rs < result_status_names.size ()); + + s.next (r.operation + "-status", result_status_names[rs]); + } + + // Serialize *-log values. + // + for (const auto& r: results) + s.next (r.operation + "-log", r.log); + + s.next ("", ""); // End of manifest. + } + + // result_request_manifest + // + result_request_manifest:: + result_request_manifest (parser& p, bool iu) + { + name_value nv (p.next ()); + + auto bad_name = [&p, &nv] (const string& d) + { + throw parsing (p.name (), nv.name_line, nv.name_column, d); + }; + + auto bad_value = [&p, &nv] (const string& d) + { + throw parsing (p.name (), nv.value_line, nv.value_column, d); + }; + + // Make sure this is the start and we support the version. + // + if (!nv.name.empty ()) + bad_name ("start of result request manifest expected"); + + if (nv.value != "1") + bad_value ("unsupported format version"); + + // Parse the result request manifest. + // + for (nv = p.next (); !nv.empty (); nv = p.next ()) + { + string& n (nv.name); + string& v (nv.value); + + if (n == "session") + { + if (!session.empty ()) + bad_name ("result request session redefinition"); + + if (v.empty ()) + bad_value ("empty result request session"); + + session = move (v); + } + else if (n == "challenge") + { + if (!challenge.empty ()) + bad_name ("result request challenge redefinition"); + + if (v.empty ()) + bad_value ("empty result request challenge"); + + challenge = move (v); + } + else if (!iu) + bad_name ("unknown name '" + n + "' in result request manifest"); + } + + // Verify all non-optional values were specified. + // + if (session.empty ()) + bad_value ("no result request session specified"); + + if (challenge.empty ()) + bad_value ("no result request challenge specified"); + + nv = p.next (); + if (nv.empty ()) + bad_value ("result manifest expected"); + + result = result_manifest (p, nv, iu); + + // Make sure this is the end. + // + nv = p.next (); + if (!nv.empty ()) + throw parsing (p.name (), nv.name_line, nv.name_column, + "single result request manifest expected"); + } + + void result_request_manifest:: + serialize (serializer& s) const + { + // @@ Should we check that all non-optional values are specified and all + // values are valid? + // + s.next ("", "1"); // Start of manifest. + s.next ("session", session); + s.next ("challenge", challenge); + s.next ("", ""); // End of manifest. + + result.serialize (s); + s.next ("", ""); // End of stream. + } +} diff --git a/bbot/variable b/bbot/variable new file mode 100644 index 0000000..c210dc0 --- /dev/null +++ b/bbot/variable @@ -0,0 +1,49 @@ +// file : bbot/variable -*- C++ -*- +// copyright : Copyright (c) 2014-2017 Code Synthesis Ltd +// license : MIT; see accompanying LICENSE file + +#ifndef BBOT_VARIABLE +#define BBOT_VARIABLE + +#include +#include +#include // uint64_t +#include // invalid_argument + +#include + +namespace bbot +{ + //@@ invalid_argument + // + class LIBBBOT_EXPORT invalid_variable: public std::invalid_argument + { + public: + invalid_variable (std::uint64_t p, const std::string& d) + : invalid_argument (d), pos (p) {} + + std::uint64_t pos; // Zero-based. + }; + + // String in the name=value format. Can contain single or double quoted + // substrings. No escaping is supported. The name must not contain spaces. + // Throw variable_error if the string doesn't conform with the above + // constraints. + // + struct LIBBBOT_EXPORT variable: std::string + { + variable (std::string); + + // Remove a single level of quotes. Don't validate the format or the + // correctness of the quotation. Note that the variable can potentially be + // modified through the std::string interface in a way that breaks + // format/quoting. + // + std::string + unquoted () const; + }; + + using variables = std::vector; +} + +#endif // BBOT_VARIABLE diff --git a/bbot/variable.cxx b/bbot/variable.cxx new file mode 100644 index 0000000..a25b007 --- /dev/null +++ b/bbot/variable.cxx @@ -0,0 +1,94 @@ +// file : bbot/variable.cxx -*- C++ -*- +// copyright : Copyright (c) 2014-2017 Code Synthesis Ltd +// license : MIT; see accompanying LICENSE file + +#include + +#include // move() + +using namespace std; + +namespace bbot +{ + variable:: + variable (string v): string (move (v)) + { + // Scan the string untill the end to check that the quoting is terminated. + // We will also make sure that the name doesn't contain spaces and the + // value is provided. + // + char quoting ('\0'); // Current quoting mode, can be used as bool. + bool name (true); // True while we are parsing the variable name. + + auto b (cbegin ()); + auto i (b); + + auto bad_variable = [&b, &i] (const string& d) + { + throw invalid_variable (i - b, d); + }; + + for (auto e (cend ()); i != e; ++i) + { + char c (*i); + + if (!quoting) + { + if (c == '"' || c == '\'') // Begin of quoted string, + { + quoting = c; + continue; + } + } + else if (c == quoting) // End of quoted string, + { + quoting = '\0'; + continue; + } + + if (name) + { + if (c == ' ' || c == '\t') + bad_variable ("expected variable assignment"); + else if (c == '=') + name = false; + } + } + + if (quoting) + bad_variable ("unterminated quoted string"); + + if (name) + bad_variable ("no variable value"); + } + + string variable:: + unquoted () const + { + string r; + char quoting ('\0'); // Current quoting mode, can be used as bool. + + for (auto i (cbegin ()), e (cend ()); i != e; ++i) + { + char c (*i); + + if (!quoting) + { + if (c == '"' || c == '\'') // Begin of quoted string. + { + quoting = c; + continue; + } + } + else if (c == quoting) // End of quoted string. + { + quoting = '\0'; + continue; + } + + r += c; + } + + return r; + } +} diff --git a/bbot/version b/bbot/version new file mode 100644 index 0000000..9454762 --- /dev/null +++ b/bbot/version @@ -0,0 +1,37 @@ +// file : bbot/version -*- C++ -*- +// copyright : Copyright (c) 2014-2017 Code Synthesis Ltd +// license : MIT; see accompanying LICENSE file + +#ifndef LIBBBOT_VERSION // Note: using the version macro itself. + +#include // LIBBUTL_VERSION + +// Version format is AABBCCDD where +// +// AA - major version number +// BB - minor version number +// CC - bugfix version number +// DD - alpha / beta (DD + 50) version number +// +// When DD is not 00, 1 is subtracted from AABBCC. For example: +// +// Version AABBCCDD +// 2.0.0 02000000 +// 2.1.0 02010000 +// 2.1.1 02010100 +// 2.2.0-a1 02019901 +// 3.0.0-b2 02999952 +// +#define LIBBBOT_VERSION 49901 +#define LIBBBOT_VERSION_STR "0.5.0-a1" + +// Generally, we expect minor versions to be source code backwards- +// compatible, thought we might have a minimum version requirement. +// +// Note: does not apply during early development. +// +#if LIBBUTL_VERSION != 49901 +# error incompatible libbutl version +#endif + +#endif // LIBBBOT_VERSION diff --git a/build/.gitignore b/build/.gitignore new file mode 100644 index 0000000..225c27f --- /dev/null +++ b/build/.gitignore @@ -0,0 +1 @@ +config.build diff --git a/build/bootstrap.build b/build/bootstrap.build new file mode 100644 index 0000000..28f6222 --- /dev/null +++ b/build/bootstrap.build @@ -0,0 +1,26 @@ +# file : build/bootstrap.build +# copyright : Copyright (c) 2014-2017 Code Synthesis Ltd +# license : MIT; see accompanying LICENSE file + +project = libbbot + +using build@0.4.0 + +version = 0.5.0-a1 + +abi_major = 0 +abi_minor = 5 +abi_patch = 0 +abi_prerelease = true + +revision = 0 + +dist.package = $project-$version + +if ($revision != 0) + dist.package += +$revision + +using config +using dist +using test +using install diff --git a/build/export.build b/build/export.build new file mode 100644 index 0000000..013e46f --- /dev/null +++ b/build/export.build @@ -0,0 +1,10 @@ +# file : build/export.build +# copyright : Copyright (c) 2014-2017 Code Synthesis Ltd +# license : MIT; see accompanying LICENSE file + +$out_root/: +{ + include bbot/ +} + +export $out_root/bbot/lib{bbot} diff --git a/build/root.build b/build/root.build new file mode 100644 index 0000000..af2c962 --- /dev/null +++ b/build/root.build @@ -0,0 +1,20 @@ +# file : build/root.build +# copyright : Copyright (c) 2014-2017 Code Synthesis Ltd +# license : MIT; see accompanying LICENSE file + +cxx.std = latest + +using cxx + +hxx{*}: extension = +ixx{*}: extension = ixx +txx{*}: extension = txx +cxx{*}: extension = cxx + +# All exe{} in tests/ are, well, tests. +# +tests/exe{*}: test = true + +# Specify the test target for cross-testing. +# +test.target = $cxx.target diff --git a/buildfile b/buildfile new file mode 100644 index 0000000..9442683 --- /dev/null +++ b/buildfile @@ -0,0 +1,12 @@ +# file : buildfile +# copyright : Copyright (c) 2014-2017 Code Synthesis Ltd +# license : MIT; see accompanying LICENSE file + +d = bbot/ tests/ +./: $d doc{INSTALL LICENSE NEWS README version} file{manifest} +include $d + +# Don't install tests or the INSTALL file. +# +dir{tests/}: install = false +doc{INSTALL}@./: install = false diff --git a/manifest b/manifest new file mode 100644 index 0000000..76d24bd --- /dev/null +++ b/manifest @@ -0,0 +1,15 @@ +: 1 +name: libbbot +version: 0.5.0-a1 +summary: build2 build bot library +license: MIT +tags: build2, bbot, build, bot +description-file: README +changes-file: NEWS +url: https://build2.org +email: users@build2.org +requires: c++14 +depends: * build2 >= 0.4.0 +depends: * bpkg >= 0.4.0 +depends: libbutl == 0.5.0-a1 +depends: libbpkg == 0.5.0-a1 diff --git a/tests/.gitignore b/tests/.gitignore new file mode 100644 index 0000000..e54525b --- /dev/null +++ b/tests/.gitignore @@ -0,0 +1 @@ +driver diff --git a/tests/buildfile b/tests/buildfile new file mode 100644 index 0000000..10e73ec --- /dev/null +++ b/tests/buildfile @@ -0,0 +1,5 @@ +# file : tests/buildfile +# copyright : Copyright (c) 2014-2017 Code Synthesis Ltd +# license : MIT; see accompanying LICENSE file + +./: */ diff --git a/tests/buildtab/buildfile b/tests/buildtab/buildfile new file mode 100644 index 0000000..f156dbf --- /dev/null +++ b/tests/buildtab/buildfile @@ -0,0 +1,9 @@ +# file : tests/buildtab/buildfile +# copyright : Copyright (c) 2014-2017 Code Synthesis Ltd +# license : MIT; see accompanying LICENSE file + +import libs += libbutl%lib{butl} + +exe{driver}: cxx{driver} ../../bbot/lib{bbot} $libs test{testscript} + +include ../../bbot/ diff --git a/tests/buildtab/driver.cxx b/tests/buildtab/driver.cxx new file mode 100644 index 0000000..c3e3a60 --- /dev/null +++ b/tests/buildtab/driver.cxx @@ -0,0 +1,48 @@ +// file : tests/buildtab/driver.cxx -*- C++ -*- +// copyright : Copyright (c) 2014-2017 Code Synthesis Ltd +// license : MIT; see accompanying LICENSE file + +#include // ios::failbit, ios::badbit +#include +#include + +#include // operator<<(ostream,exception) + +#include + +using namespace std; +using namespace butl; +using namespace bbot; + +// Usage: argv[0] +// +// Read and parse buildtab from STDIN and serialize the resulted build +// configuration to STDOUT. +// +int +main () +try +{ + cin.exceptions (ios::failbit | ios::badbit); + cout.exceptions (ios::failbit | ios::badbit); + + for (const auto& c: parse_buildtab (cin, "cin")) + { + cout << c.machine_pattern << ' ' << c.name; + + if (c.target) + cout << ' ' << *c.target; + + for (const auto& v: c.vars) + cout << ' ' << v; + + cout << '\n'; + } + + return 0; +} +catch (const tab_parsing& e) +{ + cerr << e << endl; + return 1; +} diff --git a/tests/buildtab/testscript b/tests/buildtab/testscript new file mode 100644 index 0000000..5bf6b24 --- /dev/null +++ b/tests/buildtab/testscript @@ -0,0 +1,63 @@ +# file : tests/buildtab/testscript +# copyright : Copyright (c) 2014-2017 Code Synthesis Ltd +# license : MIT; see accompanying LICENSE file + +: valid +: +: Roundtrip buildtab. +: +{ + : all-fileds-combinations + : + $* <>EOF + windows*-vc_14* windows-vc_14 + windows*-vc_14* windows-vc_14-32 i686-microsoft-win32-msvc14.0 + windows*-vc_14* windows-vc_14debug config.cc.coptions=/Z7 config.cc.loptions=/DEBUG + windows*-vc_14* windows-vc_14-32-debug i686-microsoft-win32-msvc14.0 config.cc.coptions=/Z7 config.cc.loptions=/DEBUG + EOF + + : empty-lines + : + $* <>EOO + + windows*-vc_14* windows-vc_14-32-debug + # abc + EOI + windows*-vc_14* windows-vc_14-32-debug + EOO +} + +: parse-errors +: +{ + : no-name + : + $* <>EOE == 1 + windows*-vc_14* + EOI + cin:1:16: error: no configuration name found + EOE + + : invalid-target + : + $* <>EOE == 1 + windows*-vc_14* windows-vc_14-32 microsoft + EOI + cin:1:34: error: missing cpu + EOE + + : invalid-var + : + $* <>EOE == 1 + windows*-vc_14* windows-vc_14-32 config.cc.coptions="/Z7 + EOI + cin:1:57: error: unterminated quoted string + EOE + + : dup-config-name + : + $* <'cin:2:17: error: duplicate configuration name' == 1 + windows*-vc_14* windows-vc_14-32 + windows*-vc_14* windows-vc_14-32 i686-microsoft-win32-msvc14.0 + EOI +} diff --git a/tests/manifest/buildfile b/tests/manifest/buildfile new file mode 100644 index 0000000..002dbb0 --- /dev/null +++ b/tests/manifest/buildfile @@ -0,0 +1,9 @@ +# file : tests/manifest/buildfile +# copyright : Copyright (c) 2014-2017 Code Synthesis Ltd +# license : MIT; see accompanying LICENSE file + +import libs += libbutl%lib{butl} + +exe{driver}: cxx{driver} ../../bbot/lib{bbot} $libs test{*} + +include ../../bbot/ diff --git a/tests/manifest/driver.cxx b/tests/manifest/driver.cxx new file mode 100644 index 0000000..0f24c8a --- /dev/null +++ b/tests/manifest/driver.cxx @@ -0,0 +1,71 @@ +// file : tests/manifest/driver.cxx -*- C++ -*- +// copyright : Copyright (c) 2014-2017 Code Synthesis Ltd +// license : MIT; see accompanying LICENSE file + +#include // ios_base::failbit, ios_base::badbit +#include +#include +#include + +#include // operator<<(ostream,exception) +#include +#include + +#include + +using namespace std; +using namespace butl; +using namespace bbot; + +// Usage: argv[0] (-m|-t|-r|-tq|-ts|-rq) +// +// Read and parse manifest from STDIN and serialize it to STDOUT. The +// following options specify the manifest type. +// +// -m parse machine manifest +// -t parse task manifest +// -r parse result manifest +// -tq parse task request manifest +// -ts parse task response manifest +// -rq parse result request manifest +// +int +main (int argc, char* argv[]) +try +{ + assert (argc == 2); + string opt (argv[1]); + + cin.exceptions (ios_base::failbit | ios_base::badbit); + cout.exceptions (ios_base::failbit | ios_base::badbit); + + manifest_parser p (cin, "stdin"); + manifest_serializer s (cout, "stdout"); + + if (opt == "-m") + machine_manifest (p).serialize (s); + else if (opt == "-t") + task_manifest (p).serialize (s); + else if (opt == "-r") + result_manifest (p).serialize (s); + else if (opt == "-tq") + task_request_manifest (p).serialize (s); + else if (opt == "-ts") + task_response_manifest (p).serialize (s); + else if (opt == "-rq") + result_request_manifest (p).serialize (s); + else + assert (false); + + return 0; +} +catch (const manifest_parsing& e) +{ + cerr << e << endl; + return 1; +} +catch (const manifest_serialization& e) +{ + cerr << e << endl; + return 1; +} diff --git a/tests/manifest/machine.test b/tests/manifest/machine.test new file mode 100644 index 0000000..b775a79 --- /dev/null +++ b/tests/manifest/machine.test @@ -0,0 +1,157 @@ +# file : tests/manifest/machine.test +# copyright : Copyright (c) 2014-2017 Code Synthesis Ltd +# license : MIT; see accompanying LICENSE file + +test.options += -m + +: valid +: +: Roundtrip the machine manifest. +: +{ + : vm + : + $* <>EOF + : 1 + id: a2b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 + name: windows_10-msvc_14 + type: vm + summary: Windows 10 build 1607 with VC 14 update 3 + EOF + + : container + : + $* <>EOF + : 1 + id: a2b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 + name: windows_10-msvc_14 + type: container + summary: Windows 10 build 1607 with VC 14 update 3 + EOF +} + +: multiple +: +$* <'stdin:6:1: error: single machine manifest expected' == 1 +: 1 +id: a2b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 +name: windows_10-msvc_14 +type: vm +summary: Windows 10 build 1607 with VC 14 update 3 +: +EOI + +: redefinition +: +{ + : id + : + $* <'stdin:3:1: error: machine id redefinition' == 1 + : 1 + id: 123 + id: 123 + EOI + + : name + : + $* <'stdin:3:1: error: machine name redefinition' == 1 + : 1 + name: windows + name: windows + EOI + + : type + : + $* <'stdin:3:1: error: machine type redefinition' == 1 + : 1 + type: vm + type: vm + EOI + + : summary + : + $* <'stdin:3:1: error: machine summary redefinition' == 1 + : 1 + summary: Windows + summary: Windows + EOI +} + +: empty +: +{ + : id + : + $* <'stdin:2:4: error: empty machine id' == 1 + : 1 + id: + EOI + + : name + : + $* <'stdin:2:6: error: empty machine name' == 1 + : 1 + name: + EOI + + : summary + : + $* <'stdin:2:9: error: empty machine summary' == 1 + : 1 + summary: + EOI +} + +: invalid-type +: +$* <'stdin:2:7: error: invalid machine type' == 1 +: 1 +type: unknown +EOI + +: unknown-name +: +$* <"stdin:2:1: error: unknown name 'x' in machine manifest" == 1 +: 1 +x: +EOI + +: missed +: +{ + : id + : + $* <'stdin:5:1: error: no machine id specified' == 1 + : 1 + name: windows + type: vm + summary: Windows + EOI + + : name + : + $* <'stdin:5:1: error: no machine name specified' == 1 + : 1 + id: 123 + type: vm + summary: Windows + EOI + + : type + : + $* <'stdin:5:1: error: no machine type specified' == 1 + : 1 + id: 123 + name: windows + summary: Windows + EOI + + : summary + : + $* <'stdin:5:1: error: no machine summary specified' == 1 + : 1 + id: 123 + name: windows + type: vm + EOI +} diff --git a/tests/manifest/result-request.test b/tests/manifest/result-request.test new file mode 100644 index 0000000..8cf64b9 --- /dev/null +++ b/tests/manifest/result-request.test @@ -0,0 +1,92 @@ +# file : tests/manifest/result-request.test +# copyright : Copyright (c) 2014-2017 Code Synthesis Ltd +# license : MIT; see accompanying LICENSE file + +test.options += -rq + +: valid +: +: Roundtrip the result request manifest. +: +{ + $* <>EOF + : 1 + session: abcd + challenge: xyz + : + name: libfoo + version: 1.0 + status: error + EOF +} + +: redefinition +: +{ + : session + : + $* <'stdin:3:1: error: result request session redefinition' == 1 + : 1 + session: abcd + session: abcd + EOI + + : challenge + : + $* <'stdin:3:1: error: result request challenge redefinition' == 1 + : 1 + challenge: xyz + challenge: xyz + EOI +} + +: empty +: +{ + : session + : + $* <'stdin:2:9: error: empty result request session' == 1 + : 1 + session: + EOI + + : challenge + : + $* <'stdin:2:11: error: empty result request challenge' == 1 + : 1 + challenge: + EOI +} + +: unknown-name +: +$* <"stdin:2:1: error: unknown name 'x' in result request manifest" == 1 +: 1 +x: +EOI + +: missed +: +{ + : session + : + $* <'stdin:3:1: error: no result request session specified' == 1 + : 1 + challenge: xyz + EOI + + : challenge + : + $* <'stdin:3:1: error: no result request challenge specified' == 1 + : 1 + session: abc + EOI + + : result + : + $* <'stdin:4:1: error: result manifest expected' == 1 + : 1 + session: abc + challenge: xyz + EOI +} diff --git a/tests/manifest/result.test b/tests/manifest/result.test new file mode 100644 index 0000000..52964e0 --- /dev/null +++ b/tests/manifest/result.test @@ -0,0 +1,230 @@ +# file : tests/manifest/result.test +# copyright : Copyright (c) 2014-2017 Code Synthesis Ltd +# license : MIT; see accompanying LICENSE file + +test.options += -r + +: valid +: +: Roundtrip the result manifest. +: +{ + : test-error + : + $* <>EOF + : 1 + name: libfoo + version: 1.0 + status: error + configure-status: success + update-status: warning + test-status: error + configure-log: \ + conf line 1 + conf line 2 + \ + update-log: \ + update line 1 + update line 2 + \ + test-log: \ + test line 1 + test line 2 + \ + EOF + + : update-error + : + $* <>EOF + : 1 + name: libfoo + version: 1.0 + status: error + configure-status: warning + update-status: error + configure-log: \ + conf line 1 + conf line 2 + \ + update-log: \ + update line 1 + update line 2 + \ + EOF + + : early-abort + : + $* <>EOF + : 1 + name: libfoo + version: 1.0 + status: abort + EOF +} + +: redefinition +: +{ + : name + : + $* <'stdin:3:1: error: result package name redefinition' == 1 + : 1 + name: libfoo + name: libfoo + EOI + + : version + : + $* <'stdin:3:1: error: result package version redefinition' == 1 + : 1 + version: 1.0 + version: 1.0 + EOI + + : status + : + $* <'stdin:3:1: error: result status redefinition' == 1 + : 1 + status: success + status: error + EOI + + : configure-status + : + $* <'stdin:4:1: error: result configure-status redefinition' == 1 + : 1 + status: success + configure-status: success + configure-status: abnormal + EOI + + : configure-log + : + $* <'stdin:5:1: error: unexpected configure-log' == 1 + : 1 + status: success + configure-status: success + configure-log: configured + configure-log: configured + EOI +} + +: invalid +: +{ + : name-empty + : + $* <'stdin:2:6: error: empty result package name' == 1 + : 1 + name: + EOI + + : version + : + { + : empty + : + $* <'stdin:2:9: error: invalid result package version: unexpected end' == 1 + : 1 + version: + EOI + + : release + : + $* <'stdin:2:10: error: invalid result package version release' == 1 + : 1 + version: 1.2.3- + EOI + } + + : status + : + $* <'stdin:2:9: error: invalid result status' == 1 + : 1 + status: alert + EOI + + : configure-status + : + $* <'stdin:3:19: error: invalid configure-status' == 1 + : 1 + status: abort + configure-status: alert + EOI + + : order + : + { + : op-status-before-status + : + $* <'stdin:2:1: error: result status must appear first' == 1 + : 1 + configure-status: success + EOI + + : op-status-after-log + : + $* <'stdin:5:1: error: update-status after operations logs' == 1 + : 1 + status: success + configure-status: success + configure-log: log + update-status: error + EOI + + : wrong-op-log + : + $* <'stdin:5:1: error: configure-log is expected' == 1 + : 1 + status: success + configure-status: success + update-status: error + update-log: log + EOI + } +} + +: unknown-name +: +$* <"stdin:2:1: error: unknown name 'full-logs' in result manifest" == 1 +: 1 +full-logs: log +EOI + +: missed +: +{ + : name + : + $* <'stdin:4:1: error: no result package name specified' == 1 + : 1 + version: 1.0 + status: success + EOI + + : version + : + $* <'stdin:4:1: error: no result package version specified' == 1 + : 1 + name: libfoo + status: success + EOI + + : status + : + $* <'stdin:4:1: error: no result status specified' == 1 + : 1 + name: libfoo + version: 1.0 + EOI + + : configure-log + : + $* <'stdin:6:1: error: no result configure-log specified' == 1 + : 1 + name: libfoo + version: 1.0 + status: error + configure-status: error + EOI +} diff --git a/tests/manifest/task-request.test b/tests/manifest/task-request.test new file mode 100644 index 0000000..2e3fb75 --- /dev/null +++ b/tests/manifest/task-request.test @@ -0,0 +1,96 @@ +# file : tests/manifest/task-request.test +# copyright : Copyright (c) 2014-2017 Code Synthesis Ltd +# license : MIT; see accompanying LICENSE file + +test.options += -tq + +: valid +: +: Roundtrip the task request manifest. +: +$* <>EOF +: 1 +agent: upsa +fingerprint: 1105fb394ee870adb154b7abfbbae5755df7dcef6c81db34e8d1b68d2653734e +: +id: a2b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 +name: windows_10-msvc_14 +summary: Windows 10 build 1607 with VC 14 update 3 +EOF + +: redefinition +: +{ + : agent + : + $* <'stdin:3:1: error: task request agent redefinition' == 1 + : 1 + agent: upsa + agent: upsa + EOI + + : fingerprint + : + $* <'stdin:3:1: error: task request fingerprint redefinition' == 1 + : 1 + fingerprint: 1105fb394ee870adb154b7abfbbae5755df7dcef6c81db34e8d1b68d2653734e + fingerprint: 1105fb394ee870adb154b7abfbbae5755df7dcef6c81db34e8d1b68d2653734e + EOI +} + +: empty +: +{ + : agent + : + $* <'stdin:2:7: error: empty task request agent' == 1 + : 1 + agent: + EOI +} + +: invalid-fingerprint +: +$* <'stdin:2:14: error: invalid task request fingerprint' == 1 +: 1 +fingerprint: 123x +EOI + +: missed +: +{ + : agent + : + $* <'stdin:3:1: error: no task request agent specified' == 1 + : 1 + fingerprint: 1105fb394ee870adb154b7abfbbae5755df7dcef6c81db34e8d1b68d2653734e + EOI + + : fingerprint + : + $* <'stdin:3:1: error: no task request fingerprint specified' == 1 + : 1 + agent: upsa + EOI +} + +: no-machines +: +$* <'stdin:4:1: error: no task request machines specified' == 1 +: 1 +agent: upsa +fingerprint: 1105fb394ee870adb154b7abfbbae5755df7dcef6c81db34e8d1b68d2653734e +EOI + +: type-not-allowed +: +$* <'stdin:7:1: error: machine type not allowed' == 1 +: 1 +agent: upsa +fingerprint: 1105fb394ee870adb154b7abfbbae5755df7dcef6c81db34e8d1b68d2653734e +: +id: a2b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 +name: windows_10-msvc_14 +type: vm +summary: Windows 10 build 1607 with VC 14 update 3 +EOI diff --git a/tests/manifest/task-response.test b/tests/manifest/task-response.test new file mode 100644 index 0000000..472cca7 --- /dev/null +++ b/tests/manifest/task-response.test @@ -0,0 +1,114 @@ +# file : tests/manifest/task-response.test +# copyright : Copyright (c) 2014-2017 Code Synthesis Ltd +# license : MIT; see accompanying LICENSE file + +test.options += -ts + +: valid +: +: Roundtrip the task response manifest. +: +{ + : session-not-empty + : + $* <>EOF + : 1 + session: abcd + challenge: xyz + : + name: libfoo + version: 1.0 + repository: http://pkg.example.org/1/math + machine: windows_10-msvc_14 + EOF + + : session-empty + : + $* <>EOF + : 1 + session: + EOF +} + +: redefinition +: +{ + : session + : + $* <'stdin:3:1: error: task response session redefinition' == 1 + : 1 + session: abcd + session: abcd + EOI + + : challenge + : + $* <'stdin:3:1: error: task response challenge redefinition' == 1 + : 1 + challenge: xyz + challenge: xyz + EOI +} + +: invalid +: +{ + : challenge + : + { + : empty + : + $* <'stdin:2:11: error: empty task response challenge' == 1 + : 1 + challenge: + EOI + + : redundant + : + $* <'stdin:4:1: error: unexpected task response challenge' == 1 + : 1 + session: + challenge: abc + EOI + } + + : task-unexpected + : + $* <'stdin:3:1: error: single task response manifest expected' == 1 + : 1 + session: + : + EOI +} + +: unknown-name +: +$* <"stdin:2:1: error: unknown name 'x' in task response manifest" == 1 +: 1 +x: +EOI + +: missed +: +{ + : session + : + $* <'stdin:2:1: error: no task response session specified' == 1 + : 1 + EOI + + : challenge + : + $* <'stdin:3:1: error: no task response challenge specified' == 1 + : 1 + session: abc + EOI + + : task + : + $* <'stdin:4:1: error: task manifest expected' == 1 + : 1 + session: abcd + challenge: xyz + EOI +} diff --git a/tests/manifest/task.test b/tests/manifest/task.test new file mode 100644 index 0000000..09777b1 --- /dev/null +++ b/tests/manifest/task.test @@ -0,0 +1,260 @@ +# file : tests/manifest/task.test +# copyright : Copyright (c) 2014-2017 Code Synthesis Ltd +# license : MIT; see accompanying LICENSE file + +test.options += -t + +: valid +: +: Roundtrip the task manifest. +: +{ + : all-names + : + $* <>EOF + : 1 + name: libfoo + version: 1.0 + repository: http://pkg.example.org/1/math + machine: windows_10-msvc_14 + target: x86_64-microsoft-win32-msvc14.0 + config: config.cc.coptions=/Z7 config.cc.loptions=/DEBUG + EOF + + : no-target + : + $* <>EOF + : 1 + name: libfoo + version: 1.0 + repository: http://pkg.example.org/1/math + machine: windows_10-msvc_14 + config: config.cc.coptions=/Z7 config.cc.loptions=/DEBUG + EOF + + : no-config + : + $* <>EOF + : 1 + name: libfoo + version: 1.0 + repository: http://pkg.example.org/1/math + machine: windows_10-msvc_14 + target: x86_64-microsoft-win32-msvc14.0 + EOF + + : config + : + { + : empty-var-value + : + $* <>EOF + : 1 + name: libfoo + version: 1.0 + repository: http://pkg.example.org/1/math + machine: windows + config: abc= + EOF + + : var-value-quoting + : + $* <>EOF + : 1 + name: libfoo + version: 1.0 + repository: http://pkg.example.org/1/math + machine: windows + config: abc='a "b '"d\e x y=" + EOF + } +} + +: redefinition +: +{ + : name + : + $* <'stdin:3:1: error: task package name redefinition' == 1 + : 1 + name: libfoo + name: libfoo + EOI + + : version + : + $* <'stdin:3:1: error: task package version redefinition' == 1 + : 1 + version: 1.0 + version: 1.0 + EOI + + : repository + : + $* <'stdin:3:1: error: task repository redefinition' == 1 + : 1 + repository: http://pkg.example.org/1/math + repository: http://pkg.example.org/1/math + EOI + + : machine + : + $* <'stdin:3:1: error: task machine redefinition' == 1 + : 1 + machine: windows_10-msvc_14 + machine: windows_10-msvc_14 + EOI + + : target + : + $* <'stdin:3:1: error: task target redefinition' == 1 + : 1 + target: x86_64-microsoft-win32-msvc14.0 + target: x86_64-microsoft-win32-msvc14.0 + EOI + + : config + : + $* <'stdin:3:1: error: task configuration redefinition' == 1 + : 1 + config: config.cc.coptions=/Z7 + config: config.cc.loptions=/DEBUG + EOI +} + +: invalid +: +{ + : name-empty + : + $* <'stdin:2:6: error: empty task package name' == 1 + : 1 + name: + EOI + + : version + : + { + : empty + : + $* <'stdin:2:9: error: invalid task package version: unexpected end' == 1 + : 1 + version: + EOI + + : release + : + $* <'stdin:2:10: error: invalid task package version release' == 1 + : 1 + version: 1.2.3- + EOI + } + + : machine-empty + : + $* <'stdin:2:9: error: empty task machine' == 1 + : 1 + machine: + EOI + + : target-empty + : + $* <'stdin:2:8: error: invalid task target: missing cpu' == 1 + : 1 + target: + EOI + + : config + : + { + : empty + : + $* <'stdin:2:8: error: empty task configuration' == 1 + : 1 + config: + EOI + + : bad-field + : + $* <'stdin:2:15: error: invalid task configuration: unterminated quoted string' == 1 + : 1 + config: 'abc=x + EOI + + : bad-var + : + $* <'stdin:2:12: error: invalid task configuration: no variable value' == 1 + : 1 + config: abc xyz=1 + EOI + + : multiline + : + { + : bad-field + : + $* <'stdin:3:7: error: invalid task configuration: unterminated quoted string' == 1 + : 1 + config: \ + 'abc=x + \ + EOI + + : bad-var + : + $* <'stdin:3:4: error: invalid task configuration: no variable value' == 1 + : 1 + config: \ + abc xyz=1 + \ + EOI + } + } +} + +: unknown-name +: +$* <"stdin:2:1: error: unknown name 'x' in task manifest" == 1 +: 1 +x: +EOI + +: missed +: +{ + : name + : + $* <'stdin:5:1: error: no task package name specified' == 1 + : 1 + version: 1.0 + repository: http://pkg.example.org/1/math + machine: windows_10-msvc_14 + EOI + + : version + : + $* <'stdin:5:1: error: no task package version specified' == 1 + : 1 + name: libfoo + repository: http://pkg.example.org/1/math + machine: windows_10-msvc_14 + EOI + + : repository + : + $* <'stdin:5:1: error: no task repository specified' == 1 + : 1 + name: libfoo + version: 1.0 + machine: windows_10-msvc_14 + EOI + + : machine + : + $* <'stdin:5:1: error: no task machine specified' == 1 + : 1 + name: libfoo + version: 1.0 + repository: http://pkg.example.org/1/math + EOI +} diff --git a/tests/variable/buildfile b/tests/variable/buildfile new file mode 100644 index 0000000..7fb5105 --- /dev/null +++ b/tests/variable/buildfile @@ -0,0 +1,9 @@ +# file : tests/variable/buildfile +# copyright : Copyright (c) 2014-2017 Code Synthesis Ltd +# license : MIT; see accompanying LICENSE file + +import libs += libbutl%lib{butl} + +exe{driver}: cxx{driver} ../../bbot/lib{bbot} $libs test{testscript} + +include ../../bbot/ diff --git a/tests/variable/driver.cxx b/tests/variable/driver.cxx new file mode 100644 index 0000000..88167ad --- /dev/null +++ b/tests/variable/driver.cxx @@ -0,0 +1,61 @@ +// file : tests/variable/driver.cxx -*- C++ -*- +// copyright : Copyright (c) 2014-2017 Code Synthesis Ltd +// license : MIT; see accompanying LICENSE file + +#include // ios_base::failbit, ios_base::badbit +#include +#include +#include // size_t +#include + +#include // operator<<(ostream,exception) + +#include + +using namespace std; +using namespace butl; +using namespace bbot; + +// Usage: argv[0] [-u] +// +// Read variables from STDIN (one per line) and serialize them to STDOUT (also +// one per line). +// +// -u output variables being unquoted beforehand +// +int +main (int argc, char* argv[]) +{ + assert (argc <= 2); + bool unquote (false); + + if (argc == 2) + { + assert (argv[1] == string ("-u")); + unquote = true; + } + + cin.exceptions (ios_base::badbit); + cout.exceptions (ios_base::failbit | ios_base::badbit); + + string s; + for (size_t l (1); getline (cin, s); ++l) + { + try + { + variable v (move (s)); + + cout << (unquote + ? v.unquoted () + : static_cast (v)) + << '\n'; + } + catch (const invalid_variable& e) + { + cerr << l << ':' << 1 + e.pos << ": error: " << e << endl; + return 1; + } + } + + return 0; +} diff --git a/tests/variable/testscript b/tests/variable/testscript new file mode 100644 index 0000000..d3d9302 --- /dev/null +++ b/tests/variable/testscript @@ -0,0 +1,55 @@ +# file : tests/variable/testscript +# copyright : Copyright (c) 2014-2017 Code Synthesis Ltd +# license : MIT; see accompanying LICENSE file + +test.options += -u + +: valid +: +{ + $* <> EOO + config.cc.coptions="-O3 -stdlib='libc++'" + ab'c="x y"' + var=xy + var= + EOI + config.cc.coptions=-O3 -stdlib='libc++' + abc="x y" + var=xy + var= + EOO +} + +: invalid +: +{ + : expected-assignment + : + $* <'v"a r=abc"' 2>'1:4: error: expected variable assignment' == 1 + + : unterminated-quoted-string + : + $* <'var="a b' 2>'1:9: error: unterminated quoted string' == 1 + + : no-value + : + $* <'var' 2>'1:4: error: no variable value' == 1 +} + +: unquoting +: +{ + : single + : + $* <"var='a \" b'" >'var=a " b' + + : double + : + $* <'var="a '"'"' b"' >"var=a ' b" + + : mixed + : + $* <'var=a bc e' + var='a b'"c e" + EOI +} diff --git a/version b/version new file mode 100644 index 0000000..d1f4eb1 --- /dev/null +++ b/version @@ -0,0 +1 @@ +0.5.0-a1 -- cgit v1.1