evilution 1.2.0 → 1.4.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 +4 -4
- data/.beads/interactions.jsonl +100 -0
- data/.rubocop_todo.yml +14 -0
- data/CHANGELOG.md +124 -0
- data/README.md +142 -28
- data/docs/architecture.md +123 -9
- data/docs/ast_pattern_syntax.md +50 -5
- data/docs/isolation.md +39 -2
- data/docs/migration-from-mutant.md +1 -1
- data/lib/evilution/ast/aasm_declaration.rb +133 -0
- data/lib/evilution/ast/callback_declaration.rb +91 -0
- data/lib/evilution/ast/included_block.rb +27 -0
- data/lib/evilution/ast/literal_callable.rb +21 -0
- data/lib/evilution/ast/parser.rb +120 -16
- data/lib/evilution/ast/pattern/filter.rb +1 -0
- data/lib/evilution/ast/pattern/method_name.rb +63 -0
- data/lib/evilution/ast/pattern/parser.rb +15 -15
- data/lib/evilution/ast/pattern.rb +2 -0
- data/lib/evilution/ast/regexp_pattern.rb +104 -0
- data/lib/evilution/ast/scope_declaration.rb +45 -0
- data/lib/evilution/ast/sorbet_sig_detector.rb +1 -0
- data/lib/evilution/ast/uncovered_code.rb +88 -0
- data/lib/evilution/ast/value_object_definition.rb +25 -0
- data/lib/evilution/baseline/failure_formatter.rb +27 -0
- data/lib/evilution/baseline/report.rb +66 -0
- data/lib/evilution/baseline/spec_failure.rb +38 -0
- data/lib/evilution/baseline.rb +101 -42
- data/lib/evilution/child_output.rb +1 -1
- data/lib/evilution/cli/command.rb +1 -0
- data/lib/evilution/cli/commands/tests_list.rb +4 -3
- data/lib/evilution/cli/commands.rb +2 -0
- data/lib/evilution/cli/dispatcher.rb +2 -0
- data/lib/evilution/cli/exit_guard.rb +2 -0
- data/lib/evilution/cli/parsed_args.rb +2 -0
- data/lib/evilution/cli/parser/command_extractor.rb +2 -0
- data/lib/evilution/cli/parser/file_args.rb +4 -1
- data/lib/evilution/cli/parser/options_builder.rb +4 -1
- data/lib/evilution/cli/parser/stdin_reader.rb +1 -0
- data/lib/evilution/cli/parser.rb +1 -0
- data/lib/evilution/cli/printers/tests_list.rb +5 -4
- data/lib/evilution/cli/printers.rb +2 -0
- data/lib/evilution/cli/result.rb +2 -0
- data/lib/evilution/cli.rb +8 -2
- data/lib/evilution/config/builders.rb +2 -0
- data/lib/evilution/config/env_loader.rb +2 -0
- data/lib/evilution/config/file_loader.rb +1 -0
- data/lib/evilution/config/sources.rb +1 -0
- data/lib/evilution/config/validators/example_targeting_cache.rb +1 -0
- data/lib/evilution/config/validators/example_targeting_fallback.rb +1 -0
- data/lib/evilution/config/validators/example_targeting_strategy.rb +1 -0
- data/lib/evilution/config/validators/fail_fast.rb +1 -0
- data/lib/evilution/config/validators/hooks.rb +1 -0
- data/lib/evilution/config/validators/ignore_patterns.rb +1 -0
- data/lib/evilution/config/validators/integration.rb +1 -0
- data/lib/evilution/config/validators/isolation.rb +1 -0
- data/lib/evilution/config/validators/jobs.rb +1 -0
- data/lib/evilution/config/validators/preload.rb +1 -0
- data/lib/evilution/config/validators/profile.rb +1 -0
- data/lib/evilution/config/validators/spec_mappings.rb +3 -1
- data/lib/evilution/config/validators/spec_pattern.rb +1 -0
- data/lib/evilution/config/validators/warmup.rb +17 -0
- data/lib/evilution/config/validators.rb +2 -0
- data/lib/evilution/config.rb +7 -5
- data/lib/evilution/coverage.rb +1 -1
- data/lib/evilution/coverage_example_filter.rb +1 -1
- data/lib/evilution/diagnostic.rb +1 -1
- data/lib/evilution/equivalent/detector.rb +3 -1
- data/lib/evilution/equivalent/heuristic/guarded_index_fetch/condition.rb +66 -0
- data/lib/evilution/equivalent/heuristic/guarded_index_fetch/disturbance.rb +54 -0
- data/lib/evilution/equivalent/heuristic/guarded_index_fetch/early_exit.rb +73 -0
- data/lib/evilution/equivalent/heuristic/guarded_index_fetch/guard.rb +36 -0
- data/lib/evilution/equivalent/heuristic/guarded_index_fetch/index_read.rb +82 -0
- data/lib/evilution/equivalent/heuristic/guarded_index_fetch/node_path.rb +43 -0
- data/lib/evilution/equivalent/heuristic/guarded_index_fetch.rb +65 -0
- data/lib/evilution/example_filter.rb +46 -1
- data/lib/evilution/hooks/registry.rb +2 -1
- data/lib/evilution/integration/base.rb +2 -0
- data/lib/evilution/integration/known_failures.rb +26 -0
- data/lib/evilution/integration/loading/body_call_neutralizer.rb +139 -12
- data/lib/evilution/integration/loading/callback_redeclaration.rb +186 -0
- data/lib/evilution/integration/loading/concern_redeclaration.rb +61 -0
- data/lib/evilution/integration/loading/concern_state_cleaner.rb +19 -12
- data/lib/evilution/integration/loading/mutation_applier.rb +16 -0
- data/lib/evilution/integration/loading/test_class_cache.rb +46 -0
- data/lib/evilution/integration/loading/test_load_path.rb +21 -13
- data/lib/evilution/integration/minitest/test_ids.rb +21 -0
- data/lib/evilution/integration/minitest.rb +45 -6
- data/lib/evilution/integration/rspec/baseline_runner.rb +47 -3
- data/lib/evilution/integration/rspec/example_ids.rb +35 -0
- data/lib/evilution/integration/rspec/result_builder.rb +6 -1
- data/lib/evilution/integration/rspec/state_guard/anonymous_example_group_examples.rb +36 -0
- data/lib/evilution/integration/rspec/state_guard/configuration_state.rb +1 -0
- data/lib/evilution/integration/rspec/state_guard/configuration_streams.rb +1 -0
- data/lib/evilution/integration/rspec/state_guard/example_groups_constants.rb +1 -2
- data/lib/evilution/integration/rspec/state_guard/internals.rb +1 -2
- data/lib/evilution/integration/rspec/state_guard/object_space_example_groups.rb +1 -2
- data/lib/evilution/integration/rspec/state_guard/reporter_arrays.rb +1 -0
- data/lib/evilution/integration/rspec/state_guard/world_example_groups.rb +1 -0
- data/lib/evilution/integration/rspec/state_guard/world_filtered_examples.rb +1 -0
- data/lib/evilution/integration/rspec/state_guard/world_sources_by_path.rb +1 -0
- data/lib/evilution/integration/rspec/state_guard.rb +10 -1
- data/lib/evilution/integration/rspec.rb +22 -4
- data/lib/evilution/integration/test_unit/result_builder.rb +6 -0
- data/lib/evilution/integration/test_unit/test_ids.rb +18 -0
- data/lib/evilution/integration/test_unit.rb +37 -9
- data/lib/evilution/isolation/fork.rb +2 -1
- data/lib/evilution/isolation/in_process.rb +21 -4
- data/lib/evilution/mcp/info_tool/actions/environment.rb +1 -0
- data/lib/evilution/mcp/info_tool/actions/feedback.rb +1 -0
- data/lib/evilution/mcp/info_tool/actions/statuses.rb +1 -0
- data/lib/evilution/mcp/info_tool/actions/subjects.rb +1 -0
- data/lib/evilution/mcp/info_tool/actions/tests.rb +1 -0
- data/lib/evilution/mcp/info_tool/status_glossary.rb +2 -2
- data/lib/evilution/mcp/mutate_tool/progress_streamer.rb +2 -1
- data/lib/evilution/memory/leak_check.rb +27 -3
- data/lib/evilution/mutation.rb +8 -3
- data/lib/evilution/mutator/base.rb +49 -5
- data/lib/evilution/mutator/operator/alias_removal.rb +126 -0
- data/lib/evilution/mutator/operator/argument_list_removal.rb +52 -0
- data/lib/evilution/mutator/operator/argument_order_permutation.rb +101 -0
- data/lib/evilution/mutator/operator/argument_propagation.rb +54 -0
- data/lib/evilution/mutator/operator/argument_removal.rb +9 -1
- data/lib/evilution/mutator/operator/array_coercion_to_literal.rb +34 -0
- data/lib/evilution/mutator/operator/attribute_write_to_read.rb +34 -0
- data/lib/evilution/mutator/operator/bang_method.rb +11 -3
- data/lib/evilution/mutator/operator/binary_operand_promotion.rb +63 -0
- data/lib/evilution/mutator/operator/block_body_promotion.rb +45 -0
- data/lib/evilution/mutator/operator/block_body_to_nil.rb +52 -0
- data/lib/evilution/mutator/operator/block_body_to_raise.rb +34 -0
- data/lib/evilution/mutator/operator/call_to_nil.rb +34 -0
- data/lib/evilution/mutator/operator/coercion_emptying.rb +60 -0
- data/lib/evilution/mutator/operator/collection_replacement.rb +19 -9
- data/lib/evilution/mutator/operator/comparison_operand_swap.rb +54 -0
- data/lib/evilution/mutator/operator/comparison_replacement.rb +37 -12
- data/lib/evilution/mutator/operator/const_get_to_constant_path.rb +50 -0
- data/lib/evilution/mutator/operator/data_struct_member.rb +82 -0
- data/lib/evilution/mutator/operator/dig_to_fetch_chain.rb +45 -0
- data/lib/evilution/mutator/operator/double_negation_removal.rb +22 -0
- data/lib/evilution/mutator/operator/dynamic_dispatch_resolution.rb +79 -0
- data/lib/evilution/mutator/operator/exception_swallow.rb +84 -0
- data/lib/evilution/mutator/operator/format_specifier_swap.rb +100 -0
- data/lib/evilution/mutator/operator/forwarded_argument_drop.rb +142 -0
- data/lib/evilution/mutator/operator/inequality_to_negated_identity.rb +43 -0
- data/lib/evilution/mutator/operator/integer_division_to_fdiv.rb +62 -0
- data/lib/evilution/mutator/operator/keyword_argument.rb +25 -2
- data/lib/evilution/mutator/operator/keyword_argument_removal.rb +47 -0
- data/lib/evilution/mutator/operator/keyword_value_swap.rb +75 -0
- data/lib/evilution/mutator/operator/no_matching_pattern_else.rb +47 -0
- data/lib/evilution/mutator/operator/numbered_parameter_swap.rb +78 -0
- data/lib/evilution/mutator/operator/off_by_one_boundary.rb +68 -0
- data/lib/evilution/mutator/operator/optional_parameter_to_required.rb +14 -3
- data/lib/evilution/mutator/operator/pattern_matching_array.rb +12 -1
- data/lib/evilution/mutator/operator/pattern_wildcard_widening.rb +117 -0
- data/lib/evilution/mutator/operator/pin_operator_removal.rb +48 -0
- data/lib/evilution/mutator/operator/proc_to_lambda.rb +43 -0
- data/lib/evilution/mutator/operator/receiver_constructor_swap.rb +51 -0
- data/lib/evilution/mutator/operator/reduce_to_sum.rb +58 -0
- data/lib/evilution/mutator/operator/regex_simplification.rb +53 -132
- data/lib/evilution/mutator/operator/regexp_alternation_branch_deletion.rb +54 -0
- data/lib/evilution/mutator/operator/regexp_anchor_promotion.rb +43 -0
- data/lib/evilution/mutator/operator/regexp_anchor_to_predicate.rb +136 -0
- data/lib/evilution/mutator/operator/regexp_capture_to_passive.rb +61 -0
- data/lib/evilution/mutator/operator/regexp_character_type_complement.rb +54 -0
- data/lib/evilution/mutator/operator/regexp_named_group_rename.rb +106 -0
- data/lib/evilution/mutator/operator/regexp_option_removal.rb +51 -0
- data/lib/evilution/mutator/operator/regexp_quantifier_minimum_swap.rb +45 -0
- data/lib/evilution/mutator/operator/rescue_else_concatenation.rb +62 -0
- data/lib/evilution/mutator/operator/rescue_handler_concatenation.rb +69 -0
- data/lib/evilution/mutator/operator/rescue_handler_promotion.rb +65 -0
- data/lib/evilution/mutator/operator/return_keyword_removal.rb +79 -0
- data/lib/evilution/mutator/operator/rightward_assignment.rb +46 -0
- data/lib/evilution/mutator/operator/safe_navigation_removal.rb +67 -0
- data/lib/evilution/mutator/operator/send_mutation.rb +44 -6
- data/lib/evilution/mutator/operator/splat_operator.rb +38 -13
- data/lib/evilution/mutator/operator/statement_reorder.rb +153 -0
- data/lib/evilution/mutator/operator/symbol_to_proc_replacement.rb +59 -0
- data/lib/evilution/mutator/operator/to_i_to_integer.rb +38 -0
- data/lib/evilution/mutator/primitives.rb +25 -0
- data/lib/evilution/mutator/registry.rb +54 -2
- data/lib/evilution/mutator/rescue_handlers.rb +81 -0
- data/lib/evilution/parallel/pool.rb +1 -0
- data/lib/evilution/parallel_db_warning.rb +1 -1
- data/lib/evilution/process_supervisor.rb +3 -2
- data/lib/evilution/rails_warmup.rb +61 -0
- data/lib/evilution/reporter/cli/line_formatters/baseline_neutralized_notice.rb +55 -0
- data/lib/evilution/reporter/cli/metrics_block.rb +2 -0
- data/lib/evilution/reporter/html/report.rb +1 -0
- data/lib/evilution/reporter/json/baseline.rb +15 -0
- data/lib/evilution/reporter/json.rb +7 -2
- data/lib/evilution/result/baseline_neutralization.rb +10 -0
- data/lib/evilution/result/coverage_gap_grouper.rb +1 -0
- data/lib/evilution/result/mutation_result.rb +9 -1
- data/lib/evilution/result/subject_scorer.rb +1 -0
- data/lib/evilution/result/summary.rb +42 -2
- data/lib/evilution/runner/baseline_runner.rb +26 -11
- data/lib/evilution/runner/canary.rb +2 -57
- data/lib/evilution/runner/canary_failure_message.rb +101 -0
- data/lib/evilution/runner/isolation_resolver.rb +23 -2
- data/lib/evilution/runner/mutation_executor/neutralizer/baseline_failed.rb +35 -14
- data/lib/evilution/runner/mutation_executor/result_cache.rb +3 -0
- data/lib/evilution/runner/mutation_executor/result_packer.rb +4 -2
- data/lib/evilution/runner/mutation_executor.rb +1 -1
- data/lib/evilution/runner/mutation_planner.rb +11 -2
- data/lib/evilution/runner/report_publisher.rb +3 -2
- data/lib/evilution/runner/subject_pipeline.rb +46 -1
- data/lib/evilution/runner.rb +4 -1
- data/lib/evilution/source_ast_cache.rb +1 -1
- data/lib/evilution/spec_ast_cache.rb +1 -1
- data/lib/evilution/subject.rb +8 -2
- data/lib/evilution/version.rb +1 -1
- data/lib/evilution.rb +51 -0
- data/script/memory_check +62 -16
- metadata +108 -4
|
@@ -9,8 +9,11 @@ class Evilution::Result::MutationResult
|
|
|
9
9
|
|
|
10
10
|
attr_reader :mutation, :status, :duration, :killing_test, :test_command, :memory, :error, :neutral_reason
|
|
11
11
|
|
|
12
|
+
# known_failures_only: the tests failed, but every example that did was
|
|
13
|
+
# already failing before any mutation ran, so the failure says nothing
|
|
14
|
+
# about this one.
|
|
12
15
|
def initialize(mutation:, status:, duration: 0.0, killing_test: nil,
|
|
13
|
-
test_command: nil, memory: nil, error: nil, neutral_reason: nil)
|
|
16
|
+
test_command: nil, memory: nil, error: nil, neutral_reason: nil, known_failures_only: false)
|
|
14
17
|
raise ArgumentError, "invalid status: #{status}" unless STATUSES.include?(status)
|
|
15
18
|
|
|
16
19
|
@mutation = mutation
|
|
@@ -21,6 +24,7 @@ class Evilution::Result::MutationResult
|
|
|
21
24
|
@memory = memory
|
|
22
25
|
@error = error
|
|
23
26
|
@neutral_reason = neutral_reason
|
|
27
|
+
@known_failures_only = known_failures_only == true
|
|
24
28
|
freeze
|
|
25
29
|
end
|
|
26
30
|
|
|
@@ -54,6 +58,10 @@ class Evilution::Result::MutationResult
|
|
|
54
58
|
@error.is_a?(Evilution::Result::ErrorInfo) ? @error.backtrace : nil
|
|
55
59
|
end
|
|
56
60
|
|
|
61
|
+
def known_failures_only?
|
|
62
|
+
@known_failures_only
|
|
63
|
+
end
|
|
64
|
+
|
|
57
65
|
def killed?
|
|
58
66
|
status == :killed
|
|
59
67
|
end
|
|
@@ -3,13 +3,15 @@
|
|
|
3
3
|
require_relative "../result"
|
|
4
4
|
require_relative "coverage_gap_grouper"
|
|
5
5
|
require_relative "subject_scorer"
|
|
6
|
+
require_relative "baseline_neutralization"
|
|
6
7
|
|
|
7
8
|
class Evilution::Result::Summary
|
|
8
9
|
attr_reader :results, :duration, :skipped, :disabled_mutations, :unresolved_target_files,
|
|
9
|
-
:target_file_count, :infra_retried
|
|
10
|
+
:target_file_count, :infra_retried, :uncovered_code, :baseline_failures
|
|
10
11
|
|
|
11
12
|
def initialize(results:, duration: 0.0, truncated: false, skipped: 0, disabled_mutations: [],
|
|
12
|
-
unresolved_target_files: [], target_file_count: nil, infra_retried: 0
|
|
13
|
+
unresolved_target_files: [], target_file_count: nil, infra_retried: 0, uncovered_code: [],
|
|
14
|
+
baseline_failures: [])
|
|
13
15
|
@results = results
|
|
14
16
|
@duration = duration
|
|
15
17
|
@truncated = truncated
|
|
@@ -18,9 +20,27 @@ class Evilution::Result::Summary
|
|
|
18
20
|
@unresolved_target_files = unresolved_target_files.freeze
|
|
19
21
|
@target_file_count = target_file_count
|
|
20
22
|
@infra_retried = infra_retried
|
|
23
|
+
@uncovered_code = uncovered_code.freeze
|
|
24
|
+
@baseline_failures = baseline_failures.freeze
|
|
21
25
|
freeze
|
|
22
26
|
end
|
|
23
27
|
|
|
28
|
+
# The kills not counted because only examples already failing in the
|
|
29
|
+
# baseline failed, gathered under the spec file that was red and paired with
|
|
30
|
+
# why it was. They are out of the score, so a red spec file would read as
|
|
31
|
+
# full marks unless the report says they are there.
|
|
32
|
+
def baseline_neutralizations
|
|
33
|
+
baseline_neutralized_results.group_by { |result| result.neutral_reason.detail }.map do |spec_file, results|
|
|
34
|
+
Evilution::Result::BaselineNeutralization.new(
|
|
35
|
+
spec_file: spec_file, count: results.length, failures: baseline_failures_for(spec_file)
|
|
36
|
+
)
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def baseline_neutralized
|
|
41
|
+
baseline_neutralized_results.length
|
|
42
|
+
end
|
|
43
|
+
|
|
24
44
|
# Neutral results gathered by the reason they were recorded, which is what the
|
|
25
45
|
# report needs to say which kind of neutral a reader is looking at
|
|
26
46
|
# (EV-5pob / GH #1606).
|
|
@@ -53,6 +73,12 @@ class Evilution::Result::Summary
|
|
|
53
73
|
!unresolved_target_files.empty?
|
|
54
74
|
end
|
|
55
75
|
|
|
76
|
+
# Targeted lines that hold code outside every subject, so no mutation was
|
|
77
|
+
# ever generated for them: `{ file:, lines: ["2-4", "9"] }` per file.
|
|
78
|
+
def uncovered_code?
|
|
79
|
+
!uncovered_code.empty?
|
|
80
|
+
end
|
|
81
|
+
|
|
56
82
|
def truncated?
|
|
57
83
|
@truncated
|
|
58
84
|
end
|
|
@@ -167,4 +193,18 @@ class Evilution::Result::Summary
|
|
|
167
193
|
|
|
168
194
|
max_rss && (max_rss / 1024.0)
|
|
169
195
|
end
|
|
196
|
+
|
|
197
|
+
private
|
|
198
|
+
|
|
199
|
+
def baseline_neutralized_results
|
|
200
|
+
neutral_results.select { |result| result.neutral_reason && result.neutral_reason.kind == :baseline_failure }
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
# No spec file named means the run was given its spec files explicitly, and
|
|
204
|
+
# any of them being red neutralizes the survivor.
|
|
205
|
+
def baseline_failures_for(spec_file)
|
|
206
|
+
return baseline_failures if spec_file.nil?
|
|
207
|
+
|
|
208
|
+
baseline_failures.select { |failure| failure.spec_file == spec_file }
|
|
209
|
+
end
|
|
170
210
|
end
|
|
@@ -12,6 +12,7 @@ require_relative "../integration/test_unit"
|
|
|
12
12
|
require_relative "../example_filter"
|
|
13
13
|
require_relative "../spec_ast_cache"
|
|
14
14
|
require_relative "../source_ast_cache"
|
|
15
|
+
require_relative "../diagnostic"
|
|
15
16
|
|
|
16
17
|
unless defined?(Evilution::Runner::INTEGRATIONS)
|
|
17
18
|
Evilution::Runner::INTEGRATIONS = {
|
|
@@ -33,15 +34,19 @@ class Evilution::Runner::BaselineRunner
|
|
|
33
34
|
end
|
|
34
35
|
end
|
|
35
36
|
|
|
36
|
-
|
|
37
|
+
# baseline_result: what the baseline found, when it ran. The tests it saw
|
|
38
|
+
# failing are handed to the integration, which can then tell a mutation run
|
|
39
|
+
# that fails on nothing else from one that kills.
|
|
40
|
+
def build_integration(baseline_result = nil)
|
|
37
41
|
klass = integration_class
|
|
38
|
-
kwargs = base_integration_kwargs
|
|
42
|
+
kwargs = base_integration_kwargs(baseline_result)
|
|
39
43
|
kwargs.merge!(rspec_integration_kwargs) if klass == Evilution::Integration::RSpec
|
|
40
44
|
klass.new(**kwargs)
|
|
41
45
|
end
|
|
42
46
|
|
|
43
|
-
def base_integration_kwargs
|
|
47
|
+
def base_integration_kwargs(baseline_result = nil)
|
|
44
48
|
{
|
|
49
|
+
known_failures: baseline_result ? baseline_result.failed_example_ids : [],
|
|
45
50
|
test_files: config.spec_files.empty? ? nil : config.spec_files,
|
|
46
51
|
hooks: hooks,
|
|
47
52
|
fallback_to_full_suite: config.fallback_to_full_suite?,
|
|
@@ -60,18 +65,15 @@ class Evilution::Runner::BaselineRunner
|
|
|
60
65
|
return nil unless config.baseline? && subjects.any?
|
|
61
66
|
|
|
62
67
|
log_start
|
|
63
|
-
|
|
64
|
-
timeout: config.timeout,
|
|
65
|
-
test_files: config.spec_files.empty? ? nil : config.spec_files,
|
|
66
|
-
**integration_class.baseline_options
|
|
67
|
-
)
|
|
68
|
-
result = baseline.call(subjects)
|
|
68
|
+
result = build_baseline.call(subjects)
|
|
69
69
|
log_complete(result)
|
|
70
70
|
result
|
|
71
71
|
end
|
|
72
72
|
|
|
73
|
+
# The same selector the baseline resolves with, so a surviving mutation is
|
|
74
|
+
# matched against exactly the spec files the baseline ran.
|
|
73
75
|
def neutralization_resolver
|
|
74
|
-
|
|
76
|
+
config.spec_selector
|
|
75
77
|
end
|
|
76
78
|
|
|
77
79
|
def neutralization_fallback_dir
|
|
@@ -82,6 +84,19 @@ class Evilution::Runner::BaselineRunner
|
|
|
82
84
|
|
|
83
85
|
attr_reader :config, :hooks
|
|
84
86
|
|
|
87
|
+
# Resolves through config.spec_selector -- the object `run` uses, so
|
|
88
|
+
# spec_mappings / spec_pattern apply -- and skips unresolved sources unless
|
|
89
|
+
# the run falls back to the full suite.
|
|
90
|
+
def build_baseline
|
|
91
|
+
Evilution::Baseline.new(
|
|
92
|
+
timeout: config.timeout,
|
|
93
|
+
test_files: config.spec_files.empty? ? nil : config.spec_files,
|
|
94
|
+
spec_selector: config.spec_selector,
|
|
95
|
+
fallback_to_full_suite: config.fallback_to_full_suite?,
|
|
96
|
+
**integration_class.baseline_options
|
|
97
|
+
)
|
|
98
|
+
end
|
|
99
|
+
|
|
85
100
|
def build_example_filter
|
|
86
101
|
return nil unless config.example_targeting?
|
|
87
102
|
|
|
@@ -109,7 +124,7 @@ class Evilution::Runner::BaselineRunner
|
|
|
109
124
|
|
|
110
125
|
Evilution::CoverageExampleFilter.new(map: map, lexical: lexical)
|
|
111
126
|
rescue StandardError => e
|
|
112
|
-
warn
|
|
127
|
+
Evilution::Diagnostic.warn("evilution: coverage targeting unavailable (#{e.class}: #{e.message}); using lexical targeting")
|
|
113
128
|
lexical
|
|
114
129
|
end
|
|
115
130
|
|
|
@@ -6,6 +6,7 @@ require "securerandom"
|
|
|
6
6
|
require_relative "../runner"
|
|
7
7
|
require_relative "../mutation"
|
|
8
8
|
require_relative "../subject"
|
|
9
|
+
require_relative "canary_failure_message"
|
|
9
10
|
|
|
10
11
|
# Runs one guaranteed-unobservable synthetic mutation through the configured
|
|
11
12
|
# integration + isolation at session start. The synthetic spec never references
|
|
@@ -34,7 +35,7 @@ class Evilution::Runner::Canary
|
|
|
34
35
|
test_command: ->(mutation) { build_integration(spec_path).call(mutation) },
|
|
35
36
|
timeout: @config.timeout
|
|
36
37
|
)
|
|
37
|
-
raise Failed,
|
|
38
|
+
raise Failed, Evilution::Runner::CanaryFailureMessage.new(result).to_s unless result.status == :survived
|
|
38
39
|
|
|
39
40
|
nil
|
|
40
41
|
ensure
|
|
@@ -145,60 +146,4 @@ class Evilution::Runner::Canary
|
|
|
145
146
|
def build_integration(spec_path)
|
|
146
147
|
@integration_class.new(test_files: [spec_path], hooks: @hooks)
|
|
147
148
|
end
|
|
148
|
-
|
|
149
|
-
# When the child reported an error, that error IS the diagnosis -- naming it
|
|
150
|
-
# beats guessing. The speculative list stays only for the cases that carry no
|
|
151
|
-
# error at all (:killed, :timeout), where guesses are the only help there is.
|
|
152
|
-
# Diagnosing GH #1581 meant rebuilding this canary by hand to read the field
|
|
153
|
-
# this message used to drop, and none of the four guesses was the cause.
|
|
154
|
-
# EV-65nf / GH #1586.
|
|
155
|
-
def failure_message(result)
|
|
156
|
-
"#{failure_preamble(result.status)} #{diagnosis(result)} " \
|
|
157
|
-
"Re-run with --no-canary to bypass this check."
|
|
158
|
-
end
|
|
159
|
-
|
|
160
|
-
def failure_preamble(status)
|
|
161
|
-
"evilution proof-of-life canary failed: a guaranteed-unobservable synthetic " \
|
|
162
|
-
"mutation was scored #{status.inspect} instead of :survived. The mutation " \
|
|
163
|
-
"pipeline is misreporting — every score this run would produce is unreliable."
|
|
164
|
-
end
|
|
165
|
-
|
|
166
|
-
def diagnosis(result)
|
|
167
|
-
reported = reported_error(result)
|
|
168
|
-
return speculative_causes if reported.nil?
|
|
169
|
-
|
|
170
|
-
"The child reported: #{reported}"
|
|
171
|
-
end
|
|
172
|
-
|
|
173
|
-
def reported_error(result)
|
|
174
|
-
message = result.error_message
|
|
175
|
-
return nil if message.nil? || message.empty?
|
|
176
|
-
|
|
177
|
-
described = with_class_prefix(message, result.error_class)
|
|
178
|
-
frame = first_frame(result)
|
|
179
|
-
frame.nil? ? described : "#{described} (at #{frame})"
|
|
180
|
-
end
|
|
181
|
-
|
|
182
|
-
# MutationApplier already prefixes the class onto the message it packs
|
|
183
|
-
# ("#{e.class}: #{e.message}"), while other paths pack the bare message and
|
|
184
|
-
# leave the class in its own field. Prefixing unconditionally would print
|
|
185
|
-
# "NameError: NameError: ..." for the former.
|
|
186
|
-
def with_class_prefix(message, klass)
|
|
187
|
-
return message if klass.nil? || klass.empty? || message.start_with?("#{klass}:")
|
|
188
|
-
|
|
189
|
-
"#{klass}: #{message}"
|
|
190
|
-
end
|
|
191
|
-
|
|
192
|
-
def first_frame(result)
|
|
193
|
-
backtrace = result.error_backtrace
|
|
194
|
-
return nil if backtrace.nil? || backtrace.empty?
|
|
195
|
-
|
|
196
|
-
backtrace.first
|
|
197
|
-
end
|
|
198
|
-
|
|
199
|
-
def speculative_causes
|
|
200
|
-
"Likely causes: Rails/Zeitwerk autoloading breaking child eval; an env-specific " \
|
|
201
|
-
"RSpec config (e.g. fail_if_no_examples); a classify_status fallback defect; or " \
|
|
202
|
-
"an isolation-mode defect."
|
|
203
|
-
end
|
|
204
149
|
end
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../runner"
|
|
4
|
+
|
|
5
|
+
# Explains why the proof-of-life canary failed, from the result its synthetic
|
|
6
|
+
# mutation came back with. Kept apart from Canary, which only runs the probe:
|
|
7
|
+
# the wording changes for different reasons than the mechanics do.
|
|
8
|
+
class Evilution::Runner::CanaryFailureMessage
|
|
9
|
+
# Raised by RubyGems when it cannot load a gem at a version that satisfies
|
|
10
|
+
# every requirement already in play.
|
|
11
|
+
GEM_ACTIVATION_ERRORS = %w[
|
|
12
|
+
Gem::ConflictError Gem::LoadError Gem::MissingSpecError Gem::MissingSpecVersionError
|
|
13
|
+
].freeze
|
|
14
|
+
|
|
15
|
+
def initialize(result)
|
|
16
|
+
@result = result
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# When the child reported an error, that error IS the diagnosis -- naming it
|
|
20
|
+
# beats guessing. The speculative list stays only for the cases that carry no
|
|
21
|
+
# error at all (:killed, :timeout), where guesses are the only help there is.
|
|
22
|
+
# Diagnosing GH #1581 meant rebuilding this canary by hand to read the field
|
|
23
|
+
# this message used to drop, and none of the four guesses was the cause.
|
|
24
|
+
# EV-65nf / GH #1586.
|
|
25
|
+
def to_s
|
|
26
|
+
return gem_activation_message(@result) if gem_activation_failure?(@result)
|
|
27
|
+
|
|
28
|
+
"#{failure_preamble(@result.status)} #{diagnosis(@result)} " \
|
|
29
|
+
"Re-run with --no-canary to bypass this check."
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
private
|
|
33
|
+
|
|
34
|
+
# The usual cause is evilution running outside `bundle exec`: RubyGems then
|
|
35
|
+
# activates the newest installed copy of a shared dependency, which the test
|
|
36
|
+
# framework may not accept. Every mutation would fail the same way, so
|
|
37
|
+
# skipping the canary would not help.
|
|
38
|
+
def gem_activation_message(result)
|
|
39
|
+
"evilution proof-of-life canary failed: the test framework could not be loaded " \
|
|
40
|
+
"because of a gem activation conflict. This is an environment problem, not a " \
|
|
41
|
+
"mutation-pipeline defect, and every mutation would fail the same way. Run " \
|
|
42
|
+
"evilution under `bundle exec` so the project's Gemfile.lock decides gem versions " \
|
|
43
|
+
"(for the MCP server: command `bundle`, args [\"exec\", \"evilution\", \"mcp\"]). " \
|
|
44
|
+
"The child reported: #{reported_error(result)}"
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# The class may arrive in its own field or only as the message prefix,
|
|
48
|
+
# depending on which path packed the error.
|
|
49
|
+
def gem_activation_failure?(result)
|
|
50
|
+
message = result.error_message.to_s
|
|
51
|
+
|
|
52
|
+
GEM_ACTIVATION_ERRORS.any? do |klass|
|
|
53
|
+
result.error_class == klass || message.start_with?("#{klass}:")
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def failure_preamble(status)
|
|
58
|
+
"evilution proof-of-life canary failed: a guaranteed-unobservable synthetic " \
|
|
59
|
+
"mutation was scored #{status.inspect} instead of :survived. The mutation " \
|
|
60
|
+
"pipeline is misreporting — every score this run would produce is unreliable."
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def diagnosis(result)
|
|
64
|
+
reported = reported_error(result)
|
|
65
|
+
return speculative_causes if reported.nil?
|
|
66
|
+
|
|
67
|
+
"The child reported: #{reported}"
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def reported_error(result)
|
|
71
|
+
message = result.error_message
|
|
72
|
+
return nil if message.nil? || message.empty?
|
|
73
|
+
|
|
74
|
+
described = with_class_prefix(message, result.error_class)
|
|
75
|
+
frame = first_frame(result)
|
|
76
|
+
frame.nil? ? described : "#{described} (at #{frame})"
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# MutationApplier already prefixes the class onto the message it packs
|
|
80
|
+
# ("#{e.class}: #{e.message}"), while other paths pack the bare message and
|
|
81
|
+
# leave the class in its own field. Prefixing unconditionally would print
|
|
82
|
+
# "NameError: NameError: ..." for the former.
|
|
83
|
+
def with_class_prefix(message, klass)
|
|
84
|
+
return message if klass.nil? || klass.empty? || message.start_with?("#{klass}:")
|
|
85
|
+
|
|
86
|
+
"#{klass}: #{message}"
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def first_frame(result)
|
|
90
|
+
backtrace = result.error_backtrace
|
|
91
|
+
return nil if backtrace.nil? || backtrace.empty?
|
|
92
|
+
|
|
93
|
+
backtrace.first
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def speculative_causes
|
|
97
|
+
"Likely causes: Rails/Zeitwerk autoloading breaking child eval; an env-specific " \
|
|
98
|
+
"RSpec config (e.g. fail_if_no_examples); a classify_status fallback defect; or " \
|
|
99
|
+
"an isolation-mode defect."
|
|
100
|
+
end
|
|
101
|
+
end
|
|
@@ -6,6 +6,7 @@ require_relative "../isolation/in_process"
|
|
|
6
6
|
require_relative "../rails_detector"
|
|
7
7
|
require_relative "../gem_detector"
|
|
8
8
|
require_relative "../integration/loading/test_load_path"
|
|
9
|
+
require_relative "../rails_warmup"
|
|
9
10
|
|
|
10
11
|
class Evilution::Runner::IsolationResolver
|
|
11
12
|
PRELOAD_CANDIDATES = [
|
|
@@ -48,6 +49,7 @@ class Evilution::Runner::IsolationResolver
|
|
|
48
49
|
prepare_load_path_for_preload(path)
|
|
49
50
|
prepare_integration_for_preload
|
|
50
51
|
require File.expand_path(path)
|
|
52
|
+
warm_up_rails
|
|
51
53
|
rescue Evilution::ConfigError
|
|
52
54
|
raise
|
|
53
55
|
rescue ScriptError, StandardError => e
|
|
@@ -61,6 +63,21 @@ class Evilution::Runner::IsolationResolver
|
|
|
61
63
|
|
|
62
64
|
attr_reader :config, :hooks
|
|
63
65
|
|
|
66
|
+
# Runs only after a preload (which is what loads Rails). This is primarily an
|
|
67
|
+
# optimisation for :fork isolation, where children inherit the warmed state.
|
|
68
|
+
def warm_up_rails
|
|
69
|
+
return unless config.warmup == :rails
|
|
70
|
+
|
|
71
|
+
application = ::Rails.application if defined?(::Rails) && ::Rails.respond_to?(:application)
|
|
72
|
+
unless application
|
|
73
|
+
warn_message = "[evilution] warmup: rails requested but Rails is not loaded (or has no application) after preload; skipping"
|
|
74
|
+
Evilution::Diagnostic.warn(warn_message)
|
|
75
|
+
return
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
Evilution::RailsWarmup.new(application).call
|
|
79
|
+
end
|
|
80
|
+
|
|
64
81
|
# Under :fork, allow preloading — caller resolves whether a path exists (an
|
|
65
82
|
# explicit --preload / preload: value, or an auto-detected rails_helper) and
|
|
66
83
|
# bails early when none does. Under :in_process, only allow preloading when
|
|
@@ -131,7 +148,11 @@ class Evilution::Runner::IsolationResolver
|
|
|
131
148
|
# have both (a bare `require "support/foo"` must still resolve from spec/).
|
|
132
149
|
# - minitest/test-unit: a test/test_helper.rb doing a non-relative
|
|
133
150
|
# `require "support/..."` needs test/ on $LOAD_PATH. Route through
|
|
134
|
-
# TestLoadPath
|
|
151
|
+
# TestLoadPath, the policy the per-mutation test load uses, with one
|
|
152
|
+
# deliberate difference: outside_root_dirs: false. A resolved test file
|
|
153
|
+
# outside test/ and spec/ keeps its own directory, but a preload there
|
|
154
|
+
# (config/evilution_preload.rb) must not, or config/ lands ahead of the
|
|
155
|
+
# gems and config/puma.rb shadows the puma gem (GH #1597).
|
|
135
156
|
def prepare_load_path_for_preload(preload_path)
|
|
136
157
|
if config.integration == :rspec
|
|
137
158
|
prepare_rspec_preload_load_path
|
|
@@ -148,7 +169,7 @@ class Evilution::Runner::IsolationResolver
|
|
|
148
169
|
|
|
149
170
|
def prepare_test_preload_load_path(preload_path)
|
|
150
171
|
base = detected_rails_root || Evilution.project_base_dir
|
|
151
|
-
Evilution::Integration::Loading::TestLoadPath.add!([preload_path], base: base)
|
|
172
|
+
Evilution::Integration::Loading::TestLoadPath.add!([preload_path], base: base, outside_root_dirs: false)
|
|
152
173
|
end
|
|
153
174
|
|
|
154
175
|
def resolve_spec_dir
|
|
@@ -4,6 +4,17 @@ require_relative "../neutralizer"
|
|
|
4
4
|
require_relative "../../../result/mutation_result"
|
|
5
5
|
require_relative "../../../result/neutral_reason"
|
|
6
6
|
|
|
7
|
+
# Reclassifies a kill as :neutral when the tests failed on nothing but
|
|
8
|
+
# examples that were already failing before any mutation ran.
|
|
9
|
+
#
|
|
10
|
+
# Such an example fails whatever the mutation is, so every mutation it runs
|
|
11
|
+
# against would count as killed and a red spec file would score full marks.
|
|
12
|
+
# The failure says nothing about the mutation: it has no verdict.
|
|
13
|
+
#
|
|
14
|
+
# A survivor is left alone, whatever the baseline did. Its tests passed, the
|
|
15
|
+
# examples that were red in the baseline included, so the gap it reports is
|
|
16
|
+
# real -- and neutralizing it, as used to happen for every survivor a red spec
|
|
17
|
+
# file covered, hid exactly the mutations a reader needs to see.
|
|
7
18
|
class Evilution::Runner::MutationExecutor::Neutralizer::BaselineFailed
|
|
8
19
|
def initialize(config:, spec_resolver:, fallback_dir:)
|
|
9
20
|
@config = config
|
|
@@ -12,25 +23,35 @@ class Evilution::Runner::MutationExecutor::Neutralizer::BaselineFailed
|
|
|
12
23
|
end
|
|
13
24
|
|
|
14
25
|
def call(result, baseline_result:)
|
|
15
|
-
return result unless result.
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
should_neutralize = true
|
|
19
|
-
spec_file = nil
|
|
20
|
-
else
|
|
21
|
-
spec_file = @spec_resolver.call(result.mutation.file_path) || @fallback_dir
|
|
22
|
-
should_neutralize = baseline_result.failed_spec_files.include?(spec_file)
|
|
23
|
-
end
|
|
24
|
-
return result unless should_neutralize
|
|
25
|
-
|
|
26
|
-
neutralize(result, spec_file)
|
|
26
|
+
return result unless result.killed? && result.known_failures_only?
|
|
27
|
+
|
|
28
|
+
neutralize(result, red_spec(result, baseline_result))
|
|
27
29
|
end
|
|
28
30
|
|
|
29
31
|
private
|
|
30
32
|
|
|
33
|
+
# The red spec file to name: one covering the mutated source. A run given
|
|
34
|
+
# its spec files explicitly has no single one to point at.
|
|
35
|
+
def red_spec(result, baseline_result)
|
|
36
|
+
return nil if @config.spec_files.any? || baseline_result.nil?
|
|
37
|
+
|
|
38
|
+
failed = baseline_result.failed_spec_files
|
|
39
|
+
covering_specs(result.mutation.file_path).find { |spec| failed.include?(spec) }
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# The spec files the baseline ran for this source: every file the resolver
|
|
43
|
+
# returns (a spec_selector may map one source to several), or the fallback
|
|
44
|
+
# directory when nothing resolves -- but only if the run falls back to the
|
|
45
|
+
# full suite; otherwise the baseline never ran it.
|
|
46
|
+
def covering_specs(file_path)
|
|
47
|
+
specs = Array(@spec_resolver.call(file_path))
|
|
48
|
+
return specs unless specs.empty?
|
|
49
|
+
|
|
50
|
+
@config.fallback_to_full_suite? ? [@fallback_dir] : []
|
|
51
|
+
end
|
|
52
|
+
|
|
31
53
|
# The spec that was already red is the fact a reader needs: it says what to
|
|
32
|
-
# fix before the run can say anything about these mutations
|
|
33
|
-
# (EV-5pob / GH #1606).
|
|
54
|
+
# fix before the run can say anything about these mutations.
|
|
34
55
|
def neutralize(result, spec_file)
|
|
35
56
|
Evilution::Result::MutationResult.new(
|
|
36
57
|
mutation: result.mutation,
|
|
@@ -34,6 +34,9 @@ class Evilution::Runner::MutationExecutor::ResultCache
|
|
|
34
34
|
return unless @backend
|
|
35
35
|
return unless result.killed? || result.timeout?
|
|
36
36
|
return if infra_crash?(result)
|
|
37
|
+
# Nor is one where only already-failing examples failed: the baseline
|
|
38
|
+
# that says so belongs to this run, and the cache keeps no trace of it.
|
|
39
|
+
return if result.known_failures_only?
|
|
37
40
|
|
|
38
41
|
@backend.store(mutation,
|
|
39
42
|
status: result.status,
|
|
@@ -15,7 +15,8 @@ class Evilution::Runner::MutationExecutor::ResultPacker
|
|
|
15
15
|
parent_rss_kb: result.parent_rss_kb,
|
|
16
16
|
error_message: result.error_message,
|
|
17
17
|
error_class: result.error_class,
|
|
18
|
-
error_backtrace: result.error_backtrace
|
|
18
|
+
error_backtrace: result.error_backtrace,
|
|
19
|
+
known_failures_only: result.known_failures_only?
|
|
19
20
|
}
|
|
20
21
|
end
|
|
21
22
|
|
|
@@ -35,7 +36,8 @@ class Evilution::Runner::MutationExecutor::ResultPacker
|
|
|
35
36
|
message: data[:error_message],
|
|
36
37
|
klass: data[:error_class],
|
|
37
38
|
backtrace: data[:error_backtrace]
|
|
38
|
-
)
|
|
39
|
+
),
|
|
40
|
+
known_failures_only: data[:known_failures_only]
|
|
39
41
|
)
|
|
40
42
|
end
|
|
41
43
|
end
|
|
@@ -30,7 +30,7 @@ class Evilution::Runner::MutationExecutor
|
|
|
30
30
|
end
|
|
31
31
|
|
|
32
32
|
def call(mutations, baseline_result = nil)
|
|
33
|
-
integration = @baseline_runner.build_integration
|
|
33
|
+
integration = @baseline_runner.build_integration(baseline_result)
|
|
34
34
|
spec_resolver = baseline_failed?(baseline_result) ? @baseline_runner.neutralization_resolver : nil
|
|
35
35
|
notifier = build_notifier
|
|
36
36
|
pipeline = build_pipeline(spec_resolver)
|
|
@@ -7,7 +7,7 @@ require_relative "../ast/pattern/filter"
|
|
|
7
7
|
require_relative "../equivalent/detector"
|
|
8
8
|
|
|
9
9
|
class Evilution::Runner::MutationPlanner
|
|
10
|
-
Plan = Struct.new(:enabled, :equivalent, :skipped_count, :disabled_mutations, keyword_init: true)
|
|
10
|
+
Plan = Struct.new(:enabled, :equivalent, :skipped_count, :disabled_mutations, :mutated_lines, keyword_init: true)
|
|
11
11
|
|
|
12
12
|
GenerationResult = Data.define(:mutations, :skipped)
|
|
13
13
|
DisabledFilterResult = Data.define(:enabled, :disabled)
|
|
@@ -33,13 +33,22 @@ class Evilution::Runner::MutationPlanner
|
|
|
33
33
|
sig_filter = filter_sig_blocks(disabled_filter.enabled)
|
|
34
34
|
equivalent_filter = filter_equivalent(sig_filter.enabled)
|
|
35
35
|
|
|
36
|
-
build_plan(equivalent_filter, disabled_mutations, total_skipped(generation, disabled_filter, sig_filter))
|
|
36
|
+
plan = build_plan(equivalent_filter, disabled_mutations, total_skipped(generation, disabled_filter, sig_filter))
|
|
37
|
+
plan.mutated_lines = mutated_lines(deduped)
|
|
38
|
+
plan
|
|
37
39
|
end
|
|
38
40
|
|
|
39
41
|
private
|
|
40
42
|
|
|
41
43
|
attr_reader :config, :registry
|
|
42
44
|
|
|
45
|
+
# The lines mutations were generated for, per file. Taken before any filter:
|
|
46
|
+
# a line whose mutations were all disabled or judged equivalent was still
|
|
47
|
+
# reached by an operator.
|
|
48
|
+
def mutated_lines(mutations)
|
|
49
|
+
mutations.group_by(&:file_path).transform_values { |group| group.map(&:line).uniq.sort }
|
|
50
|
+
end
|
|
51
|
+
|
|
43
52
|
def compute_disabled_mutations(disabled_filter)
|
|
44
53
|
return [] unless config.show_disabled?
|
|
45
54
|
|
|
@@ -5,6 +5,7 @@ require_relative "../reporter/json"
|
|
|
5
5
|
require_relative "../reporter/cli"
|
|
6
6
|
require_relative "../reporter/html"
|
|
7
7
|
require_relative "../session/store"
|
|
8
|
+
require_relative "../diagnostic"
|
|
8
9
|
|
|
9
10
|
class Evilution::Runner::ReportPublisher
|
|
10
11
|
def initialize(config)
|
|
@@ -46,7 +47,7 @@ class Evilution::Runner::ReportPublisher
|
|
|
46
47
|
def publish_html(output)
|
|
47
48
|
path = "evilution-report.html"
|
|
48
49
|
File.write(path, output)
|
|
49
|
-
warn
|
|
50
|
+
Evilution::Diagnostic.warn("HTML report written to #{path}")
|
|
50
51
|
end
|
|
51
52
|
|
|
52
53
|
def save_session(summary)
|
|
@@ -54,7 +55,7 @@ class Evilution::Runner::ReportPublisher
|
|
|
54
55
|
|
|
55
56
|
Evilution::Session::Store.new.save(summary)
|
|
56
57
|
rescue StandardError => e
|
|
57
|
-
warn
|
|
58
|
+
Evilution::Diagnostic.warn("[evilution] failed to save session: #{e.message}") unless config.quiet
|
|
58
59
|
end
|
|
59
60
|
|
|
60
61
|
private
|