diff options
-rw-r--r-- | Help/manual/cmake-file-api.7.rst | 125 | ||||
-rw-r--r-- | Source/cmFileAPI.cxx | 303 | ||||
-rw-r--r-- | Source/cmFileAPI.h | 73 | ||||
-rw-r--r-- | Tests/RunCMake/FileAPI/ClientStateful-check.cmake | 68 | ||||
-rw-r--r-- | Tests/RunCMake/FileAPI/ClientStateful-check.py | 258 | ||||
-rw-r--r-- | Tests/RunCMake/FileAPI/ClientStateful-prep.cmake | 73 | ||||
-rw-r--r-- | Tests/RunCMake/FileAPI/ClientStateful.cmake | 0 | ||||
-rw-r--r-- | Tests/RunCMake/FileAPI/RunCMakeTest.cmake | 1 | ||||
-rw-r--r-- | Tests/RunCMake/FileAPI/check_index.py | 6 |
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) |