diff --git a/llvm/docs/CommandGuide/llvm-cov.rst b/llvm/docs/CommandGuide/llvm-cov.rst --- a/llvm/docs/CommandGuide/llvm-cov.rst +++ b/llvm/docs/CommandGuide/llvm-cov.rst @@ -249,6 +249,12 @@ line, but show the individual regions if there are multiple on the line. Defaults to false. +.. option:: -show-directory-coverage + + Generate index files in each level of the directory to show the coverage. + Defaults to false and a single index file will be generated in the output + directory. + .. option:: -use-color Enable or disable color output. By default this is autodetected. diff --git a/llvm/test/tools/llvm-cov/Inputs/directory_coverage/a0/a1/a2.cc b/llvm/test/tools/llvm-cov/Inputs/directory_coverage/a0/a1/a2.cc new file mode 100644 --- /dev/null +++ b/llvm/test/tools/llvm-cov/Inputs/directory_coverage/a0/a1/a2.cc @@ -0,0 +1,7 @@ +#include "../../header.h" + +template<> +bool equal(int a, int b) +{ + return a == b; +} diff --git a/llvm/test/tools/llvm-cov/Inputs/directory_coverage/b/b1.h b/llvm/test/tools/llvm-cov/Inputs/directory_coverage/b/b1.h new file mode 100644 --- /dev/null +++ b/llvm/test/tools/llvm-cov/Inputs/directory_coverage/b/b1.h @@ -0,0 +1,3 @@ +int add(int a, int b); + +int sub(int a, int b); diff --git a/llvm/test/tools/llvm-cov/Inputs/directory_coverage/b/b1_1.cc b/llvm/test/tools/llvm-cov/Inputs/directory_coverage/b/b1_1.cc new file mode 100644 --- /dev/null +++ b/llvm/test/tools/llvm-cov/Inputs/directory_coverage/b/b1_1.cc @@ -0,0 +1,5 @@ +#include "b1.h" + +int add(int a, int b) { + return a + b; +} diff --git a/llvm/test/tools/llvm-cov/Inputs/directory_coverage/b/b1_2.cc b/llvm/test/tools/llvm-cov/Inputs/directory_coverage/b/b1_2.cc new file mode 100644 --- /dev/null +++ b/llvm/test/tools/llvm-cov/Inputs/directory_coverage/b/b1_2.cc @@ -0,0 +1,5 @@ +#include "b1.h" + +int sub(int a, int b) { + return a - b; +} diff --git a/llvm/test/tools/llvm-cov/Inputs/directory_coverage/c0/c1/c2.h b/llvm/test/tools/llvm-cov/Inputs/directory_coverage/c0/c1/c2.h new file mode 100644 --- /dev/null +++ b/llvm/test/tools/llvm-cov/Inputs/directory_coverage/c0/c1/c2.h @@ -0,0 +1,9 @@ +int mul(int a, int b); + +int div(int a, int b); + +#ifdef DEF +int div(int a, int b) { + return a / b; +} +#endif diff --git a/llvm/test/tools/llvm-cov/Inputs/directory_coverage/c0/c1/c2_1.cc b/llvm/test/tools/llvm-cov/Inputs/directory_coverage/c0/c1/c2_1.cc new file mode 100644 --- /dev/null +++ b/llvm/test/tools/llvm-cov/Inputs/directory_coverage/c0/c1/c2_1.cc @@ -0,0 +1,3 @@ +int mul(int a, int b) { + return a * b; +} diff --git a/llvm/test/tools/llvm-cov/Inputs/directory_coverage/c0/c1/c2_2.cc b/llvm/test/tools/llvm-cov/Inputs/directory_coverage/c0/c1/c2_2.cc new file mode 100644 --- /dev/null +++ b/llvm/test/tools/llvm-cov/Inputs/directory_coverage/c0/c1/c2_2.cc @@ -0,0 +1,2 @@ +#define DEF +#include "c2.h" diff --git a/llvm/test/tools/llvm-cov/Inputs/directory_coverage/header.h b/llvm/test/tools/llvm-cov/Inputs/directory_coverage/header.h new file mode 100644 --- /dev/null +++ b/llvm/test/tools/llvm-cov/Inputs/directory_coverage/header.h @@ -0,0 +1,5 @@ +#include "b/b1.h" +#include "c0/c1/c2.h" + +template +bool equal(T a, T b); diff --git a/llvm/test/tools/llvm-cov/Inputs/directory_coverage/main.cc b/llvm/test/tools/llvm-cov/Inputs/directory_coverage/main.cc new file mode 100644 --- /dev/null +++ b/llvm/test/tools/llvm-cov/Inputs/directory_coverage/main.cc @@ -0,0 +1,9 @@ +#include "header.h" + +int main() { + int a = 3; + int b = 4; + int c = c = add(a, b); + int d = mul(a, b); + return equal(a, sub(c, b)) - equal(a, div(d, b)); +} diff --git a/llvm/test/tools/llvm-cov/directory_coverage.test b/llvm/test/tools/llvm-cov/directory_coverage.test new file mode 100644 --- /dev/null +++ b/llvm/test/tools/llvm-cov/directory_coverage.test @@ -0,0 +1,37 @@ +// RUN: mkdir -p %t && clang -fprofile-instr-generate=%t/default.profraw -fcoverage-mapping -o %t/main.out \ +// RUN: %S/Inputs/directory_coverage/main.cc \ +// RUN: %S/Inputs/directory_coverage/a0/a1/a2.cc \ +// RUN: %S/Inputs/directory_coverage/b/b1_1.cc \ +// RUN: %S/Inputs/directory_coverage/b/b1_2.cc \ +// RUN: %S/Inputs/directory_coverage/c0/c1/c2_1.cc \ +// RUN: %S/Inputs/directory_coverage/c0/c1/c2_2.cc +// RUN: %t/main.out +// RUN: llvm-profdata merge %t/default.profraw -o %t/profdata +// RUN: llvm-cov show %t/main.out --instr-profile %t/profdata --format=html --show-directory-coverage -o %t/report +// RUN: cat %t/report/index.html | FileCheck %s --check-prefix=ROOT --allow-empty +// RUN: cat %t/report/coverage/%:S/Inputs/directory_coverage/index.html | FileCheck %s --check-prefix=TOP +// RUN: cat %t/report/coverage/%:S/Inputs/directory_coverage/b/index.html | FileCheck %s --check-prefix=B +// RUN: cat %t/report/coverage/%:S/Inputs/directory_coverage/c0/c1/index.html | FileCheck %s --check-prefix=C1 + +// ROOT: coverage{{.*}}Inputs{{/|\\}}directory_coverage{{/|\\}}index.html + + +// TOP: a0{{/|\\}}a1{{/|\\}}a2.cc +// TOP: b{{/|\\}} +// TOP-NOT: b1_1.cc +// TOP-NOT: b1_2.cc +// TOP: c0{{/|\\}}c1{{/|\\}} +// TOP-NOT: c2_1.cc +// TOP-NOT: b2_2.cc +// TOP: main.cc + + +// B: href={{"|'}}..{{/|\\}}index.html{{"|'}} +// B: b1_1.cc +// B: b1_2.cc + + +// C1: href={{"|'}}..{{/|\\}}..{{/|\\}}index.html{{"|'}} +// C1: c0{{/|\\}}c1 +// C1: c2.h +// C1: c2_1.cc 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/CoverageReport.h b/llvm/tools/llvm-cov/CoverageReport.h --- a/llvm/tools/llvm-cov/CoverageReport.h +++ b/llvm/tools/llvm-cov/CoverageReport.h @@ -19,6 +19,8 @@ namespace llvm { +class ThreadPool; + /// Displays the code coverage report. class CoverageReport { const CoverageViewOptions &Options; @@ -62,6 +64,71 @@ /// in \p Filters. void renderFileReports(raw_ostream &OS, ArrayRef Files, const CoverageFiltersMatchAll &Filters) const; + + /// Render file reports with given data. + void renderFileReports(raw_ostream &OS, + const std::vector &FileReports, + const FileCoverageSummary &Totals) const; +}; + +/// Prepare reports for every non-trivial directories (which have more than 1 +/// source files) of the source files. This class uses template method pattern. +class DirectoryCoverageReport { +public: + DirectoryCoverageReport( + const CoverageViewOptions &Options, + const coverage::CoverageMapping &Coverage, + const CoverageFiltersMatchAll &Filters = CoverageFiltersMatchAll()) + : Options(Options), Coverage(Coverage), Filters(Filters) {} + + virtual ~DirectoryCoverageReport() = default; + + /// Prepare file reports for each directory in \p SourceFiles. The total + /// report for all files is returned and its Name is set to the LCP of all + /// files. The size of \p SourceFiles must be greater than 1 or else the + /// behavior is undefined, in which case you should use + /// `CoverageReport::prepareSingleFileReport` instead. If an error occurs, + /// the recursion will stop immediately. + Expected + prepareDirectoryReports(ArrayRef SourceFiles); + +protected: + // These member variables below are used for avoiding being passed + // repeatedly in recursion. + const CoverageViewOptions &Options; + const coverage::CoverageMapping &Coverage; + const CoverageFiltersMatchAll &Filters; + + // For calling CoverageReport::prepareSingleFileReport asynchronously + // in prepareSubDirectoryReports. It's not disigned to be changed by + // operator(). + ThreadPool *TPool; + + // One report level may correspond to multiple directory levels as we omit + // directories which have only one subentry. So we use this Stack to track + // each report level's corresponding drectory level. + // Each value in the stack is the LCP prefix length length of that report + // level. LCPStack.front() is the root LCP. Current LCP is LCPStack.back(). + SmallVector LCPStack; + + using SubFileReports = StringMap; + using SubDirReports = + StringMap>>; + + /// This method is called when a report level is prepared during the + /// recursion. \p SubFiles are the reports for those files directly in the + /// current directory. \p SubDirs are the reports for subdirectories in + /// current directory. \p SubTotals is the sum of all, and its name is the + /// current LCP. Note that this method won't be called for trivial + /// directories. FileCoverageSummary::Name will be set to the relative path + /// of current LCP. For trivial directories, the name will be set to the + /// relative path of the only file in that directory. + virtual Error operator()(SubFileReports &&SubFiles, SubDirReports &&SubDirs, + FileCoverageSummary &&SubTotals) = 0; + +private: + Error prepareSubDirectoryReports(const ArrayRef &Files, + FileCoverageSummary *Totals); }; } // end namespace llvm diff --git a/llvm/tools/llvm-cov/CoverageReport.cpp b/llvm/tools/llvm-cov/CoverageReport.cpp --- a/llvm/tools/llvm-cov/CoverageReport.cpp +++ b/llvm/tools/llvm-cov/CoverageReport.cpp @@ -165,6 +165,40 @@ return PrefixLen; } +/// 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 +/// element in \p Paths, the length of the substr is returned. Note this is +/// differnet from the behavior of the function above. +unsigned getRedundantPrefixLen(ArrayRef Paths, unsigned LCP) { + 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; +} + } // end anonymous namespace namespace llvm { @@ -435,7 +469,12 @@ FileCoverageSummary Totals("TOTAL"); auto FileReports = prepareFileReports(Coverage, Totals, Files, Options, Filters); + renderFileReports(OS, FileReports, Totals); +} +void CoverageReport::renderFileReports( + raw_ostream &OS, const std::vector &FileReports, + const FileCoverageSummary &Totals) const { std::vector Filenames; Filenames.reserve(FileReports.size()); for (const FileCoverageSummary &FCS : FileReports) @@ -466,21 +505,20 @@ renderDivider(FileReportColumns, OS); OS << "\n"; - bool EmptyFiles = false; + std::vector EmptyFiles; for (const FileCoverageSummary &FCS : FileReports) { if (FCS.FunctionCoverage.getNumFunctions()) render(FCS, OS); else - EmptyFiles = true; + EmptyFiles.push_back(&FCS); } - if (EmptyFiles && Filters.empty()) { + if (!EmptyFiles.empty()) { OS << "\n" << "Files which contain no functions:\n"; - for (const FileCoverageSummary &FCS : FileReports) - if (!FCS.FunctionCoverage.getNumFunctions()) - render(FCS, OS); + for (auto FCS : EmptyFiles) + render(*FCS, OS); } renderDivider(FileReportColumns, OS); @@ -488,4 +526,94 @@ render(Totals, OS); } +Expected DirectoryCoverageReport::prepareDirectoryReports( + ArrayRef SourceFiles) { + std::vector Files(SourceFiles.begin(), SourceFiles.end()); + + unsigned RootLCP = getRedundantPrefixLen(Files, 0); + auto LCPath = Files.front().substr(0, RootLCP); + + ThreadPoolStrategy PoolS = hardware_concurrency(Options.NumThreads); + if (Options.NumThreads == 0) { + PoolS = heavyweight_hardware_concurrency(Files.size()); + PoolS.Limit = true; + } + ThreadPool Pool(PoolS); + + TPool = &Pool; + LCPStack = {RootLCP}; + FileCoverageSummary RootTotals(LCPath); + if (auto E = prepareSubDirectoryReports(Files, &RootTotals)) + return {std::move(E)}; + return {std::move(RootTotals)}; +} + +/// Filter out files in `LCPStack.back()`, group others by subdirectory name +/// and recurse on them. After returning from all subdirectories, call +/// `operator()`. \p Files must be non-empty. The FileCoverageSummary of this +/// directory will be added to \p Totals. +Error DirectoryCoverageReport::prepareSubDirectoryReports( + 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(); + + // Use ordered map to keep entries in order. + SubFileReports SubFiles; + SubDirReports SubDirs; + 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 = SubFiles.insert_or_assign(Name, SubPath).first; + // Makes files reporting overlap with subdir reporting. + TPool->async(&CoverageReport::prepareSingleFileReport, File, + &Coverage, Options, LCP, &Iter->second, &Filters); + } else { + SubDirs[Name].second.push_back(File); + } + } + + // Call recursively on subdirectories. + for (auto&& KV : SubDirs) { + auto& V = KV.second; + if (V.second.size() == 1) { + // If there's only one file in that subdirectory, we don't bother to + // recurse on it further. + V.first.Name = V.second.front().substr(LCP); + TPool->async(&CoverageReport::prepareSingleFileReport, + V.second.front(), &Coverage, Options, LCP, &V.first, + &Filters); + } else { + auto SubDirLCP = getRedundantPrefixLen(V.second, LCP); + V.first.Name = V.second.front().substr(LCP, SubDirLCP); + LCPStack.push_back(LCP + SubDirLCP); + if (auto E = prepareSubDirectoryReports(V.second, &V.first)) + return E; + } + } + + TPool->wait(); + + FileCoverageSummary CurrentTotals(LCPath); + for (auto&& KV: SubFiles) + CurrentTotals += KV.second; + for (auto&& KV: SubDirs) + CurrentTotals += KV.second.first; + *Totals += CurrentTotals; + + if (auto E = operator()(std::move(SubFiles), std::move(SubDirs), + std::move(CurrentTotals))) + return E; + + LCPStack.pop_back(); + return Error::success(); +} + } // end namespace llvm diff --git a/llvm/tools/llvm-cov/CoverageSummaryInfo.h b/llvm/tools/llvm-cov/CoverageSummaryInfo.h --- a/llvm/tools/llvm-cov/CoverageSummaryInfo.h +++ b/llvm/tools/llvm-cov/CoverageSummaryInfo.h @@ -222,6 +222,7 @@ FunctionCoverageInfo FunctionCoverage; FunctionCoverageInfo InstantiationCoverage; + FileCoverageSummary() = default; FileCoverageSummary(StringRef Name) : Name(Name) {} FileCoverageSummary &operator+=(const FileCoverageSummary &RHS) { 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 @@ -76,8 +76,12 @@ CoveragePrinter::create(const CoverageViewOptions &Opts) { switch (Opts.Format) { case CoverageViewOptions::OutputFormat::Text: + if (Opts.ShowDirectoryCoverage) + return std::make_unique(Opts); 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. @@ -36,6 +38,10 @@ CoveragePrinterHTML(const CoverageViewOptions &Opts) : CoveragePrinter(Opts) {} +protected: + Error emitStyleSheet(); + void emitReportHeader(raw_ostream &OSRef, const std::string &Title); + private: void emitFileSummary(raw_ostream &OS, StringRef SF, const FileCoverageSummary &FCS, @@ -44,6 +50,20 @@ 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: + struct Reporter; +}; + /// 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,13 @@ /// //===----------------------------------------------------------------------===// -#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 using namespace llvm; @@ -49,23 +50,41 @@ } // Create a \p Name tag around \p Str, and optionally set its \p ClassName. -std::string tag(const std::string &Name, const std::string &Str, - const std::string &ClassName = "") { - std::string Tag = "<" + Name; - if (!ClassName.empty()) - Tag += " class='" + ClassName + "'"; - return Tag + ">" + Str + ""; +std::string tag(StringRef Name, StringRef Str, StringRef ClassName = "") { + std::string Tag = "<"; + Tag += Name; + if (!ClassName.empty()) { + Tag += " class='"; + Tag += ClassName; + Tag += "'"; + } + Tag += ">"; + Tag += Str; + Tag += ""; + return Tag; } // Create an anchor to \p Link with the label \p Str. -std::string a(const std::string &Link, const std::string &Str, - const std::string &TargetName = "") { - std::string Name = TargetName.empty() ? "" : ("name='" + TargetName + "' "); - return "" + Str + ""; +std::string a(StringRef Link, StringRef Str, StringRef TargetName = "") { + std::string Tag; + Tag += "" + "" "" ""; @@ -272,6 +291,57 @@ 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 << "" << ""; @@ -330,33 +400,44 @@ return a(LinkTarget, LinkText); } +Error CoveragePrinterHTML::emitStyleSheet() { + auto CSSOrErr = createOutputStream("style", "css", /*InToplevel=*/true); + if (Error E = CSSOrErr.takeError()) + return E; + + OwnedStream CSS = std::move(CSSOrErr.get()); + CSS->operator<<(CSSForCoverage); + + return Error::success(); +} + +void CoveragePrinterHTML::emitReportHeader(raw_ostream &OSRef, + const std::string &Title) { + // Emit some basic information about the coverage report. + if (Opts.hasProjectTitle()) + OSRef << tag(ProjectTitleTag, escape(Opts.ProjectTitle, Opts)); + OSRef << tag(ReportTitleTag, Title); + 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); +} + /// Render a file coverage summary (\p FCS) in a table row. If \p IsTotals is /// 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)); - }; - // Simplify the display file path, and wrap it in a link if requested. std::string Filename; if (IsTotals) { @@ -365,43 +446,16 @@ 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( ArrayRef SourceFiles, const CoverageMapping &Coverage, const CoverageFiltersMatchAll &Filters) { // Emit the default stylesheet. - auto CSSOrErr = createOutputStream("style", "css", /*InToplevel=*/true); - if (Error E = CSSOrErr.takeError()) + if (Error E = emitStyleSheet()) return E; - OwnedStream CSS = std::move(CSSOrErr.get()); - CSS->operator<<(CSSForCoverage); - // Emit a file index along with some coverage statistics. auto OSOrErr = createOutputStream("index", "html", /*InToplevel=*/true); if (Error E = OSOrErr.takeError()) @@ -412,24 +466,8 @@ assert(Opts.hasOutputDirectory() && "No output directory for index file"); emitPrelude(OSRef, Opts, getPathToStyle("")); - // Emit some basic information about the coverage report. - if (Opts.hasProjectTitle()) - OSRef << tag(ProjectTitleTag, escape(Opts.ProjectTitle, Opts)); - OSRef << tag(ReportTitleTag, "Coverage Report"); - if (Opts.hasCreatedTime()) - OSRef << tag(CreatedTimeTag, escape(Opts.CreatedTimeStr, Opts)); + emitReportHeader(OSRef, "Coverage Report"); - // 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); FileCoverageSummary Totals("TOTALS"); auto FileReports = CoverageReport::prepareFileReports( Coverage, Totals, SourceFiles, Opts, Filters); @@ -465,6 +503,198 @@ return Error::success(); } +struct CoveragePrinterHTMLDirectory::Reporter : public DirectoryCoverageReport { + CoveragePrinterHTMLDirectory &Printer; + + Reporter(CoveragePrinterHTMLDirectory &Printer, + const coverage::CoverageMapping &Coverage, + const CoverageFiltersMatchAll &Filters) + : DirectoryCoverageReport(Printer.Opts, Coverage, Filters), + Printer(Printer) {} + + Error operator()(SubFileReports &&SubFiles, SubDirReports &&SubDirs, + FileCoverageSummary &&SubTotals) override { + auto &LCPath = SubTotals.Name; + assert(Options.hasOutputDirectory() && + "No output directory for index file"); + + SmallString<128> OSPath = LCPath; + sys::path::append(OSPath, "index"); + auto OSOrErr = Printer.createOutputStream(OSPath, "html", + /*InToplevel=*/false); + if (auto E = OSOrErr.takeError()) + return E; + auto OS = std::move(OSOrErr.get()); + raw_ostream &OSRef = *OS.get(); + + auto IndexHtmlPath = Printer.getOutputPath((LCPath + "index").str(), "html", + /*InToplevel=*/false); + emitPrelude(OSRef, Options, getPathToStyle(IndexHtmlPath)); + + auto NavLink = buildTitleLinks(LCPath); + Printer.emitReportHeader(OSRef, "Coverage Report (" + NavLink + ")"); + + std::vector EmptyFiles; + + // Make directories at the top of the table. + for (auto &&SubDir : SubDirs) { + auto &Report = SubDir.second.first; + if (!Report.FunctionCoverage.getNumFunctions()) + EmptyFiles.push_back(&Report); + else + emitTableRow(OSRef, Options, buildRelLinkToFile(Report.Name), Report, + /*IsTotals=*/false); + } + + for (auto&& SubFile: SubFiles) { + auto &Report = SubFile.second; + if (!Report.FunctionCoverage.getNumFunctions()) + EmptyFiles.push_back(&Report); + else + emitTableRow(OSRef, Options, buildRelLinkToFile(Report.Name), Report, + /*IsTotals=*/false); + } + + // Emit the totals row. + emitTableRow(OSRef, Options, "Totals", SubTotals, /*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()) { + 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 FCS : EmptyFiles) { + auto Link = buildRelLinkToFile(FCS->Name); + OSRef << tag("tr", tag("td", tag("pre", Link)), "light-row") << '\n'; + } + OSRef << EndTable << EndCenteredDiv; + } + + // Emit epilog. + OSRef << tag("h5", escape(Options.getLLVMVersionString(), Options)); + emitEpilog(OSRef); + + return Error::success(); + } + + /// Make a title with hyperlinks to the index.html files of each hierarchy + /// of the report. + std::string buildTitleLinks(StringRef LCPath) const { + // For each report level in LCPStack, extract the path component and + // calculate the number of "../" relative to current LCPath. + SmallVector, unsigned>, 16> Components; + + auto Iter = LCPStack.begin(), IterE = LCPStack.end(); + SmallString<128> RootPath; + if (*Iter == 0) { + // If llvm-cov works on relative coverage mapping data, the LCP of + // all source file paths can be 0, which makes the title path empty. + // As we like adding a slash at the back of the path to indicate a + // directory, in this case, we use "." as the root path to make it + // not be confused with the root path "/". + RootPath = "."; + } else { + RootPath = LCPath.substr(0, *Iter); + sys::path::native(RootPath); + sys::path::remove_dots(RootPath, /*remove_dot_dot=*/true); + } + Components.emplace_back(std::move(RootPath), 0); + + for (auto Last = *Iter; ++Iter != IterE; Last = *Iter) { + SmallString<128> SubPath = LCPath.substr(Last, *Iter - Last); + sys::path::native(SubPath); + sys::path::remove_dots(SubPath, /*remove_dot_dot=*/true); + auto Level = unsigned(SubPath.count(sys::path::get_separator())) + 1; + Components.back().second += Level; + Components.emplace_back(std::move(SubPath), Level); + } + + // Then we make the title accroding to Components. + 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; + } + + SmallString<128> Link; + for (unsigned J = I->second; J > 0; --J) + Link += "../"; + Link += "index.html"; + S += a(Link, Name); + S += sys::path::get_separator(); + } + return S; + } + + std::string buildRelLinkToFile(StringRef RelPath) const { + SmallString<128> LinkTextStr(RelPath); + sys::path::native(LinkTextStr); + + // remove_dots will remove trailing slash, so we need to check before it. + auto IsDir = LinkTextStr.endswith(sys::path::get_separator()); + sys::path::remove_dots(LinkTextStr, /*remove_dot_dot=*/true); + + SmallString<128> LinkTargetStr(LinkTextStr); + if (IsDir) { + LinkTextStr += sys::path::get_separator(); + sys::path::append(LinkTargetStr, "index.html"); + } else{ + LinkTargetStr += ".html"; + } + + auto LinkText = escape(LinkTextStr, Options); + auto LinkTarget = escape(LinkTargetStr, Options); + return a(LinkTarget, LinkText); + } +}; + +Error CoveragePrinterHTMLDirectory::createIndexFile( + ArrayRef SourceFiles, const CoverageMapping &Coverage, + const CoverageFiltersMatchAll &Filters) { + // The createSubIndexFile function only works when SourceFiles is + // more than one. So we fallback to CoveragePrinterHTML when it is. + if (SourceFiles.size() <= 1) + return CoveragePrinterHTML::createIndexFile(SourceFiles, Coverage, Filters); + + // Emit the default stylesheet. + if (Error E = emitStyleSheet()) + return E; + + // Emit index files in every subdirectory. + Reporter Report(*this, Coverage, Filters); + auto TotalsOrErr = Report.prepareDirectoryReports(SourceFiles); + if (auto E = TotalsOrErr.takeError()) + return E; + auto &LCPath = TotalsOrErr->Name; + + // 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").str(), "html", /*InToplevel=*/false); + *OS.get() << R"( + + + + + + + )"; + + return Error::success(); +} + void SourceCoverageViewHTML::renderViewHeader(raw_ostream &OS) { OS << BeginCenteredDiv << BeginTable; } diff --git a/llvm/tools/llvm-cov/SourceCoverageViewText.h b/llvm/tools/llvm-cov/SourceCoverageViewText.h --- a/llvm/tools/llvm-cov/SourceCoverageViewText.h +++ b/llvm/tools/llvm-cov/SourceCoverageViewText.h @@ -35,6 +35,21 @@ : CoveragePrinter(Opts) {} }; +/// A coverage printer for text output, but generates index files in every +/// subdirectory to show a hierarchical view. The implementation is similar +/// to CoveragePrinterHTMLDirectory. So please refer to that for more comments. +class CoveragePrinterTextDirectory : public CoveragePrinterText { +public: + using CoveragePrinterText::CoveragePrinterText; + + Error createIndexFile(ArrayRef SourceFiles, + const CoverageMapping &Coverage, + const CoverageFiltersMatchAll &Filters) override; + +private: + struct Reporter; +}; + /// A code coverage view which supports text-based rendering. class SourceCoverageViewText : public SourceCoverageView { void renderViewHeader(raw_ostream &OS) override; diff --git a/llvm/tools/llvm-cov/SourceCoverageViewText.cpp b/llvm/tools/llvm-cov/SourceCoverageViewText.cpp --- a/llvm/tools/llvm-cov/SourceCoverageViewText.cpp +++ b/llvm/tools/llvm-cov/SourceCoverageViewText.cpp @@ -15,6 +15,8 @@ #include "llvm/ADT/SmallString.h" #include "llvm/ADT/StringExtras.h" #include "llvm/Support/Format.h" +#include "llvm/Support/FileSystem.h" +#include "llvm/Support/Path.h" #include using namespace llvm; @@ -46,6 +48,68 @@ return Error::success(); } +struct CoveragePrinterTextDirectory::Reporter : public DirectoryCoverageReport { + CoveragePrinterTextDirectory &Printer; + + Reporter(CoveragePrinterTextDirectory &Printer, + const coverage::CoverageMapping &Coverage, + const CoverageFiltersMatchAll &Filters) + : DirectoryCoverageReport(Printer.Opts, Coverage, Filters), + Printer(Printer) {} + + Error operator()(SubFileReports &&SubFiles, SubDirReports &&SubDirs, + FileCoverageSummary &&SubTotals) override { + auto &LCPath = SubTotals.Name; + assert(Options.hasOutputDirectory() && + "No output directory for index file"); + + SmallString<128> OSPath = LCPath; + sys::path::append(OSPath, "index"); + auto OSOrErr = Printer.createOutputStream(OSPath, "txt", + /*InToplevel=*/false); + if (Error E = OSOrErr.takeError()) + return E; + auto OS = std::move(OSOrErr.get()); + raw_ostream &OSRef = *OS.get(); + + std::vector Reports; + for (auto&& SubDir:SubDirs) + Reports.push_back(std::move(SubDir.second.first)); + for (auto&& SubFile: SubFiles) + Reports.push_back(std::move(SubFile.second)); + + CoverageReport Report(Options, Coverage); + Report.renderFileReports(OSRef, Reports, SubTotals); + + Options.colored_ostream(OSRef, raw_ostream::CYAN) + << "\n" + << Options.getLLVMVersionString(); + + return Error::success(); + } +}; + +Error CoveragePrinterTextDirectory::createIndexFile( + ArrayRef SourceFiles, const CoverageMapping &Coverage, + const CoverageFiltersMatchAll &Filters) { + if (SourceFiles.size() <= 1) + return CoveragePrinterText::createIndexFile(SourceFiles, Coverage, Filters); + + Reporter Report(*this, Coverage, Filters); + auto TotalsOrErr = Report.prepareDirectoryReports(SourceFiles); + if (auto E = TotalsOrErr.takeError()) + return E; + auto &LCPath = TotalsOrErr->Name; + + auto TopIndexFilePath = + getOutputPath("index", "txt", /*InToplevel=*/true, /*Relative=*/false); + auto LCPIndexFilePath = + getOutputPath((LCPath + "index").str(), "txt", /*InToplevel=*/false, + /*Relative=*/false); + return errorCodeToError( + sys::fs::create_link(LCPIndexFilePath, TopIndexFilePath)); +} + namespace { static const unsigned LineCoverageColumnWidth = 7; 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",