| #include <cstdio> | |
| #include <cstring> | |
| #include <fstream> | |
| #include <iostream> | |
| #include <map> | |
| #include <memory> | |
| #include <random> | |
| #include <sstream> | |
| #include <streambuf> | |
| #include "../src/benchmark_api_internal.h" | |
| #include "../src/check.h" // NOTE: check.h is for internal use only! | |
| #include "../src/log.h" // NOTE: log.h is for internal use only | |
| #include "../src/re.h" // NOTE: re.h is for internal use only | |
| #include "output_test.h" | |
| // ========================================================================= // | |
| // ------------------------------ Internals -------------------------------- // | |
| // ========================================================================= // | |
| namespace internal { | |
| namespace { | |
| using TestCaseList = std::vector<TestCase>; | |
| // Use a vector because the order elements are added matters during iteration. | |
| // std::map/unordered_map don't guarantee that. | |
| // For example: | |
| // SetSubstitutions({{"%HelloWorld", "Hello"}, {"%Hello", "Hi"}}); | |
| // Substitute("%HelloWorld") // Always expands to Hello. | |
| using SubMap = std::vector<std::pair<std::string, std::string>>; | |
| TestCaseList& GetTestCaseList(TestCaseID ID) { | |
| // Uses function-local statics to ensure initialization occurs | |
| // before first use. | |
| static TestCaseList lists[TC_NumID]; | |
| return lists[ID]; | |
| } | |
| SubMap& GetSubstitutions() { | |
| // Don't use 'dec_re' from header because it may not yet be initialized. | |
| // clang-format off | |
| static std::string safe_dec_re = "[0-9]*[.]?[0-9]+([eE][-+][0-9]+)?"; | |
| static std::string time_re = "([0-9]+[.])?[0-9]+"; | |
| static std::string percentage_re = "[0-9]+[.][0-9]{2}"; | |
| static SubMap map = { | |
| {"%float", "[0-9]*[.]?[0-9]+([eE][-+][0-9]+)?"}, | |
| // human-readable float | |
| {"%hrfloat", "[0-9]*[.]?[0-9]+([eE][-+][0-9]+)?[kKMGTPEZYmunpfazy]?i?"}, | |
| {"%percentage", percentage_re}, | |
| {"%int", "[ ]*[0-9]+"}, | |
| {" %s ", "[ ]+"}, | |
| {"%time", "[ ]*" + time_re + "[ ]+ns"}, | |
| {"%console_report", "[ ]*" + time_re + "[ ]+ns [ ]*" + time_re + "[ ]+ns [ ]*[0-9]+"}, | |
| {"%console_percentage_report", "[ ]*" + percentage_re + "[ ]+% [ ]*" + percentage_re + "[ ]+% [ ]*[0-9]+"}, | |
| {"%console_us_report", "[ ]*" + time_re + "[ ]+us [ ]*" + time_re + "[ ]+us [ ]*[0-9]+"}, | |
| {"%console_ms_report", "[ ]*" + time_re + "[ ]+ms [ ]*" + time_re + "[ ]+ms [ ]*[0-9]+"}, | |
| {"%console_s_report", "[ ]*" + time_re + "[ ]+s [ ]*" + time_re + "[ ]+s [ ]*[0-9]+"}, | |
| {"%console_time_only_report", "[ ]*" + time_re + "[ ]+ns [ ]*" + time_re + "[ ]+ns"}, | |
| {"%console_us_report", "[ ]*" + time_re + "[ ]+us [ ]*" + time_re + "[ ]+us [ ]*[0-9]+"}, | |
| {"%console_us_time_only_report", "[ ]*" + time_re + "[ ]+us [ ]*" + time_re + "[ ]+us"}, | |
| {"%csv_header", | |
| "name,iterations,real_time,cpu_time,time_unit,bytes_per_second," | |
| "items_per_second,label,error_occurred,error_message"}, | |
| {"%csv_report", "[0-9]+," + safe_dec_re + "," + safe_dec_re + ",ns,,,,,"}, | |
| {"%csv_us_report", "[0-9]+," + safe_dec_re + "," + safe_dec_re + ",us,,,,,"}, | |
| {"%csv_ms_report", "[0-9]+," + safe_dec_re + "," + safe_dec_re + ",ms,,,,,"}, | |
| {"%csv_s_report", "[0-9]+," + safe_dec_re + "," + safe_dec_re + ",s,,,,,"}, | |
| {"%csv_cv_report", "[0-9]+," + safe_dec_re + "," + safe_dec_re + ",,,,,,"}, | |
| {"%csv_bytes_report", | |
| "[0-9]+," + safe_dec_re + "," + safe_dec_re + ",ns," + safe_dec_re + ",,,,"}, | |
| {"%csv_items_report", | |
| "[0-9]+," + safe_dec_re + "," + safe_dec_re + ",ns,," + safe_dec_re + ",,,"}, | |
| {"%csv_bytes_items_report", | |
| "[0-9]+," + safe_dec_re + "," + safe_dec_re + ",ns," + safe_dec_re + | |
| "," + safe_dec_re + ",,,"}, | |
| {"%csv_label_report_begin", "[0-9]+," + safe_dec_re + "," + safe_dec_re + ",ns,,,"}, | |
| {"%csv_label_report_end", ",,"}}; | |
| // clang-format on | |
| return map; | |
| } | |
| std::string PerformSubstitutions(std::string source) { | |
| SubMap const& subs = GetSubstitutions(); | |
| using SizeT = std::string::size_type; | |
| for (auto const& KV : subs) { | |
| SizeT pos = 0; | |
| SizeT next_start = 0; | |
| while ((pos = source.find(KV.first, next_start)) != std::string::npos) { | |
| next_start = pos + KV.second.size(); | |
| source.replace(pos, KV.first.size(), KV.second); | |
| } | |
| } | |
| return source; | |
| } | |
| void CheckCase(std::stringstream& remaining_output, TestCase const& TC, | |
| TestCaseList const& not_checks) { | |
| std::string first_line; | |
| bool on_first = true; | |
| std::string line; | |
| while (!remaining_output.eof()) { | |
| BM_CHECK(remaining_output.good()); | |
| std::getline(remaining_output, line); | |
| if (on_first) { | |
| first_line = line; | |
| on_first = false; | |
| } | |
| for (const auto& NC : not_checks) { | |
| BM_CHECK(!NC.regex->Match(line)) | |
| << "Unexpected match for line \"" << line << "\" for MR_Not regex \"" | |
| << NC.regex_str << "\"" | |
| << "\n actual regex string \"" << TC.substituted_regex << "\"" | |
| << "\n started matching near: " << first_line; | |
| } | |
| if (TC.regex->Match(line)) { | |
| return; | |
| } | |
| BM_CHECK(TC.match_rule != MR_Next) | |
| << "Expected line \"" << line << "\" to match regex \"" << TC.regex_str | |
| << "\"" | |
| << "\n actual regex string \"" << TC.substituted_regex << "\"" | |
| << "\n started matching near: " << first_line; | |
| } | |
| BM_CHECK(remaining_output.eof() == false) | |
| << "End of output reached before match for regex \"" << TC.regex_str | |
| << "\" was found" | |
| << "\n actual regex string \"" << TC.substituted_regex << "\"" | |
| << "\n started matching near: " << first_line; | |
| } | |
| void CheckCases(TestCaseList const& checks, std::stringstream& output) { | |
| std::vector<TestCase> not_checks; | |
| for (size_t i = 0; i < checks.size(); ++i) { | |
| const auto& TC = checks[i]; | |
| if (TC.match_rule == MR_Not) { | |
| not_checks.push_back(TC); | |
| continue; | |
| } | |
| CheckCase(output, TC, not_checks); | |
| not_checks.clear(); | |
| } | |
| } | |
| class TestReporter : public benchmark::BenchmarkReporter { | |
| public: | |
| TestReporter(std::vector<benchmark::BenchmarkReporter*> reps) | |
| : reporters_(std::move(reps)) {} | |
| bool ReportContext(const Context& context) override { | |
| bool last_ret = false; | |
| bool first = true; | |
| for (auto* rep : reporters_) { | |
| bool new_ret = rep->ReportContext(context); | |
| BM_CHECK(first || new_ret == last_ret) | |
| << "Reports return different values for ReportContext"; | |
| first = false; | |
| last_ret = new_ret; | |
| } | |
| (void)first; | |
| return last_ret; | |
| } | |
| void ReportRuns(const std::vector<Run>& report) override { | |
| for (auto* rep : reporters_) { | |
| rep->ReportRuns(report); | |
| } | |
| } | |
| void Finalize() override { | |
| for (auto* rep : reporters_) { | |
| rep->Finalize(); | |
| } | |
| } | |
| private: | |
| std::vector<benchmark::BenchmarkReporter*> reporters_; | |
| }; | |
| } // namespace | |
| } // end namespace internal | |
| // ========================================================================= // | |
| // -------------------------- Results checking ----------------------------- // | |
| // ========================================================================= // | |
| namespace internal { | |
| // Utility class to manage subscribers for checking benchmark results. | |
| // It works by parsing the CSV output to read the results. | |
| class ResultsChecker { | |
| public: | |
| struct PatternAndFn : public TestCase { // reusing TestCase for its regexes | |
| PatternAndFn(const std::string& rx, ResultsCheckFn fn_) | |
| : TestCase(rx), fn(std::move(fn_)) {} | |
| ResultsCheckFn fn; | |
| }; | |
| std::vector<PatternAndFn> check_patterns; | |
| std::vector<Results> results; | |
| std::vector<std::string> field_names; | |
| void Add(const std::string& entry_pattern, const ResultsCheckFn& fn); | |
| void CheckResults(std::stringstream& output); | |
| private: | |
| void SetHeader_(const std::string& csv_header); | |
| void SetValues_(const std::string& entry_csv_line); | |
| std::vector<std::string> SplitCsv_(const std::string& line) const; | |
| }; | |
| namespace { | |
| // store the static ResultsChecker in a function to prevent initialization | |
| // order problems | |
| ResultsChecker& GetResultsChecker() { | |
| static ResultsChecker rc; | |
| return rc; | |
| } | |
| } // end namespace | |
| // add a results checker for a benchmark | |
| void ResultsChecker::Add(const std::string& entry_pattern, | |
| const ResultsCheckFn& fn) { | |
| check_patterns.emplace_back(entry_pattern, fn); | |
| } | |
| // check the results of all subscribed benchmarks | |
| void ResultsChecker::CheckResults(std::stringstream& output) { | |
| // first reset the stream to the start | |
| { | |
| auto start = std::stringstream::pos_type(0); | |
| // clear before calling tellg() | |
| output.clear(); | |
| // seek to zero only when needed | |
| if (output.tellg() > start) { | |
| output.seekg(start); | |
| } | |
| // and just in case | |
| output.clear(); | |
| } | |
| // now go over every line and publish it to the ResultsChecker | |
| std::string line; | |
| bool on_first = true; | |
| while (!output.eof()) { | |
| BM_CHECK(output.good()); | |
| std::getline(output, line); | |
| if (on_first) { | |
| SetHeader_(line); // this is important | |
| on_first = false; | |
| continue; | |
| } | |
| SetValues_(line); | |
| } | |
| // finally we can call the subscribed check functions | |
| for (const auto& p : check_patterns) { | |
| BM_VLOG(2) << "--------------------------------\n"; | |
| BM_VLOG(2) << "checking for benchmarks matching " << p.regex_str << "...\n"; | |
| for (const auto& r : results) { | |
| if (!p.regex->Match(r.name)) { | |
| BM_VLOG(2) << p.regex_str << " is not matched by " << r.name << "\n"; | |
| continue; | |
| } | |
| BM_VLOG(2) << p.regex_str << " is matched by " << r.name << "\n"; | |
| BM_VLOG(1) << "Checking results of " << r.name << ": ... \n"; | |
| p.fn(r); | |
| BM_VLOG(1) << "Checking results of " << r.name << ": OK.\n"; | |
| } | |
| } | |
| } | |
| // prepare for the names in this header | |
| void ResultsChecker::SetHeader_(const std::string& csv_header) { | |
| field_names = SplitCsv_(csv_header); | |
| } | |
| // set the values for a benchmark | |
| void ResultsChecker::SetValues_(const std::string& entry_csv_line) { | |
| if (entry_csv_line.empty()) { | |
| return; | |
| } // some lines are empty | |
| BM_CHECK(!field_names.empty()); | |
| auto vals = SplitCsv_(entry_csv_line); | |
| BM_CHECK_EQ(vals.size(), field_names.size()); | |
| results.emplace_back(vals[0]); // vals[0] is the benchmark name | |
| auto& entry = results.back(); | |
| for (size_t i = 1, e = vals.size(); i < e; ++i) { | |
| entry.values[field_names[i]] = vals[i]; | |
| } | |
| } | |
| // a quick'n'dirty csv splitter (eliminating quotes) | |
| std::vector<std::string> ResultsChecker::SplitCsv_( | |
| const std::string& line) const { | |
| std::vector<std::string> out; | |
| if (line.empty()) { | |
| return out; | |
| } | |
| if (!field_names.empty()) { | |
| out.reserve(field_names.size()); | |
| } | |
| size_t prev = 0; | |
| size_t pos = line.find_first_of(','); | |
| size_t curr = pos; | |
| while (pos != std::string::npos) { | |
| BM_CHECK(curr > 0); | |
| if (line[prev] == '"') { | |
| ++prev; | |
| } | |
| if (line[curr - 1] == '"') { | |
| --curr; | |
| } | |
| out.push_back(line.substr(prev, curr - prev)); | |
| prev = pos + 1; | |
| pos = line.find_first_of(',', pos + 1); | |
| curr = pos; | |
| } | |
| curr = line.size(); | |
| if (line[prev] == '"') { | |
| ++prev; | |
| } | |
| if (line[curr - 1] == '"') { | |
| --curr; | |
| } | |
| out.push_back(line.substr(prev, curr - prev)); | |
| return out; | |
| } | |
| } // end namespace internal | |
| size_t AddChecker(const std::string& bm_name, const ResultsCheckFn& fn) { | |
| auto& rc = internal::GetResultsChecker(); | |
| rc.Add(bm_name, fn); | |
| return rc.results.size(); | |
| } | |
| int Results::NumThreads() const { | |
| auto pos = name.find("/threads:"); | |
| if (pos == std::string::npos) { | |
| return 1; | |
| } | |
| auto end = name.find('/', pos + 9); | |
| std::stringstream ss; | |
| ss << name.substr(pos + 9, end); | |
| int num = 1; | |
| ss >> num; | |
| BM_CHECK(!ss.fail()); | |
| return num; | |
| } | |
| double Results::NumIterations() const { return GetAs<double>("iterations"); } | |
| double Results::GetTime(BenchmarkTime which) const { | |
| BM_CHECK(which == kCpuTime || which == kRealTime); | |
| const char* which_str = which == kCpuTime ? "cpu_time" : "real_time"; | |
| double val = GetAs<double>(which_str); | |
| const auto* unit = Get("time_unit"); | |
| BM_CHECK(unit); | |
| if (*unit == "ns") { | |
| return val * 1.e-9; | |
| } | |
| if (*unit == "us") { | |
| return val * 1.e-6; | |
| } | |
| if (*unit == "ms") { | |
| return val * 1.e-3; | |
| } | |
| if (*unit == "s") { | |
| return val; | |
| } | |
| BM_CHECK(1 == 0) << "unknown time unit: " << *unit; | |
| return 0; | |
| } | |
| // ========================================================================= // | |
| // -------------------------- Public API Definitions------------------------ // | |
| // ========================================================================= // | |
| TestCase::TestCase(std::string re, int rule) | |
| : regex_str(std::move(re)), | |
| match_rule(rule), | |
| substituted_regex(internal::PerformSubstitutions(regex_str)), | |
| regex(std::make_shared<benchmark::Regex>()) { | |
| std::string err_str; | |
| regex->Init(substituted_regex, &err_str); | |
| BM_CHECK(err_str.empty()) | |
| << "Could not construct regex \"" << substituted_regex << "\"" | |
| << "\n originally \"" << regex_str << "\"" | |
| << "\n got error: " << err_str; | |
| } | |
| int AddCases(TestCaseID ID, std::initializer_list<TestCase> il) { | |
| auto& L = internal::GetTestCaseList(ID); | |
| L.insert(L.end(), il); | |
| return 0; | |
| } | |
| int SetSubstitutions( | |
| std::initializer_list<std::pair<std::string, std::string>> il) { | |
| auto& subs = internal::GetSubstitutions(); | |
| for (auto KV : il) { | |
| bool exists = false; | |
| KV.second = internal::PerformSubstitutions(KV.second); | |
| for (auto& EKV : subs) { | |
| if (EKV.first == KV.first) { | |
| EKV.second = std::move(KV.second); | |
| exists = true; | |
| break; | |
| } | |
| } | |
| if (!exists) { | |
| subs.push_back(std::move(KV)); | |
| } | |
| } | |
| return 0; | |
| } | |
| // Disable deprecated warnings temporarily because we need to reference | |
| // CSVReporter but don't want to trigger -Werror=-Wdeprecated-declarations | |
| BENCHMARK_DISABLE_DEPRECATED_WARNING | |
| void RunOutputTests(int argc, char* argv[]) { | |
| using internal::GetTestCaseList; | |
| benchmark::Initialize(&argc, argv); | |
| auto options = benchmark::internal::GetOutputOptions(/*force_no_color*/ true); | |
| benchmark::ConsoleReporter CR(options); | |
| benchmark::JSONReporter JR; | |
| benchmark::CSVReporter CSVR; | |
| struct ReporterTest { | |
| std::string name; | |
| std::vector<TestCase>& output_cases; | |
| std::vector<TestCase>& error_cases; | |
| benchmark::BenchmarkReporter& reporter; | |
| std::stringstream out_stream; | |
| std::stringstream err_stream; | |
| ReporterTest(const std::string& n, std::vector<TestCase>& out_tc, | |
| std::vector<TestCase>& err_tc, | |
| benchmark::BenchmarkReporter& br) | |
| : name(n), output_cases(out_tc), error_cases(err_tc), reporter(br) { | |
| reporter.SetOutputStream(&out_stream); | |
| reporter.SetErrorStream(&err_stream); | |
| } | |
| } TestCases[] = { | |
| {std::string("ConsoleReporter"), GetTestCaseList(TC_ConsoleOut), | |
| GetTestCaseList(TC_ConsoleErr), CR}, | |
| {std::string("JSONReporter"), GetTestCaseList(TC_JSONOut), | |
| GetTestCaseList(TC_JSONErr), JR}, | |
| {std::string("CSVReporter"), GetTestCaseList(TC_CSVOut), | |
| GetTestCaseList(TC_CSVErr), CSVR}, | |
| }; | |
| // Create the test reporter and run the benchmarks. | |
| std::cout << "Running benchmarks...\n"; | |
| internal::TestReporter test_rep({&CR, &JR, &CSVR}); | |
| benchmark::RunSpecifiedBenchmarks(&test_rep); | |
| for (auto& rep_test : TestCases) { | |
| std::string msg = | |
| std::string("\nTesting ") + rep_test.name + std::string(" Output\n"); | |
| std::string banner(msg.size() - 1, '-'); | |
| std::cout << banner << msg << banner << "\n"; | |
| std::cerr << rep_test.err_stream.str(); | |
| std::cout << rep_test.out_stream.str(); | |
| internal::CheckCases(rep_test.error_cases, rep_test.err_stream); | |
| internal::CheckCases(rep_test.output_cases, rep_test.out_stream); | |
| std::cout << "\n"; | |
| } | |
| // now that we know the output is as expected, we can dispatch | |
| // the checks to subscribees. | |
| auto& csv = TestCases[2]; | |
| // would use == but gcc spits a warning | |
| BM_CHECK(csv.name == std::string("CSVReporter")); | |
| internal::GetResultsChecker().CheckResults(csv.out_stream); | |
| } | |
| BENCHMARK_RESTORE_DEPRECATED_WARNING | |
| int SubstrCnt(const std::string& haystack, const std::string& pat) { | |
| if (pat.length() == 0) { | |
| return 0; | |
| } | |
| int count = 0; | |
| for (size_t offset = haystack.find(pat); offset != std::string::npos; | |
| offset = haystack.find(pat, offset + pat.length())) { | |
| ++count; | |
| } | |
| return count; | |
| } | |
| namespace { | |
| char ToHex(int ch) { | |
| return ch < 10 ? static_cast<char>('0' + ch) | |
| : static_cast<char>('a' + (ch - 10)); | |
| } | |
| char RandomHexChar() { | |
| static std::mt19937 rd{std::random_device{}()}; | |
| static std::uniform_int_distribution<int> mrand{0, 15}; | |
| return ToHex(mrand(rd)); | |
| } | |
| std::string GetRandomFileName() { | |
| std::string model = "test.%%%%%%"; | |
| for (auto& ch : model) { | |
| if (ch == '%') { | |
| ch = RandomHexChar(); | |
| } | |
| } | |
| return model; | |
| } | |
| bool FileExists(std::string const& name) { | |
| std::ifstream in(name.c_str()); | |
| return in.good(); | |
| } | |
| std::string GetTempFileName() { | |
| // This function attempts to avoid race conditions where two tests | |
| // create the same file at the same time. However, it still introduces races | |
| // similar to tmpnam. | |
| int retries = 3; | |
| while (--retries != 0) { | |
| std::string name = GetRandomFileName(); | |
| if (!FileExists(name)) { | |
| return name; | |
| } | |
| } | |
| std::cerr << "Failed to create unique temporary file name\n"; | |
| std::flush(std::cerr); | |
| std::exit(1); | |
| } | |
| } // end namespace | |
| std::string GetFileReporterOutput(int argc, char* argv[]) { | |
| std::vector<char*> new_argv(argv, argv + argc); | |
| assert(static_cast<decltype(new_argv)::size_type>(argc) == new_argv.size()); | |
| std::string tmp_file_name = GetTempFileName(); | |
| std::cout << "Will be using this as the tmp file: " << tmp_file_name << '\n'; | |
| std::string tmp = "--benchmark_out="; | |
| tmp += tmp_file_name; | |
| new_argv.emplace_back(const_cast<char*>(tmp.c_str())); | |
| argc = static_cast<int>(new_argv.size()); | |
| benchmark::Initialize(&argc, new_argv.data()); | |
| benchmark::RunSpecifiedBenchmarks(); | |
| // Read the output back from the file, and delete the file. | |
| std::ifstream tmp_stream(tmp_file_name); | |
| std::string output = std::string((std::istreambuf_iterator<char>(tmp_stream)), | |
| std::istreambuf_iterator<char>()); | |
| std::remove(tmp_file_name.c_str()); | |
| return output; | |
| } |