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
10 changes: 10 additions & 0 deletions lib/covered/coverage.rb
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,16 @@ def initialize(source, counts = [], annotations = {})
@annotations = annotations
end

# Initialize a copy of this coverage object.
# @parameter other [Covered::Coverage] The coverage object to copy.
def initialize_copy(other)
super

@source = other.source.dup
@counts = other.counts.dup
@annotations = other.annotations.transform_values(&:dup)
end

# @attribute [Covered::Source] The covered source metadata.
attr_accessor :source

Expand Down
47 changes: 36 additions & 11 deletions lib/covered/statistics.rb
Original file line number Diff line number Diff line change
Expand Up @@ -28,15 +28,25 @@ def self.for(coverage)
class Aggregate
include Ratio

# Build aggregate statistics from coverage objects.
# @parameter coverages [Enumerable(Covered::Coverage)] The coverage objects to summarize.
# @returns [Covered::Statistics::Aggregate] The aggregate statistics.
def self.for(coverages)
self.new.tap do |aggregate|
coverages.each do |coverage|
aggregate << coverage
end
end
end

# Initialize empty aggregate statistics.
def initialize
@count = 0
@executable_count = 0
@executed_count = 0
end

# Total number of files added.
# @returns [Integer] The number of coverage objects added.
# @attribute [Integer] The total number of coverage instances added.
attr :count

# The number of lines which could have been executed.
Expand Down Expand Up @@ -67,22 +77,28 @@ def to_json(options)

# Add coverage to these aggregate statistics.
# @parameter coverage [Covered::Coverage] The coverage object to add.
# @returns [Covered::Statistics::Aggregate] This aggregate.
def << coverage
@count += 1

@executable_count += coverage.executable_count
@executed_count += coverage.executed_count

self
end
end

# Initialize empty coverage statistics.
def initialize
@total = Aggregate.new
@total = nil
@paths = Hash.new
end

# @attribute [Covered::Statistics::Aggregate] The total aggregate statistics.
attr :total
# The total aggregate statistics.
# @returns [Covered::Statistics::Aggregate] The total aggregate statistics.
def total
@total ||= Aggregate.for(@paths.values)
end

# @attribute [Hash(String, Covered::Coverage)] Coverage statistics indexed by path.
attr :paths
Expand All @@ -96,20 +112,29 @@ def count
# The total number of executable lines.
# @returns [Integer] The total executable line count.
def executable_count
@total.executable_count
total.executable_count
end

# The total number of executed lines.
# @returns [Integer] The total executed line count.
def executed_count
@total.executed_count
total.executed_count
end

# Add coverage to these statistics.
# @parameter coverage [Covered::Coverage] The coverage object to add.
def << coverage
@total << coverage
(@paths[coverage.path] ||= coverage.empty).merge!(coverage)
if current = @paths[coverage.path]
current.merge!(coverage)

@total = nil
else
coverage = @paths[coverage.path] = coverage.dup

@total << coverage if @total
end

self
end

# Get coverage for the given path.
Expand All @@ -124,7 +149,7 @@ def [](path)
def as_json
{
total: total.as_json,
paths: @paths.map{|path, coverage| [path, coverage.as_json]}.to_h,
paths: paths.map{|path, coverage| [path, coverage.as_json]}.to_h,
}
end

Expand All @@ -151,7 +176,7 @@ def to_json(options)
# Print a human-readable coverage summary.
# @parameter output [IO] The output stream.
def print(output)
output.puts "#{count} files checked; #{@total.executed_count}/#{@total.executable_count} lines executed; #{@total.percentage.to_f.round(2)}% covered."
output.puts "#{count} files checked; #{total.executed_count}/#{total.executable_count} lines executed; #{total.percentage.to_f.round(2)}% covered."

if self.complete?
output.puts "🧘 #{COMPLETE.sample}"
Expand Down
35 changes: 35 additions & 0 deletions test/covered/coverage.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# frozen_string_literal: true

# Released under the MIT License.
# Copyright, 2018-2025, by Samuel Williams.

require "covered/coverage"

describe Covered::Coverage do
let(:source) {Covered::Source.new("foo.rb")}
let(:coverage) {subject.new(source, [nil, 1], 1 => ["covered"])}

it "can be duplicated" do
copy = coverage.dup

expect(copy.equal?(coverage)).to be == false
expect(copy.source.equal?(coverage.source)).to be == false
expect(copy.counts).to be == coverage.counts
expect(copy.counts.equal?(coverage.counts)).to be == false
expect(copy.annotations).to be == coverage.annotations
expect(copy.annotations.equal?(coverage.annotations)).to be == false
expect(copy.annotations[1].equal?(coverage.annotations[1])).to be == false
end

it "does not share mutable state with duplicates" do
copy = coverage.dup

copy.mark(2, 1)
copy.annotate(1, "copy")
copy.path = "copy.rb"

expect(coverage.path).to be == "foo.rb"
expect(coverage.counts).to be == [nil, 1]
expect(coverage.annotations).to be == {1 => ["covered"]}
end
end
104 changes: 104 additions & 0 deletions test/covered/statistics.rb
Original file line number Diff line number Diff line change
Expand Up @@ -58,4 +58,108 @@ def before
expect(statistics).not.to be(:complete?)
end
end

with "after adding overlapping coverage" do
let(:complete_coverage) {Covered::Coverage.new(source, [nil, 1, 1])}
let(:partial_coverage) {Covered::Coverage.new(source, [nil, 1, 0])}

def before
statistics << complete_coverage
statistics << partial_coverage
super
end

it "merges coverage for the same path" do
expect(statistics.count).to be == 1
expect(statistics.executable_count).to be == 2
expect(statistics.executed_count).to be == 2
end

it "is complete" do
expect(statistics).to be(:complete?)
end
end

with "after reading total before adding coverage" do
let(:partial_coverage) {Covered::Coverage.new(source, [nil, 1, 0])}
let(:complete_coverage) {Covered::Coverage.new(source, [nil, 0, 1])}
let(:other_coverage) {Covered::Coverage.new(Covered::Source.new("bar.rb"), [nil, 1])}

it "adds new paths to cached totals" do
statistics << partial_coverage

total = statistics.total

statistics << other_coverage

expect(statistics.total).to be_equal(total)
expect(statistics.count).to be == 2
expect(statistics.executable_count).to be == 3
expect(statistics.executed_count).to be == 2
end

it "invalidates cached totals" do
statistics << partial_coverage

total = statistics.total

expect(statistics.count).to be == 1
expect(statistics.executable_count).to be == 2
expect(statistics.executed_count).to be == 1
expect(statistics.total).to be_equal(total)

statistics << complete_coverage

expect(statistics.total).not.to be_equal(total)
expect(statistics.count).to be == 1
expect(statistics.executable_count).to be == 2
expect(statistics.executed_count).to be == 2
end
end

with "after adding coverage" do
let(:coverage) {Covered::Coverage.new(source, [nil, 1])}

it "does not share mutable state with the original coverage" do
statistics << coverage

coverage.mark(2, 1)
coverage.path = "bar.rb"

expect(statistics.count).to be == 1
expect(statistics["foo.rb"].counts).to be == [nil, 1]
expect(statistics.executable_count).to be == 1
expect(statistics.executed_count).to be == 1
end
end
end

describe Covered::Statistics::Aggregate do
let(:source) {Covered::Source.new("foo.rb")}
let(:other_source) {Covered::Source.new("bar.rb")}

with "multiple coverage objects" do
let(:complete_coverage) {Covered::Coverage.new(source, [nil, 1, 1])}
let(:other_coverage) {Covered::Coverage.new(other_source, [nil, 0])}
let(:aggregate) {subject.for([complete_coverage, other_coverage])}

it "summarizes coverage" do
expect(aggregate.count).to be == 2
expect(aggregate.executable_count).to be == 3
expect(aggregate.executed_count).to be == 2
end
end

with "an aggregate" do
let(:coverage) {Covered::Coverage.new(source, [nil, 1])}
let(:aggregate) {subject.for([coverage])}

it "can add coverage" do
aggregate << Covered::Coverage.new(other_source, [nil, 0])

expect(aggregate.count).to be == 2
expect(aggregate.executable_count).to be == 2
expect(aggregate.executed_count).to be == 1
end
end
end
Loading