mutaterb 0.2.0 → 0.3.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.
- checksums.yaml +4 -4
- data/README.md +6 -0
- data/lib/mutaterb/cli.rb +16 -4
- data/lib/mutaterb/config.rb +9 -4
- data/lib/mutaterb/flag_parser.rb +1 -0
- data/lib/mutaterb/mutator.rb +13 -3
- data/lib/mutaterb/progress_reporter.rb +124 -0
- data/lib/mutaterb/reporter.rb +2 -1
- data/lib/mutaterb/test_adapters/minitest_adapter.rb +6 -1
- data/lib/mutaterb/version.rb +1 -1
- data/lib/mutaterb.rb +1 -0
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 82ad4fc719ac8b83a1857348b0be06c45fbb2995fbcf431d85fb2b2b52c74342
|
|
4
|
+
data.tar.gz: add794c175e4509c69fc293090c5c60c6f9493c5477cabeea89630863b5c1832
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 1128b8a7cd0462d1aa9eff94dfeaec8e7950b59faed65cb7cd91a9731e2fbf7f18b225098cad6a1c43e576ce82ffd5b876d384e67f72ae735849fdbb6f8767dc
|
|
7
|
+
data.tar.gz: 47642ca53aa83eb67f9a5226a0452a6c6c4d8fdc4fe6369a02e4253c6c7a568d836a1be54f725dd6955097667489c6f46a25f2f38108a1c7e14ad252d5761042
|
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
|
-
|
|
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
|
data/lib/mutaterb/config.rb
CHANGED
|
@@ -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
|
data/lib/mutaterb/flag_parser.rb
CHANGED
|
@@ -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
|
data/lib/mutaterb/mutator.rb
CHANGED
|
@@ -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
|
-
|
|
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
|
data/lib/mutaterb/reporter.rb
CHANGED
|
@@ -17,9 +17,14 @@ module MutateRB
|
|
|
17
17
|
LINE_PATTERN = /^(\S+)\s*=\s*([\d.]+)\s*s\s*=\s*([.FES])/
|
|
18
18
|
FILE_MARKER = "@@MUTATERB_FILE@@"
|
|
19
19
|
|
|
20
|
+
# Files are joined with `;`, not `&&`: `bin/rails test`/`ruby` exits non-zero whenever a
|
|
21
|
+
# test fails or errors, which is the normal case for a real project's suite — `&&` would
|
|
22
|
+
# silently stop the whole chain at the first failing file, dropping every file after it
|
|
23
|
+
# from the baseline. Nothing here reads the shell's exit status (TestRunner classifies
|
|
24
|
+
# purely from parsed output), so running every file regardless is safe.
|
|
20
25
|
def self.command_for(files, project_type:)
|
|
21
26
|
files.map { |file| "echo #{FILE_MARKER}#{Shellwords.escape(file)} && #{run_one(file, project_type)}" }
|
|
22
|
-
.join("
|
|
27
|
+
.join(" ; ")
|
|
23
28
|
end
|
|
24
29
|
|
|
25
30
|
def self.run_one(file, project_type)
|
data/lib/mutaterb/version.rb
CHANGED
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.
|
|
4
|
+
version: 0.3.1
|
|
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
|