3
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

Google Testの結果をMarkdownで出力してみた

Posted at

まえがき

やんごとなき事情により、Google Testの結果をレポート出力しようと思った。
標準機能の --gtest_output だと、人が見るためではないjsonかxmlのみだった(標準出力を見ろってはなしだけど)ので、Markdownで出力するコードを書いてみた。

技術的にはここらへんjson出力のソースコードを参考にして、テストイベントハンドラでMarkdownのフォーマットに沿ってひたすらいい感じに出力するだけです。
Google Testのコードに則ってC++03で書いていきます。(最近はC++11前提みたいですが。)

出力結果

こんな感じになります。
出力サンプル

件数とエラーしか出力していないのでとても簡素です。

ソースコード

短いのでほぼ全部載せます。
エスケープ処理は適当です。

# include <stdio.h>
# include <fstream>
# include <iostream>
# include "gtest/gtest.h"

namespace {
	using namespace ::testing;

	static std::string FormatTimeInMillisAsDuration(TimeInMillis ms) {
		::std::stringstream ss;
		ss << (static_cast<double>(ms) * 1e-3) << "s";
		return ss.str();
	}

	static std::string FormatTimeInMillsAsTime(TimeInMillis ms) {
		time_t t = ms / 1000;
		struct tm* ltm = localtime(&t);
		if (ltm) {
			char buf[64] = {};
			//  YYYY-MM-DDThh:mm:ss
			std::strftime(buf, sizeof(buf), "%Y-%m-%dT%H:%M:%S", ltm);
			return buf;
		}
		else
			return "";
	}

	static const char* TestPartResultTypeToString(TestPartResult::Type type) {
		switch (type) {
		case TestPartResult::kSkip:
			return "Skipped";
		case TestPartResult::kSuccess:
			return "Success";
		case TestPartResult::kNonFatalFailure:
			return "NonFatalFailure";
		case TestPartResult::kFatalFailure:
			return "FatalFailure";
		default:
			return "Unknown result type";
		}
	}

	class MarkdownUnitTestResultPrinter : public EmptyTestEventListener {
	public:
		explicit MarkdownUnitTestResultPrinter(const char* output_file)
			: output_file_(output_file) {
			if (output_file_.empty()) {
				GTEST_LOG_(FATAL) << "Markdown output file may not be null";
			}
		}

		virtual void OnTestIterationEnd(const UnitTest& unit_test, int iteration) {
			std::ofstream out(output_file_.c_str());
			std::stringstream stream;
			PrintMdUnitTest(&stream, unit_test);
			out << stream.str();
		}

	private:
		static std::string EscapeMd(const std::string& str) {
			Message m;

			for (size_t i = 0; i < str.size(); ++i) {
				const char ch = str[i];
				switch (ch) {
				case '\\':
				case '`':
				case '*':
				case '_':
				case '{':
				case '}':
				case '[':
				case ']':
				case '(':
				case ')':
				case '#':
				case '+':
				case '-':
				case '!':
				case '.':
				case '<':
				case '>':
					m << '\\' << ch;
					break;
				case '\n':
					m << "<br>";
					break;
				default:
					m << ch;
					break;
				}
			}

			return m.GetString();
		}

		static void OutputMdTableHeader(std::ostream* stream,
			const std::string& name = "Measure of",
			const std::string& value = "Value") {
			*stream << "| " << name << " | " << value << " |\n";
			*stream << "| --- | --- |\n";
		}

		static void OutputMdTableRow(std::ostream* stream, const std::string& name,
			const std::string& value) {
			*stream << "| " << name << " | " << EscapeMd(value) << " |\n";
		}
		static void OutputMdTableRow(std::ostream* stream, const std::string& name,
			int value) {
			OutputMdTableRow(stream, name, internal::StreamableToString(value));
		}

		static void OutputMdTestInfo(::std::ostream* stream,
			const TestInfo& test_info) {
			const TestResult& result = *test_info.result();

			*stream << "### Test: " << EscapeMd(test_info.name()) << "\n";

			OutputMdTableHeader(stream);
			if (test_info.value_param() != nullptr) {
				OutputMdTableRow(stream, "value_param", test_info.value_param());
			}

			if (test_info.type_param() != nullptr) {
				OutputMdTableRow(stream, "type_param", test_info.type_param());
			}

			if (GTEST_FLAG(list_tests)) {
				OutputMdTableRow(stream, "file", test_info.file());
				OutputMdTableRow(stream, "line", test_info.line());
				return;
			}

			OutputMdTableRow(stream, "status",
				test_info.should_run() ? "RUN" : "NOTRUN");
			OutputMdTableRow(stream, "time",
				FormatTimeInMillisAsDuration(result.elapsed_time()));
			*stream << "\n";

			*stream << TestPropertiesAsMd(result);

			if (result.total_part_count() > 0) {
				*stream << "| \\# | type | location | message |\n"
					<< "| ----: | :----   | :---- | :---- |\n";
				for (int i = 0; i < result.total_part_count(); ++i) {
					const TestPartResult& part = result.GetTestPartResult(i);

					const std::string location =
						internal::FormatCompilerIndependentFileLocation(part.file_name(),
							part.line_number());
					*stream << "| " << internal::StreamableToString(i + 1)
						<< " | " << TestPartResultTypeToString(part.type())
						<< " | `" << location << "` "
						<< " | " << EscapeMd(part.message()) << " |\n";
				}
				*stream << "\n";
			}

		}

		static void PrintMdTestCase(::std::ostream* stream,
			const TestCase& test_case) {
			*stream << "## Test Case: " << EscapeMd(test_case.name()) << "\n";

			OutputMdTableHeader(stream);
			OutputMdTableRow(stream, "tests", test_case.reportable_test_count());

			if (!GTEST_FLAG(list_tests)) {
				OutputMdTableRow(stream, "failures", test_case.failed_test_count());
				OutputMdTableRow(stream, "disabled",
					test_case.reportable_disabled_test_count());
				OutputMdTableRow(stream, "errors", 0);
				OutputMdTableRow(stream, "time",
					FormatTimeInMillisAsDuration(test_case.elapsed_time()));
				*stream << "\n";

				if (test_case.ad_hoc_test_result().test_property_count() > 0) {
					*stream << "--- \n";
					OutputMdTableHeader(stream);
					*stream << TestPropertiesAsMd(test_case.ad_hoc_test_result()) << "\n";
				}
			}

			for (int i = 0; i < test_case.total_test_count(); ++i) {
				if (test_case.GetTestInfo(i)->is_reportable()) {
					OutputMdTestInfo(stream, *test_case.GetTestInfo(i));
				}
			}
			*stream << "\n";
		}

		static void PrintMdUnitTest(::std::ostream* stream,
			const UnitTest& unit_test) {
			*stream << "# Summary\n";

			OutputMdTableHeader(stream);
			OutputMdTableRow(stream, "tests", unit_test.reportable_test_count());
			OutputMdTableRow(stream, "failures", unit_test.failed_test_count());
			OutputMdTableRow(stream, "disabled",
				unit_test.reportable_disabled_test_count());
			if (GTEST_FLAG(shuffle)) {
				OutputMdTableRow(stream, "random_seed", unit_test.random_seed());
			}
			OutputMdTableRow(stream, "timestamp",
				FormatTimeInMillsAsTime(unit_test.start_timestamp()));
			OutputMdTableRow(stream, "time",
				FormatTimeInMillisAsDuration(unit_test.elapsed_time()));
			*stream << "\n";

			if (unit_test.ad_hoc_test_result().test_property_count() > 0) {
				OutputMdTableHeader(stream, "Propertiy Key", "Value");
				*stream << TestPropertiesAsMd(unit_test.ad_hoc_test_result()) << "\n";
			}

			for (int i = 0; i < unit_test.total_test_case_count(); ++i) {
				if (unit_test.GetTestCase(i)->reportable_test_count() > 0) {
					PrintMdTestCase(stream, *unit_test.GetTestCase(i));
				}
			}
		}

		static std::string TestPropertiesAsMd(const testing::TestResult& result) {
			Message attributes;
			for (int i = 0; i < result.test_property_count(); ++i) {
				const TestProperty& property = result.GetTestProperty(i);
				attributes << "| " << property.key() << " | "
					<< EscapeMd(property.value()) << " |\n";
			}
			return attributes.GetString();
		}

		const std::string output_file_;
	};
}

int main(int argc, char** argv) {
	printf("Running main() from %s\n", __FILE__);
	InitGoogleTest(&argc, argv);

	UnitTest& unit_test = *UnitTest::GetInstance();
	bool markdown_output = true;

	if (markdown_output) {
		TestEventListeners& listeners = unit_test.listeners();

		listeners.Append(new MarkdownUnitTestResultPrinter("test_report.md"));
	}
	int ret_val = RUN_ALL_TESTS();

	return ret_val;
}

3
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?