Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/sample-report.html

Large diffs are not rendered by default.

6 changes: 4 additions & 2 deletions specs/codemap-indexer.md
Original file line number Diff line number Diff line change
Expand Up @@ -360,8 +360,10 @@ reverse **blast radius** — how many scenarios reach it, via `EndpointReachAll`
an **expandable toggle**: opening a row reveals the reached scenarios grouped by feature, each with the
step text that connects it (via `EndpointScenarioDetails`, resolved only for the shown rows) — the same
drill-down `impact --endpoint` prints, capped per row with a pointer to the CLI for the exhaustive list.
All map-derived text is HTML-escaped. Deterministic: the only volatile value
(the generated timestamp) is read from the map, not the clock. When the map's `user_version`
All map-derived text is HTML-escaped. The header shows the solution path relative to the
directory `report` runs from when the solution lies under it (forward slashes), so a committed
report carries no machine-specific absolute path; otherwise it shows the path as recorded in the map.
Deterministic: the only volatile value (the generated timestamp) is read from the map, not the clock. When the map's `user_version`
predates the current schema, the report shows a **stale-schema banner** (and `report`/`search`
print a matching console note) explaining that facets like Gherkin features / coverage / search are
empty because the map is old — re-run `index` to populate them — so empty sections never read as a bug.
Expand Down
2 changes: 1 addition & 1 deletion src/CodeMap.Cli/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -254,7 +254,7 @@ public static int RunReport(string[] args)
string html;
try
{
html = HtmlReportBuilder.Build(doc);
html = HtmlReportBuilder.Build(doc, Environment.CurrentDirectory);
File.WriteAllText(output, html);
}
catch (Exception ex)
Expand Down
21 changes: 19 additions & 2 deletions src/CodeMap.Core/Reporting/HtmlReportBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,13 @@ namespace TestAtlas.Core.Reporting;
public static class HtmlReportBuilder
{
/// <summary>Builds the full HTML document as a string.</summary>
public static string Build(MapDocument doc)
/// <param name="doc">The map to render.</param>
/// <param name="baseDirectory">
/// When given, a solution path under this directory is shown relative to it (forward slashes), so a
/// committed report doesn't carry the machine-specific absolute path. A solution outside it, or no
/// base directory, shows the path as recorded in the map.
/// </param>
public static string Build(MapDocument doc, string? baseDirectory = null)
{
var sb = new StringBuilder(64 * 1024);

Expand All @@ -42,7 +48,7 @@ public static string Build(MapDocument doc)
var coveragePct = totalSteps == 0 ? 0 : (int)Math.Round(100.0 * resolved / totalSteps);

// ---- document shell -----------------------------------------------------------------
var solutionPath = Meta(doc, MapSchema.MetaSolutionPath, "(unknown solution)");
var solutionPath = DisplayPath(Meta(doc, MapSchema.MetaSolutionPath, "(unknown solution)"), baseDirectory);
var solutionName = Path.GetFileName(solutionPath);
var generated = Meta(doc, MapSchema.MetaGeneratedUtc, "");
var toolVersion = Meta(doc, MapSchema.MetaToolVersion, "");
Expand Down Expand Up @@ -616,6 +622,17 @@ private static void LegendItem(StringBuilder sb, string cls, int count, string d
.Append("</b> <span class=\"lk\">").Append(cls).Append("</span> <span class=\"ld\">")
.Append(E(desc)).Append("</span></li>");

/// <summary>The path relative to <paramref name="baseDirectory"/> when it lies under it; otherwise unchanged.</summary>
internal static string DisplayPath(string path, string? baseDirectory)
{
if (string.IsNullOrEmpty(baseDirectory) || !Path.IsPathFullyQualified(path)) return path;
var relative = Path.GetRelativePath(baseDirectory, path);
var outside = relative == ".." || relative.StartsWith("../", StringComparison.Ordinal)
|| relative.StartsWith("..\\", StringComparison.Ordinal);
if (relative == "." || outside || Path.IsPathRooted(relative)) return path;
return relative.Replace('\\', '/');
}

private static string Meta(MapDocument doc, string key, string fallback)
=> doc.Meta.TryGetValue(key, out var v) && v.Length > 0 ? v : fallback;

Expand Down
21 changes: 21 additions & 0 deletions tests/CodeMap.Tests/HtmlReportBuilderTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,27 @@ public void Emits_a_self_contained_document()
Assert.DoesNotContain("src=\"http", html); // no external script/img
}

[Fact]
public void Shows_the_solution_path_relative_to_the_base_directory()
{
var html = HtmlReportBuilder.Build(Doc(), "/repo");
Assert.Contains("<span title=\"My.sln\">My.sln</span>", html);
Assert.DoesNotContain("/repo/My.sln", html);

// No base directory: the path as recorded in the map, as before.
Assert.Contains("<span title=\"/repo/My.sln\">/repo/My.sln</span>", HtmlReportBuilder.Build(Doc()));
}

[Theory]
[InlineData("/repo/samples/Shop/Shop.sln", "/repo", "samples/Shop/Shop.sln")]
[InlineData("/repo/samples/Shop/Shop.sln", "/repo/", "samples/Shop/Shop.sln")]
[InlineData("/elsewhere/Shop.sln", "/repo", "/elsewhere/Shop.sln")] // outside the base: unchanged
[InlineData("/repo/..x/Shop.sln", "/repo", "..x/Shop.sln")] // a folder named "..x" is still inside
[InlineData("(unknown solution)", "/repo", "(unknown solution)")] // not a path: unchanged
[InlineData("/repo/Shop.sln", null, "/repo/Shop.sln")]
public void Display_path_is_relative_only_under_the_base(string path, string? baseDirectory, string expected)
=> Assert.Equal(expected, HtmlReportBuilder.DisplayPath(path, baseDirectory));

[Fact]
public void Renders_each_binding_state_with_its_evidence()
{
Expand Down
Loading