fileapi: Add protocol v1 infrastructure with support for shared query files

Add a file-based API that clients may use to get semantic information
about the buildsystem that CMake generates.  Clients will write query
files under a designated location in the build tree, and CMake will
write reply files for clients to read.

Start with support for shared stateless query files.  These allow
clients to share requests for major object versions and get all those
recognized by CMake.  Once any client has written a shared request to a
build tree it will persist.  Other clients will not need to overwrite
the request (since it is stateless) and should not remove it either.

For now we add only an undocumented object kind to use for testing the
query and reply infrastructure.  Object kinds providing real semantic
information will be added later.

Issue: #18398
This commit is contained in:
Brad King
2018-12-12 06:39:30 -05:00
parent c3635e502c
commit eb2ec41a04
25 changed files with 853 additions and 0 deletions
+1
View File
@@ -30,6 +30,7 @@ Reference Manuals
/manual/cmake-compile-features.7
/manual/cmake-developer.7
/manual/cmake-env-variables.7
/manual/cmake-file-api.7
/manual/cmake-generator-expressions.7
/manual/cmake-generators.7
/manual/cmake-language.7
+220
View File
@@ -0,0 +1,220 @@
.. cmake-manual-description: CMake File-Based API
cmake-file-api(7)
*****************
.. only:: html
.. contents::
Introduction
============
CMake provides a file-based API that clients may use to get semantic
information about the buildsystems CMake generates. Clients may use
the API by writing query files to a specific location in a build tree
to request zero or more `Object Kinds`_. When CMake generates the
buildsystem in that build tree it will read the query files and write
reply files for the client to read.
The file-based API uses a ``<build>/.cmake/api/`` directory at the top
of a build tree. The API is versioned to support changes to the layout
of files within the API directory. API file layout versioning is
orthogonal to the versioning of `Object Kinds`_ used in replies.
This version of CMake supports only one API version, `API v1`_.
API v1
======
API v1 is housed in the ``<build>/.cmake/api/v1/`` directory.
It has the following subdirectories:
``query/``
Holds query files written by clients.
These may be `v1 Shared Stateless Query Files`_.
``reply/``
Holds reply files written by CMake whenever it runs to generate a build
system. These are indexed by a `v1 Reply Index File`_ file that may
reference additional `v1 Reply Files`_. CMake owns all reply files.
Clients must never remove them.
Clients may look for and read a reply index file at any time.
Clients may optionally create the ``reply/`` directory at any time
and monitor it for the appearance of a new reply index file.
v1 Shared Stateless Query Files
-------------------------------
Shared stateless query files allow clients to share requests for
major versions of the `Object Kinds`_ and get all requested versions
recognized by the CMake that runs.
Clients may create shared requests by creating empty files in the
``v1/query/`` directory. The form is::
<build>/.cmake/api/v1/query/<kind>-v<major>
where ``<kind>`` is one of the `Object Kinds`_, ``-v`` is literal,
and ``<major>`` is the major version number.
Files of this form are stateless shared queries not owned by any specific
client. Once created they should not be removed without external client
coordination or human intervention.
v1 Reply Index File
-------------------
CMake writes an ``index-*.json`` file to the ``v1/reply/`` directory
whenever it runs to generate a build system. Clients must read the
reply index file first and may read other `v1 Reply Files`_ only by
following references. The form of the reply index file name is::
<build>/.cmake/api/v1/reply/index-<unspecified>.json
where ``index-`` is literal and ``<unspecified>`` is an unspecified
name selected by CMake. Whenever a new index file is generated it
is given a new name and any old one is deleted. During the short
time between these steps there may be multiple index files present;
the one with the largest name in lexicographic order is the current
index file.
The reply index file contains a JSON object:
.. code-block:: json
{
"cmake": {
"version": {
"major": 3, "minor": 14, "patch": 0, "suffix": "",
"string": "3.14.0", "isDirty": false
},
"paths": {
"cmake": "/prefix/bin/cmake",
"ctest": "/prefix/bin/ctest",
"cpack": "/prefix/bin/cpack",
"root": "/prefix/share/cmake-3.14"
}
},
"objects": [
{ "kind": "<kind>",
"version": { "major": 1, "minor": 0 },
"jsonFile": "<file>" },
{ "...": "..." }
],
"reply": {
"<kind>-v<major>": { "kind": "<kind>",
"version": { "major": 1, "minor": 0 },
"jsonFile": "<file>" },
"<unknown>": { "error": "unknown query file" },
"...": {}
}
}
The members are:
``cmake``
A JSON object containing information about the instance of CMake that
generated the reply. It contains members:
``version``
A JSON object specifying the version of CMake with members:
``major``, ``minor``, ``patch``
Integer values specifying the major, minor, and patch version components.
``suffix``
A string specifying the version suffix, if any, e.g. ``g0abc3``.
``string``
A string specifying the full version in the format
``<major>.<minor>.<patch>[-<suffix>]``.
``isDirty``
A boolean indicating whether the version was built from a version
controlled source tree with local modifications.
``paths``
A JSON object specifying paths to things that come with CMake.
It has members for ``cmake``, ``ctest``, and ``cpack`` whose values
are JSON strings specifying the absolute path to each tool,
represented with forward slashes. It also has a ``root`` member for
the absolute path to the directory containing CMake resources like the
``Modules/`` directory (see :variable:`CMAKE_ROOT`).
``objects``
A JSON array listing all versions of all `Object Kinds`_ generated
as part of the reply. Each array entry is a
`v1 Reply File Reference`_.
``reply``
A JSON object mirroring the content of the ``query/`` directory
that CMake loaded to produce the reply. The members are of the form
``<kind>-v<major>``
A member of this form appears for each of the
`v1 Shared Stateless Query Files`_ that CMake recognized as a
request for object kind ``<kind>`` with major version ``<major>``.
The value is a `v1 Reply File Reference`_ to the corresponding
reply file for that object kind and version.
``<unknown>``
A member of this form appears for each of the
`v1 Shared Stateless Query Files`_ that CMake did not recognize.
The value is a JSON object with a single ``error`` member
containing a string with an error message indicating that the
query file is unknown.
After reading the reply index file, clients may read the other
`v1 Reply Files`_ it references.
v1 Reply File Reference
^^^^^^^^^^^^^^^^^^^^^^^
The reply index file represents each reference to another reply file
using a JSON object with members:
``kind``
A string specifying one of the `Object Kinds`_.
``version``
A JSON object with members ``major`` and ``minor`` specifying
integer version components of the object kind.
``jsonFile``
A JSON string specifying a path relative to the reply index file
to another JSON file containing the object.
v1 Reply Files
--------------
Reply files containing specific `Object Kinds`_ are written by CMake.
The names of these files are unspecified and must not be interpreted
by clients. Clients must first read the `v1 Reply Index File`_ and
and follow references to the names of the desired response objects.
Reply files (including the index file) will never be replaced by
files of the same name but different content. This allows a client
to read the files concurrently with a running CMake that may generate
a new reply. However, after generating a new reply CMake will attempt
to remove reply files from previous runs that it did not just write.
If a client attempts to read a reply file referenced by the index but
finds the file missing, that means a concurrent CMake has generated
a new reply. The client may simply start again by reading the new
reply index file.
Object Kinds
============
The CMake file-based API reports semantic information about the build
system using the following kinds of JSON objects. Each kind of object
is versioned independently using semantic versioning with major and
minor components. Every kind of object has the form:
.. code-block:: json
{
"kind": "<kind>",
"version": { "major": 1, "minor": 0 },
"...": {}
}
The ``kind`` member is a string specifying the object kind name.
The ``version`` member is a JSON object with ``major`` and ``minor``
members specifying integer components of the object kind's version.
Additional top-level members are specific to each object kind.
+2
View File
@@ -207,6 +207,8 @@ set(SRCS
cmExtraKateGenerator.h
cmExtraSublimeTextGenerator.cxx
cmExtraSublimeTextGenerator.h
cmFileAPI.cxx
cmFileAPI.h
cmFileLock.cxx
cmFileLock.h
cmFileLockPool.cxx
+299
View File
@@ -0,0 +1,299 @@
/* Distributed under the OSI-approved BSD 3-Clause License. See accompanying
file Copyright.txt or https://cmake.org/licensing for details. */
#include "cmFileAPI.h"
#include "cmCryptoHash.h"
#include "cmSystemTools.h"
#include "cmTimestamp.h"
#include "cmake.h"
#include "cmsys/Directory.hxx"
#include "cmsys/FStream.hxx"
#include <algorithm>
#include <cassert>
#include <chrono>
#include <ctime>
#include <iomanip>
#include <sstream>
#include <utility>
cmFileAPI::cmFileAPI(cmake* cm)
: CMakeInstance(cm)
{
this->APIv1 =
this->CMakeInstance->GetHomeOutputDirectory() + "/.cmake/api/v1";
Json::StreamWriterBuilder wbuilder;
wbuilder["indentation"] = "\t";
this->JsonWriter =
std::unique_ptr<Json::StreamWriter>(wbuilder.newStreamWriter());
}
void cmFileAPI::ReadQueries()
{
std::string const query_dir = this->APIv1 + "/query";
this->QueryExists = cmSystemTools::FileIsDirectory(query_dir);
if (!this->QueryExists) {
return;
}
// Load queries at the top level.
std::vector<std::string> queries = cmFileAPI::LoadDir(query_dir);
// Read the queries and save for later.
for (std::string& query : queries) {
if (!cmFileAPI::ReadQuery(query, this->TopQuery.Known)) {
this->TopQuery.Unknown.push_back(std::move(query));
}
}
}
void cmFileAPI::WriteReplies()
{
if (this->QueryExists) {
cmSystemTools::MakeDirectory(this->APIv1 + "/reply");
this->WriteJsonFile(this->BuildReplyIndex(), "index", ComputeSuffixTime);
}
this->RemoveOldReplyFiles();
}
std::vector<std::string> cmFileAPI::LoadDir(std::string const& dir)
{
std::vector<std::string> files;
cmsys::Directory d;
d.Load(dir);
for (unsigned long i = 0; i < d.GetNumberOfFiles(); ++i) {
std::string f = d.GetFile(i);
if (f != "." && f != "..") {
files.push_back(std::move(f));
}
}
std::sort(files.begin(), files.end());
return files;
}
void cmFileAPI::RemoveOldReplyFiles()
{
std::string const reply_dir = this->APIv1 + "/reply";
std::vector<std::string> files = this->LoadDir(reply_dir);
for (std::string const& f : files) {
if (this->ReplyFiles.find(f) == this->ReplyFiles.end()) {
std::string file = reply_dir + "/" + f;
cmSystemTools::RemoveFile(file);
}
}
}
std::string cmFileAPI::WriteJsonFile(
Json::Value const& value, std::string const& prefix,
std::string (*computeSuffix)(std::string const&))
{
std::string fileName;
// Write the json file with a temporary name.
std::string const& tmpFile = this->APIv1 + "/tmp.json";
cmsys::ofstream ftmp(tmpFile.c_str());
this->JsonWriter->write(value, &ftmp);
ftmp << "\n";
ftmp.close();
if (!ftmp) {
cmSystemTools::RemoveFile(tmpFile);
return fileName;
}
// Compute the final name for the file.
fileName = prefix + "-" + computeSuffix(tmpFile) + ".json";
// Create the destination.
std::string file = this->APIv1 + "/reply";
cmSystemTools::MakeDirectory(file);
file += "/";
file += fileName;
// If the final name already exists then assume it has proper content.
// Otherwise, atomically place the reply file at its final name
if (cmSystemTools::FileExists(file, true) ||
!cmSystemTools::RenameFile(tmpFile.c_str(), file.c_str())) {
cmSystemTools::RemoveFile(tmpFile);
}
// Record this among files we have just written.
this->ReplyFiles.insert(fileName);
return fileName;
}
std::string cmFileAPI::ComputeSuffixHash(std::string const& file)
{
cmCryptoHash hasher(cmCryptoHash::AlgoSHA3_256);
std::string hash = hasher.HashFile(file);
hash.resize(20, '0');
return hash;
}
std::string cmFileAPI::ComputeSuffixTime(std::string const&)
{
std::chrono::milliseconds ms =
std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::system_clock::now().time_since_epoch());
std::chrono::seconds s =
std::chrono::duration_cast<std::chrono::seconds>(ms);
std::time_t ts = s.count();
std::size_t tms = ms.count() % 1000;
cmTimestamp cmts;
std::ostringstream ss;
ss << cmts.CreateTimestampFromTimeT(ts, "%Y-%m-%dT%H-%M-%S", true) << '-'
<< std::setfill('0') << std::setw(4) << tms;
return ss.str();
}
bool cmFileAPI::ReadQuery(std::string const& query,
std::vector<Object>& objects)
{
// Parse the "<kind>-" syntax.
std::string::size_type sep_pos = query.find('-');
if (sep_pos == std::string::npos) {
return false;
}
std::string kindName = query.substr(0, sep_pos);
std::string verStr = query.substr(sep_pos + 1);
if (kindName == ObjectKindName(ObjectKind::InternalTest)) {
Object o;
o.Kind = ObjectKind::InternalTest;
if (verStr == "v1") {
o.Version = 1;
} else if (verStr == "v2") {
o.Version = 2;
} else {
return false;
}
objects.push_back(o);
return true;
}
return false;
}
Json::Value cmFileAPI::BuildReplyIndex()
{
Json::Value index(Json::objectValue);
// Report information about this version of CMake.
index["cmake"] = this->BuildCMake();
// Reply to all queries that we loaded.
index["reply"] = this->BuildReply(this->TopQuery);
// Move our index of generated objects into its field.
Json::Value& objects = index["objects"] = Json::arrayValue;
for (auto& entry : this->ReplyIndexObjects) {
objects.append(std::move(entry.second)); // NOLINT(*)
}
return index;
}
Json::Value cmFileAPI::BuildCMake()
{
Json::Value cmake = Json::objectValue;
cmake["version"] = this->CMakeInstance->ReportVersionJson();
Json::Value& cmake_paths = cmake["paths"] = Json::objectValue;
cmake_paths["cmake"] = cmSystemTools::GetCMakeCommand();
cmake_paths["ctest"] = cmSystemTools::GetCTestCommand();
cmake_paths["cpack"] = cmSystemTools::GetCPackCommand();
cmake_paths["root"] = cmSystemTools::GetCMakeRoot();
return cmake;
}
Json::Value cmFileAPI::BuildReply(Query const& q)
{
Json::Value reply = Json::objectValue;
for (Object const& o : q.Known) {
std::string const& name = ObjectName(o);
reply[name] = this->AddReplyIndexObject(o);
}
for (std::string const& name : q.Unknown) {
reply[name] = cmFileAPI::BuildReplyError("unknown query file");
}
return reply;
}
Json::Value cmFileAPI::BuildReplyError(std::string const& error)
{
Json::Value e = Json::objectValue;
e["error"] = error;
return e;
}
Json::Value const& cmFileAPI::AddReplyIndexObject(Object const& o)
{
Json::Value& indexEntry = this->ReplyIndexObjects[o];
if (!indexEntry.isNull()) {
// The reply object has already been generated.
return indexEntry;
}
// Generate this reply object.
Json::Value const& object = this->BuildObject(o);
assert(object.isObject());
// Populate this index entry.
indexEntry = Json::objectValue;
indexEntry["kind"] = object["kind"];
indexEntry["version"] = object["version"];
indexEntry["jsonFile"] = this->WriteJsonFile(object, ObjectName(o));
return indexEntry;
}
const char* cmFileAPI::ObjectKindName(ObjectKind kind)
{
// Keep in sync with ObjectKind enum.
static const char* objectKindNames[] = {
"__test" //
};
return objectKindNames[size_t(kind)];
}
std::string cmFileAPI::ObjectName(Object const& o)
{
std::string name = ObjectKindName(o.Kind);
name += "-v";
name += std::to_string(o.Version);
return name;
}
Json::Value cmFileAPI::BuildObject(Object const& object)
{
Json::Value value;
switch (object.Kind) {
case ObjectKind::InternalTest:
value = this->BuildInternalTest(object);
break;
}
return value;
}
// The "__test" object kind is for internal testing of CMake.
static unsigned int const InternalTestV1Minor = 3;
static unsigned int const InternalTestV2Minor = 0;
Json::Value cmFileAPI::BuildInternalTest(Object const& object)
{
Json::Value test = Json::objectValue;
test["kind"] = this->ObjectKindName(object.Kind);
Json::Value& version = test["version"] = Json::objectValue;
if (object.Version == 2) {
version["major"] = 2;
version["minor"] = InternalTestV2Minor;
} else {
version["major"] = 1;
version["minor"] = InternalTestV1Minor;
}
return test;
}
+109
View File
@@ -0,0 +1,109 @@
/* Distributed under the OSI-approved BSD 3-Clause License. See accompanying
file Copyright.txt or https://cmake.org/licensing for details. */
#ifndef cmFileAPI_h
#define cmFileAPI_h
#include "cmConfigure.h" // IWYU pragma: keep
#include "cm_jsoncpp_value.h"
#include "cm_jsoncpp_writer.h"
#include <map>
#include <memory> // IWYU pragma: keep
#include <string>
#include <unordered_set>
#include <vector>
class cmake;
class cmFileAPI
{
public:
cmFileAPI(cmake* cm);
/** Read fileapi queries from disk. */
void ReadQueries();
/** Write fileapi replies to disk. */
void WriteReplies();
/** Get the "cmake" instance with which this was constructed. */
cmake* GetCMakeInstance() const { return this->CMakeInstance; }
private:
cmake* CMakeInstance;
/** The api/v1 directory location. */
std::string APIv1;
/** The set of files we have just written to the reply directory. */
std::unordered_set<std::string> ReplyFiles;
static std::vector<std::string> LoadDir(std::string const& dir);
void RemoveOldReplyFiles();
// Keep in sync with ObjectKindName.
enum class ObjectKind
{
InternalTest
};
/** Identify one object kind and major version. */
struct Object
{
ObjectKind Kind;
unsigned long Version = 0;
friend bool operator<(Object const& l, Object const& r)
{
if (l.Kind != r.Kind) {
return l.Kind < r.Kind;
}
return l.Version < r.Version;
}
};
/** Represent content of a query directory. */
struct Query
{
/** Known object kind-version pairs. */
std::vector<Object> Known;
/** Unknown object kind names. */
std::vector<std::string> Unknown;
};
/** Whether the top-level query directory exists at all. */
bool QueryExists = false;
/** The content of the top-level query directory. */
Query TopQuery;
/** 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::StreamWriter> JsonWriter;
std::string WriteJsonFile(
Json::Value const& value, std::string const& prefix,
std::string (*computeSuffix)(std::string const&) = ComputeSuffixHash);
static std::string ComputeSuffixHash(std::string const&);
static std::string ComputeSuffixTime(std::string const&);
static bool ReadQuery(std::string const& query,
std::vector<Object>& objects);
Json::Value BuildReplyIndex();
Json::Value BuildCMake();
Json::Value BuildReply(Query const& q);
static Json::Value BuildReplyError(std::string const& error);
Json::Value const& AddReplyIndexObject(Object const& o);
static const char* ObjectKindName(ObjectKind kind);
static std::string ObjectName(Object const& o);
Json::Value BuildObject(Object const& object);
Json::Value BuildInternalTest(Object const& object);
};
#endif
+10
View File
@@ -30,6 +30,7 @@
#if defined(CMAKE_BUILD_WITH_CMAKE)
# include "cm_jsoncpp_writer.h"
# include "cmFileAPI.h"
# include "cmGraphVizWriter.h"
# include "cmVariableWatch.h"
# include <unordered_map>
@@ -1443,6 +1444,11 @@ int cmake::ActualConfigure()
this->TruncateOutputLog("CMakeError.log");
}
#if defined(CMAKE_BUILD_WITH_CMAKE)
this->FileAPI = cm::make_unique<cmFileAPI>(this);
this->FileAPI->ReadQueries();
#endif
// actually do the configure
this->GlobalGenerator->Configure();
// Before saving the cache
@@ -1682,6 +1688,10 @@ int cmake::Generate()
// for the Visual Studio and Xcode generators.)
this->SaveCache(this->GetHomeOutputDirectory());
#if defined(CMAKE_BUILD_WITH_CMAKE)
this->FileAPI->WriteReplies();
#endif
return 0;
}
+3
View File
@@ -6,6 +6,7 @@
#include "cmConfigure.h" // IWYU pragma: keep
#include <map>
#include <memory> // IWYU pragma: keep
#include <set>
#include <string>
#include <unordered_set>
@@ -21,6 +22,7 @@
#endif
class cmExternalMakefileProjectGeneratorFactory;
class cmFileAPI;
class cmFileTimeComparison;
class cmGlobalGenerator;
class cmGlobalGeneratorFactory;
@@ -528,6 +530,7 @@ private:
#if defined(CMAKE_BUILD_WITH_CMAKE)
cmVariableWatch* VariableWatch;
std::unique_ptr<cmFileAPI> FileAPI;
#endif
cmState* State;
+4
View File
@@ -66,6 +66,9 @@ function(add_RunCMake_test_group test types)
endforeach()
endfunction()
# Some tests use python for extra checks.
find_package(PythonInterp QUIET)
if(XCODE_VERSION AND "${XCODE_VERSION}" VERSION_LESS 6.1)
set(Swift_ARGS -DXCODE_BELOW_6_1=1)
endif()
@@ -155,6 +158,7 @@ add_RunCMake_test(DisallowedCommands)
add_RunCMake_test(ExternalData)
add_RunCMake_test(FeatureSummary)
add_RunCMake_test(FPHSA)
add_RunCMake_test(FileAPI -DPYTHON_EXECUTABLE=${PYTHON_EXECUTABLE})
add_RunCMake_test(FindBoost)
add_RunCMake_test(FindLua)
add_RunCMake_test(FindOpenGL)
+3
View File
@@ -0,0 +1,3 @@
cmake_minimum_required(VERSION 3.12)
project(${RunCMake_TEST} NONE)
include(${RunCMake_TEST}.cmake)
+8
View File
@@ -0,0 +1,8 @@
set(expect
query
reply
reply/index-[0-9.T-]+.json
)
check_api("^${expect}$")
check_python(Empty)
+15
View File
@@ -0,0 +1,15 @@
from check_index import *
def check_reply(r):
assert is_dict(r)
assert sorted(r.keys()) == []
def check_objects(o):
assert is_list(o)
assert len(o) == 0
assert is_dict(index)
assert sorted(index.keys()) == ["cmake", "objects", "reply"]
check_cmake(index["cmake"])
check_reply(index["reply"])
check_objects(index["objects"])
+1
View File
@@ -0,0 +1 @@
file(MAKE_DIRECTORY "${RunCMake_TEST_BINARY_DIR}/.cmake/api/v1/query")
View File
@@ -0,0 +1 @@
check_api("^$")
@@ -0,0 +1 @@
file(MAKE_DIRECTORY "${RunCMake_TEST_BINARY_DIR}/.cmake/api/v1")
+40
View File
@@ -0,0 +1,40 @@
include(RunCMake)
# Function called in *-check.cmake scripts to check api files.
function(check_api expect)
file(GLOB_RECURSE actual
LIST_DIRECTORIES TRUE
RELATIVE ${RunCMake_TEST_BINARY_DIR}/.cmake/api/v1
${RunCMake_TEST_BINARY_DIR}/.cmake/api/v1/*
)
if(NOT "${actual}" MATCHES "${expect}")
set(RunCMake_TEST_FAILED "API files:
${actual}
do not match what we expected:
${expect}
in directory:
${RunCMake_TEST_BINARY_DIR}/.cmake/api/v1" PARENT_SCOPE)
endif()
endfunction()
function(check_python case)
if(RunCMake_TEST_FAILED OR NOT PYTHON_EXECUTABLE)
return()
endif()
file(GLOB index ${RunCMake_TEST_BINARY_DIR}/.cmake/api/v1/reply/index-*.json)
execute_process(
COMMAND ${PYTHON_EXECUTABLE} "${RunCMake_SOURCE_DIR}/${case}-check.py" "${index}"
RESULT_VARIABLE result
OUTPUT_VARIABLE output
ERROR_VARIABLE output
)
if(NOT result EQUAL 0)
string(REPLACE "\n" "\n " output " ${output}")
set(RunCMake_TEST_FAILED "Unexpected index:\n${output}" PARENT_SCOPE)
endif()
endfunction()
run_cmake(Nothing)
run_cmake(Empty)
run_cmake(Stale)
run_cmake(SharedStateless)
@@ -0,0 +1,15 @@
set(expect
query
query/__test-v1
query/__test-v2
query/__test-v3
query/query.json
query/unknown
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(SharedStateless)
@@ -0,0 +1,22 @@
from check_index import *
def check_reply(r):
assert is_dict(r)
assert sorted(r.keys()) == ["__test-v1", "__test-v2", "__test-v3", "query.json", "unknown"]
check_index__test(r["__test-v1"], 1, 3)
check_index__test(r["__test-v2"], 2, 0)
check_error(r["__test-v3"], "unknown query file")
check_error(r["query.json"], "unknown query file")
check_error(r["unknown"], "unknown query file")
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"])
@@ -0,0 +1,6 @@
file(WRITE "${RunCMake_TEST_BINARY_DIR}/.cmake/api/v1/query/__test-v1" "")
file(WRITE "${RunCMake_TEST_BINARY_DIR}/.cmake/api/v1/query/__test-v2" "")
file(WRITE "${RunCMake_TEST_BINARY_DIR}/.cmake/api/v1/query/__test-v3" "")
file(WRITE "${RunCMake_TEST_BINARY_DIR}/.cmake/api/v1/query/query.json" "")
file(WRITE "${RunCMake_TEST_BINARY_DIR}/.cmake/api/v1/query/unknown" "")
file(WRITE "${RunCMake_TEST_BINARY_DIR}/.cmake/api/v1/reply/object-to-be-deleted.json" "")
+4
View File
@@ -0,0 +1,4 @@
set(expect
reply
)
check_api("^${expect}$")
+1
View File
@@ -0,0 +1 @@
file(WRITE "${RunCMake_TEST_BINARY_DIR}/.cmake/api/v1/reply/object-to-be-deleted.json" "")
View File
+88
View File
@@ -0,0 +1,88 @@
import sys
import os
import json
import re
if sys.version_info[0] >= 3:
unicode = str
def is_bool(x):
return isinstance(x, bool)
def is_dict(x):
return isinstance(x, dict)
def is_list(x):
return isinstance(x, list)
def is_int(x):
return isinstance(x, int) or isinstance(x, long)
def is_string(x):
return isinstance(x, str) or isinstance(x, unicode)
def check_cmake(cmake):
assert is_dict(cmake)
assert sorted(cmake.keys()) == ["paths", "version"]
check_cmake_version(cmake["version"])
check_cmake_paths(cmake["paths"])
def check_cmake_version(v):
assert is_dict(v)
assert sorted(v.keys()) == ["isDirty", "major", "minor", "patch", "string", "suffix"]
assert is_string(v["string"])
assert is_int(v["major"])
assert is_int(v["minor"])
assert is_int(v["patch"])
assert is_string(v["suffix"])
assert is_bool(v["isDirty"])
def check_cmake_paths(v):
assert is_dict(v)
assert sorted(v.keys()) == ["cmake", "cpack", "ctest", "root"]
assert is_string(v["cmake"])
assert is_string(v["cpack"])
assert is_string(v["ctest"])
assert is_string(v["root"])
def check_index_object(indexEntry, kind, major, minor, check):
assert is_dict(indexEntry)
assert sorted(indexEntry.keys()) == ["jsonFile", "kind", "version"]
assert is_string(indexEntry["kind"])
assert indexEntry["kind"] == kind
assert is_dict(indexEntry["version"])
assert sorted(indexEntry["version"].keys()) == ["major", "minor"]
assert indexEntry["version"]["major"] == major
assert indexEntry["version"]["minor"] == minor
assert is_string(indexEntry["jsonFile"])
filepath = os.path.join(reply_dir, indexEntry["jsonFile"])
with open(filepath) as f:
object = json.load(f)
assert is_dict(object)
assert "kind" in object
assert is_string(object["kind"])
assert object["kind"] == kind
assert "version" in object
assert is_dict(object["version"])
assert sorted(object["version"].keys()) == ["major", "minor"]
assert object["version"]["major"] == major
assert object["version"]["minor"] == minor
if check:
check(object)
def check_index__test(indexEntry, major, minor):
def check(object):
assert sorted(object.keys()) == ["kind", "version"]
check_index_object(indexEntry, "__test", major, minor, check)
def check_error(value, error):
assert is_dict(value)
assert sorted(value.keys()) == ["error"]
assert is_string(value["error"])
assert value["error"] == error
reply_index = sys.argv[1]
reply_dir = os.path.dirname(reply_index)
with open(reply_index) as f:
index = json.load(f)