lib/covered/policy.rb



# frozen_string_literal: true

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

require_relative "summary"
require_relative "files"
require_relative "capture"
require_relative "persist"
require_relative "forks"

module Covered
	# Configures coverage collection, filtering, persistence and reports.
	class Policy < Wrapper
		# Initialize a policy with an empty file collection.
		def initialize
			super(Files.new)
			
			@reports = []
			@capture = nil
		end
		
		# @attribute [Covered::Base] The current output pipeline.
		attr :output
		
		# Freeze the policy and eagerly build the capture pipeline.
		# @returns [Covered::Policy] This frozen policy.
		def freeze
			return self if frozen?
			
			capture
			@reports.freeze
			
			super
		end
		
		# Include files matching the given pattern in coverage results.
		# Arguments are forwarded to {Covered::Include#initialize}.
		def include(...)
			@output = Include.new(@output, ...)
		end
		
		# Exclude files matching the given pattern from coverage results.
		# Arguments are forwarded to {Covered::Skip#initialize}.
		def skip(...)
			@output = Skip.new(@output, ...)
		end
		
		# Restrict coverage results to files matching the given pattern.
		# Arguments are forwarded to {Covered::Only#initialize}.
		def only(...)
			@output = Only.new(@output, ...)
		end
		
		# Restrict coverage results to the given project root.
		# Arguments are forwarded to {Covered::Root#initialize}.
		def root(...)
			@output = Root.new(@output, ...)
		end
		
		# Persist coverage results to a database.
		# Arguments are forwarded to {Covered::Persist#initialize}.
		def persist!(...)
			@output = Persist.new(@output, ...)
		end
		
		# The runtime capture pipeline for this policy.
		# @returns [Covered::Forks] The memoized capture pipeline.
		def capture
			@capture ||= Forks.new(
				Capture.new(@output)
			)
		end
		
		# Start collecting coverage.
		def start
			capture.start
		end
		
		# Finish collecting coverage.
		def finish
			capture.finish
		end
		
		# @attribute [Array] The configured report objects or autoloaders.
		attr :reports
		
		# Lazily loads a report class when it is first used.
		class Autoload
			# Initialize an autoloaded report with the given constant name.
			# @parameter name [String] The report class name under {Covered}.
			def initialize(name)
				@name = name
			end
			
			# Instantiate the report class.
			# @returns [Object] A new report instance.
			def new
				begin
					klass = Covered.const_get(@name)
				rescue NameError
					require_relative(snake_case(@name))
				end
				
				klass = Covered.const_get(@name)
				
				return klass.new
			end
			
			# Instantiate the report and call it.
			# Arguments are forwarded to the report.
			def call(...)
				self.new.call(...)
			end
			
			# A human-readable representation of this autoloaded report.
			# @returns [String] A debug representation of the autoloader.
			def to_s
				"\#<#{self.class} loading #{@name}>"
			end
			
			private
			
			def snake_case(string)
				return string.gsub(/::/, "/").
					gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2').
					gsub(/([a-z\d])([A-Z])/,'\1_\2').
					tr("-", "_").
					downcase
			end
		end
		
		# Configure reports from names, booleans, arrays or report objects.
		# @parameter reports [String | Boolean | Array | Object | Nil] The reports to configure.
		def reports!(reports)
			if reports.nil?
				return
			elsif reports.is_a?(String)
				names = reports.split(",")
				
				names.each do |name|
					begin
						klass = Covered.const_get(name)
						@reports << klass.new
					rescue NameError
						@reports << Autoload.new(name)
					end
				end
			elsif reports == true
				@reports << Covered::BriefSummary.new
			elsif reports == false
				@reports.clear
			elsif reports.is_a?(Array)
				@reports.concat(reports)
			else
				@reports << reports
			end
		end
		
		# Generate all configured reports.
		# Arguments are forwarded to each report.
		def call(...)
			@reports.each do |report|
				report.call(self, ...)
			end
		end
	end
end