Skip to content

Commit

Permalink
Merge pull request #2391 from Build-Squad/coverage-report-api-improve…
Browse files Browse the repository at this point in the history
…ments
  • Loading branch information
turbolent authored Mar 24, 2023
2 parents e31474a + 04b74c4 commit 2a3eac6
Show file tree
Hide file tree
Showing 3 changed files with 736 additions and 75 deletions.
273 changes: 224 additions & 49 deletions runtime/coverage.go
Original file line number Diff line number Diff line change
Expand Up @@ -104,13 +104,13 @@ func NewLocationCoverage(lineHits map[int]int) *LocationCoverage {
}

// CoverageReport collects coverage information per location.
// It keeps track of inspected programs per location, and can
// also exclude locations from coverage collection.
// It keeps track of inspected locations, and can also exclude
// locations from coverage collection.
type CoverageReport struct {
// Contains a *LocationCoverage per location.
Coverage map[common.Location]*LocationCoverage `json:"-"`
// Contains an *ast.Program per location.
Programs map[common.Location]*ast.Program `json:"-"`
// Contains locations whose programs are already inspected.
Locations map[common.Location]struct{} `json:"-"`
// Contains locations excluded from coverage collection.
ExcludedLocations map[common.Location]struct{} `json:"-"`
}
Expand All @@ -131,13 +131,13 @@ func (r *CoverageReport) IsLocationExcluded(location Location) bool {
// AddLineHit increments the hit count for the given line, on the given
// location. The method call is a NO-OP in two cases:
// - If the location is excluded from coverage collection
// - If the location's *ast.Program, has not been inspected
// - If the location has not been inspected for its statements
func (r *CoverageReport) AddLineHit(location Location, line int) {
if r.IsLocationExcluded(location) {
return
}

if !r.IsProgramInspected(location) {
if !r.IsLocationInspected(location) {
return
}

Expand All @@ -146,14 +146,14 @@ func (r *CoverageReport) AddLineHit(location Location, line int) {
}

// InspectProgram inspects the elements of the given *ast.Program, and counts its
// statements. If inspection is successful, the *ast.Program is marked as inspected.
// statements. If inspection is successful, the location is marked as inspected.
// If the given location is excluded from coverage collection, the method call
// results in a NO-OP.
func (r *CoverageReport) InspectProgram(location Location, program *ast.Program) {
if r.IsLocationExcluded(location) {
return
}
r.Programs[location] = program
r.Locations[location] = struct{}{}
lineHits := make(map[int]int, 0)
recordLine := func(hasPosition ast.HasPosition) {
line := hasPosition.StartPosition().Line
Expand Down Expand Up @@ -202,83 +202,258 @@ func (r *CoverageReport) InspectProgram(location Location, program *ast.Program)
return true
})

locationCoverage := NewLocationCoverage(lineHits)
r.Coverage[location] = locationCoverage
r.Coverage[location] = NewLocationCoverage(lineHits)
}

// IsProgramInspected checks whether the *ast.Program on the given
// location, has been inspected or not.
func (r *CoverageReport) IsProgramInspected(location Location) bool {
_, isInspected := r.Programs[location]
// IsLocationInspected checks whether the given location,
// has been inspected or not.
func (r *CoverageReport) IsLocationInspected(location Location) bool {
_, isInspected := r.Locations[location]
return isInspected
}

// CoveredStatementsPercentage returns a string representation of
// the covered statements percentage. It is defined as the ratio
// of total covered lines over total statements, for all locations.
func (r *CoverageReport) CoveredStatementsPercentage() string {
totalStatements := 0
totalCoveredLines := 0
for _, locationCoverage := range r.Coverage { // nolint:maprange
totalStatements += locationCoverage.Statements
totalCoveredLines += locationCoverage.CoveredLines()
}
percentage := fmt.Sprintf(
// Percentage returns a string representation of the covered statements
// percentage. It is defined as the ratio of total covered lines over
// total statements, for all locations.
func (r *CoverageReport) Percentage() string {
totalStatements := r.Statements()
totalCoveredLines := r.Hits()
return fmt.Sprintf(
"%0.1f%%",
100*float64(totalCoveredLines)/float64(totalStatements),
)
return fmt.Sprintf("Coverage: %v of statements", percentage)
}

// String returns a human-friendly message for the covered
// statements percentage.
func (r *CoverageReport) String() string {
return fmt.Sprintf("Coverage: %v of statements", r.Percentage())
}

// Reset flushes the collected coverage information for all locations
// and inspected programs. Excluded locations remain intact.
// and inspected locations. Excluded locations remain intact.
func (r *CoverageReport) Reset() {
for location := range r.Coverage { // nolint:maprange
delete(r.Coverage, location)
}
for program := range r.Programs { // nolint:maprange
delete(r.Programs, program)
for location := range r.Locations { // nolint:maprange
delete(r.Locations, location)
}
}

// Merge adds all the collected coverage information to the
// calling object. Excluded locations are also taken into
// account.
func (r *CoverageReport) Merge(other CoverageReport) {
for location, locationCoverage := range other.Coverage { // nolint:maprange
r.Coverage[location] = locationCoverage
}
for location, v := range other.Locations { // nolint:maprange
r.Locations[location] = v
}
for location, v := range other.ExcludedLocations { // nolint:maprange
r.ExcludedLocations[location] = v
}
}

// ExcludedLocationIDs returns the ID of each excluded location. This
// is helpful in order to marshal/unmarshal a CoverageReport, without
// losing any valuable information.
func (r *CoverageReport) ExcludedLocationIDs() []string {
excludedLocationIDs := make([]string, 0, len(r.ExcludedLocations))
for location := range r.ExcludedLocations { // nolint:maprange
excludedLocationIDs = append(excludedLocationIDs, location.ID())
}
return excludedLocationIDs
}

// TotalLocations returns the count of locations included in
// the CoverageReport. This implies that these locations are:
// - inspected,
// - not marked as exlucded.
func (r *CoverageReport) TotalLocations() int {
return len(r.Coverage)
}

// Statements returns the total count of statements, for all the
// locations included in the CoverageReport.
func (r *CoverageReport) Statements() int {
totalStatements := 0
for _, locationCoverage := range r.Coverage { // nolint:maprange
totalStatements += locationCoverage.Statements
}
return totalStatements
}

// Hits returns the total count of covered lines, for all the
// locations included in the CoverageReport.
func (r *CoverageReport) Hits() int {
totalCoveredLines := 0
for _, locationCoverage := range r.Coverage { // nolint:maprange
totalCoveredLines += locationCoverage.CoveredLines()
}
return totalCoveredLines
}

// Misses returns the total count of non-covered lines, for all
// the locations included in the CoverageReport.
func (r *CoverageReport) Misses() int {
return r.Statements() - r.Hits()
}

// Summary returns a CoverageReportSummary object, containing
// key metrics for a CoverageReport, such as:
// - Total Locations,
// - Total Statements,
// - Total Hits,
// - Total Misses,
// - Overall Coverage Percentage.
func (r *CoverageReport) Summary() CoverageReportSummary {
return CoverageReportSummary{
Locations: r.TotalLocations(),
Statements: r.Statements(),
Hits: r.Hits(),
Misses: r.Misses(),
Coverage: r.Percentage(),
}
}

// Diff computes the incremental diff between the calling object and
// a new CoverageReport. The returned result is a CoverageReportSummary
// object.
//
// CoverageReportSummary{
// Locations: 0,
// Statements: 0,
// Hits: 2,
// Misses: -2,
// Coverage: "100.0%",
// }
//
// The above diff is interpreted as follows:
// - No diff in locations,
// - No diff in statements,
// - Hits increased by 2,
// - Misses decreased by 2,
// - Coverage Δ increased by 100.0%.
func (r *CoverageReport) Diff(other CoverageReport) CoverageReportSummary {
baseCoverage := 100 * float64(r.Hits()) / float64(r.Statements())
newCoverage := 100 * float64(other.Hits()) / float64(other.Statements())
coverageDelta := fmt.Sprintf(
"%0.1f%%",
100*(newCoverage-baseCoverage)/baseCoverage,
)
return CoverageReportSummary{
Locations: other.TotalLocations() - r.TotalLocations(),
Statements: other.Statements() - r.Statements(),
Hits: other.Hits() - r.Hits(),
Misses: other.Misses() - r.Misses(),
Coverage: coverageDelta,
}
}

// CoverageReportSummary contains key metrics that are derived
// from a CoverageReport object, such as:
// - Total Locations,
// - Total Statements,
// - Total Hits,
// - Total Misses,
// - Overall Coverage Percentage.
// This metrics can be utilized in various ways, such as a CI
// plugin/app.
type CoverageReportSummary struct {
Locations int `json:"locations"`
Statements int `json:"statements"`
Hits int `json:"hits"`
Misses int `json:"misses"`
Coverage string `json:"coverage"`
}

// NewCoverageReport creates and returns a *CoverageReport.
func NewCoverageReport() *CoverageReport {
return &CoverageReport{
Coverage: map[common.Location]*LocationCoverage{},
Programs: map[common.Location]*ast.Program{},
Locations: map[common.Location]struct{}{},
ExcludedLocations: map[common.Location]struct{}{},
}
}

type crAlias CoverageReport

// To avoid the overhead of having the Percentage & MissedLines
// as fields in the LocationCoverage struct, we simply populate
// this lcAlias struct, with the corresponding methods, upon marshalling.
type lcAlias struct {
LineHits map[int]int `json:"line_hits"`
MissedLines []int `json:"missed_lines"`
Statements int `json:"statements"`
Percentage string `json:"percentage"`
}

// MarshalJSON serializes each common.Location/*LocationCoverage
// key/value pair on the *CoverageReport.Coverage map.
// key/value pair on the *CoverageReport.Coverage map, as well
// as the IDs on the *CoverageReport.ExcludedLocations map.
func (r *CoverageReport) MarshalJSON() ([]byte, error) {
type Alias CoverageReport

// To avoid the overhead of having the Percentage & MissedLines
// as fields in the LocationCoverage struct, we simply populate
// this LC struct, with the corresponding methods, upon marshalling.
type LC struct {
LineHits map[int]int `json:"line_hits"`
MissedLines []int `json:"missed_lines"`
Statements int `json:"statements"`
Percentage string `json:"percentage"`
}

coverage := make(map[string]LC, len(r.Coverage))
coverage := make(map[string]lcAlias, len(r.Coverage))
for location, locationCoverage := range r.Coverage { // nolint:maprange
coverage[location.ID()] = LC{
coverage[location.ID()] = lcAlias{
LineHits: locationCoverage.LineHits,
MissedLines: locationCoverage.MissedLines(),
Statements: locationCoverage.Statements,
Percentage: locationCoverage.Percentage(),
}
}
return json.Marshal(&struct {
Coverage map[string]LC `json:"coverage"`
*Alias
Coverage map[string]lcAlias `json:"coverage"`
ExcludedLocations []string `json:"excluded_locations"`
*crAlias
}{
Coverage: coverage,
Alias: (*Alias)(r),
Coverage: coverage,
ExcludedLocations: r.ExcludedLocationIDs(),
crAlias: (*crAlias)(r),
})
}

// UnmarshalJSON deserializes a JSON structure and populates
// the calling object with the respective *CoverageReport.Coverage &
// *CoverageReport.ExcludedLocations maps.
func (r *CoverageReport) UnmarshalJSON(data []byte) error {
cr := &struct {
Coverage map[string]lcAlias `json:"coverage"`
ExcludedLocations []string `json:"excluded_locations"`
*crAlias
}{
crAlias: (*crAlias)(r),
}

if err := json.Unmarshal(data, cr); err != nil {
return err
}

for locationID, locationCoverage := range cr.Coverage { // nolint:maprange
location, _, err := common.DecodeTypeID(nil, locationID)
if err != nil {
return err
}
if location == nil {
return fmt.Errorf("invalid Location ID: %s", locationID)
}
r.Coverage[location] = &LocationCoverage{
LineHits: locationCoverage.LineHits,
Statements: locationCoverage.Statements,
}
r.Locations[location] = struct{}{}
}
for _, locationID := range cr.ExcludedLocations {
location, _, err := common.DecodeTypeID(nil, locationID)
if err != nil {
return err
}
if location == nil {
return fmt.Errorf("invalid Location ID: %s", locationID)
}
r.ExcludedLocations[location] = struct{}{}
}

return nil
}
Loading

0 comments on commit 2a3eac6

Please sign in to comment.