mutaterb 0.2.0 → 0.3.0

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 71841b126edbdca7f12d77aaca2622dde361a8011a49fbd2973cde656737c134
4
- data.tar.gz: 2c45a913486f515d16ab0217d331b35e98f84f620a43b9a6496ad8c94681c940
3
+ metadata.gz: 0fcfd443fe916c062548f30e91d88d26a3315995867ef5171ae65a59075a8f30
4
+ data.tar.gz: 349a834f109c1ab01cbf632ca042fa2850691c88ef77c61250c2d37d8e7def6b
5
5
  SHA512:
6
- metadata.gz: 1b716efda6a38584d538c4b76d82113be716dea2f466efd293afcef20d9b40aaf628a80ffd5aef8d3c8f80848314e3a6269bcb2877cf42d8c6c9b5bcb75dabe6
7
- data.tar.gz: 24655446fcfc728007c10e89d7b8be5df414aaf99baf3d441f154add892c1dfcb59251570ca9747b132d606c167acb3ecb9e3ea71bcc230c8df6105dd912440c
6
+ metadata.gz: c46d5fcaf78d48abde487f77f96803ec0748e011b20d6d00cfb9d00766ae69e94c3403b3dd9be35867f2ff854608cfb13e218cd82c5a94d7635b5077861b6df2
7
+ data.tar.gz: a7327b9f520ebae9231729c43b8f29cbb171e35f6781eef35889c63ff5502dc9cd3ce431833e7b77ae9b759065648cc805af39b7b2fb8f4b3c14c440e2be145e
data/README.md CHANGED
@@ -31,6 +31,11 @@ and displays a summary: how many mutations were "killed" (caught by a test) and
31
31
  many "survived" (no test caught them — weak tests), with file, line, and related
32
32
  test(s) for each survivor.
33
33
 
34
+ While it runs, it prints the baseline status and a live counter of mutants processed
35
+ so far out of the total, so a long run never looks stuck. Pass `--verbose` to replace
36
+ that counter with one line per mutant (file, line, mutation type, and result) as it is
37
+ evaluated.
38
+
34
39
  ### Main flags
35
40
 
36
41
  | Flag | Description |
@@ -42,6 +47,7 @@ test(s) for each survivor.
42
47
  | `--framework FRAMEWORK` | `auto`, `rspec`, or `minitest` — forces the test framework instead of auto-detecting it |
43
48
  | `--mutation-types TYPES` | Mutation types to apply, comma-separated |
44
49
  | `--exit-zero` | Do not fail (exit 0) even if "survived" mutations exist |
50
+ | `--verbose` | Print one line per mutant as it runs, instead of the default counter |
45
51
  | `--json-output PATH` | Export the results summary to a JSON file |
46
52
  | `--config PATH` | Use a config file other than `.mutaterb.yml` |
47
53
 
data/lib/mutaterb/cli.rb CHANGED
@@ -45,9 +45,10 @@ module MutateRB
45
45
  end
46
46
  warn_ambiguous_frameworks(detection)
47
47
 
48
- test_suite = build_test_suite(config, detection)
48
+ progress = ProgressReporter.new(verbose: config.verbose)
49
+ test_suite = build_test_suite(config, detection, progress)
49
50
  run = MutationRun.new(config: config, test_suite: test_suite)
50
- run_mutations(run, test_suite, config) { interrupted }
51
+ run_mutations(run, test_suite, config, progress) { interrupted }
51
52
 
52
53
  run.finished_at = Time.now
53
54
  run.interrupted = interrupted
@@ -67,20 +68,31 @@ module MutateRB
67
68
  "(force one explicitly with --framework)"
68
69
  end
69
70
 
70
- def build_test_suite(config, detection)
71
+ def build_test_suite(config, detection, progress)
71
72
  test_runner = TestRunner.new(config: config, project_type: detection.project_type,
72
73
  framework: detection.test_framework)
74
+ progress.start_baseline
73
75
  baseline_examples = test_runner.run_baseline(detection.test_files)
76
+ progress.finish_baseline
74
77
  TestSuite.from_baseline(project_type: detection.project_type, framework: detection.test_framework,
75
78
  baseline_examples: baseline_examples)
76
79
  end
77
80
 
78
- def run_mutations(run, test_suite, config)
81
+ def run_mutations(run, test_suite, config, progress)
79
82
  mutator = Mutator.new(config: config, test_suite: test_suite)
83
+ total = mutator.total_mutants
84
+ if total.zero?
85
+ warn "mutaterb: no mutants to run for the given scope"
86
+ return
87
+ end
88
+
89
+ progress.start_mutants(total)
80
90
  mutator.each_mutant do |mutant|
81
91
  run.add_mutant(mutant)
92
+ progress.mutant_finished(mutant)
82
93
  break if yield
83
94
  end
95
+ progress.finish_mutants
84
96
  end
85
97
  end
86
98
  end
@@ -12,11 +12,13 @@ module MutateRB
12
12
  DEFAULT_FILE_NAME = ".mutaterb.yml"
13
13
 
14
14
  attr_accessor :target_dir, :include_paths, :exclude_paths, :strictness,
15
- :mutation_types, :exit_on_survivors, :json_output_path, :test_framework
15
+ :mutation_types, :exit_on_survivors, :json_output_path, :test_framework,
16
+ :verbose
16
17
 
17
18
  def initialize(target_dir: ".", include_paths: [], exclude_paths: [],
18
19
  strictness: :default, mutation_types: ALL_MUTATION_TYPES.dup,
19
- exit_on_survivors: true, json_output_path: nil, test_framework: :auto)
20
+ exit_on_survivors: true, json_output_path: nil, test_framework: :auto,
21
+ verbose: false)
20
22
  @target_dir = target_dir
21
23
  @include_paths = include_paths
22
24
  @exclude_paths = exclude_paths
@@ -25,6 +27,7 @@ module MutateRB
25
27
  @exit_on_survivors = exit_on_survivors
26
28
  @json_output_path = json_output_path
27
29
  @test_framework = test_framework
30
+ @verbose = verbose
28
31
  validate!
29
32
  end
30
33
 
@@ -48,7 +51,7 @@ module MutateRB
48
51
 
49
52
  def self.attributes_from_yaml(raw)
50
53
  known_keys = %w[target_dir include_paths exclude_paths strictness mutation_types
51
- exit_on_survivors json_output_path test_framework]
54
+ exit_on_survivors json_output_path test_framework verbose]
52
55
  raw.each_key do |key|
53
56
  warn "mutaterb: ignoring unknown config key #{key.inspect}" unless known_keys.include?(key)
54
57
  end
@@ -67,7 +70,8 @@ module MutateRB
67
70
  end,
68
71
  exit_on_survivors: raw.fetch("exit_on_survivors", true),
69
72
  json_output_path: raw["json_output_path"],
70
- test_framework: raw.key?("test_framework") ? symbolize(raw["test_framework"], "test_framework") : :auto
73
+ test_framework: raw.key?("test_framework") ? symbolize(raw["test_framework"], "test_framework") : :auto,
74
+ verbose: raw.fetch("verbose", false)
71
75
  }
72
76
  end
73
77
  private_class_method :attributes_from_yaml
@@ -105,6 +109,7 @@ module MutateRB
105
109
  unknown = mutation_types - ALL_MUTATION_TYPES
106
110
  raise ConfigError, "unknown mutation_types: #{unknown.join(', ')}" unless unknown.empty?
107
111
  raise ConfigError, "exit_on_survivors must be true or false" unless [true, false].include?(exit_on_survivors)
112
+ raise ConfigError, "verbose must be true or false" unless [true, false].include?(verbose)
108
113
 
109
114
  validate_json_output_path!
110
115
  end
@@ -48,6 +48,7 @@ module MutateRB
48
48
  opts.on("--config PATH", "Use a config file other than .mutaterb.yml") do |v|
49
49
  flags[:config_path] = v
50
50
  end
51
+ opts.on("--verbose", "Print one line per mutant as it runs") { flags[:verbose] = true }
51
52
  opts.on("-h", "--help", "Show this help") do
52
53
  puts opts
53
54
  exit 0
@@ -19,17 +19,27 @@ module MutateRB
19
19
  framework: test_suite.framework)
20
20
  end
21
21
 
22
+ # Total number of mutants this run will process, known upfront without running any
23
+ # test (FR-002, FR-008) — memoized alongside `candidates` (research.md #3).
24
+ def total_mutants
25
+ candidates.size
26
+ end
27
+
22
28
  # Yields each finished Mutant, one at a time.
23
29
  def each_mutant
24
- source_files.each do |file|
25
- candidates_for(file).each { |mutant| yield run_one(mutant) }
26
- end
30
+ candidates.each { |mutant| yield run_one(mutant) }
27
31
  end
28
32
 
29
33
  private
30
34
 
31
35
  attr_reader :config, :test_suite, :test_runner
32
36
 
37
+ # Memoized: discovering candidates is pure parsing (no test execution), so computing
38
+ # it once upfront lets `total_mutants` be known before `each_mutant` starts (FR-002).
39
+ def candidates
40
+ @candidates ||= source_files.flat_map { |file| candidates_for(file) }
41
+ end
42
+
33
43
  def source_files
34
44
  Dir.glob(File.join(config.target_dir, "**", "*.rb"))
35
45
  .reject { |f| f.include?("/spec/") || f.include?("/test/") }
@@ -0,0 +1,124 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MutateRB
4
+ # Live feedback for a run in progress (FR-001 to FR-005, FR-007): mutaterb used to print
5
+ # nothing between start and the final summary, so a long run was indistinguishable from a
6
+ # hang. A background thread ticks a status line on a timer, independent of whether the
7
+ # baseline suite or a single mutant's tests have actually finished (FR-003a) — the real work
8
+ # happens inside an opaque, blocking subprocess call, so this is the only way to keep the
9
+ # terminal changing during a slow phase.
10
+ class ProgressReporter
11
+ SPINNER_FRAMES = %w[| / - \\].freeze
12
+ TTY_TICK_INTERVAL = 0.3
13
+ NON_TTY_TICK_INTERVAL = 5
14
+
15
+ def initialize(io: $stdout, verbose: false, tick_interval: nil)
16
+ @io = io
17
+ @verbose = verbose
18
+ @tick_interval = tick_interval || (io.tty? ? TTY_TICK_INTERVAL : NON_TTY_TICK_INTERVAL)
19
+ @mutex = Mutex.new
20
+ @phase = :idle
21
+ @processed = 0
22
+ @total = 0
23
+ @phase_started_at = nil
24
+ @ticker_thread = nil
25
+ end
26
+
27
+ def start_baseline
28
+ @mutex.synchronize do
29
+ @phase = :baseline
30
+ @phase_started_at = Time.now
31
+ end
32
+ @io.puts "Running baseline tests..."
33
+ start_ticker
34
+ end
35
+
36
+ def finish_baseline
37
+ stop_ticker
38
+ end
39
+
40
+ def start_mutants(total)
41
+ @mutex.synchronize do
42
+ @phase = :mutating
43
+ @processed = 0
44
+ @total = total
45
+ @phase_started_at = Time.now
46
+ end
47
+ start_ticker unless @verbose
48
+ end
49
+
50
+ # In verbose mode, this line IS the progress indicator for the mutant loop (FR-005):
51
+ # the bare counter/ticker never runs, so there is nothing to interleave with it.
52
+ def mutant_finished(mutant)
53
+ if @verbose
54
+ print_mutant_line(mutant)
55
+ else
56
+ @mutex.synchronize { @processed += 1 }
57
+ end
58
+ end
59
+
60
+ def finish_mutants
61
+ stop_ticker
62
+ @mutex.synchronize { @phase = :done }
63
+ end
64
+
65
+ private
66
+
67
+ def print_mutant_line(mutant)
68
+ @io.puts "#{mutant.file_path}:#{mutant.line} [#{mutant.operator_type}] " \
69
+ "'#{mutant.original_fragment}' -> '#{mutant.mutated_fragment}' => #{mutant.status}"
70
+ end
71
+
72
+ def start_ticker
73
+ @ticker_thread = Thread.new { tick_loop }
74
+ end
75
+
76
+ def stop_ticker
77
+ return unless @ticker_thread
78
+
79
+ @ticker_thread.kill
80
+ @ticker_thread.join
81
+ @ticker_thread = nil
82
+ @io.puts if @io.tty?
83
+ end
84
+
85
+ # Never lets a rendering bug abort the real mutation run (Principio V): worst case, the
86
+ # ticker just stops drawing and the run continues silently, exactly like before this
87
+ # feature existed.
88
+ def tick_loop
89
+ loop do
90
+ sleep @tick_interval
91
+ draw
92
+ end
93
+ rescue StandardError
94
+ nil
95
+ end
96
+
97
+ def draw
98
+ line = current_line
99
+ return unless line
100
+
101
+ if @io.tty?
102
+ @io.print "\r#{line}"
103
+ @io.flush
104
+ else
105
+ @io.puts line
106
+ end
107
+ end
108
+
109
+ def current_line
110
+ @mutex.synchronize do
111
+ next nil unless @phase_started_at
112
+
113
+ elapsed = Time.now - @phase_started_at
114
+ spinner = SPINNER_FRAMES[(elapsed / @tick_interval).to_i % SPINNER_FRAMES.size]
115
+ case @phase
116
+ when :baseline
117
+ "Running baseline tests... (#{spinner} #{elapsed.round}s)"
118
+ when :mutating
119
+ "Mutating... #{@processed}/#{@total} (#{spinner} #{elapsed.round}s)"
120
+ end
121
+ end
122
+ end
123
+ end
124
+ end
@@ -75,7 +75,8 @@ module MutateRB
75
75
  exclude_paths: c.exclude_paths,
76
76
  strictness: c.strictness.to_s,
77
77
  mutation_types: c.mutation_types.map(&:to_s),
78
- exit_on_survivors: c.exit_on_survivors
78
+ exit_on_survivors: c.exit_on_survivors,
79
+ verbose: c.verbose
79
80
  }
80
81
  end
81
82
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module MutateRB
4
- VERSION = "0.2.0"
4
+ VERSION = "0.3.0"
5
5
  end
data/lib/mutaterb.rb CHANGED
@@ -18,6 +18,7 @@ require_relative "mutaterb/mutation_operators/boolean_literal_operator"
18
18
  require_relative "mutaterb/mutation_operators/nil_literal_operator"
19
19
  require_relative "mutaterb/mutation_operators/arithmetic_comparison_operator"
20
20
  require_relative "mutaterb/mutator"
21
+ require_relative "mutaterb/progress_reporter"
21
22
  require_relative "mutaterb/reporter"
22
23
  require_relative "mutaterb/cli"
23
24
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: mutaterb
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - MarceloM47
@@ -76,6 +76,7 @@ files:
76
76
  - lib/mutaterb/mutation_operators/nil_literal_operator.rb
77
77
  - lib/mutaterb/mutation_run.rb
78
78
  - lib/mutaterb/mutator.rb
79
+ - lib/mutaterb/progress_reporter.rb
79
80
  - lib/mutaterb/project_detector.rb
80
81
  - lib/mutaterb/reporter.rb
81
82
  - lib/mutaterb/test_adapters/minitest_adapter.rb