class Covered::Coverage

Stores line execution counts and source metadata for a single file.

def self.deserialize(unpacker)

@returns [Covered::Coverage] The deserialized coverage object.
@parameter unpacker [Object] The MessagePack-compatible unpacker.
Deserialize a coverage object from the given unpacker.
def self.deserialize(unpacker)
	source = unpacker.read
	counts = unpacker.read
	annotations = unpacker.read
	
	self.new(source, counts, annotations)
end

def self.for(path, **options)

@returns [Covered::Coverage] The coverage object for the source path.
@parameter options [Hash] Options forwarded to {Covered::Source.for}.
@parameter path [String] The source path.
Build coverage for the given source path.
def self.for(path, **options)
	self.new(Source.for(path, **options))
end

def [] line_number

@returns [Integer | Nil] The execution count for the line.
@parameter line_number [Integer] The line number to query.
The raw coverage count for the given line number.
def [] line_number
	@counts[line_number]
end

def annotate(line_number, annotation)

@parameter annotation [String] The annotation text.
@parameter line_number [Integer] The line number to annotate.
Add an annotation to the given line number.
def annotate(line_number, annotation)
	@annotations[line_number] ||= []
	@annotations[line_number] << annotation
end

def as_json

@returns [Hash] The coverage counts and summary statistics.
A JSON-compatible representation of this coverage object.
def as_json
	{
		counts: counts,
		executable_count: executable_count,
		executed_count: executed_count,
		percentage: percentage.to_f.round(2),
	}
end

def empty

@returns [Covered::Coverage] Coverage with the same source and `nil` counts.
Create an empty coverage with the same source.
def empty
	self.class.new(@source, [nil] * @counts.size)
end

def executable_count

@returns [Integer] The number of executable lines.
The number of executable lines.
def executable_count
	executable_lines.count
end

def executable_lines

@returns [Array(Integer)] Execution counts for executable lines.
Counts for lines that are executable.
def executable_lines
	@counts.compact
end

def executed_count

@returns [Integer] The number of executed lines.
The number of executable lines that were executed.
def executed_count
	executed_lines.count
end

def executed_lines

@returns [Array(Integer)] Non-zero execution counts for executable lines.
Counts for executable lines that were executed.
def executed_lines
	executable_lines.reject(&:zero?)
end

def for_lines(line_numbers)

@returns [Covered::Coverage] A coverage object containing counts for the selected lines.
@parameter line_numbers [Array(Integer)] The line numbers to include in the new coverage object.
Construct a new coverage object for the given line numbers. Only the given line numbers will be considered for the purposes of computing coverage.
def for_lines(line_numbers)
	counts = [nil] * @counts.size
	line_numbers.each do |line_number|
		counts[line_number] = @counts[line_number]
	end
	
	self.class.new(@source, counts, @annotations)
end

def freeze

@returns [Covered::Coverage] This frozen coverage object.
Freeze this coverage and its mutable collections.
def freeze
	return self if frozen?
	
	@counts.freeze
	@annotations.freeze
	
	super
end

def fresh?

@returns [Boolean] Whether the source exists and its modification time is not newer than the recorded time.
Whether the source file has not changed since this coverage was recorded.
def fresh?
	if @source.modified_time.nil?
		# We don't know when the file was last modified, so we assume it is stale:
		return false
	end
	
	unless File.exist?(@source.path)
		# The file no longer exists, so we assume it is stale:
		return false
	end
	
	if @source.modified_time >= File.mtime(@source.path)
		# The file has not been modified since we last processed it, so we assume it is fresh:
		return true
	end
	
	return false
end

def initialize(source, counts = [], annotations = {})

@parameter annotations [Hash(Integer, Array(String))] Line annotations indexed by line number.
@parameter counts [Array(Integer | Nil)] Line execution counts indexed by line number.
@parameter source [Covered::Source] The covered source metadata.
Initialize coverage with the given source, line counts and annotations.
def initialize(source, counts = [], annotations = {})
	@source = source
	@counts = counts
	@annotations = annotations
end

def mark(line_number, value = 1)

@parameter value [Integer | Array(Integer)] The execution count or counts to add.
@parameter line_number [Integer] The first line number to mark.
Add the given execution count to one or more line numbers.
def mark(line_number, value = 1)
	# As currently implemented, @counts is base-zero rather than base-one.
	# Line numbers generally start at line 1, so the first line, line 1, is at index 1. This means that index[0] is usually nil.
	Array(value).each_with_index do |value, index|
		offset = line_number + index
		if @counts[offset]
			@counts[offset] += value
		else
			@counts[offset] = value
		end
	end
end

def merge!(other)

@parameter other [Covered::Coverage] The coverage object to merge.
Merge another coverage object into this coverage object.
def merge!(other)
	# If the counts are non-zero and don't match, that can indicate a problem.
	
	other.counts.each_with_index do |count, index|
		if count
			@counts[index] ||= 0
			@counts[index] += count
		end
	end
	
	@annotations.merge!(other.annotations) do |line_number, a, b|
		Array(a) + Array(b)
	end
end

def missing_count

@returns [Integer] The number of missing executable lines.
The number of executable lines that were not executed.
def missing_count
	executable_count - executed_count
end

def path

@returns [String] The covered source path.
The covered source path.
def path
	@source.path
end

def path= value

@parameter value [String] The new covered source path.
Assign the covered source path.
def path= value
	@source.path = value
end

def print(output)

@parameter output [IO] The output stream.
Print a human-readable coverage summary.
def print(output)
	output.puts "** #{executed_count}/#{executable_count} lines executed; #{percentage.to_f.round(2)}% covered."
end

def read(&block)

@returns [String | Object] The source contents without a block, or the block result with a block.
@parameter file [File] The open source file.
@yields {|file| ...} If a block is given, yields an open source file.
Read the covered source.
def read(&block)
	@source.read(&block)
end

def serialize(packer)

@parameter packer [Object] The MessagePack-compatible packer.
Serialize this coverage object with the given packer.
def serialize(packer)
	packer.write(@source)
	packer.write(@counts)
	packer.write(@annotations)
end

def to_a

@returns [Array(Integer | Nil)] The raw coverage counts.
The raw coverage counts array.
def to_a
	@counts
end

def to_s

@returns [String] A summary including the source path and percentage.
A human-readable representation of this coverage object.
def to_s
	"\#<#{self.class} path=#{self.path} #{self.percentage.to_f.round(2)}% covered>"
end

def total

@returns [Integer] The sum of all non-`nil` execution counts.
The total number of executions across all tracked lines.
def total
	counts.sum{|count| count || 0}
end

def zero?

@returns [Boolean] Whether the total execution count is zero.
Whether this coverage has no executions.
def zero?
	total.zero?
end