lib/simplecov/cli.rb



# frozen_string_literal: true

require_relative "color"
require_relative "cli/dotfile"
require_relative "cli/clean"
require_relative "cli/coverage"
require_relative "cli/diff"
require_relative "cli/merge"
require_relative "cli/open"
require_relative "cli/report"
require_relative "cli/run"
require_relative "cli/serve"
require_relative "cli/uncovered"

module SimpleCov
  # Lightweight command-line front-end. `run` dispatches a subcommand
  # (`coverage`, `report`, `uncovered`, `merge`, `diff`, `open`, etc.) —
  # see the `usage` text below for the full list, or run `simplecov help`.
  #
  # Read-only subcommands consume JSONFormatter output (`coverage.json`),
  # which the bundled HTMLFormatter already drops alongside the HTML, so
  # no runtime hooking is needed for those. Default paths follow the
  # project's `.simplecov` `SimpleCov.coverage_dir` setting when one is
  # present, so a project that writes its report somewhere other than
  # `coverage/` doesn't have to pass `--input` / `--report` every
  # invocation.
  module CLI
    COMMANDS = {
      "coverage" => Coverage,
      "run" => Run,
      "open" => Open,
      "report" => Report,
      "uncovered" => Uncovered,
      "merge" => Merge,
      "diff" => Diff,
      "serve" => Serve,
      "clean" => Clean
    }.freeze

  module_function

    # Resolved once per process. Walks up from cwd looking for a
    # `.simplecov`; if present, the file is loaded with
    # `SimpleCov.start` neutered so it can't trigger coverage tracking
    # or an at_exit hook just because we asked it for a config value.
    def coverage_dir
      @coverage_dir ||= Dotfile.coverage_dir
    end

    def default_input
      File.join(coverage_dir, "coverage.json")
    end

    # Resolve "should this subcommand colorize?" once per invocation.
    # `--no-color` (opts[:no_color]) is the per-invocation kill-switch;
    # otherwise we defer to `SimpleCov::Color.enabled?`, which honors
    # `NO_COLOR` / `FORCE_COLOR` and falls back to `stream.tty?`.
    def color_enabled?(opts, stream)
      return false if opts[:no_color]

      SimpleCov::Color.enabled?(stream)
    end

    def default_report
      File.join(coverage_dir, "index.html")
    end

    def default_resultset
      File.join(coverage_dir, ".resultset.json")
    end

    # Returns a process exit status (0 on success, non-zero on error).
    def run(argv, stdout: $stdout, stderr: $stderr)
      command, *rest = argv
      handler = COMMANDS[command]
      return handler.run(rest, stdout: stdout, stderr: stderr) if handler
      return stdout.puts(usage) || 0 if [nil, "help", "--help", "-h"].include?(command)

      stderr.puts("simplecov: unknown command #{command.inspect}", usage)
      1
    end

    def usage
      <<~USAGE
        Usage: simplecov <command> [options]

        Commands:
          run <command...>          Execute <command> with simplecov pre-loaded
                                    (so a coverage report is generated even
                                    when the project has no test_helper hook)
          coverage <path>           Print coverage stats for the given file
          report                    Print the overall summary and group totals
          uncovered                 List the lowest-coverage files
          merge <files...>          Merge multiple .resultset.json files
          diff <baseline>           Show per-file coverage delta vs baseline
          open                      Open the HTML report in the default browser
          serve                     Serve the coverage report over HTTP
          clean                     Remove the coverage report directory
          help                      Show this message

        Default paths follow SimpleCov.coverage_dir from a project's
        `.simplecov` when one is present (#{coverage_dir} for this run).

        coverage / report / uncovered / diff options:
          --input PATH              Read from PATH instead of #{default_input}
          --no-color                Disable colorized percentages
                                    (also honors NO_COLOR / FORCE_COLOR env)

        coverage options:
          --json                    Print the file's JSON entry verbatim

        report options:
          --json                    Emit totals and group sections as JSON

        uncovered options:
          --threshold N             Only show files below N% coverage
          --top N                   Show at most N files (default: 10)
          --criterion C             line, branch, or method (default: line)
          --json                    Emit results as a JSON array (for CI)

        merge options:
          --output PATH             Write merged resultset to PATH
                                    (default: #{default_resultset})
          --honor-timeout           Drop entries older than merge_timeout
          --dry-run                 Print what would be written without
                                    actually writing
          -q, --quiet               Suppress the success status line

        diff options:
          --fail-on-drop            Exit non-zero when any file's coverage
                                    dropped vs the baseline
          --json                    Emit results as a JSON array (for CI)
          --threshold N             Only show files whose absolute delta
                                    in any criterion is at least N%

        open options:
          --report PATH             Open PATH instead of #{default_report}

        serve options:
          --port N                  Bind to port N (default: random open port)
          --host HOST               Bind to HOST (default: 127.0.0.1)

        clean options:
          --dry-run                 Print what would be removed without
                                    deleting anything
          -q, --quiet               Suppress status lines
      USAGE
    end
  end
end