blob: 629db4ba85f85422f5edeaea0b5626d13c86dbbf [file] [log] [blame] [edit]
// Copyright 2021 The Verible Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// A utility similar to expect(1) but for json-rpc.
#include <fstream>
#include <iostream>
#include <streambuf>
#include <string>
#include "absl/status/status.h"
#include "absl/strings/match.h"
#include "common/lsp/message-stream-splitter.h"
#include "nlohmann/json.hpp"
#ifndef _WIN32
#include <unistd.h>
#else
#include <fcntl.h>
#include <io.h>
#include <stdio.h>
// Windows doesn't have Posix read(), but something called _read
#define read(fd, buf, size) _read(fd, buf, size)
#endif
using nlohmann::json;
using verible::lsp::MessageStreamSplitter;
static int usage(const char *progname, const char *msg) {
fprintf(stderr, "%s\n\nUsage: %s <expect-script-file>\n", msg, progname);
fprintf(stderr, "%s", R"(
The program receives JSON-RPC header/body responses on stdin
and compares if the response is contained in the response array.
Right now, the responses are checked to arrive in the same sequence they
are mentioned in the array (this might change in the future if we consider
async responses).
The exit code will be 0 if all expected responses have been received or the
index (1-based) where they failed.
Matching done 'fuzzy' so that expect strings can only contain what is relevant.
- json structures are only checked for the keys mentioned and ignores
additional keys
- arrays must match in length and contain the matching content (by these
matching rules)
- strings are substring matched
- numeric literals are matched exactly.
A typical expect-script file would be a json array like this
[
{
"json_contains": { ... some json, but only interesting fields ... }
},
]
)");
return 1;
}
static bool ValuesMatch(const json &expected, const json &received);
static bool CheckNested(const json &expected, const json &received) {
for (const auto &[key, value] : expected.items()) {
auto received_value = received.find(key);
if (received_value == received.end()) {
std::cerr << "key '" << key << "' missing in " << received << std::endl;
return false;
}
if (!ValuesMatch(value, *received_value)) {
std::cerr << "^ Issue with value at key '" << key << "'" << std::endl;
return false;
}
}
return true;
}
static bool ValuesMatch(const json &expected, const json &received) {
if (expected.type() != received.type()) {
std::cerr << "type mismatch " << received << std::endl;
return false;
}
if (expected.is_object()) {
return CheckNested(expected, received);
} else if (expected.is_array()) {
if (expected.size() != received.size()) {
std::cerr << "array size mismatch. Expected: " << expected.size()
<< "; got: " << received.size() << std::endl;
return false;
}
for (size_t i = 0; i < expected.size(); ++i) {
if (!ValuesMatch(expected[i], received[i])) return false;
}
return true;
} else if (expected.is_string()) {
return absl::StrContains(received.get<std::string>(),
expected.get<std::string>());
}
if (expected != received) {
std::cerr << "expected: " << expected << "; got: " << received << std::endl;
return false;
}
return true;
}
static bool CheckExpectedMatch(const json &expected, const json &received) {
auto json_contains = expected.find("json_contains");
if (json_contains == expected.end()) {
std::cerr << "'json_contains' key missing " << expected << std::endl;
return false;
}
const json &partial_data = *json_contains;
return CheckNested(partial_data, received);
}
int main(int argc, char *argv[]) {
#ifdef _WIN32
_setmode(_fileno(stdin), _O_BINARY);
#endif
static constexpr int kInFiledescriptor = 0; // STDIN_FILENO on Unix
if (argc != 2) {
return usage(argv[0], "Required filename");
}
std::ifstream file(argv[1]);
if (!file.is_open()) return usage(argv[0], "Could not open file");
const std::string expect_script((std::istreambuf_iterator<char>(file)),
std::istreambuf_iterator<char>());
const json expect_data = json::parse(expect_script);
if (!expect_data.is_array()) {
return usage(argv[0], "Input needs to be a json array");
}
// Let's be lenient in parsing.
const bool kStrictCRLFrequirement = true;
MessageStreamSplitter stream_splitter(4096, !kStrictCRLFrequirement);
int first_error = -1;
size_t expect_pos = 0;
stream_splitter.SetMessageProcessor(
[&expect_data, &expect_pos, &first_error](absl::string_view,
absl::string_view body) {
std::cerr << "Got: " << body << std::endl;
const json received = json::parse(body);
const json &expected = expect_data[expect_pos];
if (!CheckExpectedMatch(expected, received)) {
if (first_error < 0) first_error = expect_pos;
}
++expect_pos;
});
absl::Status status = absl::OkStatus();
while (status.ok()) {
status = stream_splitter.PullFrom([](char *buf, int size) -> int { //
return read(kInFiledescriptor, buf, size);
});
}
if (status.code() != absl::StatusCode::kUnavailable) {
std::cerr << "Expected EOF, got " << status << std::endl;
return expect_pos;
}
return expect_pos == expect_data.size() ? (first_error + 1) : expect_pos;
}