diff --git a/llvm/tools/llvm-cov/CodeCoverage.cpp b/llvm/tools/llvm-cov/CodeCoverage.cpp --- a/llvm/tools/llvm-cov/CodeCoverage.cpp +++ b/llvm/tools/llvm-cov/CodeCoverage.cpp @@ -971,6 +971,10 @@ cl::desc("Show function instantiations"), cl::init(true), cl::cat(ViewCategory)); + cl::opt ShowDirectoryCoverage("show-directory-coverage", cl::Optional, + cl::desc("Show directory coverage"), + cl::cat(ViewCategory)); + cl::opt ShowOutputDirectory( "output-dir", cl::init(""), cl::desc("Directory in which coverage information is written out")); @@ -1051,6 +1055,7 @@ ViewOpts.ShowBranchPercents = ShowBranches == CoverageViewOptions::BranchOutputType::Percent; ViewOpts.ShowFunctionInstantiations = ShowInstantiations; + ViewOpts.ShowDirectoryCoverage = ShowDirectoryCoverage; ViewOpts.ShowOutputDirectory = ShowOutputDirectory; ViewOpts.TabSize = TabSize; ViewOpts.ProjectTitle = ProjectTitle; diff --git a/llvm/tools/llvm-cov/CoverageViewOptions.h b/llvm/tools/llvm-cov/CoverageViewOptions.h --- a/llvm/tools/llvm-cov/CoverageViewOptions.h +++ b/llvm/tools/llvm-cov/CoverageViewOptions.h @@ -38,6 +38,7 @@ bool ShowBranchSummary; bool ShowRegionSummary; bool ShowInstantiationSummary; + bool ShowDirectoryCoverage; bool ExportSummaryOnly; bool SkipExpansions; bool SkipFunctions; 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,6 +78,8 @@ case CoverageViewOptions::OutputFormat::Text: return std::make_unique(Opts); case CoverageViewOptions::OutputFormat::HTML: + if (Opts.ShowDirectoryCoverage) + return std::make_unique(Opts); return std::make_unique(Opts); case CoverageViewOptions::OutputFormat::Lcov: // Unreachable because CodeCoverage.cpp should terminate with an error 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,39 @@ 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. + std::vector LCPStack; // For calling createSubIndexFile + // recursively. [0] is root LCP length. + + // 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. The + // FileCoverageSummary of this directory will be added to \p Totals. + // Current LCP is LCPStack.back(). + Error createSubIndexFile(const ArrayRef &Files, + FileCoverageSummary *Totals); + + std::string buildTitleLinks(StringRef LCPath) const; + + std::string buildRelLinkToFile(StringRef RelPath, bool IsDir) 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 @@ -335,28 +423,6 @@ 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)); - }; - // 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,253 @@ 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. + + unsigned RootLCP; + if (SourceFiles.size() == 1) + RootLCP = 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 + RootLCP = getRedundantPrefixLen(Files); + auto LCPath = SourceFiles.front().substr(0, RootLCP); + + CoverageNow = &Coverage; + FiltersNow = &Filters; + PoolNow = &Pool; + LCPStack = {RootLCP}; + FileCoverageSummary RootTotal({}); // The root total is useless and ignored. + if (auto E = createSubIndexFile(Files, &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, FileCoverageSummary *Totals) { + assert(!Files.empty() && "Only works when Files is not empty"); + auto LCP = LCPStack.back(); + auto LCPath = Files.front().substr(0, LCP).str(); + assert(LCPath.back() == '/' && "LCPath should end with a slash"); + 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); + 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 = buildTitleLinks(LCPath); + 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 CurrentFiles; + std::map> CurrentDirs; + 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 = CurrentFiles.emplace(Name, File).first; + PoolNow->async(&CoverageReport::prepareSingleFileReport, File, + CoverageNow, Opts, LCP, &Iter->second, FiltersNow); + } + + else + CurrentDirs[Name].push_back(File); + } + + // Emit subdirectories first to make them at the top of the table. + FileCoverageSummary CurrentTotals(LCPath); + for (auto &&Dir : CurrentDirs) { + 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); + CurrentTotals += FCS; + } else { + auto SubDirLCP = getRedundantPrefixLen(Dir.second, LCP); + auto RelPath = Dir.second.front().substr(LCP, SubDirLCP); + FileCoverageSummary FCS(RelPath); + LCPStack.push_back(LCP + SubDirLCP); + if (auto E = createSubIndexFile(Dir.second, &FCS)) + return E; + emitTableRow(OSRef, Opts, buildRelLinkToFile(RelPath, /*IsDir=*/true), + FCS, /*IsTotals=*/false); + CurrentTotals += FCS; + } + } + + // Then emit the files in current directory. + PoolNow->wait(); // Makes files reporting overlap with subdir reporting. + std::vector EmptyFiles; + for (auto I = CurrentFiles.begin(), E = CurrentFiles.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); + CurrentTotals += I->second; + } + + // Emit the totals row. + emitTableRow(OSRef, Opts, "Totals", CurrentTotals, /*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 += CurrentTotals; + LCPStack.pop_back(); + return Error::success(); +} + +std::string +CoveragePrinterHTMLDirectory::buildTitleLinks(StringRef LCPath) const { + SmallVector, 16> Components; + auto Iter = LCPStack.begin(), IterE = LCPStack.end(); + if (*Iter > 0) + Components.push_back({ + LCPath.substr(0, *Iter - 1).str(), // Remove trailing slash. + 0, + }); + for (auto Last = *Iter; ++Iter != IterE; Last = *Iter) { + auto SubPath = LCPath.substr(Last, *Iter - Last - 1); + auto Level = unsigned(SubPath.count(sys::path::get_separator())) + 1; + Components.back().second += Level; + Components.push_back({SubPath.str(), Level}); + } + + std::string S; + for (auto I = Components.begin(), E = Components.end();;) { + auto& Name = I->first; + if (++I == E) { + S += a("./index.html", Name); + S += sys::path::get_separator(); + break; + } + + std::string Link; + for (unsigned J = I->second; J > 0; --J) { + Link += ".."; + Link += sys::path::get_separator(); + } + Link += "index.html"; + S += a(Link, Name); + S += sys::path::get_separator(); + } + return S; +} + +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); +} + void SourceCoverageViewHTML::renderViewHeader(raw_ostream &OS) { OS << BeginCenteredDiv << BeginTable; } @@ -593,8 +883,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) diff --git a/llvm/utils/prepare-code-coverage-artifact.py b/llvm/utils/prepare-code-coverage-artifact.py --- a/llvm/utils/prepare-code-coverage-artifact.py +++ b/llvm/utils/prepare-code-coverage-artifact.py @@ -67,6 +67,7 @@ "-o", report_dir, "-show-line-counts-or-regions", + "-show-directory-coverage", "-Xdemangler", "c++filt", "-Xdemangler",