summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--Help/manual/cmake-file-api.7.rst125
-rw-r--r--Source/cmFileAPI.cxx303
-rw-r--r--Source/cmFileAPI.h73
-rw-r--r--Tests/RunCMake/FileAPI/ClientStateful-check.cmake68
-rw-r--r--Tests/RunCMake/FileAPI/ClientStateful-check.py258
-rw-r--r--Tests/RunCMake/FileAPI/ClientStateful-prep.cmake73
-rw-r--r--Tests/RunCMake/FileAPI/ClientStateful.cmake0
-rw-r--r--Tests/RunCMake/FileAPI/RunCMakeTest.cmake1
-rw-r--r--Tests/RunCMake/FileAPI/check_index.py6
9 files changed, 898 insertions, 9 deletions
diff --git a/Help/manual/cmake-file-api.7.rst b/Help/manual/cmake-file-api.7.rst
index 51655b2..f5e4eeb 100644
--- a/Help/manual/cmake-file-api.7.rst
+++ b/Help/manual/cmake-file-api.7.rst
@@ -31,8 +31,8 @@ It has the following subdirectories:
``query/``
Holds query files written by clients.
- These may be `v1 Shared Stateless Query Files`_ or
- `v1 Client Stateless Query Files`_.
+ These may be `v1 Shared Stateless Query Files`_,
+ `v1 Client Stateless Query Files`_, or `v1 Client Stateful Query Files`_.
``reply/``
Holds reply files written by CMake whenever it runs to generate a build
@@ -84,6 +84,87 @@ own means.
Files of this form are stateless queries owned by the client ``<client>``.
The owning client may remove them at any time.
+v1 Client Stateful Query Files
+------------------------------
+
+Stateful query files allow clients to request a list of versions of
+each of the `Object Kinds`_ and get only the most recent version
+recognized by the CMake that runs.
+
+Clients may create owned stateful queries by creating ``query.json``
+files in client-specific query subdirectories. The form is::
+
+ <build>/.cmake/api/v1/query/client-<client>/query.json
+
+where ``client-`` is literal, ``<client>`` is a string uniquely
+identifying the client, and ``query.json`` is literal. Each client
+must choose a unique ``<client>`` identifier via its own means.
+
+``query.json`` files are stateful queries owned by the client ``<client>``.
+The owning client may update or remove them at any time. When a
+given client installation is updated it may then update the stateful
+query it writes to build trees to request newer object versions.
+This can be used to avoid asking CMake to generate multiple object
+versions unnecessarily.
+
+A ``query.json`` file must contain a JSON object:
+
+.. code-block:: json
+
+ {
+ "requests": [
+ { "kind": "<kind>" , "version": 1 },
+ { "kind": "<kind>" , "version": { "major": 1, "minor": 2 } },
+ { "kind": "<kind>" , "version": [2, 1] },
+ { "kind": "<kind>" , "version": [2, { "major": 1, "minor": 2 }] },
+ { "kind": "<kind>" , "version": 1, "client": {} },
+ { "kind": "..." }
+ ],
+ "client": {}
+ }
+
+The members are:
+
+``requests``
+ A JSON array containing zero or more requests. Each request is
+ a JSON object with members:
+
+ ``kind``
+ Specifies one of the `Object Kinds`_ to be included in the reply.
+
+ ``version``
+ Indicates the version(s) of the object kind that the client
+ understands. Versions have major and minor components following
+ semantic version conventions. The value must be
+
+ * a JSON integer specifying a (non-negative) major version number, or
+ * a JSON object containing ``major`` and (optionally) ``minor``
+ members specifying non-negative integer version components, or
+ * a JSON array whose elements are each one of the above.
+
+ ``client``
+ Optional member reserved for use by the client. This value is
+ preserved in the reply written for the client in the
+ `v1 Reply Index File`_ but is otherwise ignored. Clients may use
+ this to pass custom information with a request through to its reply.
+
+ For each requested object kind CMake will choose the *first* version
+ that it recognizes for that kind among those listed in the request.
+ The response will use the selected *major* version with the highest
+ *minor* version known to the running CMake for that major version.
+ Therefore clients should list all supported major versions in
+ preferred order along with the minimal minor version required
+ for each major version.
+
+``client``
+ Optional member reserved for use by the client. This value is
+ preserved in the reply written for the client in the
+ `v1 Reply Index File`_ but is otherwise ignored. Clients may use
+ this to pass custom information with a query through to its reply.
+
+Other ``query.json`` top-level members are reserved for future use.
+If present they are ignored for forward compatibility.
+
v1 Reply Index File
-------------------
@@ -135,7 +216,18 @@ The reply index file contains a JSON object:
"version": { "major": 1, "minor": 0 },
"jsonFile": "<file>" },
"<unknown>": { "error": "unknown query file" },
- "...": {}
+ "...": {},
+ "query.json": {
+ "requests": [ {}, {}, {} ],
+ "responses": [
+ { "kind": "<kind>",
+ "version": { "major": 1, "minor": 0 },
+ "jsonFile": "<file>" },
+ { "error": "unknown query file" },
+ { "...": {} }
+ ],
+ "client": {}
+ }
}
}
}
@@ -211,6 +303,33 @@ The members are:
containing a string with an error message indicating that the
query file is unknown.
+ ``query.json``
+ This member appears for clients using
+ `v1 Client Stateful Query Files`_.
+ If the ``query.json`` file failed to read or parse as a JSON object,
+ this member is a JSON object with a single ``error`` member
+ containing a string with an error message. Otherwise, this member
+ is a JSON object mirroring the content of the ``query.json`` file.
+ The members are:
+
+ ``client``
+ A copy of the ``query.json`` file ``client`` member, if it exists.
+
+ ``requests``
+ A copy of the ``query.json`` file ``requests`` member, if it exists.
+
+ ``responses``
+ If the ``query.json`` file ``requests`` member is missing or invalid,
+ this member is a JSON object with a single ``error`` member
+ containing a string with an error message. Otherwise, this member
+ contains a JSON array with a response for each entry of the
+ ``requests`` array, in the same order. Each response is
+
+ * a JSON object with a single ``error`` member containing a string
+ with an error message, or
+ * a `v1 Reply File Reference`_ to the corresponding reply file for
+ the requested object kind and selected version.
+
After reading the reply index file, clients may read the other
`v1 Reply Files`_ it references.
diff --git a/Source/cmFileAPI.cxx b/Source/cmFileAPI.cxx
index e401b58..cf772d9 100644
--- a/Source/cmFileAPI.cxx
+++ b/Source/cmFileAPI.cxx
@@ -24,6 +24,14 @@ cmFileAPI::cmFileAPI(cmake* cm)
this->APIv1 =
this->CMakeInstance->GetHomeOutputDirectory() + "/.cmake/api/v1";
+ Json::CharReaderBuilder rbuilder;
+ rbuilder["collectComments"] = false;
+ rbuilder["failIfExtra"] = true;
+ rbuilder["rejectDupKeys"] = false;
+ rbuilder["strictRoot"] = true;
+ this->JsonReader =
+ std::unique_ptr<Json::CharReader>(rbuilder.newCharReader());
+
Json::StreamWriterBuilder wbuilder;
wbuilder["indentation"] = "\t";
this->JsonWriter =
@@ -88,6 +96,46 @@ void cmFileAPI::RemoveOldReplyFiles()
}
}
+bool cmFileAPI::ReadJsonFile(std::string const& file, Json::Value& value,
+ std::string& error)
+{
+ std::vector<char> content;
+
+ cmsys::ifstream fin;
+ if (!cmSystemTools::FileIsDirectory(file)) {
+ fin.open(file.c_str(), std::ios::binary);
+ }
+ auto finEnd = fin.rdbuf()->pubseekoff(0, std::ios::end);
+ if (finEnd > 0) {
+ size_t finSize = finEnd;
+ try {
+ // Allocate a buffer to read the whole file.
+ content.resize(finSize);
+
+ // Now read the file from the beginning.
+ fin.seekg(0, std::ios::beg);
+ fin.read(content.data(), finSize);
+ } catch (...) {
+ fin.setstate(std::ios::failbit);
+ }
+ }
+ fin.close();
+ if (!fin) {
+ value = Json::Value();
+ error = "failed to read from file";
+ return false;
+ }
+
+ // Parse our buffer as json.
+ if (!this->JsonReader->parse(content.data(), content.data() + content.size(),
+ &value, &error)) {
+ value = Json::Value();
+ return false;
+ }
+
+ return true;
+}
+
std::string cmFileAPI::WriteJsonFile(
Json::Value const& value, std::string const& prefix,
std::string (*computeSuffix)(std::string const&))
@@ -186,14 +234,38 @@ void cmFileAPI::ReadClient(std::string const& client)
std::vector<std::string> queries = this->LoadDir(clientDir);
// Read the queries and save for later.
- Query& clientQuery = this->ClientQueries[client];
+ ClientQuery& clientQuery = this->ClientQueries[client];
for (std::string& query : queries) {
- if (!this->ReadQuery(query, clientQuery.Known)) {
- clientQuery.Unknown.push_back(std::move(query));
+ if (query == "query.json") {
+ clientQuery.HaveQueryJson = true;
+ this->ReadClientQuery(client, clientQuery.QueryJson);
+ } else if (!this->ReadQuery(query, clientQuery.DirQuery.Known)) {
+ clientQuery.DirQuery.Unknown.push_back(std::move(query));
}
}
}
+void cmFileAPI::ReadClientQuery(std::string const& client, ClientQueryJson& q)
+{
+ // Read the query.json file.
+ std::string queryFile = this->APIv1 + "/query/" + client + "/query.json";
+ Json::Value query;
+ if (!this->ReadJsonFile(queryFile, query, q.Error)) {
+ return;
+ }
+ if (!query.isObject()) {
+ q.Error = "query root is not an object";
+ return;
+ }
+
+ Json::Value const& clientValue = query["client"];
+ if (!clientValue.isNull()) {
+ q.ClientValue = clientValue;
+ }
+ q.RequestsValue = std::move(query["requests"]);
+ q.Requests = this->BuildClientRequests(q.RequestsValue);
+}
+
Json::Value cmFileAPI::BuildReplyIndex()
{
Json::Value index(Json::objectValue);
@@ -205,8 +277,8 @@ Json::Value cmFileAPI::BuildReplyIndex()
Json::Value& reply = index["reply"] = this->BuildReply(this->TopQuery);
for (auto const& client : this->ClientQueries) {
std::string const& clientName = client.first;
- Query const& clientQuery = client.second;
- reply[clientName] = this->BuildReply(clientQuery);
+ ClientQuery const& clientQuery = client.second;
+ reply[clientName] = this->BuildClientReply(clientQuery);
}
// Move our index of generated objects into its field.
@@ -301,11 +373,232 @@ Json::Value cmFileAPI::BuildObject(Object const& object)
return value;
}
+cmFileAPI::ClientRequests cmFileAPI::BuildClientRequests(
+ Json::Value const& requests)
+{
+ ClientRequests result;
+ if (requests.isNull()) {
+ result.Error = "'requests' member missing";
+ return result;
+ }
+ if (!requests.isArray()) {
+ result.Error = "'requests' member is not an array";
+ return result;
+ }
+
+ result.reserve(requests.size());
+ for (Json::Value const& request : requests) {
+ result.emplace_back(this->BuildClientRequest(request));
+ }
+
+ return result;
+}
+
+cmFileAPI::ClientRequest cmFileAPI::BuildClientRequest(
+ Json::Value const& request)
+{
+ ClientRequest r;
+
+ if (!request.isObject()) {
+ r.Error = "request is not an object";
+ return r;
+ }
+
+ Json::Value const& kind = request["kind"];
+ if (kind.isNull()) {
+ r.Error = "'kind' member missing";
+ return r;
+ }
+ if (!kind.isString()) {
+ r.Error = "'kind' member is not a string";
+ return r;
+ }
+ std::string const& kindName = kind.asString();
+
+ if (kindName == this->ObjectKindName(ObjectKind::InternalTest)) {
+ r.Kind = ObjectKind::InternalTest;
+ } else {
+ r.Error = "unknown request kind '" + kindName + "'";
+ return r;
+ }
+
+ Json::Value const& version = request["version"];
+ if (version.isNull()) {
+ r.Error = "'version' member missing";
+ return r;
+ }
+ std::vector<RequestVersion> versions;
+ if (!cmFileAPI::ReadRequestVersions(version, versions, r.Error)) {
+ return r;
+ }
+
+ switch (r.Kind) {
+ case ObjectKind::InternalTest:
+ this->BuildClientRequestInternalTest(r, versions);
+ break;
+ }
+
+ return r;
+}
+
+Json::Value cmFileAPI::BuildClientReply(ClientQuery const& q)
+{
+ Json::Value reply = this->BuildReply(q.DirQuery);
+
+ if (!q.HaveQueryJson) {
+ return reply;
+ }
+
+ Json::Value& reply_query_json = reply["query.json"];
+ ClientQueryJson const& qj = q.QueryJson;
+
+ if (!qj.Error.empty()) {
+ reply_query_json = this->BuildReplyError(qj.Error);
+ return reply;
+ }
+
+ if (!qj.ClientValue.isNull()) {
+ reply_query_json["client"] = qj.ClientValue;
+ }
+
+ if (!qj.RequestsValue.isNull()) {
+ reply_query_json["requests"] = qj.RequestsValue;
+ }
+
+ reply_query_json["responses"] = this->BuildClientReplyResponses(qj.Requests);
+
+ return reply;
+}
+
+Json::Value cmFileAPI::BuildClientReplyResponses(
+ ClientRequests const& requests)
+{
+ Json::Value responses;
+
+ if (!requests.Error.empty()) {
+ responses = this->BuildReplyError(requests.Error);
+ return responses;
+ }
+
+ responses = Json::arrayValue;
+ for (ClientRequest const& request : requests) {
+ responses.append(this->BuildClientReplyResponse(request));
+ }
+
+ return responses;
+}
+
+Json::Value cmFileAPI::BuildClientReplyResponse(ClientRequest const& request)
+{
+ Json::Value response;
+ if (!request.Error.empty()) {
+ response = this->BuildReplyError(request.Error);
+ return response;
+ }
+ response = this->AddReplyIndexObject(request);
+ return response;
+}
+
+bool cmFileAPI::ReadRequestVersions(Json::Value const& version,
+ std::vector<RequestVersion>& versions,
+ std::string& error)
+{
+ if (version.isArray()) {
+ for (Json::Value const& v : version) {
+ if (!ReadRequestVersion(v, /*inArray=*/true, versions, error)) {
+ return false;
+ }
+ }
+ } else {
+ if (!ReadRequestVersion(version, /*inArray=*/false, versions, error)) {
+ return false;
+ }
+ }
+ return true;
+}
+
+bool cmFileAPI::ReadRequestVersion(Json::Value const& version, bool inArray,
+ std::vector<RequestVersion>& result,
+ std::string& error)
+{
+ if (version.isUInt()) {
+ RequestVersion v;
+ v.Major = version.asUInt();
+ result.push_back(v);
+ return true;
+ }
+
+ if (!version.isObject()) {
+ if (inArray) {
+ error = "'version' array entry is not a non-negative integer or object";
+ } else {
+ error =
+ "'version' member is not a non-negative integer, object, or array";
+ }
+ return false;
+ }
+
+ Json::Value const& major = version["major"];
+ if (major.isNull()) {
+ error = "'version' object 'major' member missing";
+ return false;
+ }
+ if (!major.isUInt()) {
+ error = "'version' object 'major' member is not a non-negative integer";
+ return false;
+ }
+
+ RequestVersion v;
+ v.Major = major.asUInt();
+
+ Json::Value const& minor = version["minor"];
+ if (minor.isUInt()) {
+ v.Minor = minor.asUInt();
+ } else if (!minor.isNull()) {
+ error = "'version' object 'minor' member is not a non-negative integer";
+ return false;
+ }
+
+ result.push_back(v);
+
+ return true;
+}
+
+std::string cmFileAPI::NoSupportedVersion(
+ std::vector<RequestVersion> const& versions)
+{
+ std::ostringstream msg;
+ msg << "no supported version specified";
+ if (!versions.empty()) {
+ msg << " among:";
+ for (RequestVersion const& v : versions) {
+ msg << " " << v.Major << "." << v.Minor;
+ }
+ }
+ return msg.str();
+}
+
// The "__test" object kind is for internal testing of CMake.
static unsigned int const InternalTestV1Minor = 3;
static unsigned int const InternalTestV2Minor = 0;
+void cmFileAPI::BuildClientRequestInternalTest(
+ ClientRequest& r, std::vector<RequestVersion> const& versions)
+{
+ // Select a known version from those requested.
+ for (RequestVersion const& v : versions) {
+ if ((v.Major == 1 && v.Minor <= InternalTestV1Minor) || //
+ (v.Major == 2 && v.Minor <= InternalTestV2Minor)) {
+ r.Version = v.Major;
+ break;
+ }
+ }
+ if (!r.Version) {
+ r.Error = NoSupportedVersion(versions);
+ }
+}
+
Json::Value cmFileAPI::BuildInternalTest(Object const& object)
{
Json::Value test = Json::objectValue;
diff --git a/Source/cmFileAPI.h b/Source/cmFileAPI.h
index 589b837..4f4506f 100644
--- a/Source/cmFileAPI.h
+++ b/Source/cmFileAPI.h
@@ -5,6 +5,7 @@
#include "cmConfigure.h" // IWYU pragma: keep
+#include "cm_jsoncpp_reader.h"
#include "cm_jsoncpp_value.h"
#include "cm_jsoncpp_writer.h"
@@ -71,6 +72,49 @@ private:
std::vector<std::string> Unknown;
};
+ /** Represent one request in a client 'query.json'. */
+ struct ClientRequest : public Object
+ {
+ /** Empty if request is valid, else the error string. */
+ std::string Error;
+ };
+
+ /** Represent the "requests" in a client 'query.json'. */
+ struct ClientRequests : public std::vector<ClientRequest>
+ {
+ /** Empty if requests field is valid, else the error string. */
+ std::string Error;
+ };
+
+ /** Represent the content of a client query.json file. */
+ struct ClientQueryJson
+ {
+ /** The error string if parsing failed, else empty. */
+ std::string Error;
+
+ /** The 'query.json' object "client" member if it exists, else null. */
+ Json::Value ClientValue;
+
+ /** The 'query.json' object "requests" member if it exists, else null. */
+ Json::Value RequestsValue;
+
+ /** Requests extracted from 'query.json'. */
+ ClientRequests Requests;
+ };
+
+ /** Represent content of a client query directory. */
+ struct ClientQuery
+ {
+ /** The content of the client query directory except 'query.json'. */
+ Query DirQuery;
+
+ /** True if 'query.json' exists. */
+ bool HaveQueryJson = false;
+
+ /** The 'query.json' content. */
+ ClientQueryJson QueryJson;
+ };
+
/** Whether the top-level query directory exists at all. */
bool QueryExists = false;
@@ -78,14 +122,18 @@ private:
Query TopQuery;
/** The content of each "client-$client" query directory. */
- std::map<std::string, Query> ClientQueries;
+ std::map<std::string, ClientQuery> ClientQueries;
/** Reply index object generated for object kind/version.
This populates the "objects" field of the reply index. */
std::map<Object, Json::Value> ReplyIndexObjects;
+ std::unique_ptr<Json::CharReader> JsonReader;
std::unique_ptr<Json::StreamWriter> JsonWriter;
+ bool ReadJsonFile(std::string const& file, Json::Value& value,
+ std::string& error);
+
std::string WriteJsonFile(
Json::Value const& value, std::string const& prefix,
std::string (*computeSuffix)(std::string const&) = ComputeSuffixHash);
@@ -95,6 +143,7 @@ private:
static bool ReadQuery(std::string const& query,
std::vector<Object>& objects);
void ReadClient(std::string const& client);
+ void ReadClientQuery(std::string const& client, ClientQueryJson& q);
Json::Value BuildReplyIndex();
Json::Value BuildCMake();
@@ -107,6 +156,28 @@ private:
Json::Value BuildObject(Object const& object);
+ ClientRequests BuildClientRequests(Json::Value const& requests);
+ ClientRequest BuildClientRequest(Json::Value const& request);
+ Json::Value BuildClientReply(ClientQuery const& q);
+ Json::Value BuildClientReplyResponses(ClientRequests const& requests);
+ Json::Value BuildClientReplyResponse(ClientRequest const& request);
+
+ struct RequestVersion
+ {
+ unsigned int Major = 0;
+ unsigned int Minor = 0;
+ };
+ static bool ReadRequestVersions(Json::Value const& version,
+ std::vector<RequestVersion>& versions,
+ std::string& error);
+ static bool ReadRequestVersion(Json::Value const& version, bool inArray,
+ std::vector<RequestVersion>& result,
+ std::string& error);
+ static std::string NoSupportedVersion(
+ std::vector<RequestVersion> const& versions);
+
+ void BuildClientRequestInternalTest(
+ ClientRequest& r, std::vector<RequestVersion> const& versions);
Json::Value BuildInternalTest(Object const& object);
};
diff --git a/Tests/RunCMake/FileAPI/ClientStateful-check.cmake b/Tests/RunCMake/FileAPI/ClientStateful-check.cmake
new file mode 100644
index 0000000..1e9aab6
--- /dev/null
+++ b/Tests/RunCMake/FileAPI/ClientStateful-check.cmake
@@ -0,0 +1,68 @@
+set(expect
+ query
+ query/client-client-member
+ query/client-client-member/query.json
+ query/client-empty-array
+ query/client-empty-array/query.json
+ query/client-empty-object
+ query/client-empty-object/query.json
+ query/client-json-bad-root
+ query/client-json-bad-root/query.json
+ query/client-json-empty
+ query/client-json-empty/query.json
+ query/client-json-extra
+ query/client-json-extra/query.json
+ query/client-not-file
+ query/client-not-file/query.json
+ query/client-request-array-negative-major-version
+ query/client-request-array-negative-major-version/query.json
+ query/client-request-array-negative-minor-version
+ query/client-request-array-negative-minor-version/query.json
+ query/client-request-array-negative-version
+ query/client-request-array-negative-version/query.json
+ query/client-request-array-no-major-version
+ query/client-request-array-no-major-version/query.json
+ query/client-request-array-no-supported-version
+ query/client-request-array-no-supported-version-among
+ query/client-request-array-no-supported-version-among/query.json
+ query/client-request-array-no-supported-version/query.json
+ query/client-request-array-version-1
+ query/client-request-array-version-1-1
+ query/client-request-array-version-1-1/query.json
+ query/client-request-array-version-1/query.json
+ query/client-request-array-version-2
+ query/client-request-array-version-2/query.json
+ query/client-request-negative-major-version
+ query/client-request-negative-major-version/query.json
+ query/client-request-negative-minor-version
+ query/client-request-negative-minor-version/query.json
+ query/client-request-negative-version
+ query/client-request-negative-version/query.json
+ query/client-request-no-major-version
+ query/client-request-no-major-version/query.json
+ query/client-request-no-version
+ query/client-request-no-version/query.json
+ query/client-request-version-1
+ query/client-request-version-1-1
+ query/client-request-version-1-1/query.json
+ query/client-request-version-1/query.json
+ query/client-request-version-2
+ query/client-request-version-2/query.json
+ query/client-requests-bad
+ query/client-requests-bad/query.json
+ query/client-requests-empty
+ query/client-requests-empty/query.json
+ query/client-requests-not-kinded
+ query/client-requests-not-kinded/query.json
+ query/client-requests-not-objects
+ query/client-requests-not-objects/query.json
+ query/client-requests-unknown
+ query/client-requests-unknown/query.json
+ reply
+ reply/__test-v1-[0-9a-f]+.json
+ reply/__test-v2-[0-9a-f]+.json
+ reply/index-[0-9.T-]+.json
+ )
+check_api("^${expect}$")
+
+check_python(ClientStateful)
diff --git a/Tests/RunCMake/FileAPI/ClientStateful-check.py b/Tests/RunCMake/FileAPI/ClientStateful-check.py
new file mode 100644
index 0000000..f3d20d1
--- /dev/null
+++ b/Tests/RunCMake/FileAPI/ClientStateful-check.py
@@ -0,0 +1,258 @@
+from check_index import *
+
+def check_reply(q):
+ assert is_dict(q)
+ assert sorted(q.keys()) == [
+ "client-client-member",
+ "client-empty-array",
+ "client-empty-object",
+ "client-json-bad-root",
+ "client-json-empty",
+ "client-json-extra",
+ "client-not-file",
+ "client-request-array-negative-major-version",
+ "client-request-array-negative-minor-version",
+ "client-request-array-negative-version",
+ "client-request-array-no-major-version",
+ "client-request-array-no-supported-version",
+ "client-request-array-no-supported-version-among",
+ "client-request-array-version-1",
+ "client-request-array-version-1-1",
+ "client-request-array-version-2",
+ "client-request-negative-major-version",
+ "client-request-negative-minor-version",
+ "client-request-negative-version",
+ "client-request-no-major-version",
+ "client-request-no-version",
+ "client-request-version-1",
+ "client-request-version-1-1",
+ "client-request-version-2",
+ "client-requests-bad",
+ "client-requests-empty",
+ "client-requests-not-kinded",
+ "client-requests-not-objects",
+ "client-requests-unknown",
+ ]
+ expected = [
+ (check_query_client_member, "client-client-member"),
+ (check_query_empty_array, "client-empty-array"),
+ (check_query_empty_object, "client-empty-object"),
+ (check_query_json_bad_root, "client-json-bad-root"),
+ (check_query_json_empty, "client-json-empty"),
+ (check_query_json_extra, "client-json-extra"),
+ (check_query_not_file, "client-not-file"),
+ (check_query_requests_bad, "client-requests-bad"),
+ (check_query_requests_empty, "client-requests-empty"),
+ (check_query_requests_not_kinded, "client-requests-not-kinded"),
+ (check_query_requests_not_objects, "client-requests-not-objects"),
+ (check_query_requests_unknown, "client-requests-unknown"),
+ ]
+ for (f, k) in expected:
+ assert is_dict(q[k])
+ assert sorted(q[k].keys()) == ["query.json"]
+ f(q[k]["query.json"])
+ expected = [
+ (check_query_response_array_negative_major_version, "client-request-array-negative-major-version"),
+ (check_query_response_array_negative_minor_version, "client-request-array-negative-minor-version"),
+ (check_query_response_array_negative_version, "client-request-array-negative-version"),
+ (check_query_response_array_no_major_version, "client-request-array-no-major-version"),
+ (check_query_response_array_no_supported_version, "client-request-array-no-supported-version"),
+ (check_query_response_array_no_supported_version_among, "client-request-array-no-supported-version-among"),
+ (check_query_response_array_version_1, "client-request-array-version-1"),
+ (check_query_response_array_version_1_1, "client-request-array-version-1-1"),
+ (check_query_response_array_version_2, "client-request-array-version-2"),
+ (check_query_response_negative_major_version, "client-request-negative-major-version"),
+ (check_query_response_negative_minor_version, "client-request-negative-minor-version"),
+ (check_query_response_negative_version, "client-request-negative-version"),
+ (check_query_response_no_major_version, "client-request-no-major-version"),
+ (check_query_response_no_version, "client-request-no-version"),
+ (check_query_response_version_1, "client-request-version-1"),
+ (check_query_response_version_1_1, "client-request-version-1-1"),
+ (check_query_response_version_2, "client-request-version-2"),
+ ]
+ for (f, k) in expected:
+ assert is_dict(q[k])
+ assert sorted(q[k].keys()) == ["query.json"]
+ assert is_dict(q[k]["query.json"])
+ assert sorted(q[k]["query.json"].keys()) == ["requests", "responses"]
+ r = q[k]["query.json"]["requests"]
+ assert is_list(r)
+ assert len(r) == 1
+ assert is_dict(r[0])
+ assert r[0]["kind"] == "__test"
+ r = q[k]["query.json"]["responses"]
+ assert is_list(r)
+ assert len(r) == 1
+ assert is_dict(r[0])
+ f(r[0])
+
+def check_query_client_member(q):
+ assert is_dict(q)
+ assert sorted(q.keys()) == ["client", "responses"]
+ assert is_dict(q["client"])
+ assert sorted(q["client"].keys()) == []
+ check_error(q["responses"], "'requests' member missing")
+
+def check_query_empty_array(q):
+ check_error(q, "query root is not an object")
+
+def check_query_empty_object(q):
+ assert is_dict(q)
+ assert sorted(q.keys()) == ["responses"]
+ check_error(q["responses"], "'requests' member missing")
+
+def check_query_json_bad_root(q):
+ check_error_re(q, "A valid JSON document must be either an array or an object value")
+
+def check_query_json_empty(q):
+ check_error_re(q, "value, object or array expected")
+
+def check_query_json_extra(q):
+ check_error_re(q, "Extra non-whitespace after JSON value")
+
+def check_query_not_file(q):
+ check_error_re(q, "failed to read from file")
+
+def check_query_requests_bad(q):
+ assert is_dict(q)
+ assert sorted(q.keys()) == ["requests", "responses"]
+ r = q["requests"]
+ assert is_dict(r)
+ assert sorted(r.keys()) == []
+ check_error(q["responses"], "'requests' member is not an array")
+
+def check_query_requests_empty(q):
+ assert is_dict(q)
+ assert sorted(q.keys()) == ["requests", "responses"]
+ r = q["requests"]
+ assert is_list(r)
+ assert len(r) == 0
+ r = q["responses"]
+ assert is_list(r)
+ assert len(r) == 0
+
+def check_query_requests_not_kinded(q):
+ assert is_dict(q)
+ assert sorted(q.keys()) == ["requests", "responses"]
+ r = q["requests"]
+ assert is_list(r)
+ assert len(r) == 4
+ assert is_dict(r[0])
+ assert sorted(r[0].keys()) == []
+ assert is_dict(r[1])
+ assert sorted(r[1].keys()) == ["kind"]
+ assert is_dict(r[1]["kind"])
+ assert is_dict(r[2])
+ assert sorted(r[2].keys()) == ["kind"]
+ assert is_list(r[2]["kind"])
+ assert is_dict(r[3])
+ assert sorted(r[3].keys()) == ["kind"]
+ assert is_int(r[3]["kind"])
+ r = q["responses"]
+ assert is_list(r)
+ assert len(r) == 4
+ check_error(r[0], "'kind' member missing")
+ check_error(r[1], "'kind' member is not a string")
+ check_error(r[2], "'kind' member is not a string")
+ check_error(r[3], "'kind' member is not a string")
+
+def check_query_requests_not_objects(q):
+ assert is_dict(q)
+ assert sorted(q.keys()) == ["requests", "responses"]
+ r = q["requests"]
+ assert is_list(r)
+ assert len(r) == 3
+ assert is_int(r[0])
+ assert is_string(r[1])
+ assert is_list(r[2])
+ r = q["responses"]
+ assert is_list(r)
+ assert len(r) == 3
+ check_error(r[0], "request is not an object")
+ check_error(r[1], "request is not an object")
+ check_error(r[2], "request is not an object")
+
+def check_query_requests_unknown(q):
+ assert is_dict(q)
+ assert sorted(q.keys()) == ["requests", "responses"]
+ r = q["requests"]
+ assert is_list(r)
+ assert len(r) == 3
+ assert is_dict(r[0])
+ assert sorted(r[0].keys()) == ["kind"]
+ assert r[0]["kind"] == "unknownC"
+ assert is_dict(r[1])
+ assert sorted(r[1].keys()) == ["kind"]
+ assert r[1]["kind"] == "unknownB"
+ assert is_dict(r[2])
+ assert sorted(r[2].keys()) == ["kind"]
+ assert r[2]["kind"] == "unknownA"
+ r = q["responses"]
+ assert is_list(r)
+ assert len(r) == 3
+ check_error(r[0], "unknown request kind 'unknownC'")
+ check_error(r[1], "unknown request kind 'unknownB'")
+ check_error(r[2], "unknown request kind 'unknownA'")
+
+def check_query_response_array_negative_major_version(r):
+ check_error(r, "'version' object 'major' member is not a non-negative integer")
+
+def check_query_response_array_negative_minor_version(r):
+ check_error(r, "'version' object 'minor' member is not a non-negative integer")
+
+def check_query_response_array_negative_version(r):
+ check_error(r, "'version' array entry is not a non-negative integer or object")
+
+def check_query_response_array_no_major_version(r):
+ check_error(r, "'version' object 'major' member missing")
+
+def check_query_response_array_no_supported_version(r):
+ check_error(r, "no supported version specified")
+
+def check_query_response_array_no_supported_version_among(r):
+ check_error(r, "no supported version specified among: 4.0 3.0")
+
+def check_query_response_array_version_1(r):
+ check_index__test(r, 1, 3)
+
+def check_query_response_array_version_1_1(r):
+ check_index__test(r, 1, 3) # always uses latest minor version
+
+def check_query_response_array_version_2(r):
+ check_index__test(r, 2, 0)
+
+def check_query_response_negative_major_version(r):
+ check_error(r, "'version' object 'major' member is not a non-negative integer")
+
+def check_query_response_negative_minor_version(r):
+ check_error(r, "'version' object 'minor' member is not a non-negative integer")
+
+def check_query_response_negative_version(r):
+ check_error(r, "'version' member is not a non-negative integer, object, or array")
+
+def check_query_response_no_major_version(r):
+ check_error(r, "'version' object 'major' member missing")
+
+def check_query_response_no_version(r):
+ check_error(r, "'version' member missing")
+
+def check_query_response_version_1(r):
+ check_index__test(r, 1, 3)
+
+def check_query_response_version_1_1(r):
+ check_index__test(r, 1, 3) # always uses latest minor version
+
+def check_query_response_version_2(r):
+ check_index__test(r, 2, 0)
+
+def check_objects(o):
+ assert is_list(o)
+ assert len(o) == 2
+ check_index__test(o[0], 1, 3)
+ check_index__test(o[1], 2, 0)
+
+assert is_dict(index)
+assert sorted(index.keys()) == ["cmake", "objects", "reply"]
+check_cmake(index["cmake"])
+check_reply(index["reply"])
+check_objects(index["objects"])
diff --git a/Tests/RunCMake/FileAPI/ClientStateful-prep.cmake b/Tests/RunCMake/FileAPI/ClientStateful-prep.cmake
new file mode 100644
index 0000000..5b41d7a
--- /dev/null
+++ b/Tests/RunCMake/FileAPI/ClientStateful-prep.cmake
@@ -0,0 +1,73 @@
+file(WRITE "${RunCMake_TEST_BINARY_DIR}/.cmake/api/v1/query/client-client-member/query.json" [[{ "client": {} }]])
+file(WRITE "${RunCMake_TEST_BINARY_DIR}/.cmake/api/v1/query/client-empty-array/query.json" "[]")
+file(WRITE "${RunCMake_TEST_BINARY_DIR}/.cmake/api/v1/query/client-empty-object/query.json" "{}")
+file(WRITE "${RunCMake_TEST_BINARY_DIR}/.cmake/api/v1/query/client-json-bad-root/query.json" [["invalid root"]])
+file(WRITE "${RunCMake_TEST_BINARY_DIR}/.cmake/api/v1/query/client-json-empty/query.json" "")
+file(WRITE "${RunCMake_TEST_BINARY_DIR}/.cmake/api/v1/query/client-json-extra/query.json" "{}x")
+file(MAKE_DIRECTORY "${RunCMake_TEST_BINARY_DIR}/.cmake/api/v1/query/client-not-file/query.json")
+
+file(WRITE "${RunCMake_TEST_BINARY_DIR}/.cmake/api/v1/query/client-requests-bad/query.json" [[{ "requests": {} }]])
+file(WRITE "${RunCMake_TEST_BINARY_DIR}/.cmake/api/v1/query/client-requests-empty/query.json" [[{ "requests": [] }]])
+
+file(WRITE "${RunCMake_TEST_BINARY_DIR}/.cmake/api/v1/query/client-requests-not-objects/query.json" [[
+{ "requests": [ 0, "", [] ] }
+]])
+file(WRITE "${RunCMake_TEST_BINARY_DIR}/.cmake/api/v1/query/client-requests-not-kinded/query.json" [[
+{ "requests": [ {}, { "kind": {} }, { "kind": [] }, { "kind": 0 } ] }
+]])
+file(WRITE "${RunCMake_TEST_BINARY_DIR}/.cmake/api/v1/query/client-requests-unknown/query.json" [[
+{ "requests": [ { "kind": "unknownC" }, { "kind": "unknownB" }, { "kind": "unknownA" } ] }
+]])
+file(WRITE "${RunCMake_TEST_BINARY_DIR}/.cmake/api/v1/query/client-request-no-version/query.json" [[
+{ "requests": [ { "kind": "__test" } ] }
+]])
+file(WRITE "${RunCMake_TEST_BINARY_DIR}/.cmake/api/v1/query/client-request-negative-version/query.json" [[
+{ "requests": [ { "kind": "__test", "version" : -1 } ] }
+]])
+file(WRITE "${RunCMake_TEST_BINARY_DIR}/.cmake/api/v1/query/client-request-no-major-version/query.json" [[
+{ "requests": [ { "kind": "__test", "version" : {} } ] }
+]])
+file(WRITE "${RunCMake_TEST_BINARY_DIR}/.cmake/api/v1/query/client-request-negative-major-version/query.json" [[
+{ "requests": [ { "kind": "__test", "version" : { "major": -1 } } ] }
+]])
+file(WRITE "${RunCMake_TEST_BINARY_DIR}/.cmake/api/v1/query/client-request-negative-minor-version/query.json" [[
+{ "requests": [ { "kind": "__test", "version" : { "major": 0, "minor": -1 } } ] }
+]])
+file(WRITE "${RunCMake_TEST_BINARY_DIR}/.cmake/api/v1/query/client-request-array-negative-version/query.json" [[
+{ "requests": [ { "kind": "__test", "version" : [ 1, -1 ] } ] }
+]])
+file(WRITE "${RunCMake_TEST_BINARY_DIR}/.cmake/api/v1/query/client-request-array-no-major-version/query.json" [[
+{ "requests": [ { "kind": "__test", "version" : [ 1, {} ] } ] }
+]])
+file(WRITE "${RunCMake_TEST_BINARY_DIR}/.cmake/api/v1/query/client-request-array-negative-major-version/query.json" [[
+{ "requests": [ { "kind": "__test", "version" : [ 1, { "major": -1 } ] } ] }
+]])
+file(WRITE "${RunCMake_TEST_BINARY_DIR}/.cmake/api/v1/query/client-request-array-negative-minor-version/query.json" [[
+{ "requests": [ { "kind": "__test", "version" : [ 1, { "major": 0, "minor": -1 } ] } ] }
+]])
+file(WRITE "${RunCMake_TEST_BINARY_DIR}/.cmake/api/v1/query/client-request-array-no-supported-version/query.json" [[
+{ "requests": [ { "kind": "__test", "version" : [] } ] }
+]])
+file(WRITE "${RunCMake_TEST_BINARY_DIR}/.cmake/api/v1/query/client-request-array-no-supported-version-among/query.json" [[
+{ "requests": [ { "kind": "__test", "version" : [4, 3] } ] }
+]])
+file(WRITE "${RunCMake_TEST_BINARY_DIR}/.cmake/api/v1/query/client-request-version-1/query.json" [[
+{ "requests": [ { "kind": "__test", "version" : 1 } ] }
+]])
+file(WRITE "${RunCMake_TEST_BINARY_DIR}/.cmake/api/v1/query/client-request-version-1-1/query.json" [[
+{ "requests": [ { "kind": "__test", "version" : { "major": 1, "minor": 1 } } ] }
+]])
+file(WRITE "${RunCMake_TEST_BINARY_DIR}/.cmake/api/v1/query/client-request-version-2/query.json" [[
+{ "requests": [ { "kind": "__test", "version" : { "major": 2 } } ] }
+]])
+file(WRITE "${RunCMake_TEST_BINARY_DIR}/.cmake/api/v1/query/client-request-array-version-1/query.json" [[
+{ "requests": [ { "kind": "__test", "version" : [3, 1] } ] }
+]])
+file(WRITE "${RunCMake_TEST_BINARY_DIR}/.cmake/api/v1/query/client-request-array-version-1-1/query.json" [[
+{ "requests": [ { "kind": "__test", "version" : [3, { "major": 1, "minor": 1 }, 2 ] } ] }
+]])
+file(WRITE "${RunCMake_TEST_BINARY_DIR}/.cmake/api/v1/query/client-request-array-version-2/query.json" [[
+{ "requests": [ { "kind": "__test", "version" : [3, { "major": 2 } ] } ] }
+]])
+
+file(WRITE "${RunCMake_TEST_BINARY_DIR}/.cmake/api/v1/reply/object-to-be-deleted.json" "")
diff --git a/Tests/RunCMake/FileAPI/ClientStateful.cmake b/Tests/RunCMake/FileAPI/ClientStateful.cmake
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/Tests/RunCMake/FileAPI/ClientStateful.cmake
diff --git a/Tests/RunCMake/FileAPI/RunCMakeTest.cmake b/Tests/RunCMake/FileAPI/RunCMakeTest.cmake
index a7351b3..1cc10ee 100644
--- a/Tests/RunCMake/FileAPI/RunCMakeTest.cmake
+++ b/Tests/RunCMake/FileAPI/RunCMakeTest.cmake
@@ -42,3 +42,4 @@ run_cmake(SharedStateless)
run_cmake(ClientStateless)
run_cmake(MixedStateless)
run_cmake(DuplicateStateless)
+run_cmake(ClientStateful)
diff --git a/Tests/RunCMake/FileAPI/check_index.py b/Tests/RunCMake/FileAPI/check_index.py
index 6cc16a5..4d90d59 100644
--- a/Tests/RunCMake/FileAPI/check_index.py
+++ b/Tests/RunCMake/FileAPI/check_index.py
@@ -81,6 +81,12 @@ def check_error(value, error):
assert is_string(value["error"])
assert value["error"] == error
+def check_error_re(value, error):
+ assert is_dict(value)
+ assert sorted(value.keys()) == ["error"]
+ assert is_string(value["error"])
+ assert re.search(error, value["error"])
+
reply_index = sys.argv[1]
reply_dir = os.path.dirname(reply_index)