| /** |
| * Licensed to the Apache Software Foundation (ASF) under one |
| * or more contributor license agreements. See the NOTICE file |
| * distributed with this work for additional information |
| * regarding copyright ownership. The ASF licenses this file |
| * to you 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. |
| **/ |
| |
| #include "utility/textbased_test/TextBasedTestDriver.hpp" |
| |
| #include <algorithm> |
| #include <cctype> |
| #include <cstdio> |
| #include <fstream> |
| #include <iostream> |
| #include <istream> |
| #include <set> |
| #include <string> |
| #include <vector> |
| |
| #include "utility/textbased_test/TextBasedTest.hpp" |
| |
| #include "glog/logging.h" |
| |
| namespace quickstep { |
| |
| namespace { |
| |
| static const char *kSameAsAboveString = "[same as above]"; |
| |
| /** |
| * @brief Returns a newline-terminated line from \p is. |
| * |
| * @param is The istream object from which line characters are extracted. |
| * @param line The string object to store the extracted line. |
| * @return True if \p line is not empty. |
| */ |
| bool GetLineWithNewLineNotTrimmed(std::istream *is, std::string *line) { |
| line->clear(); |
| |
| std::istream::sentry se(*is, true); |
| if (se) { |
| std::streambuf* sb = is->rdbuf(); |
| for (;;) { |
| const char c = sb->sgetc(); |
| switch (c) { |
| case '\n': |
| line->push_back(c); |
| sb->sbumpc(); |
| return true; |
| case '\r': |
| *line += c; |
| if (sb->snextc() == '\n') { |
| line->push_back(sb->sgetc()); |
| sb->sbumpc(); |
| } |
| return true; |
| case EOF: |
| is->setstate(std::ios_base::eofbit); |
| if (line->empty()) { |
| return false; |
| } |
| return true; |
| default: |
| line->push_back(c); |
| sb->sbumpc(); |
| } |
| } |
| } |
| |
| return false; |
| } |
| |
| } // namespace |
| |
| TextBasedTestDriver::TextBasedTestDriver(std::istream *input_stream, TextBasedTestRunner *test_runner) |
| : input_stream_(input_stream), |
| test_runner_(test_runner) { |
| CHECK(input_stream_->good()); |
| } |
| |
| TextBasedTestDriver::~TextBasedTestDriver() { |
| clearTestCases(); |
| } |
| |
| void TextBasedTestDriver::clearTestCases() { |
| for (const TextBasedTestCase *test_case : test_cases_) { |
| delete test_case; |
| } |
| test_cases_.clear(); |
| } |
| |
| std::vector<TextBasedTestCase*>* TextBasedTestDriver::populateAndReturnTestCases() { |
| populateTestCases(); |
| return &test_cases_; |
| } |
| |
| void TextBasedTestDriver::writeActualOutput(std::ostream *out) const { |
| const TextBasedTestCase *previous_case = nullptr; |
| for (const TextBasedTestCase *test_case : test_cases_) { |
| if (previous_case != nullptr) { |
| *out << "==" << std::endl; |
| } |
| *out << test_case->preceding_text; |
| *out << test_case->input_text; |
| *out << "--" << std::endl; |
| if (previous_case != nullptr && |
| test_case->actual_output_text == previous_case->actual_output_text) { |
| *out << kSameAsAboveString << std::endl; |
| } else { |
| *out << test_case->actual_output_text; |
| } |
| previous_case = test_case; |
| } |
| } |
| |
| void TextBasedTestDriver::writeActualOutputToFile(const std::string &output_filename) const { |
| std::ofstream file_stream(output_filename); |
| CHECK(file_stream.is_open()) << "Output file " << output_filename << " cannot be written to"; |
| writeActualOutput(&file_stream); |
| } |
| |
| void TextBasedTestDriver::populateTestCases() { |
| TextBasedTestCase *test_case = readNextTestCase(); |
| while (test_case != nullptr) { |
| CHECK(!std::all_of(test_case->input_text.begin(), test_case->input_text.end(), ::isspace)) << "Input text is empty"; |
| test_cases_.push_back(test_case); |
| test_case = readNextTestCase(); |
| } |
| } |
| |
| TextBasedTestCase* TextBasedTestDriver::readNextTestCase() { |
| if (input_stream_->eof()) { |
| return nullptr; |
| } |
| |
| std::string line; |
| std::ostringstream test_input; |
| std::ostringstream non_test_input; |
| std::ostringstream test_output; |
| |
| // Read leading comments. |
| while (GetLineWithNewLineNotTrimmed(input_stream_, &line)) { |
| DCHECK(!line.empty()); |
| if (line.at(0) == '[') { |
| break; |
| } |
| if (line.at(0) != '#') { |
| if (!std::all_of(line.begin(), line.end(), ::isspace)) { |
| break; |
| } |
| } |
| non_test_input << line; |
| } |
| |
| if (line.empty()) { |
| // We are reaching the end. |
| CHECK(input_stream_->eof()); |
| return nullptr; |
| } |
| |
| // Read option specifications. |
| std::set<std::string> options; |
| std::set<std::string> default_options; |
| if (line.at(0) == '[') { |
| readOptions(line, &options, &default_options); |
| non_test_input << line; |
| while (GetLineWithNewLineNotTrimmed(input_stream_, &line)) { |
| DCHECK(!line.empty()); |
| if (line.at(0) == '[') { |
| readOptions(line, &options, &default_options); |
| } else { |
| if (!std::all_of(line.begin(), line.end(), ::isspace)) { |
| break; |
| } |
| } |
| non_test_input << line; |
| } |
| } |
| |
| if (!default_options.empty()) { |
| default_options_ = std::move(default_options); |
| } |
| options.insert(default_options_.begin(), default_options_.end()); |
| |
| CHECK(!line.empty() || IsInputOutputSeparator(line)) << "Has options but no input test text"; |
| test_input << line; |
| |
| while (GetLineWithNewLineNotTrimmed(input_stream_, &line)) { |
| if (IsInputOutputSeparator(line)) { |
| break; |
| } |
| test_input << line; |
| } |
| |
| while (GetLineWithNewLineNotTrimmed(input_stream_, &line)) { |
| if (IsTestCaseSeparator(line)) { |
| break; |
| } |
| test_output << line; |
| } |
| |
| if (test_output.str().compare(0, ::strlen(kSameAsAboveString), kSameAsAboveString) == 0) { |
| CHECK(!test_cases_.empty()) << "Cannot specify \"same as above\" in the first test case"; |
| test_output.str(""); |
| test_output << test_cases_.back()->expected_output_text; |
| } |
| |
| return new TextBasedTestCase(non_test_input.str(), |
| test_input.str(), |
| test_output.str(), |
| std::move(options), |
| test_runner_); |
| } |
| |
| void TextBasedTestDriver::readOptions(const std::string &line, |
| std::set<std::string> *options_out, |
| std::set<std::string> *default_options_out) { |
| DCHECK(!line.empty() && line.at(0) == '[') << line; |
| std::string token; |
| std::istringstream line_stream(line); |
| line_stream.get(); |
| |
| // Create a temporary vector to store the options in the current line. |
| // The options cannot be directly inserted to <options_out> and |
| // copy the content to <default_options_out> if default is specified, |
| // because <options_out> may contain non-default options given in |
| // preceding lines. |
| std::vector<std::string> options; |
| while (line_stream >> token) { |
| size_t bracket_pos = token.find(']'); |
| if (bracket_pos != std::string::npos) { |
| CHECK_EQ(bracket_pos + 1, token.size()) << "Cannot have text after options"; |
| if (token.size() > 1) { |
| options.push_back(token.substr(0, bracket_pos)); |
| } |
| break; |
| } |
| options.push_back(token); |
| } |
| |
| CHECK(!(line_stream >> token)) << "Cannot have text after options"; |
| |
| bool is_default = false; |
| std::vector<std::string>::const_iterator start_it = options.begin(); |
| std::vector<std::string>::const_iterator end_it = options.end(); |
| if (start_it != end_it && *start_it == "default") { |
| ++start_it; |
| is_default = true; |
| } |
| |
| CHECK(start_it != end_it) << "Has no options specified in the brackets"; |
| |
| if (is_default) { |
| default_options_out->insert(start_it, end_it); |
| } |
| |
| while (start_it != end_it) { |
| CHECK(isRegisteredOption(*start_it)) << "Option " << *start_it << " is not found"; |
| options_out->insert(*start_it); |
| ++start_it; |
| } |
| } |
| |
| bool TextBasedTestDriver::IsInputOutputSeparator(const std::string &str) { |
| if (str.size() > 1 && str.at(0) == '-' && str.at(1) == '-') { |
| return true; |
| } |
| return false; |
| } |
| |
| bool TextBasedTestDriver::IsTestCaseSeparator(const std::string &str) { |
| if (str.size() > 1 && str.at(0) == '=' && str.at(1) == '=') { |
| return true; |
| } |
| return false; |
| } |
| |
| bool TextBasedTestDriver::isValidOption(const std::string &name) const { |
| if (name.empty()) return false; |
| |
| for (const char c : name) { |
| if (!std::isalnum(c) && c != '_') { |
| return false; |
| } |
| } |
| return true; |
| } |
| |
| void TextBasedTestDriver::registerOptions(const std::vector<std::string> &option_names) { |
| for (const std::string &option_name : option_names) { |
| registerOption(option_name); |
| } |
| } |
| |
| void TextBasedTestDriver::registerOption(const std::string &option_name) { |
| CHECK(isValidOption(option_name)) |
| << "Option " << option_name << " is not valid (must contain only alphanumeric characters and underscores)"; |
| registered_options_.insert(option_name); |
| } |
| |
| bool TextBasedTestDriver::isRegisteredOption(const std::string &option_name) const { |
| return registered_options_.find(option_name) != registered_options_.end(); |
| } |
| |
| } // namespace quickstep |