| // 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; |
| } |