lib/simplecov/formatter/base.rb



# frozen_string_literal: true

require "pathname"

module SimpleCov
  module Formatter
    # @api private
    #
    # Shared scaffolding for formatters that write a coverage report to
    # an output directory and emit a "Coverage report generated for X
    # to Y" summary on stderr (it's a status message, not data).
    # Subclasses override `format` to do their actual writing, and may
    # override `message_prefix` (e.g. JSON prepends "JSON ").
    class Base
      # `output_dir` defaults to `SimpleCov.coverage_path` so the at_exit
      # pipeline keeps working unchanged. Pass it explicitly to write
      # somewhere else (handy for tests that don't want to clobber the
      # project's `coverage/` directory).
      def initialize(silent: false, output_dir: nil)
        @silent = silent
        @output_dir = output_dir
      end

    private

      # Subclasses override to prepend a marker (e.g. "JSON ") to the
      # summary line. Default empty for the HTML formatter, which has
      # historically been the unmarked default.
      def message_prefix
        ""
      end

      def output_path
        @output_dir || SimpleCov.coverage_path
      end

      # The path shown in the "Coverage report generated for X to Y"
      # status line. Renders relative to cwd when `output_path` lives
      # inside cwd (e.g. `coverage` instead of `/Users/me/proj/coverage`)
      # and appends the formatter's `entry_point_filename` so the line
      # points at a concrete file the user (or a terminal that
      # hyperlinks paths) can act on — e.g. `coverage/index.html`
      # instead of the bare directory `coverage`. Paths outside cwd
      # stay absolute; a `../../../tmp/cov` display would be more
      # confusing than the absolute form. See issue #197.
      def displayable_output_path
        directory = relative_or_absolute_output_path
        entry_point = entry_point_filename
        entry_point ? File.join(directory, entry_point) : directory
      end

      def relative_or_absolute_output_path
        absolute = output_path
        relative = Pathname.new(absolute).relative_path_from(Pathname.pwd).to_s
        relative.start_with?("..") ? absolute : relative
      rescue ArgumentError
        # Pathname#relative_path_from raises across mixed absolute/
        # relative inputs (and across Windows drives) — keep the
        # absolute form on any unresolvable case.
        output_path
      end

      # Subclasses override to name the report's entry-point file
      # (e.g. `index.html` for HTML, `coverage.json` for JSON), which
      # gets appended to the directory in the status line. Default nil
      # leaves the bare directory in place for any third-party formatter
      # that has no single canonical entry point.
      def entry_point_filename
        nil
      end

      # Emit one summary line per criterion that the run actually
      # measured. The header line ("Coverage report generated for X
      # to Y") is always first; per-criterion lines follow in the
      # order of `result.coverage_statistics` (which is the same
      # insertion order as `SourceFile#coverage_statistics`, which in
      # turn reflects what the user enabled).
      def output_message(result)
        header = "#{message_prefix}Coverage report generated for #{result.command_name} to #{displayable_output_path}"
        body   = result.coverage_statistics.filter_map { |criterion, stat| stats_line(criterion, stat) }
        [header, *body].join("\n")
      end

      # Returns nil for branch/method criteria that have nothing to
      # measure (e.g. a file with no branches under branch coverage).
      # Showing "Branch coverage: 0 / 0 (100.00%)" is noise; the older
      # output specifically suppressed it.
      def stats_line(criterion, stat)
        return if criterion != :line && !stat.total.positive?

        percent = SimpleCov.round_coverage(stat.percent)
        Kernel.format(
          "%<label>s coverage: %<covered>d / %<total>d (%<percent>s)",
          label: criterion.to_s.capitalize,
          covered: stat.covered,
          total: stat.total,
          percent: SimpleCov::Color.colorize_percent(percent)
        )
      end
    end
  end
end