Feature Plan: Report Source Visualization
This document outlines the implementation plan for visualizing the source of code coverage. The feature will allow users to identify exactly which input report file is responsible for the coverage of any given line of code, directly within the final HTML report.
1. Feature Overview
When multiple coverage reports (e.g., from unit tests and integration tests) are merged, it becomes difficult to trace the origin of the coverage data. This feature addresses that by tagging each line of code with its source report during the analysis phase.
The final HTML report will then use this information to provide enhanced tooltips and enable new filtering capabilities, allowing a user to see not just if a line is covered, but by which test suite.
2. User Impact & Benefits
- Traceability & Debugging: Developers can instantly see which report (and by extension, which test suite) covered a specific line. This is invaluable for debugging why a line is, or is not, being covered by a particular set of tests.
- Granular Analysis: Enables a clear understanding of coverage contributions from different test strategies (e.g., backend vs. frontend, unit vs. E2E). This helps teams assess the effectiveness of each part of their testing pipeline.
- Confidence in Merged Reports: Removes the "black box" nature of merged reports, giving users confidence that coverage data from all sources is being correctly processed and attributed.
- Actionable Reporting: The new data provides a foundation for more advanced filtering in the UI, such as "show me only lines covered by
integration_tests.xml
".
3. High-Level Architectural Approach
The implementation will be integrated into the existing Parse -> Analyze -> Report
pipeline with minimal disruption to the overall architecture.
- Model (
internal/model
): The coremodel.Line
struct will be extended to store a map of source report file paths to their corresponding hit counts for that line. - Parser (
internal/parser
): TheParserResult
struct will be tagged with the path of the file it was parsed from. This happens in the application entrypoint (cmd/main.go
). - Analyzer (
internal/analyzer
): The merging logic in theanalyzer
will be enhanced to perform a "deep merge". Instead of simply combining file lists, it will now merge line-level data for files that appear in multiple reports, aggregating hit counts and populating the new source report map on each line. - Reporter (
internal/reporter/htmlreport
): The HTML reporter's view models will be updated to carry this new source data. The builder will populate the view models, which will then be serialized to JSON and embedded in the final HTML file for use by the Angular frontend.
4. Detailed Implementation Plan
Phase 1: Core Model and Parser Result Enhancement
Goal: Update the data model to support per-line report attribution.
-
Update the
Line
Model:- File:
internal/model/analysis.go
- Action: Add a map to the
Line
struct to store hits per source report.
// in: internal/model/analysis.go type Line struct { // ... existing fields ... LineVisitStatus LineVisitStatus CoverageByReport map[string]int // Key: report file path, Value: hits from that report }
- File:
-
Update the
ParserResult
Struct:- File:
internal/parser/parser_config.go
- Action: Add a field to
ParserResult
to track its origin file path. This makes the path available to the analyzer during merging.
// in: internal/parser/parser_config.go type ParserResult struct { ReportFilePath string // Add this field Assemblies []model.Assembly // ... existing fields ... }
- File:
-
Tag the
ParserResult
at Creation:- File:
cmd/main.go
- Action: In the
parseAndMergeReports
function, assign thereportFile
path to the newReportFilePath
field immediately after a successful parse.
// in: cmd/main.go (inside parseAndMergeReports loop) result, err := parserInstance.Parse(reportFile, reportConfig) if err != nil { // ... error handling ... continue } result.ReportFilePath = reportFile // Tag the result with its source file path parserResults = append(parserResults, result)
- File:
Phase 2: Refactor the Analyzer for Deep Merging
Goal: Enhance the analyzer to merge coverage data at the line level and populate the CoverageByReport
map.
-
Modify
mergeAssemblies
Logic:- File:
internal/analyzer/analyzer.go
- Action: The core change is within the
mergeAssemblies
function. The current logic for merging classes is too shallow. It must be modified to handle deep merging of files and their lines.
- File:
-
Implement
mergeFiles
andmergeLines
(Conceptual Representation):- File:
internal/analyzer/analyzer.go
- Action: The logic inside the
if existingClass, found := classMap[classFromParser.Name]; found
block needs to be replaced with a more robust file-merging strategy.
// Conceptual change in internal/analyzer/analyzer.go inside mergeAssemblies // ... existing loop over classFromParser.Classes ... if existingClass, found := classMap[classFromParser.Name]; found { // ... merge class-level statistics ... // --- DEEP MERGE LOGIC --- // This replaces the simple file list append. existingFilesMap := make(map[string]*model.CodeFile) for i := range existingClass.Files { existingFilesMap[existingClass.Files[i].Path] = &existingClass.Files[i] } for _, fileFromParser := range classFromParser.Files { if existingFile, fileFound := existingFilesMap[fileFromParser.Path]; fileFound { // File already exists, merge lines mergeLines(existingFile, &fileFromParser, res.ReportFilePath) // res is the current ParserResult } else { // New file for this class, tag all its lines and append tagLinesOfNewFile(&fileFromParser, res.ReportFilePath) existingClass.Files = append(existingClass.Files, fileFromParser) existingFilesMap[fileFromParser.Path] = &existingClass.Files[len(existingClass.Files)-1] } } // --- END OF DEEP MERGE --- } else { // New class, just tag all lines in all its files before adding for i := range asmCopy.Classes { for j := range asmCopy.Classes[i].Files { tagLinesOfNewFile(&asmCopy.Classes[i].Files[j], res.ReportFilePath) } } mergedAssembliesMap[asmCopy.Name] = &asmCopy }
- File:
-
Define Helper Functions for Merging and Tagging:
- File:
internal/analyzer/analyzer.go
- Action: Add new helper functions to support the logic above.
// in: internal/analyzer/analyzer.go // mergeLines merges line data from newFile into existingFile. func mergeLines(existingFile, newFile *model.CodeFile, reportPath string) { // Create a map for quick lookup of existing lines by number linesMap := make(map[int]*model.Line, len(existingFile.Lines)) for i := range existingFile.Lines { linesMap[existingFile.Lines[i].Number] = &existingFile.Lines[i] } for _, newLine := range newFile.Lines { if existingLine, found := linesMap[newLine.Number]; found { // Line exists, merge hits and report data existingLine.Hits += newLine.Hits if existingLine.CoverageByReport == nil { existingLine.CoverageByReport = make(map[string]int) } existingLine.CoverageByReport[reportPath] = newLine.Hits } else { // New line for this file, tag and append if newLine.CoverageByReport == nil { newLine.CoverageByReport = make(map[string]int) } newLine.CoverageByReport[reportPath] = newLine.Hits existingFile.Lines = append(existingFile.Lines, newLine) linesMap[newLine.Number] = &existingFile.Lines[len(existingFile.Lines)-1] } } } // tagLinesOfNewFile initializes the CoverageByReport map for all lines in a file. func tagLinesOfNewFile(file *model.CodeFile, reportPath string) { for i := range file.Lines { line := &file.Lines[i] if line.CoverageByReport == nil { line.CoverageByReport = make(map[string]int) } line.CoverageByReport[reportPath] = line.Hits } }
- File:
Phase 3: HTML Reporter Integration
Goal: Pass the new report source data to the frontend via the window
object.
-
Update View Models:
- File:
internal/reporter/htmlreport/viewmodels.go
- Action: Add the
CoverageByReport
map to theAngularLineAnalysisViewModel
.
// in: internal/reporter/htmlreport/viewmodels.go type AngularLineAnalysisViewModel struct { // ... existing fields ... CoverageByReport map[string]int `json:"cbr,omitempty"` // Add this field }
- File:
-
Populate View Models in Builder:
- File:
internal/reporter/htmlreport/class_detail_builder.go
- Action: In
buildAngularLineViewModelForJS
, copy the map from the model to the view model.
// in: internal/reporter/htmlreport/class_detail_builder.go func (b *HtmlReportBuilder) buildAngularLineViewModelForJS(...) AngularLineAnalysisViewModel { lineVM := AngularLineAnalysisViewModel{ // ... existing assignments ... } if hasCoverageData { // ... existing assignments ... if len(modelCovLine.CoverageByReport) > 0 { // Only include the map if it's not empty lineVM.CoverageByReport = modelCovLine.CoverageByReport } } // ... return lineVM }
- File:
Phase 4: Frontend Enhancement (Conceptual)
Goal: Use the new data in the Angular frontend to provide a better user experience.
-
Tooltip Enhancement:
- Action: Modify the Angular component responsible for rendering code lines. When generating the tooltip for a line, check for the presence of the new
cbr
(CoverageByReport
) map. - Logic: If the map exists, iterate through its key-value pairs to build a detailed tooltip string.
- Example Tooltip: "Covered (5 visits) by: report_unit.xml (3), report_integ.xml (2)".
- Action: Modify the Angular component responsible for rendering code lines. When generating the tooltip for a line, check for the presence of the new
-
Source Report Filtering (Stretch Goal):
- Action: Add a new dropdown or multi-select filter to the report controls. This control will be populated with a unique list of all report file paths found across all lines.
- Logic: When a user selects one or more reports from the filter, JavaScript will iterate through the line elements, adding a "highlight" CSS class to lines whose
cbr
map contains a key matching the selected report(s).
5. Testing Strategy
-
Unit Tests (Analyzer): Create a new test in
internal/analyzer/analyzer_test.go
that specifically validates the deep merge logic.- Define two
parser.ParserResult
objects originating from differentReportFilePath
values. - Ensure they cover the same assembly, class, and file.
- Have them cover some of the same lines and some unique lines.
- Assert that the final merged
model.Line
objects have correctly summedHits
and aCoverageByReport
map containing entries for both source reports with the correct hit counts.
- Define two
-
Unit Tests (Reporter): In
internal/reporter/htmlreport/
, add a test to verify that amodel.Line
with a populatedCoverageByReport
map results in anAngularLineAnalysisViewModel
with thecbr
field correctly populated. -
End-to-End Test: Create a small, temporary test that runs the main application
run()
function with two simple, mock coverage files. Inspect the generatedindex.html
and verify that thewindow.classDetails
JSON object contains the expectedcbr
data for the relevant lines.
6. Definition of Done
- [ ] The
model.Line
andparser.ParserResult
structs are updated as specified. - [ ] The
analyzer
package correctly performs a deep merge of line-level data, populating theCoverageByReport
map. - [ ] Unit tests for the analyzer's deep merge functionality are implemented and passing.
- [ ] The
htmlreport
builder correctly populates the newcbr
field in theAngularLineAnalysisViewModel
. - [ ] The final HTML report contains the
cbr
data in thewindow.classDetails
JSON object. - [ ] The line-level tooltips in the HTML report are enhanced to display the source report(s) and their respective hit counts.
- [ ] All existing tests continue to pass.