From ef10ea8525b04c263d82038393f0a9dcb9f1e5de Mon Sep 17 00:00:00 2001 From: Robin Salkeld Date: Fri, 10 Nov 2023 13:30:40 -0800 Subject: [PATCH 1/9] =?UTF-8?q?Breaking=20spans=20by=20module=20(not=20wor?= =?UTF-8?q?king=20yet,=20tokens=20aren=E2=80=99t=20right)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../CoverageReport/CoverageReport.cs | 19 ++++++++++-- Source/DafnyDriver/CoverageReporter.cs | 30 +++++++++++++++---- 2 files changed, 40 insertions(+), 9 deletions(-) diff --git a/Source/DafnyCore/CoverageReport/CoverageReport.cs b/Source/DafnyCore/CoverageReport/CoverageReport.cs index 3e2bb0c5cce..f67f8f8873a 100644 --- a/Source/DafnyCore/CoverageReport/CoverageReport.cs +++ b/Source/DafnyCore/CoverageReport/CoverageReport.cs @@ -12,6 +12,7 @@ public class CoverageReport { private static int nextUniqueId = 0; private readonly Dictionary> labelsByFile; + private readonly Dictionary> modulesByFile; public readonly string Name; // the name to assign to this coverage report public readonly string Units; // the units of coverage (plural). This will be written in the coverage report table. private readonly string suffix; // user-provided suffix to add to filenames that are part of this report @@ -31,6 +32,7 @@ public CoverageReport(string name, string units, string suffix, Program program) Units = units; this.suffix = suffix; labelsByFile = new(); + modulesByFile = new(); if (program != null) { RegisterFiles(program); } @@ -49,6 +51,10 @@ public void LabelCode(RangeToken span, CoverageLabel label) { public IEnumerable CoverageSpansForFile(Uri uri) { return labelsByFile.GetOrDefault(uri, () => new List()); } + + public IEnumerable ModulesInFile(Uri uri) { + return modulesByFile.GetOrDefault(uri, () => new HashSet()); + } public IEnumerable AllFiles() { return labelsByFile.Keys; @@ -65,10 +71,17 @@ public void RegisterFile(Uri uri) { } private void RegisterFiles(Node astNode) { - if (astNode.StartToken.ActualFilename != null && !labelsByFile.ContainsKey(astNode.StartToken.Uri)) { - labelsByFile[astNode.StartToken.Uri] = new(); + if (astNode.StartToken.ActualFilename != null) { + var uri = astNode.StartToken.Uri; + labelsByFile.GetOrCreate(uri, () => new List()); + + if (astNode is LiteralModuleDecl moduleDecl) { + modulesByFile.GetOrCreate(uri, () => new HashSet()).Add(moduleDecl.ModuleDef); + + RegisterFiles(moduleDecl.ModuleDef); + } } - + foreach (var declaration in astNode.Children.OfType()) { RegisterFiles(declaration); } diff --git a/Source/DafnyDriver/CoverageReporter.cs b/Source/DafnyDriver/CoverageReporter.cs index f1f3671f10d..6cd5655e8c5 100644 --- a/Source/DafnyDriver/CoverageReporter.cs +++ b/Source/DafnyDriver/CoverageReporter.cs @@ -215,7 +215,7 @@ private void CreateIndexFile(CoverageReport report, Dictionary sour return; } var coverageLabels = Enum.GetValues(typeof(CoverageLabel)).Cast().ToList(); - List header = new() { "File" }; + List header = new() { "File", "Module" }; header.AddRange(coverageLabels .Where(label => label != CoverageLabel.None && label != CoverageLabel.NotApplicable) .Select(label => $"{report.Units} {CoverageLabelExtension.ToString(label)}")); @@ -223,16 +223,34 @@ private void CreateIndexFile(CoverageReport report, Dictionary sour List> body = new(); foreach (var sourceFile in sourceFileToCoverageReportFile.Keys) { var relativePath = Path.GetRelativePath(baseDirectory, sourceFileToCoverageReportFile[sourceFile]); + body.Add(new() { $"{relativePath}" + $"class = \"el_package\">{relativePath}", + "All modules" }); + body.Last().AddRange(coverageLabels .Where(label => label != CoverageLabel.None && label != CoverageLabel.NotApplicable) - .Select(label => report.CoverageSpansForFile(sourceFile).Count(span => span.Label == label)).OfType()); + .Select(label => report.CoverageSpansForFile(sourceFile) + .Count(span => span.Label == label)).OfType()); + + foreach (var module in report.ModulesInFile(sourceFile).OrderBy(m => m.FullName)) { + body.Add(new() { + $"{relativePath}", + module.FullName + }); + + body.Last().AddRange(coverageLabels + .Where(label => label != CoverageLabel.None && label != CoverageLabel.NotApplicable) + .Select(label => report.CoverageSpansForFile(sourceFile) + .Where(span => span.Span.Intersects(module.RangeToken)) + .Count(span => span.Label == label)).OfType()); + } } - List footer = new() { "Total" }; + List footer = new() { "Total", "" }; footer.AddRange(coverageLabels .Where(label => label != CoverageLabel.None && label != CoverageLabel.NotApplicable) .Select(label => report.AllFiles().Select(sourceFile => @@ -243,8 +261,8 @@ private void CreateIndexFile(CoverageReport report, Dictionary sour templateText = FileNameRegex.Replace(templateText, report.Name); templateText = TableHeaderRegex.Replace(templateText, MakeIndexFileTableRow(header)); templateText = TableFooterRegex.Replace(templateText, MakeIndexFileTableRow(footer)); - File.WriteAllText(Path.Combine(baseDirectory, $"index{report.UniqueSuffix}.html"), - TableBodyRegex.Replace(templateText, string.Join("\n", body.Select(MakeIndexFileTableRow)))); + templateText = TableBodyRegex.Replace(templateText, string.Join("\n", body.Select(MakeIndexFileTableRow))); + File.WriteAllText(Path.Combine(baseDirectory, $"index{report.UniqueSuffix}.html"), templateText); } /// From 5b093f0c913c5f4ef77f0074e844fd6a691a9af0 Mon Sep 17 00:00:00 2001 From: Robin Salkeld Date: Fri, 10 Nov 2023 14:04:17 -0800 Subject: [PATCH 2/9] Better formatting --- Source/DafnyDriver/CoverageReporter.cs | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/Source/DafnyDriver/CoverageReporter.cs b/Source/DafnyDriver/CoverageReporter.cs index 6cd5655e8c5..fa0947a865d 100644 --- a/Source/DafnyDriver/CoverageReporter.cs +++ b/Source/DafnyDriver/CoverageReporter.cs @@ -237,15 +237,18 @@ private void CreateIndexFile(CoverageReport report, Dictionary sour foreach (var module in report.ModulesInFile(sourceFile).OrderBy(m => m.FullName)) { body.Add(new() { - $"{relativePath}", + "", module.FullName }); - + + var moduleRange = module.RangeToken.ToDafnyRange(); body.Last().AddRange(coverageLabels .Where(label => label != CoverageLabel.None && label != CoverageLabel.NotApplicable) .Select(label => report.CoverageSpansForFile(sourceFile) - .Where(span => span.Span.Intersects(module.RangeToken)) + // span.Span.Intersects(module.RangeToken) would be cleaner, + // but unfortunately coverage span tokens don't currently always + // have Token.pos set correctly. :( + .Where(span => moduleRange.Contains(span.Span.ToDafnyRange().Start)) .Count(span => span.Label == label)).OfType()); } } From 08d73c7655ccce7bb69799bacaa0d883b96c0c83 Mon Sep 17 00:00:00 2001 From: Robin Salkeld Date: Fri, 10 Nov 2023 15:32:18 -0800 Subject: [PATCH 3/9] Improve Base64 coverage --- Source/DafnyStandardLibraries/examples/Base64Examples.dfy | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Source/DafnyStandardLibraries/examples/Base64Examples.dfy b/Source/DafnyStandardLibraries/examples/Base64Examples.dfy index ebdc3874b3c..48da3ab5e00 100644 --- a/Source/DafnyStandardLibraries/examples/Base64Examples.dfy +++ b/Source/DafnyStandardLibraries/examples/Base64Examples.dfy @@ -25,8 +25,8 @@ module Base64Examples { } method {:test} TestRoundTripMedium() { - var medUints := seq(512, _ => 22); - var medBytes := seq(512, _ => 22); + var medUints := seq(512, i => (i % 256) as uint8); + var medBytes := seq(512, i => (i % 256) as bv8); CheckEncodeDecode(medUints, medBytes); } From 98349d41f6db292dc3fe1f3d2d3ef93cbceae1a7 Mon Sep 17 00:00:00 2001 From: Robin Salkeld Date: Mon, 13 Nov 2023 12:18:48 -0800 Subject: [PATCH 4/9] Align first two columns left --- Source/DafnyDriver/CoverageReporter.cs | 5 ++++- Source/DafnyDriver/assets/.resources/coverage.css | 11 +++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/Source/DafnyDriver/CoverageReporter.cs b/Source/DafnyDriver/CoverageReporter.cs index fa0947a865d..b16952967af 100644 --- a/Source/DafnyDriver/CoverageReporter.cs +++ b/Source/DafnyDriver/CoverageReporter.cs @@ -196,7 +196,10 @@ private void SerializeCoverageReports(List reports, string repor private string MakeIndexFileTableRow(List row) { var result = new StringBuilder("\n"); - foreach (var cell in row) { + foreach (var cell in row.Take(2)) { + result.Append($"\t{cell}\n"); + } + foreach (var cell in row.Skip(2)) { result.Append($"\t{cell}\n"); } result.Append("\n"); diff --git a/Source/DafnyDriver/assets/.resources/coverage.css b/Source/DafnyDriver/assets/.resources/coverage.css index 06ec1515b50..af92c4fc608 100644 --- a/Source/DafnyDriver/assets/.resources/coverage.css +++ b/Source/DafnyDriver/assets/.resources/coverage.css @@ -49,6 +49,11 @@ table.coverage thead td { border-bottom:#b0b0b0 1px solid; } +table.coverage thead td.name { + text-align:left; + padding-left:2px; +} + table.coverage thead td.ctr2 { text-align:right; padding-left:2px; @@ -64,6 +69,12 @@ table.coverage tbody tr:hover { background: #f0f0d0 !important; } +table.coverage tbody td.name { + text-align:left; + padding-right:14px; + padding-left:2px; +} + table.coverage tbody td.ctr2 { text-align:right; padding-right:14px; From 56cd2b4df8da330c44c8baa1aa8950acf848789d Mon Sep 17 00:00:00 2001 From: Robin Salkeld Date: Mon, 13 Nov 2023 14:52:18 -0800 Subject: [PATCH 5/9] Partial work to handle implicit parent modules (just capturing to come back to later) --- .../DafnyCore/AST/Modules/ModuleDefinition.cs | 2 +- .../CoverageReport/CoverageReport.cs | 17 ++--- Source/DafnyDriver/CoverageReporter.cs | 71 ++++++++++++++++--- .../coverage_report_index_template.html | 17 ++++- 4 files changed, 85 insertions(+), 22 deletions(-) diff --git a/Source/DafnyCore/AST/Modules/ModuleDefinition.cs b/Source/DafnyCore/AST/Modules/ModuleDefinition.cs index e5ab45f5425..f17d3e9fedd 100644 --- a/Source/DafnyCore/AST/Modules/ModuleDefinition.cs +++ b/Source/DafnyCore/AST/Modules/ModuleDefinition.cs @@ -9,7 +9,7 @@ namespace Microsoft.Dafny; public record PrefixNameModule(IReadOnlyList Parts, LiteralModuleDecl Module); -public class ModuleDefinition : RangeNode, IAttributeBearingDeclaration, ICloneable, IHasSymbolChildren { +public class ModuleDefinition : RangeNode, IAttributeBearingDeclaration, ICloneable, IHasSymbolChildren { public IToken BodyStartTok = Token.NoToken; public IToken TokenWithTrailingDocString = Token.NoToken; diff --git a/Source/DafnyCore/CoverageReport/CoverageReport.cs b/Source/DafnyCore/CoverageReport/CoverageReport.cs index f67f8f8873a..0af99f0753e 100644 --- a/Source/DafnyCore/CoverageReport/CoverageReport.cs +++ b/Source/DafnyCore/CoverageReport/CoverageReport.cs @@ -13,6 +13,7 @@ public class CoverageReport { private readonly Dictionary> labelsByFile; private readonly Dictionary> modulesByFile; + public readonly string Name; // the name to assign to this coverage report public readonly string Units; // the units of coverage (plural). This will be written in the coverage report table. private readonly string suffix; // user-provided suffix to add to filenames that are part of this report @@ -72,14 +73,14 @@ public void RegisterFile(Uri uri) { private void RegisterFiles(Node astNode) { if (astNode.StartToken.ActualFilename != null) { - var uri = astNode.StartToken.Uri; - labelsByFile.GetOrCreate(uri, () => new List()); - - if (astNode is LiteralModuleDecl moduleDecl) { - modulesByFile.GetOrCreate(uri, () => new HashSet()).Add(moduleDecl.ModuleDef); - - RegisterFiles(moduleDecl.ModuleDef); - } + labelsByFile.GetOrCreate(astNode.StartToken.Uri, () => new List()); + } + + if (astNode is LiteralModuleDecl moduleDecl) { + // Uri may be null for implicit modules + modulesByFile.GetOrCreate(astNode.StartToken.Uri, () => new HashSet()).Add(moduleDecl.ModuleDef); + + RegisterFiles(moduleDecl.ModuleDef); } foreach (var declaration in astNode.Children.OfType()) { diff --git a/Source/DafnyDriver/CoverageReporter.cs b/Source/DafnyDriver/CoverageReporter.cs index b16952967af..62c6cc2a3d0 100644 --- a/Source/DafnyDriver/CoverageReporter.cs +++ b/Source/DafnyDriver/CoverageReporter.cs @@ -20,9 +20,12 @@ public class CoverageReporter { private static readonly Regex UriRegexInversed = new(@"

([^\n]*)

"); private static readonly Regex IndexLinkRegex = new(@"\{\{INDEX_LINK\}\}"); private static readonly Regex LinksToOtherReportsRegex = new(@"\{\{LINKS_TO_OTHER_REPORTS\}\}"); - private static readonly Regex TableHeaderRegex = new(@"\{\{TABLE_HEADER\}\}"); - private static readonly Regex TableFooterRegex = new(@"\{\{TABLE_FOOTER\}\}"); - private static readonly Regex TableBodyRegex = new(@"\{\{TABLE_BODY\}\}"); + private static readonly Regex ByFileTableHeaderRegex = new(@"\{\{BY_FILE_TABLE_HEADER\}\}"); + private static readonly Regex ByFileTableFooterRegex = new(@"\{\{BY_FILE_TABLE_FOOTER\}\}"); + private static readonly Regex ByFileTableBodyRegex = new(@"\{\{BY_FILE_TABLE_BODY\}\}"); + private static readonly Regex ByModuleTableHeaderRegex = new(@"\{\{BY_MODULE_TABLE_HEADER\}\}"); + private static readonly Regex ByModuleTableFooterRegex = new(@"\{\{BY_MODULE_TABLE_FOOTER\}\}"); + private static readonly Regex ByModuleTableBodyRegex = new(@"\{\{BY_MODULE_TABLE_BODY\}\}"); private static readonly Regex IndexFileNameRegex = new(@"index(.*)\.html"); private static readonly Regex PosRegexInverse = new("class=\"([a-z]+)\" id=\"([0-9]+):([0-9]+)\""); private const string CoverageReportTemplatePath = "coverage_report_template.html"; @@ -218,7 +221,7 @@ private void CreateIndexFile(CoverageReport report, Dictionary sour return; } var coverageLabels = Enum.GetValues(typeof(CoverageLabel)).Cast().ToList(); - List header = new() { "File", "Module" }; + List header = new() { "File" }; header.AddRange(coverageLabels .Where(label => label != CoverageLabel.None && label != CoverageLabel.NotApplicable) .Select(label => $"{report.Units} {CoverageLabelExtension.ToString(label)}")); @@ -229,8 +232,7 @@ private void CreateIndexFile(CoverageReport report, Dictionary sour body.Add(new() { $"{relativePath}", - "All modules" + $"class = \"el_package\">{relativePath}" }); body.Last().AddRange(coverageLabels @@ -240,7 +242,6 @@ private void CreateIndexFile(CoverageReport report, Dictionary sour foreach (var module in report.ModulesInFile(sourceFile).OrderBy(m => m.FullName)) { body.Add(new() { - "", module.FullName }); @@ -255,7 +256,15 @@ private void CreateIndexFile(CoverageReport report, Dictionary sour .Count(span => span.Label == label)).OfType()); } } + + header = new() { "Module" }; + header.AddRange(coverageLabels + .Where(label => label != CoverageLabel.None && label != CoverageLabel.NotApplicable) + .Select(label => $"{report.Units} {CoverageLabelExtension.ToString(label)}")); + body = new(); + PopulateByModuleTableBody(report, ) + List footer = new() { "Total", "" }; footer.AddRange(coverageLabels .Where(label => label != CoverageLabel.None && label != CoverageLabel.NotApplicable) @@ -265,12 +274,54 @@ private void CreateIndexFile(CoverageReport report, Dictionary sour var templateText = new StreamReader(templateStream).ReadToEnd(); templateText = LinksToOtherReportsRegex.Replace(templateText, linksToOtherReports); templateText = FileNameRegex.Replace(templateText, report.Name); - templateText = TableHeaderRegex.Replace(templateText, MakeIndexFileTableRow(header)); - templateText = TableFooterRegex.Replace(templateText, MakeIndexFileTableRow(footer)); - templateText = TableBodyRegex.Replace(templateText, string.Join("\n", body.Select(MakeIndexFileTableRow))); + + templateText = ByFileTableBodyRegex.Replace(templateText, MakeIndexFileTableRow(header)); + templateText = ByFileTableFooterRegex.Replace(templateText, MakeIndexFileTableRow(footer)); + templateText = ByFileTableBodyRegex.Replace(templateText, string.Join("\n", body.Select(MakeIndexFileTableRow))); + + templateText = ByModuleTableBodyRegex.Replace(templateText, MakeIndexFileTableRow(header)); + templateText = ByModuleTableFooterRegex.Replace(templateText, MakeIndexFileTableRow(footer)); + templateText = ByModuleTableBodyRegex.Replace(templateText, string.Join("\n", body.Select(MakeIndexFileTableRow))); + File.WriteAllText(Path.Combine(baseDirectory, $"index{report.UniqueSuffix}.html"), templateText); } + private static void PopulateByModuleTableBody(CoverageReport report, ModuleDefinition module, List coverageLabels, List> body) { + body.Add(CountsForModule(report, module, coverageLabels).OfType().ToList()); + + foreach (var child in module.TopLevelDecls.OrderBy(decl => decl.Name)) { + if (child is LiteralModuleDecl moduleDecl) { + PopulateByModuleTableBody(report, moduleDecl.ModuleDef, coverageLabels, body); + } + } + } + + private static List CountsForModule(CoverageReport report, ModuleDefinition module, List coverageLabels) { + if (module.StartToken.Uri != null) { + var moduleRange = module.RangeToken.ToDafnyRange(); + return coverageLabels + .Where(label => label != CoverageLabel.None && label != CoverageLabel.NotApplicable) + .Select(label => report.CoverageSpansForFile(module.StartToken.Uri) + // span.Span.Intersects(module.RangeToken) would be cleaner, + // but unfortunately coverage span tokens don't currently always + // have Token.pos set correctly. :( + .Where(span => moduleRange.Contains(span.Span.ToDafnyRange().Start)) + .Count(span => span.Label == label)).ToList(); + } else { + var sums = new int[coverageLabels.Count]; + foreach (var child in module.TopLevelDecls) { + if (child is LiteralModuleDecl moduleDecl) { + var childModuleCounts = CountsForModule(report, moduleDecl.ModuleDef, coverageLabels); + for (var labelIndex = 0; labelIndex < coverageLabels.Count; labelIndex++) { + sums[labelIndex] += childModuleCounts[labelIndex]; + } + } + } + + return sums.ToList(); + } + } + /// /// Creates a set of links to be inserted in that point to corresponding /// report files for the same diff --git a/Source/DafnyDriver/assets/coverage_report_index_template.html b/Source/DafnyDriver/assets/coverage_report_index_template.html index bbba468d06d..0061f94dca2 100644 --- a/Source/DafnyDriver/assets/coverage_report_index_template.html +++ b/Source/DafnyDriver/assets/coverage_report_index_template.html @@ -14,13 +14,24 @@

{{FILENAME}}

- {{TABLE_HEADER}} + {{BY_FILE_TABLE_HEADER}} - {{TABLE_FOOTER}} + {{BY_FILE_TABLE_FOOTER}} - {{TABLE_BODY}} + {{BY_FILE_TABLE_BODY}} + +
+ + + {{BY_MODULE_TABLE_HEADER}} + + + {{BY_MODULE_TABLE_FOOTER}} + + + {{BY_MODULE_TABLE_BODY}}