ultra-pro-pkg 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (117) hide show
  1. checksums.yaml +7 -0
  2. data/simplecov-1.0.0/LICENSE +20 -0
  3. data/simplecov-1.0.0/README.md +1524 -0
  4. data/simplecov-1.0.0/doc/alternate-formatters.md +66 -0
  5. data/simplecov-1.0.0/doc/commercial-services.md +25 -0
  6. data/simplecov-1.0.0/doc/editor-integration.md +18 -0
  7. data/simplecov-1.0.0/exe/simplecov +11 -0
  8. data/simplecov-1.0.0/lib/minitest/simplecov_plugin.rb +171 -0
  9. data/simplecov-1.0.0/lib/simplecov/autostart.rb +11 -0
  10. data/simplecov-1.0.0/lib/simplecov/cli/clean.rb +47 -0
  11. data/simplecov-1.0.0/lib/simplecov/cli/coverage.rb +91 -0
  12. data/simplecov-1.0.0/lib/simplecov/cli/diff.rb +151 -0
  13. data/simplecov-1.0.0/lib/simplecov/cli/dotfile.rb +100 -0
  14. data/simplecov-1.0.0/lib/simplecov/cli/merge.rb +116 -0
  15. data/simplecov-1.0.0/lib/simplecov/cli/open.rb +50 -0
  16. data/simplecov-1.0.0/lib/simplecov/cli/report.rb +84 -0
  17. data/simplecov-1.0.0/lib/simplecov/cli/run.rb +36 -0
  18. data/simplecov-1.0.0/lib/simplecov/cli/serve.rb +139 -0
  19. data/simplecov-1.0.0/lib/simplecov/cli/uncovered.rb +107 -0
  20. data/simplecov-1.0.0/lib/simplecov/cli.rb +150 -0
  21. data/simplecov-1.0.0/lib/simplecov/color.rb +74 -0
  22. data/simplecov-1.0.0/lib/simplecov/combine/branches_combiner.rb +54 -0
  23. data/simplecov-1.0.0/lib/simplecov/combine/files_combiner.rb +30 -0
  24. data/simplecov-1.0.0/lib/simplecov/combine/lines_combiner.rb +45 -0
  25. data/simplecov-1.0.0/lib/simplecov/combine/methods_combiner.rb +26 -0
  26. data/simplecov-1.0.0/lib/simplecov/combine/results_combiner.rb +61 -0
  27. data/simplecov-1.0.0/lib/simplecov/combine.rb +30 -0
  28. data/simplecov-1.0.0/lib/simplecov/command_guesser.rb +78 -0
  29. data/simplecov-1.0.0/lib/simplecov/configuration/coverage.rb +171 -0
  30. data/simplecov-1.0.0/lib/simplecov/configuration/coverage_criteria.rb +156 -0
  31. data/simplecov-1.0.0/lib/simplecov/configuration/filters.rb +197 -0
  32. data/simplecov-1.0.0/lib/simplecov/configuration/formatting.rb +119 -0
  33. data/simplecov-1.0.0/lib/simplecov/configuration/ignored_entries.rb +63 -0
  34. data/simplecov-1.0.0/lib/simplecov/configuration/merging.rb +164 -0
  35. data/simplecov-1.0.0/lib/simplecov/configuration/thresholds.rb +174 -0
  36. data/simplecov-1.0.0/lib/simplecov/configuration.rb +184 -0
  37. data/simplecov-1.0.0/lib/simplecov/coverage_statistics.rb +59 -0
  38. data/simplecov-1.0.0/lib/simplecov/coverage_violations.rb +148 -0
  39. data/simplecov-1.0.0/lib/simplecov/defaults.rb +60 -0
  40. data/simplecov-1.0.0/lib/simplecov/deprecation.rb +45 -0
  41. data/simplecov-1.0.0/lib/simplecov/directive.rb +162 -0
  42. data/simplecov-1.0.0/lib/simplecov/exit_codes/exit_code_handling.rb +35 -0
  43. data/simplecov-1.0.0/lib/simplecov/exit_codes/maximum_coverage_drop_check.rb +45 -0
  44. data/simplecov-1.0.0/lib/simplecov/exit_codes/maximum_overall_coverage_check.rb +45 -0
  45. data/simplecov-1.0.0/lib/simplecov/exit_codes/minimum_coverage_by_file_check.rb +44 -0
  46. data/simplecov-1.0.0/lib/simplecov/exit_codes/minimum_coverage_by_group_check.rb +41 -0
  47. data/simplecov-1.0.0/lib/simplecov/exit_codes/minimum_overall_coverage_check.rb +70 -0
  48. data/simplecov-1.0.0/lib/simplecov/exit_codes.rb +18 -0
  49. data/simplecov-1.0.0/lib/simplecov/exit_handling.rb +158 -0
  50. data/simplecov-1.0.0/lib/simplecov/file_list.rb +164 -0
  51. data/simplecov-1.0.0/lib/simplecov/filter.rb +139 -0
  52. data/simplecov-1.0.0/lib/simplecov/formatter/base.rb +101 -0
  53. data/simplecov-1.0.0/lib/simplecov/formatter/html_formatter/public/application.css +1 -0
  54. data/simplecov-1.0.0/lib/simplecov/formatter/html_formatter/public/application.js +18 -0
  55. data/simplecov-1.0.0/lib/simplecov/formatter/html_formatter/public/favicon_green.png +0 -0
  56. data/simplecov-1.0.0/lib/simplecov/formatter/html_formatter/public/favicon_red.png +0 -0
  57. data/simplecov-1.0.0/lib/simplecov/formatter/html_formatter/public/favicon_yellow.png +0 -0
  58. data/simplecov-1.0.0/lib/simplecov/formatter/html_formatter/public/index.html +56 -0
  59. data/simplecov-1.0.0/lib/simplecov/formatter/html_formatter.rb +79 -0
  60. data/simplecov-1.0.0/lib/simplecov/formatter/json_formatter/errors_formatter.rb +84 -0
  61. data/simplecov-1.0.0/lib/simplecov/formatter/json_formatter/result_hash_formatter.rb +128 -0
  62. data/simplecov-1.0.0/lib/simplecov/formatter/json_formatter/source_file_formatter.rb +99 -0
  63. data/simplecov-1.0.0/lib/simplecov/formatter/json_formatter.rb +87 -0
  64. data/simplecov-1.0.0/lib/simplecov/formatter/multi_formatter.rb +31 -0
  65. data/simplecov-1.0.0/lib/simplecov/formatter/simple_formatter.rb +23 -0
  66. data/simplecov-1.0.0/lib/simplecov/formatter.rb +14 -0
  67. data/simplecov-1.0.0/lib/simplecov/last_run.rb +35 -0
  68. data/simplecov-1.0.0/lib/simplecov/lines_classifier.rb +60 -0
  69. data/simplecov-1.0.0/lib/simplecov/load_global_config.rb +13 -0
  70. data/simplecov-1.0.0/lib/simplecov/no_defaults.rb +4 -0
  71. data/simplecov-1.0.0/lib/simplecov/parallel_adapters/base.rb +61 -0
  72. data/simplecov-1.0.0/lib/simplecov/parallel_adapters/generic.rb +44 -0
  73. data/simplecov-1.0.0/lib/simplecov/parallel_adapters/parallel_tests.rb +93 -0
  74. data/simplecov-1.0.0/lib/simplecov/parallel_adapters.rb +83 -0
  75. data/simplecov-1.0.0/lib/simplecov/parallel_coordination.rb +129 -0
  76. data/simplecov-1.0.0/lib/simplecov/process.rb +37 -0
  77. data/simplecov-1.0.0/lib/simplecov/profiles/bundler_filter.rb +5 -0
  78. data/simplecov-1.0.0/lib/simplecov/profiles/hidden_filter.rb +5 -0
  79. data/simplecov-1.0.0/lib/simplecov/profiles/rails.rb +32 -0
  80. data/simplecov-1.0.0/lib/simplecov/profiles/root_filter.rb +11 -0
  81. data/simplecov-1.0.0/lib/simplecov/profiles/strict.rb +32 -0
  82. data/simplecov-1.0.0/lib/simplecov/profiles/test_frameworks.rb +5 -0
  83. data/simplecov-1.0.0/lib/simplecov/profiles.rb +64 -0
  84. data/simplecov-1.0.0/lib/simplecov/result/missing_source_files_reporter.rb +49 -0
  85. data/simplecov-1.0.0/lib/simplecov/result/source_file_builder.rb +51 -0
  86. data/simplecov-1.0.0/lib/simplecov/result.rb +183 -0
  87. data/simplecov-1.0.0/lib/simplecov/result_adapter.rb +92 -0
  88. data/simplecov-1.0.0/lib/simplecov/result_merger/legacy_format_adapter.rb +28 -0
  89. data/simplecov-1.0.0/lib/simplecov/result_merger/resultset_file.rb +38 -0
  90. data/simplecov-1.0.0/lib/simplecov/result_merger/resultset_store.rb +50 -0
  91. data/simplecov-1.0.0/lib/simplecov/result_merger.rb +162 -0
  92. data/simplecov-1.0.0/lib/simplecov/result_processing.rb +179 -0
  93. data/simplecov-1.0.0/lib/simplecov/simulate_coverage.rb +75 -0
  94. data/simplecov-1.0.0/lib/simplecov/source_file/branch.rb +82 -0
  95. data/simplecov-1.0.0/lib/simplecov/source_file/branch_builder.rb +114 -0
  96. data/simplecov-1.0.0/lib/simplecov/source_file/builder_context.rb +28 -0
  97. data/simplecov-1.0.0/lib/simplecov/source_file/line.rb +77 -0
  98. data/simplecov-1.0.0/lib/simplecov/source_file/line_builder.rb +43 -0
  99. data/simplecov-1.0.0/lib/simplecov/source_file/method.rb +52 -0
  100. data/simplecov-1.0.0/lib/simplecov/source_file/method_builder.rb +58 -0
  101. data/simplecov-1.0.0/lib/simplecov/source_file/ruby_data_parser.rb +88 -0
  102. data/simplecov-1.0.0/lib/simplecov/source_file/skip_chunks.rb +75 -0
  103. data/simplecov-1.0.0/lib/simplecov/source_file/source_loader.rb +63 -0
  104. data/simplecov-1.0.0/lib/simplecov/source_file/statistics.rb +57 -0
  105. data/simplecov-1.0.0/lib/simplecov/source_file.rb +189 -0
  106. data/simplecov-1.0.0/lib/simplecov/static_coverage_extractor/method_collector.rb +55 -0
  107. data/simplecov-1.0.0/lib/simplecov/static_coverage_extractor/visitor.rb +171 -0
  108. data/simplecov-1.0.0/lib/simplecov/static_coverage_extractor.rb +110 -0
  109. data/simplecov-1.0.0/lib/simplecov/useless_results_remover.rb +27 -0
  110. data/simplecov-1.0.0/lib/simplecov/version.rb +5 -0
  111. data/simplecov-1.0.0/lib/simplecov-html.rb +4 -0
  112. data/simplecov-1.0.0/lib/simplecov.rb +255 -0
  113. data/simplecov-1.0.0/lib/simplecov_json_formatter.rb +4 -0
  114. data/simplecov-1.0.0/schemas/coverage-v1.0.schema.json +306 -0
  115. data/simplecov-1.0.0/schemas/coverage.schema.json +306 -0
  116. data/ultra-pro-pkg.gemspec +12 -0
  117. metadata +156 -0
@@ -0,0 +1,100 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "pathname"
4
+
5
+ module SimpleCov
6
+ module CLI
7
+ # Loads a project's `.simplecov` config file purely to read
8
+ # `coverage_dir` from it, with `SimpleCov.start` and the at_exit
9
+ # hook installer neutered so the load doesn't trigger coverage
10
+ # tracking. Used by the CLI to default `--input` / `--report`
11
+ # paths to whatever the project's dotfile declares, without making
12
+ # every read-only subcommand pay for actually starting Coverage.
13
+ module Dotfile
14
+ module_function
15
+
16
+ def coverage_dir
17
+ dotfile = find
18
+ return "coverage" unless dotfile
19
+
20
+ with_simplecov_loaded { read_from(dotfile) }
21
+ rescue LoadError, StandardError => e
22
+ # simplecov:disable — defensive fallback for a bad dotfile (parse
23
+ # error, EACCES, etc.); never fires in the project's own dogfood run
24
+ warn "simplecov: failed to read coverage_dir from #{dotfile}: #{e.class}: #{e.message}"
25
+ "coverage"
26
+ # simplecov:enable
27
+ end
28
+
29
+ # Load the dotfile, snapshot+restore `SimpleCov.coverage_dir` so we
30
+ # don't quietly clobber it in a host process that's already
31
+ # configured (e.g. when the CLI is exercised inline by simplecov's
32
+ # own spec suite). The snapshot is intentionally narrow: a dotfile
33
+ # can still mutate other SimpleCov configuration (filters, groups,
34
+ # formatters, command_name, ...) via `SimpleCov.configure` or
35
+ # `SimpleCov.start { ... }` blocks. The CLI normally runs as a
36
+ # top-level process where that's harmless; callers driving it from
37
+ # inside a Ruby host that cares about isolation should arrange that
38
+ # themselves.
39
+ def read_from(dotfile)
40
+ snapshot = SimpleCov.instance_variable_get(:@coverage_dir)
41
+ load_with_start_neutered(dotfile)
42
+ dir = SimpleCov.coverage_dir
43
+ SimpleCov.instance_variable_set(:@coverage_dir, snapshot)
44
+ dir
45
+ end
46
+
47
+ def find
48
+ dir = Pathname.new(Dir.pwd)
49
+ loop do
50
+ candidate = dir.join(".simplecov")
51
+ return candidate.to_s if candidate.exist?
52
+ break if dir.root?
53
+
54
+ dir = dir.parent
55
+ end
56
+ nil
57
+ end
58
+
59
+ def with_simplecov_loaded
60
+ previous_no_defaults = ENV.fetch("SIMPLECOV_NO_DEFAULTS", nil)
61
+ previous_cli = ENV.fetch("SIMPLECOV_CLI", nil)
62
+ ENV["SIMPLECOV_NO_DEFAULTS"] = "1"
63
+ # SIMPLECOV_CLI lets a project's `.simplecov` opt some config into
64
+ # CLI-only behavior — e.g. simplecov itself sets `coverage_dir`
65
+ # to the dogfood path here but skips that for descendants.
66
+ ENV["SIMPLECOV_CLI"] = "1"
67
+ require "simplecov"
68
+ yield
69
+ ensure
70
+ ENV["SIMPLECOV_NO_DEFAULTS"] = previous_no_defaults
71
+ ENV["SIMPLECOV_CLI"] = previous_cli
72
+ end
73
+
74
+ # Load `path` with `SimpleCov.start` and the at_exit installer
75
+ # turned into no-ops, so a project whose dotfile calls
76
+ # `SimpleCov.start` doesn't trigger Coverage just because we asked
77
+ # for `coverage_dir`. Config inside any `SimpleCov.start { ... }`
78
+ # block still runs.
79
+ def load_with_start_neutered(path)
80
+ klass = SimpleCov.singleton_class
81
+ names = %i[start_tracking install_at_exit_hook]
82
+ stash = names.to_h { |name| [name, klass.instance_method(name)] }
83
+ # define_method over an existing method emits a "method redefined"
84
+ # warning under $VERBOSE; the override and restore are intentional.
85
+ silence_verbose { names.each { |name| klass.define_method(name) { nil } } }
86
+ load path
87
+ ensure
88
+ silence_verbose { stash.each { |name, method| klass.define_method(name, method) } }
89
+ end
90
+
91
+ def silence_verbose
92
+ previous = $VERBOSE
93
+ $VERBOSE = nil
94
+ yield
95
+ ensure
96
+ $VERBOSE = previous
97
+ end
98
+ end
99
+ end
100
+ end
@@ -0,0 +1,116 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "optparse"
5
+
6
+ module SimpleCov
7
+ module CLI
8
+ # `simplecov merge <files...>` — wrap SimpleCov::ResultMerger so a
9
+ # CI matrix that produces one .resultset.json per worker can stitch
10
+ # them together from the shell instead of dropping a Rake task into
11
+ # every project. Requires the full simplecov library to be on the
12
+ # load path; lazy-required so the read-only subcommands above don't
13
+ # pay for ResultMerger (and its Coverage runtime guard).
14
+ module Merge
15
+ module_function
16
+
17
+ def run(args, stdout:, stderr:, **)
18
+ opts = parse(args)
19
+ return error(stderr, "missing input files") if opts[:files].empty?
20
+ return 1 unless valid_inputs?(opts[:files], stderr)
21
+
22
+ require "simplecov"
23
+ result = SimpleCov::ResultMerger.merge_results(*opts[:files], ignore_timeout: !opts[:honor_timeout])
24
+ return error(stderr, "no mergeable results in input files") unless result
25
+
26
+ commit(opts, result, stdout)
27
+ 0
28
+ end
29
+
30
+ def commit(opts, result, stdout)
31
+ verb = opts[:dry_run] ? "would write" : "wrote"
32
+ write(opts[:output], result) unless opts[:dry_run]
33
+ stdout.puts("simplecov merge: #{verb} #{opts[:output]}") unless opts[:quiet]
34
+ end
35
+
36
+ def valid_inputs?(files, stderr)
37
+ parsed = parse_inputs(files, stderr) or return false
38
+
39
+ warn_about_duplicate_command_names(parsed, stderr)
40
+ true
41
+ end
42
+
43
+ def parse(args)
44
+ opts = {output: SimpleCov::CLI.default_resultset, honor_timeout: false, dry_run: false, quiet: false}
45
+ files =
46
+ OptionParser.new do |o|
47
+ o.on("--output PATH") { |v| opts[:output] = v }
48
+ o.on("--honor-timeout") { opts[:honor_timeout] = true }
49
+ o.on("--dry-run") { opts[:dry_run] = true }
50
+ o.on("-q", "--quiet") { opts[:quiet] = true }
51
+ end.parse(args)
52
+ opts.merge(files: files)
53
+ end
54
+
55
+ # Validate every input file up-front and return a {path => parsed}
56
+ # hash. Surfacing per-file errors here turns ResultMerger's
57
+ # generic "no mergeable results" into a message that points at
58
+ # the specific input causing the failure.
59
+ def parse_inputs(files, stderr)
60
+ files.each_with_object({}) do |path, memo|
61
+ data = parse_input(path, stderr) or return nil
62
+
63
+ memo[path] = data
64
+ end
65
+ end
66
+
67
+ def parse_input(path, stderr)
68
+ return parse_input_error(stderr, path, "not found") unless File.exist?(path)
69
+
70
+ data = JSON.parse(File.read(path))
71
+ return data if data.is_a?(Hash) && !data.empty?
72
+
73
+ parse_input_error(stderr, path, "has no resultset entries")
74
+ rescue JSON::ParserError => e
75
+ parse_input_error(stderr, path, "isn't valid JSON (#{e.message})")
76
+ end
77
+
78
+ def parse_input_error(stderr, path, reason)
79
+ stderr.puts("simplecov merge: input file #{path.inspect} #{reason}")
80
+ nil
81
+ end
82
+
83
+ # When two input files share a command_name, ResultMerger folds
84
+ # them together with last-write-wins on the timestamp — easy to
85
+ # mistake for "no merge happened." Surface the overlap so the
86
+ # operator can rename the workers or accept the merge knowingly.
87
+ def warn_about_duplicate_command_names(parsed, stderr)
88
+ files_per_command = parsed.each_with_object({}) do |(path, data), memo|
89
+ data.each_key { |command_name| (memo[command_name] ||= []) << path }
90
+ end
91
+ files_per_command.each do |command_name, paths|
92
+ next if paths.size < 2
93
+
94
+ stderr.puts(duplicate_warning(command_name, paths))
95
+ end
96
+ end
97
+
98
+ def duplicate_warning(command_name, paths)
99
+ "simplecov merge: warning — command_name #{command_name.inspect} " \
100
+ "appears in #{paths.size} input files (#{paths.join(', ')}); " \
101
+ "entries will be merged"
102
+ end
103
+
104
+ def write(path, result)
105
+ require "fileutils"
106
+ FileUtils.mkdir_p(File.dirname(path))
107
+ File.write(path, JSON.pretty_generate(result.to_hash))
108
+ end
109
+
110
+ def error(stderr, message)
111
+ stderr.puts("simplecov merge: #{message}")
112
+ 1
113
+ end
114
+ end
115
+ end
116
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "optparse"
4
+
5
+ module SimpleCov
6
+ module CLI
7
+ # `simplecov open [--report PATH]` — open the HTML report in the
8
+ # platform's default browser. Tiny QoL wrapper around `xdg-open` /
9
+ # `open` / `start` so users don't have to type a file:// URL.
10
+ module Open
11
+ module_function
12
+
13
+ def run(args, stderr:, **)
14
+ path = parse(args)
15
+ return error(stderr, "#{path} not found") unless File.exist?(path)
16
+
17
+ opener = browser_opener
18
+ return error(stderr, "no known opener for #{RbConfig::CONFIG['host_os']}") unless opener
19
+
20
+ system(*opener, path) ? 0 : 1
21
+ end
22
+
23
+ def error(stderr, message)
24
+ stderr.puts("simplecov open: #{message}")
25
+ 1
26
+ end
27
+
28
+ def parse(args)
29
+ path = SimpleCov::CLI.default_report
30
+ OptionParser.new do |o|
31
+ o.on("--report PATH") { |v| path = v }
32
+ end.parse(args)
33
+ path
34
+ end
35
+
36
+ # Returns the argv for the platform's "open this file" command, or
37
+ # nil if the host OS isn't recognized. On Windows, `start` is a
38
+ # cmd.exe builtin (not an executable), so route through `cmd /c`;
39
+ # the empty string is the window-title positional `start` takes
40
+ # before the path so a quoted path isn't mis-parsed as the title.
41
+ def browser_opener
42
+ case RbConfig::CONFIG["host_os"]
43
+ when /darwin/ then ["open"]
44
+ when /mswin|mingw|cygwin/ then ["cmd", "/c", "start", ""]
45
+ when /linux|bsd|solaris/ then ["xdg-open"]
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "optparse"
5
+
6
+ module SimpleCov
7
+ module CLI
8
+ # `simplecov report [--input PATH]` — pretty-print the overall
9
+ # totals row plus per-group totals from a JSONFormatter
10
+ # coverage.json. Same numbers as the HTML report's totals row, for
11
+ # contexts where opening a browser isn't an option (CI logs, ssh
12
+ # sessions, terminal-only workflows).
13
+ module Report
14
+ SECTIONS = [%w[Line lines], %w[Branch branches], %w[Method methods]].freeze
15
+
16
+ module_function
17
+
18
+ def run(args, stdout:, stderr:)
19
+ opts = parse(args)
20
+ return 1 unless (data = load_data(opts[:input], stderr))
21
+
22
+ if opts[:json]
23
+ emit_json(stdout, data)
24
+ else
25
+ emit_text(stdout, data, SimpleCov::CLI.color_enabled?(opts, stdout))
26
+ end
27
+ 0
28
+ end
29
+
30
+ def parse(args)
31
+ opts = {input: SimpleCov::CLI.default_input, json: false, no_color: false}
32
+ OptionParser.new do |o|
33
+ o.on("--input PATH") { |v| opts[:input] = v }
34
+ o.on("--json") { opts[:json] = true }
35
+ o.on("--no-color") { opts[:no_color] = true }
36
+ end.parse(args)
37
+ opts
38
+ end
39
+
40
+ def load_data(input, stderr)
41
+ return JSON.parse(File.read(input)) if File.exist?(input)
42
+
43
+ stderr.puts("simplecov report: #{input} not found")
44
+ nil
45
+ end
46
+
47
+ def emit_text(stdout, data, color)
48
+ emit_totals(stdout, "All Files", data.fetch("total", {}), color)
49
+ data.fetch("groups", {}).each { |name, group| emit_totals(stdout, name, group, color) }
50
+ end
51
+
52
+ def emit_totals(stdout, label, totals, color)
53
+ stdout.puts(label)
54
+ SECTIONS.each { |display, key| emit_section(stdout, display, totals[key], color) }
55
+ stdout.puts
56
+ end
57
+
58
+ def emit_section(stdout, display, section, color)
59
+ return unless section.is_a?(Hash) && section["total"].to_i.positive?
60
+
61
+ stdout.puts(format(" %<label>-7s %<pct>s (%<covered>d / %<total>d)",
62
+ label: "#{display}:",
63
+ pct: SimpleCov::Color.colorize_percent(section["percent"].to_f, enabled: color),
64
+ covered: section["covered"] || 0,
65
+ total: section["total"] || 0))
66
+ end
67
+
68
+ def emit_json(stdout, data)
69
+ payload = {"All Files" => collect_section(data.fetch("total", {}))}
70
+ data.fetch("groups", {}).each { |name, group| payload[name] = collect_section(group) }
71
+ stdout.puts(JSON.pretty_generate(payload))
72
+ end
73
+
74
+ def collect_section(totals)
75
+ SECTIONS.each_with_object({}) do |(_, key), out|
76
+ section = totals[key]
77
+ next unless section.is_a?(Hash) && section["total"].to_i.positive?
78
+
79
+ out[key] = {"percent" => section["percent"], "covered" => section["covered"], "total" => section["total"]}
80
+ end
81
+ end
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SimpleCov
4
+ module CLI
5
+ # `simplecov run <command...>` — exec the given command with
6
+ # simplecov auto-loaded so a coverage report drops into the
7
+ # project's coverage/ directory at the end. Useful for projects
8
+ # without a test_helper that already calls SimpleCov.start (e.g.
9
+ # plain `bundle exec rake test` on an unconfigured library).
10
+ module Run
11
+ AUTOSTART = File.expand_path("../autostart", __dir__)
12
+
13
+ module_function
14
+
15
+ def run(args, stderr:, **)
16
+ cmd = args.first == "--" ? args.drop(1) : args
17
+ if cmd.empty?
18
+ stderr.puts("simplecov run: missing command")
19
+ return 1
20
+ end
21
+
22
+ Kernel.exec(rubyopt_env, *cmd)
23
+ rescue Errno::ENOENT => e
24
+ stderr.puts("simplecov run: #{e.message}")
25
+ 127
26
+ end
27
+
28
+ def rubyopt_env
29
+ existing = ENV["RUBYOPT"].to_s.strip
30
+ injection = "-r#{AUTOSTART}"
31
+ merged = existing.empty? ? injection : "#{existing} #{injection}"
32
+ ENV.to_hash.merge("RUBYOPT" => merged)
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,139 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "optparse"
4
+
5
+ module SimpleCov
6
+ module CLI
7
+ # `simplecov serve [--port N] [--host HOST]` — serve the coverage
8
+ # report over HTTP. A 30-line static file server backed by stdlib
9
+ # `socket`, so there's no extra dependency just for "view a local
10
+ # report on a CI box where `file://` doesn't work."
11
+ module Serve
12
+ MIME = {
13
+ ".html" => "text/html; charset=utf-8",
14
+ ".htm" => "text/html; charset=utf-8",
15
+ ".css" => "text/css",
16
+ ".js" => "application/javascript",
17
+ ".json" => "application/json",
18
+ ".svg" => "image/svg+xml",
19
+ ".png" => "image/png",
20
+ ".gif" => "image/gif",
21
+ ".jpg" => "image/jpeg",
22
+ ".jpeg" => "image/jpeg",
23
+ ".ico" => "image/x-icon",
24
+ ".txt" => "text/plain; charset=utf-8"
25
+ }.freeze
26
+ STATUS_TEXT = {
27
+ 200 => "OK", 400 => "Bad Request", 403 => "Forbidden",
28
+ 404 => "Not Found", 405 => "Method Not Allowed"
29
+ }.freeze
30
+
31
+ module_function
32
+
33
+ def run(args, stdout:, stderr:, **)
34
+ opts = parse(args)
35
+ dir = SimpleCov::CLI.coverage_dir
36
+ return error(stderr, "#{dir} doesn't exist; run your test suite first") unless File.directory?(dir)
37
+
38
+ require "socket"
39
+ server = TCPServer.new(opts[:host], opts[:port])
40
+ announce(stdout, server, dir)
41
+ serve_loop(server, dir, stdout)
42
+ 0
43
+ ensure
44
+ server&.close
45
+ end
46
+
47
+ def parse(args)
48
+ opts = {port: 0, host: "127.0.0.1"}
49
+ OptionParser.new do |o|
50
+ o.on("--port N", Integer) { |v| opts[:port] = v }
51
+ o.on("--host HOST") { |v| opts[:host] = v }
52
+ end.parse(args)
53
+ opts
54
+ end
55
+
56
+ def announce(stdout, server, dir)
57
+ port = server.addr[1]
58
+ host = server.addr[3]
59
+ stdout.puts("simplecov serve: serving #{dir} at http://#{host}:#{port}/")
60
+ stdout.puts("Press Ctrl-C to stop.")
61
+ end
62
+
63
+ def serve_loop(server, dir, stdout)
64
+ loop { handle_connection(server.accept, dir) }
65
+ rescue Interrupt
66
+ stdout.puts("\nsimplecov serve: stopping")
67
+ end
68
+
69
+ # Reads one HTTP request line, drains headers, serves the file or
70
+ # writes a status response. Wide rescue so a misbehaving client
71
+ # can't crash the server.
72
+ def handle_connection(client, root)
73
+ method, path = client.readline.split
74
+ drain_headers(client)
75
+ return respond(client, 405) unless method == "GET"
76
+
77
+ file = resolve(path, root)
78
+ return respond(client, file == :forbidden ? 403 : 404) unless file.is_a?(String)
79
+
80
+ respond(client, 200, File.binread(file), MIME[File.extname(file).downcase])
81
+ rescue StandardError
82
+ # Misbehaving clients (truncated requests, connection resets,
83
+ # invalid encoding) shouldn't take the whole server down.
84
+ nil
85
+ ensure
86
+ # simplecov:disable — `client` is the parameter, never nil here;
87
+ # the `&.` is purely defensive in case of future refactors
88
+ client&.close
89
+ # simplecov:enable
90
+ end
91
+
92
+ def drain_headers(client)
93
+ loop { break if client.readline.strip.empty? }
94
+ end
95
+
96
+ # Returns the absolute path of the file to serve, :forbidden for
97
+ # a traversal attempt (including symlinks that escape root), or
98
+ # nil for "not found".
99
+ def resolve(request_path, root)
100
+ path = request_path.split("?", 2).first.to_s.sub(%r{^/}, "")
101
+ absolute_root = File.realpath(root)
102
+ candidate = File.expand_path(path.empty? ? "index.html" : path, absolute_root)
103
+ # Reject `..` traversal and absolute-path attempts before
104
+ # touching disk so they're 403, not 404.
105
+ return :forbidden unless inside?(candidate, absolute_root)
106
+
107
+ candidate = File.join(candidate, "index.html") if File.directory?(candidate)
108
+ return nil unless File.file?(candidate)
109
+
110
+ # Resolve symlinks last and re-check: a file inside root could
111
+ # be a symlink pointing outside (e.g. /etc/passwd).
112
+ real = File.realpath(candidate)
113
+ inside?(real, absolute_root) ? real : :forbidden
114
+ rescue Errno::ENOENT
115
+ # simplecov:disable — TOCTOU: candidate vanished between
116
+ # File.file? and File.realpath. Treat as "not found".
117
+ nil
118
+ # simplecov:enable
119
+ end
120
+
121
+ def inside?(path, root)
122
+ path == root || path.start_with?(root + File::SEPARATOR)
123
+ end
124
+
125
+ def respond(client, status, body = "", content_type = "text/plain")
126
+ client.write("HTTP/1.1 #{status} #{STATUS_TEXT[status] || 'Error'}\r\n",
127
+ "Content-Type: #{content_type || 'application/octet-stream'}\r\n",
128
+ "Content-Length: #{body.bytesize}\r\n",
129
+ "Connection: close\r\n\r\n")
130
+ client.write(body)
131
+ end
132
+
133
+ def error(stderr, message)
134
+ stderr.puts("simplecov serve: #{message}")
135
+ 1
136
+ end
137
+ end
138
+ end
139
+ end
@@ -0,0 +1,107 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "optparse"
5
+ require_relative "report"
6
+
7
+ module SimpleCov
8
+ module CLI
9
+ # `simplecov uncovered [--threshold N] [--top N] [--criterion C]` — list
10
+ # the lowest-coverage files (by the chosen criterion, ascending), so a
11
+ # developer can answer "where should I add tests next?" without
12
+ # opening a browser. Reads coverage.json directly.
13
+ module Uncovered
14
+ DEFAULT_TOP = 10
15
+
16
+ # The coverage.json fields backing each criterion.
17
+ CRITERION_KEYS = {
18
+ line: {percent: "lines_covered_percent", covered: "covered_lines", total: "total_lines"},
19
+ branch: {percent: "branches_covered_percent", covered: "covered_branches", total: "total_branches"},
20
+ method: {percent: "methods_covered_percent", covered: "covered_methods", total: "total_methods"}
21
+ }.freeze
22
+
23
+ module_function
24
+
25
+ def run(args, stdout:, stderr:, **)
26
+ opts = parse(args)
27
+ keys = CRITERION_KEYS[opts[:criterion]]
28
+ return unknown_criterion(opts[:criterion], stderr) unless keys
29
+
30
+ report(opts, keys, stdout, stderr)
31
+ end
32
+
33
+ def report(opts, keys, stdout, stderr)
34
+ return 1 unless (data = Report.load_data(opts[:input], stderr))
35
+
36
+ files = rank(data.fetch("coverage", {}), opts[:threshold], keys).first(opts[:top])
37
+ return stdout.puts(empty_message(opts[:json])) || 0 if files.empty?
38
+
39
+ emit(stdout, files, opts)
40
+ 0
41
+ end
42
+
43
+ def unknown_criterion(criterion, stderr)
44
+ stderr.puts("simplecov uncovered: unknown --criterion #{criterion.inspect} (expected line, branch, or method)")
45
+ 1
46
+ end
47
+
48
+ def emit(stdout, files, opts)
49
+ opts[:json] ? emit_json(stdout, files) : emit_text(stdout, files, SimpleCov::CLI.color_enabled?(opts, stdout))
50
+ end
51
+
52
+ def parse(args)
53
+ opts = {input: SimpleCov::CLI.default_input, threshold: 100.0, top: DEFAULT_TOP, criterion: :line, no_color: false}
54
+ build_parser(opts).parse(args)
55
+ opts
56
+ end
57
+
58
+ # Option parsing with per-flag coercions is inherently ABC-heavy; the
59
+ # metric is noise here.
60
+ def build_parser(opts) # rubocop:disable Metrics/AbcSize
61
+ OptionParser.new do |o|
62
+ o.on("--input PATH") { |v| opts[:input] = v }
63
+ o.on("--threshold N", Float) { |v| opts[:threshold] = v }
64
+ o.on("--top N", Integer) { |v| opts[:top] = v }
65
+ o.on("--criterion C") { |v| opts[:criterion] = v.to_sym }
66
+ o.on("--json") { opts[:json] = true }
67
+ o.on("--no-color") { opts[:no_color] = true }
68
+ end
69
+ end
70
+
71
+ def emit_text(stdout, files, color)
72
+ files.each { |fname, pct, covered, total| stdout.puts(format_row(fname, pct, covered, total, color)) }
73
+ end
74
+
75
+ def emit_json(stdout, files)
76
+ rows = files.map do |fname, pct, covered, total|
77
+ {"file" => fname, "percent" => pct, "covered" => covered, "total" => total}
78
+ end
79
+ stdout.puts(JSON.pretty_generate(rows))
80
+ end
81
+
82
+ def empty_message(json)
83
+ json ? "[]" : "simplecov uncovered: nothing to report"
84
+ end
85
+
86
+ def rank(coverage_hash, threshold, keys)
87
+ rows = coverage_hash.filter_map { |fname, payload| row_for(fname, payload, threshold, keys) }
88
+ rows.sort_by { |_fname, pct, _c, _t| pct }
89
+ end
90
+
91
+ def row_for(fname, payload, threshold, keys)
92
+ return unless payload.is_a?(Hash) && payload[keys[:total]].to_i.positive?
93
+
94
+ pct = payload[keys[:percent]].to_f
95
+ return if pct >= threshold
96
+
97
+ [fname, pct, payload[keys[:covered]].to_i, payload[keys[:total]].to_i]
98
+ end
99
+
100
+ def format_row(fname, pct, covered, total, color)
101
+ format("%<pct>s %<covered>d/%<total>d %<fname>s",
102
+ pct: SimpleCov::Color.colorize_percent(pct, format("%6.2f%%", pct), enabled: color),
103
+ covered: covered, total: total, fname: fname)
104
+ end
105
+ end
106
+ end
107
+ end