Skip to content

Commit 645aa38

Browse files
Use coverage dup for statistics aggregation
1 parent 29e232c commit 645aa38

4 files changed

Lines changed: 131 additions & 43 deletions

File tree

lib/covered/coverage.rb

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,16 @@ def initialize(source, counts = [], annotations = {})
5151
@annotations = annotations
5252
end
5353

54+
# Initialize a copy of this coverage object.
55+
# @parameter other [Covered::Coverage] The coverage object to copy.
56+
def initialize_copy(other)
57+
super
58+
59+
@source = other.source.dup
60+
@counts = other.counts.dup
61+
@annotations = other.annotations.transform_values(&:dup)
62+
end
63+
5464
# @attribute [Covered::Source] The covered source metadata.
5565
attr_accessor :source
5666

lib/covered/statistics.rb

Lines changed: 37 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -24,36 +24,29 @@ def self.for(coverage)
2424
end
2525
end
2626

27-
# Immutable aggregate coverage statistics.
27+
# Aggregate coverage totals.
2828
class Aggregate
2929
include Ratio
3030

31-
# Initialize aggregate statistics from coverage objects.
31+
# Build aggregate statistics from coverage objects.
3232
# @parameter coverages [Enumerable(Covered::Coverage)] The coverage objects to summarize.
33-
def initialize(coverages = [])
34-
paths = Hash.new
35-
36-
coverages.each do |coverage|
37-
current = paths[coverage.path]
38-
39-
unless current
40-
current = paths[coverage.path] = coverage.empty
33+
# @returns [Covered::Statistics::Aggregate] The aggregate statistics.
34+
def self.for(coverages)
35+
self.new.tap do |aggregate|
36+
coverages.each do |coverage|
37+
aggregate << coverage
4138
end
42-
43-
current.merge!(coverage)
4439
end
45-
46-
paths.each_value(&:freeze)
47-
48-
@count = paths.size
49-
@executable_count = paths.sum{|_path, coverage| coverage.executable_count}
50-
@executed_count = paths.sum{|_path, coverage| coverage.executed_count}
51-
52-
freeze
5340
end
5441

55-
# Total number of files added.
56-
# @returns [Integer] The number of covered files.
42+
# Initialize empty aggregate statistics.
43+
def initialize
44+
@count = 0
45+
@executable_count = 0
46+
@executed_count = 0
47+
end
48+
49+
# @attribute [Integer] The total number of coverage instances added.
5750
attr :count
5851

5952
# The number of lines which could have been executed.
@@ -81,6 +74,18 @@ def as_json
8174
def to_json(options)
8275
as_json.to_json(options)
8376
end
77+
78+
# Add coverage to these aggregate statistics.
79+
# @parameter coverage [Covered::Coverage] The coverage object to add.
80+
# @returns [Covered::Statistics::Aggregate] This aggregate.
81+
def << coverage
82+
@count += 1
83+
84+
@executable_count += coverage.executable_count
85+
@executed_count += coverage.executed_count
86+
87+
self
88+
end
8489
end
8590

8691
# Initialize empty coverage statistics.
@@ -92,7 +97,7 @@ def initialize
9297
# The total aggregate statistics.
9398
# @returns [Covered::Statistics::Aggregate] The total aggregate statistics.
9499
def total
95-
@total ||= Aggregate.new(@paths.values)
100+
@total ||= Aggregate.for(@paths.values)
96101
end
97102

98103
# @attribute [Hash(String, Covered::Coverage)] Coverage statistics indexed by path.
@@ -119,17 +124,17 @@ def executed_count
119124
# Add coverage to these statistics.
120125
# @parameter coverage [Covered::Coverage] The coverage object to add.
121126
def << coverage
122-
current = @paths[coverage.path]
123-
124-
unless current
125-
current = @paths[coverage.path] = coverage.empty
127+
if current = @paths[coverage.path]
128+
current.merge!(coverage)
129+
130+
@total = nil
131+
else
132+
coverage = @paths[coverage.path] = coverage.dup
133+
134+
@total << coverage if @total
126135
end
127136

128-
current.merge!(coverage)
129-
130-
@total = nil
131-
132-
return self
137+
self
133138
end
134139

135140
# Get coverage for the given path.

test/covered/coverage.rb

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
# frozen_string_literal: true
2+
3+
# Released under the MIT License.
4+
# Copyright, 2018-2025, by Samuel Williams.
5+
6+
require "covered/coverage"
7+
8+
describe Covered::Coverage do
9+
let(:source) {Covered::Source.new("foo.rb")}
10+
let(:coverage) {subject.new(source, [nil, 1], 1 => ["covered"])}
11+
12+
it "can be duplicated" do
13+
copy = coverage.dup
14+
15+
expect(copy.equal?(coverage)).to be == false
16+
expect(copy.source.equal?(coverage.source)).to be == false
17+
expect(copy.counts).to be == coverage.counts
18+
expect(copy.counts.equal?(coverage.counts)).to be == false
19+
expect(copy.annotations).to be == coverage.annotations
20+
expect(copy.annotations.equal?(coverage.annotations)).to be == false
21+
expect(copy.annotations[1].equal?(coverage.annotations[1])).to be == false
22+
end
23+
24+
it "does not share mutable state with duplicates" do
25+
copy = coverage.dup
26+
27+
copy.mark(2, 1)
28+
copy.annotate(1, "copy")
29+
copy.path = "copy.rb"
30+
31+
expect(coverage.path).to be == "foo.rb"
32+
expect(coverage.counts).to be == [nil, 1]
33+
expect(coverage.annotations).to be == {1 => ["covered"]}
34+
end
35+
end

test/covered/statistics.rb

Lines changed: 49 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -83,18 +83,53 @@ def before
8383
with "after reading total before adding coverage" do
8484
let(:partial_coverage) {Covered::Coverage.new(source, [nil, 1, 0])}
8585
let(:complete_coverage) {Covered::Coverage.new(source, [nil, 0, 1])}
86+
let(:other_coverage) {Covered::Coverage.new(Covered::Source.new("bar.rb"), [nil, 1])}
8687

87-
def before
88+
it "adds new paths to cached totals" do
8889
statistics << partial_coverage
89-
statistics.total
90-
statistics << complete_coverage
91-
super
90+
91+
total = statistics.total
92+
93+
statistics << other_coverage
94+
95+
expect(statistics.total).to be_equal(total)
96+
expect(statistics.count).to be == 2
97+
expect(statistics.executable_count).to be == 3
98+
expect(statistics.executed_count).to be == 2
9299
end
93100

94101
it "invalidates cached totals" do
102+
statistics << partial_coverage
103+
104+
total = statistics.total
105+
95106
expect(statistics.count).to be == 1
96107
expect(statistics.executable_count).to be == 2
97108
expect(statistics.executed_count).to be == 1
109+
expect(statistics.total).to be_equal(total)
110+
111+
statistics << complete_coverage
112+
113+
expect(statistics.total).not.to be_equal(total)
114+
expect(statistics.count).to be == 1
115+
expect(statistics.executable_count).to be == 2
116+
expect(statistics.executed_count).to be == 2
117+
end
118+
end
119+
120+
with "after adding coverage" do
121+
let(:coverage) {Covered::Coverage.new(source, [nil, 1])}
122+
123+
it "does not share mutable state with the original coverage" do
124+
statistics << coverage
125+
126+
coverage.mark(2, 1)
127+
coverage.path = "bar.rb"
128+
129+
expect(statistics.count).to be == 1
130+
expect(statistics["foo.rb"].counts).to be == [nil, 1]
131+
expect(statistics.executable_count).to be == 1
132+
expect(statistics.executed_count).to be == 1
98133
end
99134
end
100135
end
@@ -105,23 +140,26 @@ def before
105140

106141
with "multiple coverage objects" do
107142
let(:complete_coverage) {Covered::Coverage.new(source, [nil, 1, 1])}
108-
let(:partial_coverage) {Covered::Coverage.new(source, [nil, 1, 0])}
109143
let(:other_coverage) {Covered::Coverage.new(other_source, [nil, 0])}
110-
let(:aggregate) {subject.new([complete_coverage, partial_coverage, other_coverage])}
144+
let(:aggregate) {subject.for([complete_coverage, other_coverage])}
111145

112-
it "merges coverage for the same path" do
146+
it "summarizes coverage" do
113147
expect(aggregate.count).to be == 2
114148
expect(aggregate.executable_count).to be == 3
115149
expect(aggregate.executed_count).to be == 2
116150
end
117151
end
118152

119-
with "an existing aggregate" do
153+
with "an aggregate" do
120154
let(:coverage) {Covered::Coverage.new(source, [nil, 1])}
121-
let(:aggregate) {subject.new([coverage])}
155+
let(:aggregate) {subject.for([coverage])}
122156

123-
it "is immutable" do
124-
expect(aggregate).to be(:frozen?)
157+
it "can add coverage" do
158+
aggregate << Covered::Coverage.new(other_source, [nil, 0])
159+
160+
expect(aggregate.count).to be == 2
161+
expect(aggregate.executable_count).to be == 2
162+
expect(aggregate.executed_count).to be == 1
125163
end
126164
end
127165
end

0 commit comments

Comments
 (0)