lib/covered/statistics.rb
# frozen_string_literal: true # Released under the MIT License. # Copyright, 2018-2025, by Samuel Williams. require_relative "wrapper" require_relative "coverage" module Covered # Raised when coverage does not meet the configured minimum. class CoverageError < StandardError end # Aggregates coverage statistics across files. class Statistics include Ratio # Build statistics for a single coverage object. # @parameter coverage [Covered::Coverage] The coverage object to summarize. # @returns [Covered::Statistics] Statistics containing the given coverage. def self.for(coverage) self.new.tap do |statistics| statistics << coverage end end # Aggregate coverage totals. 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 # @attribute [Integer] The total number of coverage instances added. attr :count # The number of lines which could have been executed. # @returns [Integer] The executable line count. attr :executable_count # The number of lines that were executed. # @returns [Integer] The executed line count. attr :executed_count # A JSON-compatible representation of these aggregate statistics. # @returns [Hash] The aggregate count, line counts and percentage. def as_json { count: count, executable_count: executable_count, executed_count: executed_count, percentage: percentage.to_f.round(2), } end # Convert these aggregate statistics to JSON. # @parameter options [Hash] Options forwarded to `to_json`. # @returns [String] The JSON representation. def to_json(options) as_json.to_json(options) end # 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 = nil @paths = Hash.new end # 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 # The number of unique paths with coverage data. # @returns [Integer] The number of unique paths. def count @paths.size end # The total number of executable lines. # @returns [Integer] The total executable line count. def 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 end # Add coverage to these statistics. # @parameter coverage [Covered::Coverage] The coverage object to add. def << 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. # @parameter path [String] The source path. # @returns [Covered::Coverage | Nil] The merged coverage for the path. def [](path) @paths[path] end # A JSON-compatible representation of these statistics. # @returns [Hash] The total statistics and path statistics. def as_json { total: total.as_json, paths: paths.map{|path, coverage| [path, coverage.as_json]}.to_h, } end # Convert these statistics to JSON. # @parameter options [Hash] Options forwarded to `to_json`. # @returns [String] The JSON representation. def to_json(options) as_json.to_json(options) end COMPLETE = [ "Enter the code dojo: 100% coverage attained, bugs defeated with one swift strike.", "Nirvana reached: 100% code coverage, where bugs meditate and vanish like a passing cloud.", "With 100% coverage, your code has unlocked the path to enlightenment – bugs have no place to hide.", "In the realm of code serenity, 100% coverage is your ticket to coding enlightenment.", "100% coverage, where code and bugs coexist in perfect harmony, like Yin and Yang.", "Achieving the Zen of code coverage, your code is a peaceful garden where bugs find no shelter.", "Congratulations on coding enlightenment! 100% coverage means your code is one with the universe.", "With 100% coverage, your code is a tranquil pond where bugs cause no ripples.", "At the peak of code mastery: 100% coverage, where bugs bow down before the wisdom of your code.", "100% code coverage: Zen achieved! Bugs in harmony, code at peace.", ] # 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." if self.complete? output.puts "🧘 #{COMPLETE.sample}" end end # Validate that coverage meets the given minimum ratio. # @parameter minimum [Numeric] The minimum accepted coverage ratio. # @raises [Covered::CoverageError] If coverage is below the minimum ratio. def validate!(minimum = 1.0) if total.ratio < minimum raise CoverageError, "Coverage of #{self.percentage.to_f.round(2)}% is less than required minimum of #{(minimum * 100.0).round(2)}%!" end end end end