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

## まえがき

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

技術的には[ここらへん](http://opencv.jp/googletestdocs/advancedguide.html#adv-extending-google-test-by-handling-test-events)や[json出力のソースコード](https://github.com/google/googletest/blob/master/googletest/src/gtest.cc#L3923)を参考にして、テストイベントハンドラでMarkdownのフォーマットに沿ってひたすらいい感じに出力するだけです。
Google Testのコードに則ってC++03で書いていきます。(最近はC++11前提みたいですが。)

## 出力結果

こんな感じになります。
<img width="710" alt="出力サンプル" src="https://qiita-image-store.s3.amazonaws.com/0/124276/f8136566-1f70-2d04-b509-e912a2506716.png">

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

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

```cpp
#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;
}

```