diff --git a/llvm/tools/llvm-cov/SourceCoverageView.h b/llvm/tools/llvm-cov/SourceCoverageView.h --- a/llvm/tools/llvm-cov/SourceCoverageView.h +++ b/llvm/tools/llvm-cov/SourceCoverageView.h @@ -99,7 +99,7 @@ CoveragePrinter(const CoverageViewOptions &Opts) : Opts(Opts) {} /// Return `OutputDir/ToplevelDir/Path.Extension`. If \p InToplevel is - /// false, skip the ToplevelDir component. If \p Relative is false, skip the + /// true, skip the ToplevelDir component. If \p Relative is true, skip the /// OutputDir component. std::string getOutputPath(StringRef Path, StringRef Extension, bool InToplevel, bool Relative = true) const; diff --git a/llvm/tools/llvm-cov/SourceCoverageView.cpp b/llvm/tools/llvm-cov/SourceCoverageView.cpp --- a/llvm/tools/llvm-cov/SourceCoverageView.cpp +++ b/llvm/tools/llvm-cov/SourceCoverageView.cpp @@ -78,7 +78,7 @@ case CoverageViewOptions::OutputFormat::Text: return std::make_unique(Opts); case CoverageViewOptions::OutputFormat::HTML: - return std::make_unique(Opts); + return std::make_unique(Opts); case CoverageViewOptions::OutputFormat::Lcov: // Unreachable because CodeCoverage.cpp should terminate with an error // before we get here. diff --git a/llvm/tools/llvm-cov/SourceCoverageViewHTML.h b/llvm/tools/llvm-cov/SourceCoverageViewHTML.h --- a/llvm/tools/llvm-cov/SourceCoverageViewHTML.h +++ b/llvm/tools/llvm-cov/SourceCoverageViewHTML.h @@ -19,6 +19,8 @@ using namespace coverage; +class ThreadPool; + struct FileCoverageSummary; /// A coverage printer for html output. @@ -44,6 +46,38 @@ const FileCoverageSummary &FCS) const; }; +/// A coverage printer for html output, but generates index files in every +/// subdirectory to show a hierarchical view. +class CoveragePrinterHTMLDirectory : public CoveragePrinterHTML { +public: + using CoveragePrinterHTML::CoveragePrinterHTML; + + Error createIndexFile(ArrayRef SourceFiles, + const coverage::CoverageMapping &Coverage, + const CoverageFiltersMatchAll &Filters) override; + +private: + // Member variables for avoiding passing arguments repeatedly in recursive + // calls. + const coverage::CoverageMapping *CoverageNow; + const CoverageFiltersMatchAll *FiltersNow; + + ThreadPool *PoolNow; // For calling CoverageReport::prepareSingleFileReport + // asynchronously in createSubIndexFile. + unsigned RootLCPNow; // The common prefix length of all source files. + + // Filter out files in LCP directory, group others by subdirectory and call + // recursively on them. The size of \p Files must be greater than 0. \p LCP + // is the common prefix length in chars/bytes. The FileCoverageSummary of + // this directory will be added to \p Totals. + Error createSubIndexFile(const ArrayRef &Files, const unsigned LCP, + FileCoverageSummary *Totals) const; + + std::string buildRelLinkToFile(StringRef RelPath, bool IsDir) const; + + std::string buildNavLink(StringRef LCPath, unsigned LCP) const; +}; + /// A code coverage view which supports html-based rendering. class SourceCoverageViewHTML : public SourceCoverageView { void renderViewHeader(raw_ostream &OS) override; diff --git a/llvm/tools/llvm-cov/SourceCoverageViewHTML.cpp b/llvm/tools/llvm-cov/SourceCoverageViewHTML.cpp --- a/llvm/tools/llvm-cov/SourceCoverageViewHTML.cpp +++ b/llvm/tools/llvm-cov/SourceCoverageViewHTML.cpp @@ -10,12 +10,15 @@ /// //===----------------------------------------------------------------------===// -#include "CoverageReport.h" #include "SourceCoverageViewHTML.h" +#include "CoverageReport.h" #include "llvm/ADT/SmallString.h" #include "llvm/ADT/StringExtras.h" #include "llvm/Support/Format.h" #include "llvm/Support/Path.h" +#include "llvm/Support/ThreadPool.h" +#include +#include #include using namespace llvm; @@ -65,7 +68,7 @@ } const char *BeginHeader = - "" + "" "" ""; @@ -272,11 +275,96 @@ OS << EndHeader << ""; } +void emitTableRow(raw_ostream &OS, const CoverageViewOptions &Opts, + const std::string &FirstCol, const FileCoverageSummary &FCS, + bool IsTotals) { + SmallVector Columns; + + // Format a coverage triple and add the result to the list of columns. + auto AddCoverageTripleToColumn = + [&Columns, &Opts](unsigned Hit, unsigned Total, float Pctg) { + std::string S; + { + raw_string_ostream RSO{S}; + if (Total) + RSO << format("%*.2f", 7, Pctg) << "% "; + else + RSO << "- "; + RSO << '(' << Hit << '/' << Total << ')'; + } + const char *CellClass = "column-entry-yellow"; + if (Pctg >= Opts.HighCovWatermark) + CellClass = "column-entry-green"; + else if (Pctg < Opts.LowCovWatermark) + CellClass = "column-entry-red"; + Columns.emplace_back(tag("td", tag("pre", S), CellClass)); + }; + + Columns.emplace_back(tag("td", tag("pre", FirstCol))); + AddCoverageTripleToColumn(FCS.FunctionCoverage.getExecuted(), + FCS.FunctionCoverage.getNumFunctions(), + FCS.FunctionCoverage.getPercentCovered()); + if (Opts.ShowInstantiationSummary) + AddCoverageTripleToColumn(FCS.InstantiationCoverage.getExecuted(), + FCS.InstantiationCoverage.getNumFunctions(), + FCS.InstantiationCoverage.getPercentCovered()); + AddCoverageTripleToColumn(FCS.LineCoverage.getCovered(), + FCS.LineCoverage.getNumLines(), + FCS.LineCoverage.getPercentCovered()); + if (Opts.ShowRegionSummary) + AddCoverageTripleToColumn(FCS.RegionCoverage.getCovered(), + FCS.RegionCoverage.getNumRegions(), + FCS.RegionCoverage.getPercentCovered()); + if (Opts.ShowBranchSummary) + AddCoverageTripleToColumn(FCS.BranchCoverage.getCovered(), + FCS.BranchCoverage.getNumBranches(), + FCS.BranchCoverage.getPercentCovered()); + + if (IsTotals) + OS << tag("tr", join(Columns.begin(), Columns.end(), ""), "light-row-bold"); + else + OS << tag("tr", join(Columns.begin(), Columns.end(), ""), "light-row"); +} + void emitEpilog(raw_ostream &OS) { OS << "" << ""; } +/// Determine the length of the longest redundant prefix of the substrs starts +/// from \p LCP in \p Paths. \p Paths can't be empty. If there's only one path +/// in \p Paths, the length of the path is returned. Note this is differnet from +/// the behavior for the function of the same name in CoverageReport.cpp . +unsigned getRedundantPrefixLen(ArrayRef Paths, unsigned LCP = 0) { + assert(!Paths.empty() && "Paths must have at least one element"); + + auto Iter = Paths.begin(); + auto IterE = Paths.end(); + auto First = Iter->substr(LCP); + SmallVector CommonComponents{sys::path::begin(First), + sys::path::end(First)}; + unsigned Len = First.size() - LCP; + while (++Iter != IterE) { + auto Substr = Iter->substr(LCP); + auto I = sys::path::begin(Substr); + auto E = sys::path::end(Substr); + + unsigned Index = 0; + while (I != E) { + if (Index >= CommonComponents.size()) + break; + if (CommonComponents[Index] != *I) { + CommonComponents.resize(Index); + Len = I - sys::path::begin(Substr); + break; + } + ++Index, ++I; + } + } + + return Len; +} + } // anonymous namespace Expected @@ -334,29 +422,7 @@ /// false, link the summary to \p SF. void CoveragePrinterHTML::emitFileSummary(raw_ostream &OS, StringRef SF, const FileCoverageSummary &FCS, - bool IsTotals) const { - SmallVector Columns; - - // Format a coverage triple and add the result to the list of columns. - auto AddCoverageTripleToColumn = - [&Columns, this](unsigned Hit, unsigned Total, float Pctg) { - std::string S; - { - raw_string_ostream RSO{S}; - if (Total) - RSO << format("%*.2f", 7, Pctg) << "% "; - else - RSO << "- "; - RSO << '(' << Hit << '/' << Total << ')'; - } - const char *CellClass = "column-entry-yellow"; - if (Pctg >= Opts.HighCovWatermark) - CellClass = "column-entry-green"; - else if (Pctg < Opts.LowCovWatermark) - CellClass = "column-entry-red"; - Columns.emplace_back(tag("td", tag("pre", S), CellClass)); - }; - + bool IsTotals) const { // Simplify the display file path, and wrap it in a link if requested. std::string Filename; if (IsTotals) { @@ -365,30 +431,7 @@ Filename = buildLinkToFile(SF, FCS); } - Columns.emplace_back(tag("td", tag("pre", Filename))); - AddCoverageTripleToColumn(FCS.FunctionCoverage.getExecuted(), - FCS.FunctionCoverage.getNumFunctions(), - FCS.FunctionCoverage.getPercentCovered()); - if (Opts.ShowInstantiationSummary) - AddCoverageTripleToColumn(FCS.InstantiationCoverage.getExecuted(), - FCS.InstantiationCoverage.getNumFunctions(), - FCS.InstantiationCoverage.getPercentCovered()); - AddCoverageTripleToColumn(FCS.LineCoverage.getCovered(), - FCS.LineCoverage.getNumLines(), - FCS.LineCoverage.getPercentCovered()); - if (Opts.ShowRegionSummary) - AddCoverageTripleToColumn(FCS.RegionCoverage.getCovered(), - FCS.RegionCoverage.getNumRegions(), - FCS.RegionCoverage.getPercentCovered()); - if (Opts.ShowBranchSummary) - AddCoverageTripleToColumn(FCS.BranchCoverage.getCovered(), - FCS.BranchCoverage.getNumBranches(), - FCS.BranchCoverage.getPercentCovered()); - - if (IsTotals) - OS << tag("tr", join(Columns.begin(), Columns.end(), ""), "light-row-bold"); - else - OS << tag("tr", join(Columns.begin(), Columns.end(), ""), "light-row"); + emitTableRow(OS, Opts, Filename, FCS, IsTotals); } Error CoveragePrinterHTML::createIndexFile( @@ -465,6 +508,242 @@ return Error::success(); } +Error CoveragePrinterHTMLDirectory::createIndexFile( + ArrayRef SourceFiles, const CoverageMapping &Coverage, + const CoverageFiltersMatchAll &Filters) { + // The createSubIndexFile function only works when SourceFiles is + // not empty. So we fallback to CoveragePrinterHTML when it is. + if (SourceFiles.empty()) + return CoveragePrinterHTML::createIndexFile(SourceFiles, Coverage, Filters); + + // Emit the default stylesheet. + auto CSSOrErr = createOutputStream("style", "css", /*InToplevel=*/true); + if (Error E = CSSOrErr.takeError()) + return E; + + OwnedStream CSS = std::move(CSSOrErr.get()); + CSS->operator<<(CSSForCoverage); + + // Emit index files in every subdirectory. + std::vector Files(SourceFiles.begin(), SourceFiles.end()); + ThreadPoolStrategy PoolS = hardware_concurrency(Opts.NumThreads); + if (Opts.NumThreads == 0) { + PoolS = heavyweight_hardware_concurrency(Files.size()); + PoolS.Limit = true; + } + ThreadPool Pool(PoolS); // The above initialization code was copied from + // CoverageReport::prepareFileReports. + + if (SourceFiles.size() == 1) + RootLCPNow = sys::path::parent_path(SourceFiles.front()).size() + 1; + // We handle this case specially. If there's only one file in total, + // its parent directory is considered as the LCP. + else + RootLCPNow = getRedundantPrefixLen(Files); + auto LCPath = SourceFiles.front().substr(0, RootLCPNow); + + CoverageNow = &Coverage; + FiltersNow = &Filters; + PoolNow = &Pool; + FileCoverageSummary RootTotal({}); // The root total is useless and ignored. + if (auto E = createSubIndexFile(Files, RootLCPNow, &RootTotal)) + return E; + + // Emit the top level index file. Top level index file is just a redirection + // to the index file in the LCP directory. + auto OSOrErr = createOutputStream("index", "html", /*InToplevel=*/true); + if (auto E = OSOrErr.takeError()) + return E; + auto OS = std::move(OSOrErr.get()); + auto LCPIndexFilePath = + getOutputPath(LCPath + "index", "html", /*InToplevel=*/false); + *OS.get() << R"( + + + + + + + )"; + + return Error::success(); +} + +Error CoveragePrinterHTMLDirectory::createSubIndexFile( + const ArrayRef &Files, const unsigned LCP, + FileCoverageSummary *Totals) const { + assert(!Files.empty() && "Only works when Files is not empty"); + + auto LCPath = Files.front().substr(0, LCP).str(); + auto OSOrErr = + createOutputStream(LCPath + "index", "html", /*InToplevel=*/false); + if (auto E = OSOrErr.takeError()) + return E; + auto OS = std::move(OSOrErr.get()); + raw_ostream &OSRef = *OS.get(); + + assert(Opts.hasOutputDirectory() && "No output directory for index file"); + auto IndexHtmlPath = getOutputPath(LCPath + "index", "html", + /*InToplevel=*/false, /*Relative=*/false); + emitPrelude(OSRef, Opts, getPathToStyle(IndexHtmlPath)); + + // Emit some basic information about the coverage report. + if (Opts.hasProjectTitle()) + OSRef << tag(ProjectTitleTag, escape(Opts.ProjectTitle, Opts)); + auto NavLink = buildNavLink(LCPath, LCP); + OSRef << tag(ReportTitleTag, "Coverage Report (" + NavLink + ")"); + if (Opts.hasCreatedTime()) + OSRef << tag(CreatedTimeTag, escape(Opts.CreatedTimeStr, Opts)); + + // Emit a link to some documentation. + OSRef << tag("p", "Click " + + a("http://clang.llvm.org/docs/" + "SourceBasedCodeCoverage.html#interpreting-reports", + "here") + + " for information about interpreting this report."); + + // Emit a table containing links to reports for each file in the covmapping. + // Exclude files which don't contain any regions. + OSRef << BeginCenteredDiv << BeginTable; + emitColumnLabelsForIndex(OSRef, Opts); + + // Filter out files in current directory and group others by subdiretory. + // std::map is used to keep entries in order. + std::map FilesHere; + std::map> DirsHere; + for (auto &&File : Files) { + auto SubPath = File.substr(LCPath.size()); + + auto I = sys::path::begin(SubPath); + auto E = sys::path::end(SubPath); + assert(I != E && "Such case should have been filtered out in the caller"); + + auto Name = *I; + if (++I == E) { + auto Iter = FilesHere.emplace(Name, File).first; + PoolNow->async(&CoverageReport::prepareSingleFileReport, File, + CoverageNow, Opts, LCP, &Iter->second, FiltersNow); + } + + else + DirsHere[Name].push_back(File); + } + + // Emit subdirectories first to make them at the top of the table. + FileCoverageSummary TotalsHere(LCPath); + for (auto &&Dir : DirsHere) { + if (Dir.second.size() == 1) { + // If there's only one file in that subdirectory, we don't bother to + // create index file for it. Instead, we show the file report directly at + // this level. + auto RelPath = Dir.second.front().substr(LCP); + FileCoverageSummary FCS(RelPath); + CoverageReport::prepareSingleFileReport(Dir.second[0], CoverageNow, Opts, + LCP, &FCS, FiltersNow); + emitTableRow(OSRef, Opts, buildRelLinkToFile(RelPath, /*IsDir=*/false), + FCS, /*IsTotals=*/false); + TotalsHere += FCS; + } + + else { + auto SubDirLCP = getRedundantPrefixLen(Dir.second, LCP); + auto RelPath = Dir.second.front().substr(LCP, SubDirLCP); + FileCoverageSummary FCS(RelPath); + if (auto E = + createSubIndexFile(Dir.second, LCP + SubDirLCP, &FCS)) + return E; + emitTableRow(OSRef, Opts, buildRelLinkToFile(RelPath, /*IsDir=*/true), + FCS, /*IsTotals=*/false); + TotalsHere += FCS; + } + } + + // Then emit the files in current directory. + PoolNow->wait(); // Makes files reporting overlap with subdir reporting. + std::vector EmptyFiles; + for (auto I = FilesHere.begin(), E = FilesHere.end(); I != E; ++I) { + if (I->second.FunctionCoverage.getNumFunctions()) + emitTableRow(OSRef, Opts, buildRelLinkToFile(I->first, /*IsDir=*/false), + I->second, /*IsTotals=*/false); + else + EmptyFiles.push_back(I); + TotalsHere += I->second; + } + + // Emit the totals row. + emitTableRow(OSRef, Opts, "Totals", TotalsHere, /*IsTotals=*/false); + OSRef << EndTable << EndCenteredDiv; + + // Emit links to files which don't contain any functions. These are normally + // not very useful, but could be relevant for code which abuses the + // preprocessor. + if (!EmptyFiles.empty() && FiltersNow->empty()) { + OSRef << tag("p", "Files which contain no functions. (These " + "files contain code pulled into other files " + "by the preprocessor.)\n"); + OSRef << BeginCenteredDiv << BeginTable; + for (auto I : EmptyFiles) { + auto Link = buildRelLinkToFile(I->first, /*IsDir=*/false); + OSRef << tag("tr", tag("td", tag("pre", Link)), "light-row") << '\n'; + } + OSRef << EndTable << EndCenteredDiv; + } + + // Emit epilog. + OSRef << tag("h5", escape(Opts.getLLVMVersionString(), Opts)); + emitEpilog(OSRef); + + *Totals += TotalsHere; + return Error::success(); +} + +std::string CoveragePrinterHTMLDirectory::buildRelLinkToFile(StringRef RelPath, + bool IsDir) const { + SmallString<128> LinkTextStr(RelPath); + sys::path::remove_dots(LinkTextStr, /*remove_dot_dot=*/true); + sys::path::native(LinkTextStr); + + std::string LinkTargetStr; + if (IsDir) { + LinkTextStr += sys::path::get_separator(); + LinkTargetStr = (RelPath + "index.html").str(); + } else + LinkTargetStr = (RelPath + ".html").str(); + + auto LinkText = escape(LinkTextStr, Opts); + auto LinkTarget = escape(LinkTargetStr, Opts); + return a(LinkTarget, LinkText); +} + +std::string CoveragePrinterHTMLDirectory::buildNavLink(StringRef LCPath, + unsigned LCP) const { + assert(LCP >= RootLCPNow); + + StringRef RootPath, SubPath; + if (RootLCPNow > 0) + RootPath = LCPath.substr(0, RootLCPNow - 1); // Remove trailing slash. + if (LCP > RootLCPNow) + SubPath = LCPath.substr(RootLCPNow, LCP - RootLCPNow - 1); + std::vector Components{RootPath}; + Components.insert(Components.end(), sys::path::begin(SubPath), + sys::path::end(SubPath)); + + std::string S; + for (int I = 0, E = Components.size(); I < E; ++I) { + std::string Link("."); + Link += sys::path::get_separator(); + for (int J = Components.size() - I; --J > 0;) { + Link += std::string(".."); + Link += sys::path::get_separator(); + } + S += a(Link, Components[I].str()); + S += sys::path::get_separator(); + } + + return S; +} + void SourceCoverageViewHTML::renderViewHeader(raw_ostream &OS) { OS << BeginCenteredDiv << BeginTable; } @@ -593,8 +872,9 @@ continue; Snippets[I + 1] = - tag("div", Snippets[I + 1] + tag("span", formatCount(CurSeg->Count), - "tooltip-content"), + tag("div", + Snippets[I + 1] + + tag("span", formatCount(CurSeg->Count), "tooltip-content"), "tooltip"); if (getOptions().Debug)