aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorKaren Arutyunov <karen@codesynthesis.com>2017-03-29 00:45:30 +0300
committerKaren Arutyunov <karen@codesynthesis.com>2017-04-04 13:35:14 +0300
commitdd973d03bf5f3f439dcdacbb22470105e66e698a (patch)
tree6856ab313c75f3459df80511c98d3f788bf1c22b
parentc72bbd5d04084547c53e9593af4d76d9e5135a53 (diff)
Implement manifests and build_config
-rw-r--r--INSTALL6
-rw-r--r--LICENSE20
-rw-r--r--NEWS3
-rw-r--r--README25
-rw-r--r--bbot/build-config54
-rw-r--r--bbot/build-config.cxx121
-rw-r--r--bbot/buildfile35
-rw-r--r--bbot/export41
-rw-r--r--bbot/manifest215
-rw-r--r--bbot/manifest.cxx931
-rw-r--r--bbot/variable49
-rw-r--r--bbot/variable.cxx94
-rw-r--r--bbot/version37
-rw-r--r--build/.gitignore1
-rw-r--r--build/bootstrap.build26
-rw-r--r--build/export.build10
-rw-r--r--build/root.build20
-rw-r--r--buildfile12
-rw-r--r--manifest15
-rw-r--r--tests/.gitignore1
-rw-r--r--tests/buildfile5
-rw-r--r--tests/buildtab/buildfile9
-rw-r--r--tests/buildtab/driver.cxx48
-rw-r--r--tests/buildtab/testscript63
-rw-r--r--tests/manifest/buildfile9
-rw-r--r--tests/manifest/driver.cxx71
-rw-r--r--tests/manifest/machine.test157
-rw-r--r--tests/manifest/result-request.test92
-rw-r--r--tests/manifest/result.test230
-rw-r--r--tests/manifest/task-request.test96
-rw-r--r--tests/manifest/task-response.test114
-rw-r--r--tests/manifest/task.test260
-rw-r--r--tests/variable/buildfile9
-rw-r--r--tests/variable/driver.cxx61
-rw-r--r--tests/variable/testscript55
-rw-r--r--version1
36 files changed, 2996 insertions, 0 deletions
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 <string>
+#include <vector>
+#include <iosfwd>
+
+#include <butl/path>
+#include <butl/optional>
+#include <butl/tab-parser>
+#include <butl/target-triplet>
+
+#include <bbot/export>
+
+#include <bbot/variable>
+
+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<butl::target_triplet> target;
+
+ variables vars;
+ };
+
+ using build_configs = std::vector<build_config>;
+
+ // 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:
+ //
+ // <machine-name-pattern> <config-name> [<target>] [<config-vars>]
+ //
+ 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 <bbot/build-config>
+
+#include <string>
+#include <cstddef> // size_t
+#include <utility> // move()
+#include <stdexcept> // invalid_argument
+
+#include <butl/path>
+#include <butl/fdstream>
+#include <butl/tab-parser>
+
+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 <string>
+#include <vector>
+#include <iosfwd>
+
+#include <butl/optional>
+#include <butl/small-vector>
+#include <butl/target-triplet>
+#include <butl/manifest-forward>
+
+#include <bpkg/manifest> // version, repository_location
+
+#include <bbot/export>
+
+#include <bbot/variable>
+
+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<machine_type> 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<machine_manifest> 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<butl::target_triplet> 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<std::string> challenge;
+ butl::optional<task_manifest> 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<std::uint8_t> (r) > static_cast<std::uint8_t> (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<operation_result, 3>;
+
+ 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 <bbot/manifest>
+
+#include <vector>
+#include <string>
+#include <cassert>
+#include <sstream>
+#include <cstddef> // size_t
+#include <utility> // move()
+#include <cstdint> // uint64_t
+#include <algorithm> // find()
+
+#include <butl/utility> // digit()
+#include <butl/tab-parser>
+#include <butl/manifest-parser>
+#include <butl/manifest-serializer>
+
+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<string>;
+
+ 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<size_t> (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<machine_type> (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<size_t> (*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<string> 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<result_status> (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<result_status> 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<size_t> (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<size_t> (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 <string>
+#include <vector>
+#include <cstdint> // uint64_t
+#include <stdexcept> // invalid_argument
+
+#include <bbot/export>
+
+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<variable>;
+}
+
+#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 <bbot/variable>
+
+#include <utility> // 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 <butl/version> // 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> // ios::failbit, ios::badbit
+#include <cassert>
+#include <iostream>
+
+#include <butl/utility> // operator<<(ostream,exception)
+
+#include <bbot/build-config>
+
+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 >>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
+ :
+ $* <<EOI >>EOO
+
+ windows*-vc_14* windows-vc_14-32-debug
+ # abc
+ EOI
+ windows*-vc_14* windows-vc_14-32-debug
+ EOO
+}
+
+: parse-errors
+:
+{
+ : no-name
+ :
+ $* <<EOI 2>>EOE == 1
+ windows*-vc_14*
+ EOI
+ cin:1:16: error: no configuration name found
+ EOE
+
+ : invalid-target
+ :
+ $* <<EOI 2>>EOE == 1
+ windows*-vc_14* windows-vc_14-32 microsoft
+ EOI
+ cin:1:34: error: missing cpu
+ EOE
+
+ : invalid-var
+ :
+ $* <<EOI 2>>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
+ :
+ $* <<EOI 2>'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> // ios_base::failbit, ios_base::badbit
+#include <string>
+#include <cassert>
+#include <iostream>
+
+#include <butl/utility> // operator<<(ostream,exception)
+#include <butl/manifest-parser>
+#include <butl/manifest-serializer>
+
+#include <bbot/manifest>
+
+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 >>EOF
+ : 1
+ id: a2b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
+ name: windows_10-msvc_14
+ type: vm
+ summary: Windows 10 build 1607 with VC 14 update 3
+ EOF
+
+ : container
+ :
+ $* <<EOF >>EOF
+ : 1
+ id: a2b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
+ name: windows_10-msvc_14
+ type: container
+ summary: Windows 10 build 1607 with VC 14 update 3
+ EOF
+}
+
+: multiple
+:
+$* <<EOI 2>'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
+ :
+ $* <<EOI 2>'stdin:3:1: error: machine id redefinition' == 1
+ : 1
+ id: 123
+ id: 123
+ EOI
+
+ : name
+ :
+ $* <<EOI 2>'stdin:3:1: error: machine name redefinition' == 1
+ : 1
+ name: windows
+ name: windows
+ EOI
+
+ : type
+ :
+ $* <<EOI 2>'stdin:3:1: error: machine type redefinition' == 1
+ : 1
+ type: vm
+ type: vm
+ EOI
+
+ : summary
+ :
+ $* <<EOI 2>'stdin:3:1: error: machine summary redefinition' == 1
+ : 1
+ summary: Windows
+ summary: Windows
+ EOI
+}
+
+: empty
+:
+{
+ : id
+ :
+ $* <<EOI 2>'stdin:2:4: error: empty machine id' == 1
+ : 1
+ id:
+ EOI
+
+ : name
+ :
+ $* <<EOI 2>'stdin:2:6: error: empty machine name' == 1
+ : 1
+ name:
+ EOI
+
+ : summary
+ :
+ $* <<EOI 2>'stdin:2:9: error: empty machine summary' == 1
+ : 1
+ summary:
+ EOI
+}
+
+: invalid-type
+:
+$* <<EOI 2>'stdin:2:7: error: invalid machine type' == 1
+: 1
+type: unknown
+EOI
+
+: unknown-name
+:
+$* <<EOI 2>"stdin:2:1: error: unknown name 'x' in machine manifest" == 1
+: 1
+x:
+EOI
+
+: missed
+:
+{
+ : id
+ :
+ $* <<EOI 2>'stdin:5:1: error: no machine id specified' == 1
+ : 1
+ name: windows
+ type: vm
+ summary: Windows
+ EOI
+
+ : name
+ :
+ $* <<EOI 2>'stdin:5:1: error: no machine name specified' == 1
+ : 1
+ id: 123
+ type: vm
+ summary: Windows
+ EOI
+
+ : type
+ :
+ $* <<EOI 2>'stdin:5:1: error: no machine type specified' == 1
+ : 1
+ id: 123
+ name: windows
+ summary: Windows
+ EOI
+
+ : summary
+ :
+ $* <<EOI 2>'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 >>EOF
+ : 1
+ session: abcd
+ challenge: xyz
+ :
+ name: libfoo
+ version: 1.0
+ status: error
+ EOF
+}
+
+: redefinition
+:
+{
+ : session
+ :
+ $* <<EOI 2>'stdin:3:1: error: result request session redefinition' == 1
+ : 1
+ session: abcd
+ session: abcd
+ EOI
+
+ : challenge
+ :
+ $* <<EOI 2>'stdin:3:1: error: result request challenge redefinition' == 1
+ : 1
+ challenge: xyz
+ challenge: xyz
+ EOI
+}
+
+: empty
+:
+{
+ : session
+ :
+ $* <<EOI 2>'stdin:2:9: error: empty result request session' == 1
+ : 1
+ session:
+ EOI
+
+ : challenge
+ :
+ $* <<EOI 2>'stdin:2:11: error: empty result request challenge' == 1
+ : 1
+ challenge:
+ EOI
+}
+
+: unknown-name
+:
+$* <<EOI 2>"stdin:2:1: error: unknown name 'x' in result request manifest" == 1
+: 1
+x:
+EOI
+
+: missed
+:
+{
+ : session
+ :
+ $* <<EOI 2>'stdin:3:1: error: no result request session specified' == 1
+ : 1
+ challenge: xyz
+ EOI
+
+ : challenge
+ :
+ $* <<EOI 2>'stdin:3:1: error: no result request challenge specified' == 1
+ : 1
+ session: abc
+ EOI
+
+ : result
+ :
+ $* <<EOI 2>'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 >>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 >>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 >>EOF
+ : 1
+ name: libfoo
+ version: 1.0
+ status: abort
+ EOF
+}
+
+: redefinition
+:
+{
+ : name
+ :
+ $* <<EOI 2>'stdin:3:1: error: result package name redefinition' == 1
+ : 1
+ name: libfoo
+ name: libfoo
+ EOI
+
+ : version
+ :
+ $* <<EOI 2>'stdin:3:1: error: result package version redefinition' == 1
+ : 1
+ version: 1.0
+ version: 1.0
+ EOI
+
+ : status
+ :
+ $* <<EOI 2>'stdin:3:1: error: result status redefinition' == 1
+ : 1
+ status: success
+ status: error
+ EOI
+
+ : configure-status
+ :
+ $* <<EOI 2>'stdin:4:1: error: result configure-status redefinition' == 1
+ : 1
+ status: success
+ configure-status: success
+ configure-status: abnormal
+ EOI
+
+ : configure-log
+ :
+ $* <<EOI 2>'stdin:5:1: error: unexpected configure-log' == 1
+ : 1
+ status: success
+ configure-status: success
+ configure-log: configured
+ configure-log: configured
+ EOI
+}
+
+: invalid
+:
+{
+ : name-empty
+ :
+ $* <<EOI 2>'stdin:2:6: error: empty result package name' == 1
+ : 1
+ name:
+ EOI
+
+ : version
+ :
+ {
+ : empty
+ :
+ $* <<EOI 2>'stdin:2:9: error: invalid result package version: unexpected end' == 1
+ : 1
+ version:
+ EOI
+
+ : release
+ :
+ $* <<EOI 2>'stdin:2:10: error: invalid result package version release' == 1
+ : 1
+ version: 1.2.3-
+ EOI
+ }
+
+ : status
+ :
+ $* <<EOI 2>'stdin:2:9: error: invalid result status' == 1
+ : 1
+ status: alert
+ EOI
+
+ : configure-status
+ :
+ $* <<EOI 2>'stdin:3:19: error: invalid configure-status' == 1
+ : 1
+ status: abort
+ configure-status: alert
+ EOI
+
+ : order
+ :
+ {
+ : op-status-before-status
+ :
+ $* <<EOI 2>'stdin:2:1: error: result status must appear first' == 1
+ : 1
+ configure-status: success
+ EOI
+
+ : op-status-after-log
+ :
+ $* <<EOI 2>'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
+ :
+ $* <<EOI 2>'stdin:5:1: error: configure-log is expected' == 1
+ : 1
+ status: success
+ configure-status: success
+ update-status: error
+ update-log: log
+ EOI
+ }
+}
+
+: unknown-name
+:
+$* <<EOI 2>"stdin:2:1: error: unknown name 'full-logs' in result manifest" == 1
+: 1
+full-logs: log
+EOI
+
+: missed
+:
+{
+ : name
+ :
+ $* <<EOI 2>'stdin:4:1: error: no result package name specified' == 1
+ : 1
+ version: 1.0
+ status: success
+ EOI
+
+ : version
+ :
+ $* <<EOI 2>'stdin:4:1: error: no result package version specified' == 1
+ : 1
+ name: libfoo
+ status: success
+ EOI
+
+ : status
+ :
+ $* <<EOI 2>'stdin:4:1: error: no result status specified' == 1
+ : 1
+ name: libfoo
+ version: 1.0
+ EOI
+
+ : configure-log
+ :
+ $* <<EOI 2>'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 >>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
+ :
+ $* <<EOI 2>'stdin:3:1: error: task request agent redefinition' == 1
+ : 1
+ agent: upsa
+ agent: upsa
+ EOI
+
+ : fingerprint
+ :
+ $* <<EOI 2>'stdin:3:1: error: task request fingerprint redefinition' == 1
+ : 1
+ fingerprint: 1105fb394ee870adb154b7abfbbae5755df7dcef6c81db34e8d1b68d2653734e
+ fingerprint: 1105fb394ee870adb154b7abfbbae5755df7dcef6c81db34e8d1b68d2653734e
+ EOI
+}
+
+: empty
+:
+{
+ : agent
+ :
+ $* <<EOI 2>'stdin:2:7: error: empty task request agent' == 1
+ : 1
+ agent:
+ EOI
+}
+
+: invalid-fingerprint
+:
+$* <<EOI 2>'stdin:2:14: error: invalid task request fingerprint' == 1
+: 1
+fingerprint: 123x
+EOI
+
+: missed
+:
+{
+ : agent
+ :
+ $* <<EOI 2>'stdin:3:1: error: no task request agent specified' == 1
+ : 1
+ fingerprint: 1105fb394ee870adb154b7abfbbae5755df7dcef6c81db34e8d1b68d2653734e
+ EOI
+
+ : fingerprint
+ :
+ $* <<EOI 2>'stdin:3:1: error: no task request fingerprint specified' == 1
+ : 1
+ agent: upsa
+ EOI
+}
+
+: no-machines
+:
+$* <<EOI 2>'stdin:4:1: error: no task request machines specified' == 1
+: 1
+agent: upsa
+fingerprint: 1105fb394ee870adb154b7abfbbae5755df7dcef6c81db34e8d1b68d2653734e
+EOI
+
+: type-not-allowed
+:
+$* <<EOI 2>'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 >>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 >>EOF
+ : 1
+ session:
+ EOF
+}
+
+: redefinition
+:
+{
+ : session
+ :
+ $* <<EOI 2>'stdin:3:1: error: task response session redefinition' == 1
+ : 1
+ session: abcd
+ session: abcd
+ EOI
+
+ : challenge
+ :
+ $* <<EOI 2>'stdin:3:1: error: task response challenge redefinition' == 1
+ : 1
+ challenge: xyz
+ challenge: xyz
+ EOI
+}
+
+: invalid
+:
+{
+ : challenge
+ :
+ {
+ : empty
+ :
+ $* <<EOI 2>'stdin:2:11: error: empty task response challenge' == 1
+ : 1
+ challenge:
+ EOI
+
+ : redundant
+ :
+ $* <<EOI 2>'stdin:4:1: error: unexpected task response challenge' == 1
+ : 1
+ session:
+ challenge: abc
+ EOI
+ }
+
+ : task-unexpected
+ :
+ $* <<EOI 2>'stdin:3:1: error: single task response manifest expected' == 1
+ : 1
+ session:
+ :
+ EOI
+}
+
+: unknown-name
+:
+$* <<EOI 2>"stdin:2:1: error: unknown name 'x' in task response manifest" == 1
+: 1
+x:
+EOI
+
+: missed
+:
+{
+ : session
+ :
+ $* <<EOI 2>'stdin:2:1: error: no task response session specified' == 1
+ : 1
+ EOI
+
+ : challenge
+ :
+ $* <<EOI 2>'stdin:3:1: error: no task response challenge specified' == 1
+ : 1
+ session: abc
+ EOI
+
+ : task
+ :
+ $* <<EOI 2>'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 >>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 >>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 >>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 >>EOF
+ : 1
+ name: libfoo
+ version: 1.0
+ repository: http://pkg.example.org/1/math
+ machine: windows
+ config: abc=
+ EOF
+
+ : var-value-quoting
+ :
+ $* <<EOF >>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
+ :
+ $* <<EOI 2>'stdin:3:1: error: task package name redefinition' == 1
+ : 1
+ name: libfoo
+ name: libfoo
+ EOI
+
+ : version
+ :
+ $* <<EOI 2>'stdin:3:1: error: task package version redefinition' == 1
+ : 1
+ version: 1.0
+ version: 1.0
+ EOI
+
+ : repository
+ :
+ $* <<EOI 2>'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
+ :
+ $* <<EOI 2>'stdin:3:1: error: task machine redefinition' == 1
+ : 1
+ machine: windows_10-msvc_14
+ machine: windows_10-msvc_14
+ EOI
+
+ : target
+ :
+ $* <<EOI 2>'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
+ :
+ $* <<EOI 2>'stdin:3:1: error: task configuration redefinition' == 1
+ : 1
+ config: config.cc.coptions=/Z7
+ config: config.cc.loptions=/DEBUG
+ EOI
+}
+
+: invalid
+:
+{
+ : name-empty
+ :
+ $* <<EOI 2>'stdin:2:6: error: empty task package name' == 1
+ : 1
+ name:
+ EOI
+
+ : version
+ :
+ {
+ : empty
+ :
+ $* <<EOI 2>'stdin:2:9: error: invalid task package version: unexpected end' == 1
+ : 1
+ version:
+ EOI
+
+ : release
+ :
+ $* <<EOI 2>'stdin:2:10: error: invalid task package version release' == 1
+ : 1
+ version: 1.2.3-
+ EOI
+ }
+
+ : machine-empty
+ :
+ $* <<EOI 2>'stdin:2:9: error: empty task machine' == 1
+ : 1
+ machine:
+ EOI
+
+ : target-empty
+ :
+ $* <<EOI 2>'stdin:2:8: error: invalid task target: missing cpu' == 1
+ : 1
+ target:
+ EOI
+
+ : config
+ :
+ {
+ : empty
+ :
+ $* <<EOI 2>'stdin:2:8: error: empty task configuration' == 1
+ : 1
+ config:
+ EOI
+
+ : bad-field
+ :
+ $* <<EOI 2>'stdin:2:15: error: invalid task configuration: unterminated quoted string' == 1
+ : 1
+ config: 'abc=x
+ EOI
+
+ : bad-var
+ :
+ $* <<EOI 2>'stdin:2:12: error: invalid task configuration: no variable value' == 1
+ : 1
+ config: abc xyz=1
+ EOI
+
+ : multiline
+ :
+ {
+ : bad-field
+ :
+ $* <<EOI 2>'stdin:3:7: error: invalid task configuration: unterminated quoted string' == 1
+ : 1
+ config: \
+ 'abc=x
+ \
+ EOI
+
+ : bad-var
+ :
+ $* <<EOI 2>'stdin:3:4: error: invalid task configuration: no variable value' == 1
+ : 1
+ config: \
+ abc xyz=1
+ \
+ EOI
+ }
+ }
+}
+
+: unknown-name
+:
+$* <<EOI 2>"stdin:2:1: error: unknown name 'x' in task manifest" == 1
+: 1
+x:
+EOI
+
+: missed
+:
+{
+ : name
+ :
+ $* <<EOI 2>'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
+ :
+ $* <<EOI 2>'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
+ :
+ $* <<EOI 2>'stdin:5:1: error: no task repository specified' == 1
+ : 1
+ name: libfoo
+ version: 1.0
+ machine: windows_10-msvc_14
+ EOI
+
+ : machine
+ :
+ $* <<EOI 2>'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> // ios_base::failbit, ios_base::badbit
+#include <string>
+#include <cassert>
+#include <cstddef> // size_t
+#include <iostream>
+
+#include <butl/utility> // operator<<(ostream,exception)
+
+#include <bbot/variable>
+
+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<const string&> (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
+:
+{
+ $* <<EOI >> 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
+ :
+ $* <<EOI >'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