Index: llvm/trunk/test/tools/sancov/html-report.test =================================================================== --- llvm/trunk/test/tools/sancov/html-report.test +++ llvm/trunk/test/tools/sancov/html-report.test @@ -0,0 +1,6 @@ +REQUIRES: x86_64-linux +RUN: sancov -obj %p/Inputs/test-linux_x86_64 -html-report %p/Inputs/test-linux_x86_64.sancov | FileCheck %s + +// It's very difficult to test html report. Do basic smoke check. +CHECK: {{<a name=".*/Inputs/test.cpp">}} + Index: llvm/trunk/tools/sancov/sancov.cc =================================================================== --- llvm/trunk/tools/sancov/sancov.cc +++ llvm/trunk/tools/sancov/sancov.cc @@ -55,7 +55,8 @@ enum ActionType { PrintAction, CoveredFunctionsAction, - NotCoveredFunctionsAction + NotCoveredFunctionsAction, + HtmlReportAction }; cl::opt<ActionType> Action( @@ -65,6 +66,8 @@ "Print all covered funcions."), clEnumValN(NotCoveredFunctionsAction, "not-covered-functions", "Print all not covered funcions."), + clEnumValN(HtmlReportAction, "html-report", + "Print HTML coverage report."), clEnumValEnd)); static cl::list<std::string> ClInputFiles(cl::Positional, cl::OneOrMore, @@ -167,19 +170,24 @@ return Path.substr(Pos + ClStripPathPrefix.size()); } +static std::unique_ptr<symbolize::LLVMSymbolizer> createSymbolizer() { + symbolize::LLVMSymbolizer::Options SymbolizerOptions; + SymbolizerOptions.Demangle = ClDemangle; + SymbolizerOptions.UseSymbolTable = true; + return std::unique_ptr<symbolize::LLVMSymbolizer>( + new symbolize::LLVMSymbolizer(SymbolizerOptions)); +} + // Compute [FileLoc -> FunctionName] map for given addresses. static std::map<FileLoc, std::string> computeFunctionsMap(const std::set<uint64_t> &Addrs) { std::map<FileLoc, std::string> Fns; - symbolize::LLVMSymbolizer::Options SymbolizerOptions; - SymbolizerOptions.Demangle = ClDemangle; - SymbolizerOptions.UseSymbolTable = true; - symbolize::LLVMSymbolizer Symbolizer(SymbolizerOptions); + auto Symbolizer(createSymbolizer()); // Fill in Fns map. for (auto Addr : Addrs) { - auto InliningInfo = Symbolizer.symbolizeInlinedCode(ClBinaryName, Addr); + auto InliningInfo = Symbolizer->symbolizeInlinedCode(ClBinaryName, Addr); FailIfError(InliningInfo); for (uint32_t I = 0; I < InliningInfo->getNumberOfFrames(); ++I) { auto FrameInfo = InliningInfo->getFrame(I); @@ -398,6 +406,56 @@ } } +static std::string escapeHtml(const std::string &S) { + std::string Result; + Result.reserve(S.size()); + for (char Ch : S) { + switch (Ch) { + case '&': + Result.append("&"); + break; + case '\'': + Result.append("'"); + break; + case '"': + Result.append("""); + break; + case '<': + Result.append("<"); + break; + case '>': + Result.append(">"); + break; + default: + Result.push_back(Ch); + break; + } + } + return Result; +} + +// Computes a map file_name->{line_number} +static std::map<std::string, std::set<int>> +getFileLines(const std::set<uint64_t> &Addrs) { + std::map<std::string, std::set<int>> FileLines; + + auto Symbolizer(createSymbolizer()); + + // Fill in FileLines map. + for (auto Addr : Addrs) { + auto InliningInfo = Symbolizer->symbolizeInlinedCode(ClBinaryName, Addr); + FailIfError(InliningInfo); + for (uint32_t I = 0; I < InliningInfo->getNumberOfFrames(); ++I) { + auto FrameInfo = InliningInfo->getFrame(I); + SmallString<256> FileName(FrameInfo.FileName); + sys::path::remove_dots(FileName, /* remove_dot_dot */ true); + FileLines[FileName.str()].insert(FrameInfo.Line); + } + } + + return FileLines; +} + class CoverageData { public: // Read single file coverage data. @@ -471,6 +529,82 @@ } } + void printReport(raw_ostream &OS) { + // file_name -> set of covered lines; + std::map<std::string, std::set<int>> CoveredFileLines = + getFileLines(*Addrs); + std::map<std::string, std::set<int>> CoveragePoints = + getFileLines(getCoveragePoints(ClBinaryName)); + + std::string Title = stripPathPrefix(ClBinaryName) + " Coverage Report"; + + OS << "<html>\n"; + OS << "<head>\n"; + + // Stylesheet + OS << "<style>\n"; + OS << ".covered { background: #7F7; }\n"; + OS << ".notcovered { background: #F77; }\n"; + OS << "</style>\n"; + OS << "<title>" << Title << "</title>\n"; + OS << "</head>\n"; + OS << "<body>\n"; + + // Title + OS << "<h1>" << Title << "</h1>\n"; + OS << "<p>Coverage files: "; + for (auto InputFile : ClInputFiles) { + llvm::sys::fs::file_status Status; + llvm::sys::fs::status(InputFile, Status); + OS << stripPathPrefix(InputFile) << " (" + << Status.getLastModificationTime().str() << ")"; + } + OS << "</p>\n"; + + // TOC + OS << "<ul>\n"; + for (auto It : CoveredFileLines) { + auto FileName = It.first; + OS << "<li><a href=\"#" << escapeHtml(FileName) << "\">" + << stripPathPrefix(FileName) << "</a></li>\n"; + } + OS << "</ul>\n"; + + // Source + for (auto It : CoveredFileLines) { + auto FileName = It.first; + auto Lines = It.second; + auto CovLines = CoveragePoints[FileName]; + + OS << "<a name=\"" << escapeHtml(FileName) << "\"> </a>\n"; + OS << "<h2>" << stripPathPrefix(FileName) << "</h2>\n"; + ErrorOr<std::unique_ptr<MemoryBuffer>> BufOrErr = + MemoryBuffer::getFile(FileName); + if (!BufOrErr) { + OS << "Error reading file: " << FileName << " : " + << BufOrErr.getError().message() << "(" + << BufOrErr.getError().value() << ")\n"; + continue; + } + + OS << "<pre>\n"; + for (line_iterator I = line_iterator(*BufOrErr.get(), false); + !I.is_at_eof(); ++I) { + OS << "<span "; + if (Lines.find(I.line_number()) != Lines.end()) + OS << "class=covered"; + else if (CovLines.find(I.line_number()) != CovLines.end()) + OS << "class=notcovered"; + OS << ">"; + OS << escapeHtml(*I) << "</span>\n"; + } + OS << "</pre>\n"; + } + + OS << "</body>\n"; + OS << "</html>\n"; + } + // Print list of covered functions. // Line format: <file_name>:<line> <function_name> void printCoveredFunctions(raw_ostream &OS) { @@ -527,6 +661,10 @@ CovData.get()->printNotCoveredFunctions(outs()); return 0; } + case HtmlReportAction: { + CovData.get()->printReport(outs()); + return 0; + } } llvm_unreachable("unsupported action");