| // Copyright 2019 The Fuchsia Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "tools/fidlcat/lib/comparator.h" |
| |
| #include <zircon/assert.h> |
| |
| #include <iostream> |
| #include <string> |
| #include <utility> |
| #include <vector> |
| |
| namespace fidlcat { |
| |
| void Comparator::CompareInput(std::string_view syscall_inputs, std::string_view actual_process_name, |
| uint64_t actual_pid, uint64_t actual_tid) { |
| // remove header from the message |
| syscall_inputs = AnalyzesAndRemovesHeader(syscall_inputs); |
| auto actual_message_node = actual_message_graph_.InsertMessage(actual_process_name, actual_pid, |
| actual_tid, syscall_inputs); |
| // Is there a unique match for this message in the golden messages? If so, we propagate this |
| // match. |
| std::shared_ptr<GoldenNode> matching_golden_node = UniqueMatchToGolden(actual_message_node); |
| if (matching_golden_node) { |
| PropagateMatch(actual_message_node, matching_golden_node, false); |
| } |
| last_unmatched_input_from_tid_[actual_tid] = actual_message_node; |
| } |
| |
| void Comparator::CompareOutput(std::string_view syscall_outputs, |
| std::string_view actual_process_name, uint64_t actual_pid, |
| uint64_t actual_tid) { |
| // If present, remove header from message |
| syscall_outputs = AnalyzesAndRemovesHeader(syscall_outputs); |
| |
| // Create the output node, linking it to its corresponding input node if there is one. |
| auto matching_input = last_unmatched_input_from_tid_.find(actual_tid); |
| auto actual_message_node = |
| matching_input != last_unmatched_input_from_tid_.end() |
| ? actual_message_graph_.InsertMessage(actual_process_name, actual_pid, actual_tid, |
| syscall_outputs, matching_input->second) |
| : actual_message_graph_.InsertMessage(actual_process_name, actual_pid, actual_tid, |
| syscall_outputs); |
| |
| // Is there a unique match for this message in the golden messages? If so, we propagate this |
| // match. |
| std::shared_ptr<GoldenNode> matching_golden_node = UniqueMatchToGolden(actual_message_node); |
| if (matching_golden_node) { |
| PropagateMatch(actual_message_node, matching_golden_node, false); |
| } |
| } |
| |
| void Comparator::DecodingError(std::string_view error) { |
| compare_results_ << "Unexpected decoding error in the current execution:\n" << error; |
| } |
| |
| std::shared_ptr<GoldenMessageNode> Comparator::UniqueMatchToGolden( |
| std::shared_ptr<ActualMessageNode> actual_message_node) { |
| auto poss_golden_messages = |
| golden_message_graph_.message_nodes().find(actual_message_node->message()); |
| if (poss_golden_messages == golden_message_graph_.message_nodes().end()) { // No message matched |
| compare_results_ << "No golden message could match " << *actual_message_node; |
| return nullptr; |
| } |
| if (poss_golden_messages->second.size() == 1) { |
| // exactly one message from golden matched this string |
| return poss_golden_messages->second[0]; |
| } |
| // More than one golden message matched |
| return nullptr; |
| } |
| |
| bool Comparator::PropagateMatch(std::shared_ptr<ActualNode> actual_node, |
| std::shared_ptr<GoldenNode> golden_node, bool reverse_propagate) { |
| if (!actual_node || !golden_node) { |
| return false; |
| } |
| if (actual_node->matching_golden_node()) { |
| if (actual_node->matching_golden_node() == golden_node) { |
| return true; |
| } |
| compare_results_ << "Conflicting matches for " << *actual_node << "matched to " << *golden_node |
| << " and " << *(actual_node->matching_golden_node()) << "\n"; |
| return false; |
| } |
| if (golden_node->has_matching_actual_node()) { |
| compare_results_ << *golden_node << "was matched twice.\n"; |
| return false; |
| } |
| if (golden_node->dependencies().size() != actual_node->dependencies().size()) { |
| compare_results_ << *actual_node << " with " << actual_node->dependencies().size() |
| << " dependencies was matched with " << *golden_node << " which has " |
| << golden_node->dependencies().size() << " dependencies \n"; |
| return false; |
| } |
| actual_node->set_matching_golden_node(golden_node); |
| golden_node->set_has_matching_actual_node(); |
| |
| auto golden_i = golden_node->dependencies().begin(); |
| |
| for (auto i = actual_node->dependencies().begin(); i != actual_node->dependencies().end(); i++) { |
| auto actual_dependency_node = i->second; |
| // The golden node that should match actual_dependency_node according to the dependency links |
| // of golden_node |
| auto golden_dependency_node = golden_i->second; |
| |
| if (!PropagateMatch(actual_dependency_node, golden_dependency_node, reverse_propagate)) { |
| return false; |
| } |
| golden_i++; |
| } |
| if (reverse_propagate) { |
| return ReversePropagateMatch(actual_node); |
| } |
| return true; |
| } |
| |
| bool Comparator::ReversePropagateMatch(std::shared_ptr<ActualNode> actual_node) { |
| // Should only be called after Propagate has been called on actual_node, so actual_node should |
| // have a matching golden_node. |
| auto golden_node = actual_node->matching_golden_node(); |
| ZX_ASSERT(golden_node != nullptr); |
| |
| // We can only propagate along a reverse dependency if it is the only one of its type. |
| for (auto actual_link_type_pair = actual_node->reverse_dependencies().begin(); |
| actual_link_type_pair != actual_node->reverse_dependencies().end(); |
| actual_link_type_pair++) { |
| size_t actual_nb_link_of_type = actual_link_type_pair->second.size(); |
| if (actual_nb_link_of_type > 1) { |
| // multiple links with same type, we can't do any propagation |
| continue; |
| } |
| auto golden_link_type_pair = |
| golden_node->get_reverse_dependencies_by_type(actual_link_type_pair->first); |
| // This reverse link is not present in golden_node, there is no possible matching between the |
| // current execution and the one stored in the golden file. |
| if (golden_link_type_pair == golden_node->reverse_dependencies().end()) { |
| compare_results_ << *actual_node << " with a reverse dependency of type " |
| << actual_link_type_pair->first.second << " was matched to " << *golden_node |
| << " which has no such reverse dependency \n"; |
| return false; |
| } |
| size_t golden_nb_link_of_type = golden_link_type_pair->second.size(); |
| // golden node also has more reverse dependencies than actual_node, this means the matching is |
| // not possible as we only call ReversePropagate when the actual_message_graph_ is complete |
| if (golden_nb_link_of_type > 1) { |
| compare_results_ << *actual_node << " with one reverse dependency of type " |
| << actual_link_type_pair->first.second << " was matched to " << *golden_node |
| << " which has " << golden_nb_link_of_type |
| << " such reverse dependencies \n"; |
| return false; |
| } |
| auto actual_dependency_node = actual_link_type_pair->second[0].lock(); |
| auto golden_dependency_node = golden_link_type_pair->second[0].lock(); |
| if (!PropagateMatch(actual_dependency_node, golden_dependency_node, true)) { |
| return false; |
| } |
| } |
| return true; |
| } |
| |
| void Comparator::ParseGolden(std::string_view golden_file_contents) { |
| size_t processed_char_count = 0; |
| // We use this map to link output messages to their corresponding input messages. |
| std::map<uint64_t, std::shared_ptr<GoldenMessageNode>> last_unmatched_input_from_tid_golden; |
| |
| std::string_view cur_msg = GetMessage(golden_file_contents, &processed_char_count); |
| uint64_t previous_pid = 0; |
| uint64_t previous_tid = 0; |
| std::string previous_process_name; |
| while (!cur_msg.empty()) { |
| uint64_t pid = 0, tid = 0; |
| std::string process_name; |
| |
| cur_msg = AnalyzesAndRemovesHeader(cur_msg, &process_name, &pid, &tid); |
| |
| // AnalyzesAndRemovesHeader did not update the values of pid, tid and process_name, as there was |
| // no header to the message (or it could not be parsed). |
| if (pid == 0) { |
| pid = previous_pid; |
| tid = previous_tid; |
| process_name = previous_process_name; |
| } |
| |
| auto last_unmatched = last_unmatched_input_from_tid_golden.find(tid); |
| if (last_unmatched != last_unmatched_input_from_tid_golden.end()) { |
| // This is an output message, with a corresponding input messge |
| auto message_node = golden_message_graph_.InsertMessage(process_name, pid, tid, cur_msg, |
| last_unmatched->second); |
| last_unmatched_input_from_tid_golden.erase(tid); |
| } else { |
| auto message_node = golden_message_graph_.InsertMessage(process_name, pid, tid, cur_msg); |
| if (HasReturn(cur_msg)) { |
| last_unmatched_input_from_tid_golden[tid] = message_node; |
| } |
| } |
| |
| golden_file_contents = golden_file_contents.substr(processed_char_count); |
| cur_msg = GetMessage(golden_file_contents, &processed_char_count); |
| previous_pid = pid; |
| previous_tid = tid; |
| previous_process_name = process_name; |
| } |
| } |
| |
| std::string_view Comparator::GetMessage(std::string_view messages, size_t* processed_char_count) { |
| // begin points to the beginning of the line, end to its end. |
| size_t begin = 0; |
| size_t end = messages.find('\n', begin); |
| // Ignore fidlcat startup lines or empty lines. |
| while (end != std::string::npos) { |
| if (!IgnoredLine(messages.substr(begin, end - begin))) { |
| break; |
| } |
| begin = end + 1; |
| end = messages.find('\n', begin); |
| } |
| |
| // Now we get the message. |
| size_t bpos = begin; |
| while (end != std::string::npos) { |
| // New line indicates end of syscall input or output. |
| if (bpos < begin && end == begin && messages[begin] == '\n') { |
| break; |
| } |
| // Beginning of syscall output display. |
| if (bpos < begin && messages.substr(begin, end - begin).find(" ->") == 0) { |
| break; |
| } |
| // If the current line is the beginning of a multiline "sent " or "received ", we skip lines |
| // until we get to the closing " }". To find this closing "}", we rely on fidl_codec printing |
| // indentation: if the message is a request (begins with " sent"), indentation is 2 spaces, if |
| // it is a response (" received"), indentation is 4. Note: if fidl_codec fails to find the |
| // direction fo the message (request or response), this may fail to separate the messages |
| // properly, or if the first line of the message contains an opening { and some more {} couples, |
| // we mail fail to detect this as the beginning of a mutliline message. This should be removed |
| // when we can get access to a serialized version of the messages, and not only their text |
| // representation. |
| std::string_view cur_line = messages.substr(begin, end - begin); |
| bool is_received = cur_line.rfind(" received ", 0) != std::string::npos; |
| bool is_sent = cur_line.rfind(" sent ", 0) != std::string::npos; |
| if (is_sent || is_received) { |
| // This is the beginning of a request/response message |
| size_t pos_open = cur_line.find('{'); |
| if (pos_open != std::string::npos && |
| cur_line.substr(pos_open).find('}') == std::string::npos) { |
| // We have an open '{', we hence skip lines until we find the matching closing '}' |
| size_t indentation = is_sent ? 2 : 4; |
| while (end != std::string::npos && |
| !ClosingSequence(messages.substr(begin, end - begin), indentation)) { |
| begin = end + 1; |
| end = messages.find('\n', begin); |
| } |
| } |
| } |
| begin = end + 1; |
| end = messages.find('\n', begin); |
| } |
| *processed_char_count = begin; |
| |
| // Note that as expected_output_ is a string_view, substr() returns a string_view as well. |
| return messages.substr(bpos, begin - bpos); |
| } |
| |
| bool Comparator::ClosingSequence(std::string_view line, size_t indentation) { |
| if (line.length() < indentation + 1) { |
| return false; |
| } |
| // exactly one tabulation before the first closing ] or } |
| for (size_t i = 0; i < indentation; i++) { |
| if (line[i] != ' ') { |
| return false; |
| } |
| } |
| if (line[indentation] != ']' && line[indentation] != '}') { |
| return false; |
| } |
| // then a sequence of " ]" or " }" |
| size_t pos = indentation + 1; |
| while (pos + 2 <= line.length()) { |
| if (line[pos] != ' ') { |
| return false; |
| } |
| if (line[pos + 1] != '}' && line[pos + 1] != ']') { |
| return false; |
| } |
| pos += 2; |
| } |
| return true; |
| } |
| |
| bool Comparator::IgnoredLine(std::string_view line) { |
| if (line.length() == 0) { |
| return true; |
| } |
| if (line.length() == 1 && line[0] == '\n') { |
| return true; |
| } |
| static std::vector<std::string> to_be_ignored = {"Checking", "Debug", "Launched", "Monitoring", |
| "Stop"}; |
| for (size_t i = 0; i < to_be_ignored.size(); i++) { |
| if (line.find(to_be_ignored[i]) == 0) { |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| std::string_view Comparator::AnalyzesAndRemovesHeader(std::string_view message, |
| std::string* process_name, uint64_t* pid, |
| uint64_t* tid) { |
| constexpr size_t kMinNbCharHeader = 5; |
| // The message is a syscall output with no header. |
| if (message.find("->") <= kMinNbCharHeader) { |
| return message; |
| } |
| |
| size_t pos_pid = message.find(' '); |
| size_t pos_tid = message.find(':'); |
| // Either there is no header, or we cannot parse it so leave it as is. |
| if (pos_pid == std::string::npos || pos_tid == std::string::npos) { |
| return message; |
| } |
| |
| // If we have pointers to pid, tid and process_name, update those. |
| if (pid) { |
| *pid = ExtractUint64(message.substr(pos_pid + 1)); |
| } |
| if (tid) { |
| *tid = ExtractUint64(message.substr(pos_tid + 1)); |
| } |
| if (process_name) { |
| *process_name = message.substr(0, pos_pid); |
| } |
| size_t end_header = message.find(' ', pos_tid); |
| if (end_header != std::string::npos) { |
| return message.substr(end_header + 1); |
| } |
| return message; |
| } |
| |
| uint64_t Comparator::ExtractUint64(std::string_view str) { |
| uint64_t result = 0; |
| for (size_t i = 0; i < str.size(); ++i) { |
| char value = str[i]; |
| if ((value < '0') || (value > '9')) { |
| break; |
| } |
| result = 10 * result + (value - '0'); |
| } |
| return result; |
| } |
| |
| bool Comparator::HasReturn(std::string_view message) { |
| // Only three syscalls have no return value. Besides as we removed the header from the message, |
| // the syscall name is the first word of the message. |
| if (message.find("zx_thread_exit") == 0 || message.find("zx_process_exit") == 0 || |
| message.find("zx_futex_wake_handle_close_thread_exit") == 0) { |
| return false; |
| } |
| return true; |
| } |
| |
| void Comparator::FinishComparison() { |
| // All the messages have been intercepted, we now want to check our graph: |
| // - First propagates matchings along reverse dependencies now that the graph is complete, |
| // - Then checks if there still are unmatched nodes, either golden or actual. |
| |
| for (auto i = actual_message_graph_.message_nodes().begin(); |
| i != actual_message_graph_.message_nodes().end(); i++) { |
| for (size_t j = 0; j < i->second.size(); j++) { |
| if (i->second[j]->matching_golden_node() && !ReversePropagateMatch(i->second[j])) { |
| // The matching failed, with a proper error message |
| return; |
| } |
| } |
| } |
| |
| for (auto i = actual_message_graph_.pid_nodes().begin(); |
| i != actual_message_graph_.pid_nodes().end(); i++) { |
| if (i->second->matching_golden_node() && !ReversePropagateMatch(i->second)) { |
| return; |
| } |
| } |
| |
| for (auto i = actual_message_graph_.tid_nodes().begin(); |
| i != actual_message_graph_.tid_nodes().end(); i++) { |
| if (i->second->matching_golden_node() && !ReversePropagateMatch(i->second)) { |
| return; |
| } |
| } |
| |
| for (auto i = actual_message_graph_.handle_nodes().begin(); |
| i != actual_message_graph_.handle_nodes().end(); i++) { |
| if (i->second->matching_golden_node() && !ReversePropagateMatch(i->second)) { |
| return; |
| } |
| } |
| |
| // We check that all message nodes are matched to a golden node. |
| bool unmatched_message = false; |
| for (auto i = actual_message_graph_.message_nodes().begin(); |
| i != actual_message_graph_.message_nodes().end(); i++) { |
| for (size_t j = 0; j < i->second.size(); j++) { |
| if (!i->second[j]->matching_golden_node()) { |
| compare_results_ << "Unmatched actual message " << i->second[j]->message(); |
| unmatched_message = true; |
| } |
| } |
| } |
| for (auto i = golden_message_graph_.message_nodes().begin(); |
| i != golden_message_graph_.message_nodes().end(); i++) { |
| for (size_t j = 0; j < i->second.size(); j++) { |
| if (!i->second[j]->has_matching_actual_node()) { |
| compare_results_ << "Unmatched golden message " << i->second[j]->message(); |
| unmatched_message = true; |
| } |
| } |
| } |
| |
| // There is no need to check that handles, pids and tids are matched: as all of them have at least |
| // one message that depends from them, if all messages are matched, so are they. |
| if (!unmatched_message) { |
| compare_results_ << "Messages from the current execution matched the golden file.\n"; |
| } |
| } |
| } // namespace fidlcat |