kimera 0.1.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 +7 -0
- data/.kimera.yml.example +59 -0
- data/CHANGELOG.md +36 -0
- data/LICENSE.txt +21 -0
- data/README.md +528 -0
- data/exe/kimera +22 -0
- data/lib/kimera/audit/operator_audit.rb +71 -0
- data/lib/kimera/cli/argv.rb +15 -0
- data/lib/kimera/cli/baseline.rb +87 -0
- data/lib/kimera/cli/changed.rb +27 -0
- data/lib/kimera/cli/ci.rb +27 -0
- data/lib/kimera/cli/completion.rb +37 -0
- data/lib/kimera/cli/doctor.rb +115 -0
- data/lib/kimera/cli/flag.rb +55 -0
- data/lib/kimera/cli/help_text.rb +45 -0
- data/lib/kimera/cli/init.rb +105 -0
- data/lib/kimera/cli/mutant.rb +82 -0
- data/lib/kimera/cli/report_file.rb +20 -0
- data/lib/kimera/cli/run/arguments.rb +58 -0
- data/lib/kimera/cli/run/cycle.rb +78 -0
- data/lib/kimera/cli/run/digest.rb +49 -0
- data/lib/kimera/cli/run/emission.rb +45 -0
- data/lib/kimera/cli/run/gate.rb +43 -0
- data/lib/kimera/cli/run/options.rb +85 -0
- data/lib/kimera/cli/run/pass.rb +100 -0
- data/lib/kimera/cli/run/sources.rb +56 -0
- data/lib/kimera/cli/run.rb +52 -0
- data/lib/kimera/cli/skill.rb +23 -0
- data/lib/kimera/cli/suggestion.rb +36 -0
- data/lib/kimera/cli/survivors/panel.rb +81 -0
- data/lib/kimera/cli/survivors.rb +89 -0
- data/lib/kimera/cli/synthesize.rb +66 -0
- data/lib/kimera/cli/workflows.rb +8 -0
- data/lib/kimera/cli.rb +132 -0
- data/lib/kimera/error.rb +3 -0
- data/lib/kimera/execution/baseline_failure.rb +47 -0
- data/lib/kimera/execution/baseline_pass.rb +123 -0
- data/lib/kimera/execution/boot.rb +50 -0
- data/lib/kimera/execution/callback.rb +33 -0
- data/lib/kimera/execution/child_process.rb +48 -0
- data/lib/kimera/execution/composite_isolation.rb +24 -0
- data/lib/kimera/execution/harness.rb +150 -0
- data/lib/kimera/execution/isolated.rb +64 -0
- data/lib/kimera/execution/isolated_child.rb +15 -0
- data/lib/kimera/execution/isolated_child_command.rb +44 -0
- data/lib/kimera/execution/isolated_child_minitest.rb +34 -0
- data/lib/kimera/execution/isolated_plan.rb +54 -0
- data/lib/kimera/execution/isolated_scheduling.rb +54 -0
- data/lib/kimera/execution/isolated_test_selection.rb +22 -0
- data/lib/kimera/execution/isolated_verdict.rb +72 -0
- data/lib/kimera/execution/isolated_watchdog.rb +59 -0
- data/lib/kimera/execution/isolation.rb +17 -0
- data/lib/kimera/execution/null_progress.rb +17 -0
- data/lib/kimera/execution/overlay_guard_modules.rb +95 -0
- data/lib/kimera/execution/overlay_guards.rb +56 -0
- data/lib/kimera/execution/parallel_test_databases.rb +64 -0
- data/lib/kimera/execution/pool_driver.rb +101 -0
- data/lib/kimera/execution/priority.rb +23 -0
- data/lib/kimera/execution/reload.rb +104 -0
- data/lib/kimera/execution/rig.rb +41 -0
- data/lib/kimera/execution/schedule.rb +105 -0
- data/lib/kimera/execution/schemata.rb +88 -0
- data/lib/kimera/execution/shift/attempt.rb +34 -0
- data/lib/kimera/execution/shift/coverage_channel.rb +43 -0
- data/lib/kimera/execution/shift/killer_memory.rb +20 -0
- data/lib/kimera/execution/shift/leak_guard.rb +34 -0
- data/lib/kimera/execution/shift.rb +137 -0
- data/lib/kimera/execution/sweep.rb +30 -0
- data/lib/kimera/execution/transaction_isolation.rb +32 -0
- data/lib/kimera/execution/trial.rb +16 -0
- data/lib/kimera/execution/verdicts.rb +78 -0
- data/lib/kimera/execution/worker_pool/fleet.rb +104 -0
- data/lib/kimera/execution/worker_pool/stillborn_guard.rb +40 -0
- data/lib/kimera/execution/worker_pool/worker.rb +32 -0
- data/lib/kimera/execution/worker_pool.rb +101 -0
- data/lib/kimera/frameworks/adapter.rb +82 -0
- data/lib/kimera/frameworks/minitest_adapter.rb +102 -0
- data/lib/kimera/frameworks/rspec_adapter.rb +123 -0
- data/lib/kimera/frameworks/rspec_group_index.rb +45 -0
- data/lib/kimera/incremental/git_diff.rb +98 -0
- data/lib/kimera/incremental/selection.rb +27 -0
- data/lib/kimera/incremental/session.rb +109 -0
- data/lib/kimera/memoization.rb +104 -0
- data/lib/kimera/operators/argument_drop.rb +19 -0
- data/lib/kimera/operators/arithmetic.rb +12 -0
- data/lib/kimera/operators/audit.rb +3 -0
- data/lib/kimera/operators/base.rb +40 -0
- data/lib/kimera/operators/binary_swap.rb +23 -0
- data/lib/kimera/operators/boolean.rb +26 -0
- data/lib/kimera/operators/chain_link_deletion.rb +25 -0
- data/lib/kimera/operators/collection_literal.rb +18 -0
- data/lib/kimera/operators/comparison.rb +13 -0
- data/lib/kimera/operators/conditional.rb +36 -0
- data/lib/kimera/operators/default_argument.rb +17 -0
- data/lib/kimera/operators/element_drop.rb +31 -0
- data/lib/kimera/operators/index_fetch.rb +15 -0
- data/lib/kimera/operators/kernel_coercion.rb +17 -0
- data/lib/kimera/operators/method_unwrap.rb +20 -0
- data/lib/kimera/operators/negation.rb +15 -0
- data/lib/kimera/operators/node_swap.rb +20 -0
- data/lib/kimera/operators/numeric_literal.rb +39 -0
- data/lib/kimera/operators/op_assign.rb +18 -0
- data/lib/kimera/operators/plugins.rb +27 -0
- data/lib/kimera/operators/rails_association.rb +24 -0
- data/lib/kimera/operators/rails_callback.rb +49 -0
- data/lib/kimera/operators/rails_declaration.rb +16 -0
- data/lib/kimera/operators/rails_permit.rb +34 -0
- data/lib/kimera/operators/rails_validation.rb +17 -0
- data/lib/kimera/operators/range.rb +15 -0
- data/lib/kimera/operators/regexp_literal.rb +16 -0
- data/lib/kimera/operators/respond_to_guard.rb +24 -0
- data/lib/kimera/operators/return_value.rb +36 -0
- data/lib/kimera/operators/safe_navigation.rb +15 -0
- data/lib/kimera/operators/selector_swap.rb +24 -0
- data/lib/kimera/operators/statement_deletion.rb +26 -0
- data/lib/kimera/operators/string_literal.rb +22 -0
- data/lib/kimera/operators/symbol_literal.rb +26 -0
- data/lib/kimera/operators/vocabulary.rb +82 -0
- data/lib/kimera/operators.rb +141 -0
- data/lib/kimera/plugins.rb +3 -0
- data/lib/kimera/registry/builder.rb +59 -0
- data/lib/kimera/registry/mutation_point.rb +146 -0
- data/lib/kimera/registry/numbering.rb +15 -0
- data/lib/kimera/registry/registry.rb +120 -0
- data/lib/kimera/registry/source_file.rb +71 -0
- data/lib/kimera/registry/tally.rb +11 -0
- data/lib/kimera/registry/walking.rb +55 -0
- data/lib/kimera/report/actions.rb +38 -0
- data/lib/kimera/report/coloring.rb +25 -0
- data/lib/kimera/report/formats.rb +78 -0
- data/lib/kimera/report/live.rb +56 -0
- data/lib/kimera/report/log.rb +46 -0
- data/lib/kimera/report/progress.rb +55 -0
- data/lib/kimera/report/screen.rb +44 -0
- data/lib/kimera/report/sections.rb +50 -0
- data/lib/kimera/report/tally.rb +84 -0
- data/lib/kimera/report/text.rb +104 -0
- data/lib/kimera/results/result.rb +72 -0
- data/lib/kimera/results/run_report.rb +114 -0
- data/lib/kimera/rewrite/ast_walk.rb +20 -0
- data/lib/kimera/rewrite/directive.rb +61 -0
- data/lib/kimera/rewrite/directive_extra_handlers.rb +52 -0
- data/lib/kimera/rewrite/directive_handlers.rb +138 -0
- data/lib/kimera/rewrite/directive_kwarg_handlers.rb +44 -0
- data/lib/kimera/rewrite/numbered_params.rb +50 -0
- data/lib/kimera/runtime.rb +51 -0
- data/lib/kimera/scope/config.rb +90 -0
- data/lib/kimera/scope/file_set.rb +51 -0
- data/lib/kimera/scope/ignore_list.rb +55 -0
- data/lib/kimera/self_protection.rb +56 -0
- data/lib/kimera/support/duration.rb +13 -0
- data/lib/kimera/support/errors.rb +8 -0
- data/lib/kimera/support/operator_protocol.rb +10 -0
- data/lib/kimera/support/syntax.rb +68 -0
- data/lib/kimera/support/syntax_types.rb +20 -0
- data/lib/kimera/support/unparse.rb +15 -0
- data/lib/kimera/support/version.rb +5 -0
- data/lib/kimera/support/warnings.rb +15 -0
- data/lib/kimera/synthesis/file_weave.rb +78 -0
- data/lib/kimera/synthesis/guard_weaver.rb +77 -0
- data/lib/kimera/synthesis/guardrail_dispatch.rb +68 -0
- data/lib/kimera/synthesis/guardrail_interpolation.rb +59 -0
- data/lib/kimera/synthesis/guardrail_value_objects.rb +46 -0
- data/lib/kimera/synthesis/overlay.rb +52 -0
- data/lib/kimera/synthesis/overlay_splice.rb +101 -0
- data/lib/kimera/synthesis/project.rb +58 -0
- data/lib/kimera/synthesis/source_map.rb +42 -0
- data/lib/kimera/version.rb +3 -0
- data/lib/kimera.rb +13 -0
- data/skills/kimera/SKILL.md +118 -0
- metadata +259 -0
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../../operators"
|
|
4
|
+
require_relative "../../report/formats"
|
|
5
|
+
require_relative "../../scope/config"
|
|
6
|
+
require_relative "../../scope/file_set"
|
|
7
|
+
require_relative "options"
|
|
8
|
+
|
|
9
|
+
class Kimera::CLI::Run::Arguments
|
|
10
|
+
def parse(argv)
|
|
11
|
+
options = defaults.merge(Kimera::Config.parse(argv))
|
|
12
|
+
finish(options, Kimera::CLI::RunOptions::OPTIONS.parse(argv, options))
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
private
|
|
16
|
+
|
|
17
|
+
def finish(options, rest)
|
|
18
|
+
validate(options)
|
|
19
|
+
options.except(:cli_tests).merge(scoped(options, rest))
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def validate(options) = Kimera::Report::Formats.new.normalize(options[:format])
|
|
23
|
+
|
|
24
|
+
def scoped(options, rest)
|
|
25
|
+
{
|
|
26
|
+
tests: Kimera::Config.prefer(options[:cli_tests], options[:tests], Kimera::CLI::RunOptions::DEFAULT_TESTS),
|
|
27
|
+
paths: Kimera::Config.prefer(rest, options[:paths], Kimera::FileSet::DEFAULT_GLOBS)
|
|
28
|
+
}.merge(arrays(options))
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def arrays(options)
|
|
32
|
+
{ exclude: Array(options[:exclude]), ignore: Array(options[:ignore]) }
|
|
33
|
+
.merge(isolate_when_covered_by: Array(options[:isolate_when_covered_by]), focus: Array(options[:focus]))
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def defaults
|
|
37
|
+
core.merge(gates).merge(scope)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def core
|
|
41
|
+
{ framework: "rspec", source_root: ".", tests: [] }
|
|
42
|
+
.merge(paths: nil, operators: Kimera::Operators::DEFAULT_KEYS, soft_timeout: 5.0)
|
|
43
|
+
.merge(hard_timeout: nil, leak_every: 10, registry: nil)
|
|
44
|
+
.merge(report: nil, format: "text", focus: [], gate: true, coverage: true, require: [])
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def gates
|
|
48
|
+
{ since: nil, session: nil, max_survivors: nil }.merge(max_ignored: nil, max_errors: 0, jobs: 1)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def scope
|
|
52
|
+
{ exclude: [], exclude_tests: [], config: nil }
|
|
53
|
+
.merge(ignore: [], isolate_db: false, isolated: false)
|
|
54
|
+
.merge(fail_on_no_coverage: false, progress: nil, color: nil, quiet: false, verbose: false, log: nil)
|
|
55
|
+
.merge(isolate_when_covered_by: [])
|
|
56
|
+
.merge(cli_tests: [])
|
|
57
|
+
end
|
|
58
|
+
end
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../../incremental/selection"
|
|
4
|
+
require_relative "../../incremental/session"
|
|
5
|
+
require_relative "digest"
|
|
6
|
+
require_relative "pass"
|
|
7
|
+
require_relative "sources"
|
|
8
|
+
|
|
9
|
+
class Kimera::CLI::Run::Cycle
|
|
10
|
+
def initialize(options, registry, changed, digest:)
|
|
11
|
+
@options = options
|
|
12
|
+
@registry = registry
|
|
13
|
+
@changed = changed
|
|
14
|
+
@digest = digest
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def call
|
|
18
|
+
@digest.verbose(@options) if @options[:verbose]
|
|
19
|
+
scope(@changed && Kimera::Incremental::Selection.select(@registry, @changed))
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
def scope(selected)
|
|
25
|
+
eligible = focus(selected || @registry.each.map { |mutant, _| mutant.id })
|
|
26
|
+
ignored = Kimera::IgnoreList.ids(@registry, @options[:ignore]) & eligible
|
|
27
|
+
notify(selected, ignored)
|
|
28
|
+
session(eligible - ignored, ignored)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def notify(selected, ignored)
|
|
32
|
+
@digest.stale(@registry, @options[:ignore])
|
|
33
|
+
@digest.announce(selected, ignored, files: @registry.files.size, since: @options[:since])
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def session(todo, ignored)
|
|
37
|
+
loaded = Kimera::Incremental::Session.load(@options[:session], registry: @registry)
|
|
38
|
+
perform(todo, loaded)
|
|
39
|
+
conclude(todo, ignored, loaded)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def perform(todo, loaded)
|
|
43
|
+
remaining = todo.reject { |id| loaded.done?(id) }
|
|
44
|
+
harness(remaining, loaded) unless remaining.empty?
|
|
45
|
+
path = @options[:session]
|
|
46
|
+
loaded.save(path, registry: @registry, meta: { "since" => @options[:since] }) if path
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def conclude(todo, ignored, loaded)
|
|
50
|
+
report = loaded.report(todo, ignored, @registry)
|
|
51
|
+
@digest.emit(report, @registry, **emission)
|
|
52
|
+
@options[:gate] == false ? 0 : @digest.gate(report, @options)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def emission
|
|
56
|
+
{ path: @options[:report], format: @options.fetch(:format, "text"), metadata: provenance }
|
|
57
|
+
.merge(coverage: coverage, log: @options[:log])
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def coverage = @options[:fail_on_no_coverage] ? :list : :hint
|
|
61
|
+
|
|
62
|
+
def focus(eligible)
|
|
63
|
+
ids = Array(@options[:focus])
|
|
64
|
+
return eligible if ids.empty?
|
|
65
|
+
missing = ids - eligible
|
|
66
|
+
missing.empty? ? ids : raise(Kimera::UsageError, "focused mutant(s) not in scope: #{missing.join(", ")}")
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def provenance
|
|
70
|
+
@options.slice(:framework, :source_root, :tests, :exclude_tests, :operators, :coverage, :isolated, :jobs)
|
|
71
|
+
.transform_keys(&:to_s)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def harness(remaining, loaded)
|
|
75
|
+
adapter = Kimera::Frameworks::Adapter.load(@options[:framework])
|
|
76
|
+
Kimera::CLI::Run::Pass.new(@registry, @options, adapter).call(remaining, loaded)
|
|
77
|
+
end
|
|
78
|
+
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../../results/run_report"
|
|
4
|
+
require_relative "../../scope/ignore_list"
|
|
5
|
+
require_relative "emission"
|
|
6
|
+
require_relative "gate"
|
|
7
|
+
|
|
8
|
+
class Kimera::CLI::Run::Digest
|
|
9
|
+
def initialize(emission:, io: $stdout, errors: $stderr)
|
|
10
|
+
@io = io
|
|
11
|
+
@errors = errors
|
|
12
|
+
@emission = emission
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def emit(...) = @emission.emit(...)
|
|
16
|
+
|
|
17
|
+
def gate(report, options) = Kimera::CLI::Run::Gate.new(report, options).status(@errors)
|
|
18
|
+
|
|
19
|
+
def announce(selected, ignored, files:, since:)
|
|
20
|
+
incremental(selected, files, since) if selected
|
|
21
|
+
@io.puts("Ignoring #{ignored.size} mutant(s) marked equivalent.") unless ignored.empty?
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def stale(registry, rules)
|
|
25
|
+
Kimera::IgnoreList.stale(registry, rules).each { |rule| @errors.puts(anchor(rule)) }
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def verbose(options)
|
|
29
|
+
@io.puts(
|
|
30
|
+
"Kimera: framework=#{options[:framework]} jobs=#{options[:jobs]} " \
|
|
31
|
+
"sources=#{Array(options[:paths]).join(",")} tests=#{Array(options[:tests]).join(",")} " \
|
|
32
|
+
"format=#{options.fetch(:format, "text")}"
|
|
33
|
+
)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
def incremental(selected, files, since)
|
|
39
|
+
@io.puts("Incremental: #{selected.size} mutants on lines changed since #{since} (#{files} changed files).")
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def anchor(rule)
|
|
43
|
+
"kimera: warning: ignore entry matches no mutant (stale anchor?): #{spot(rule)} #{rule[:label]}".rstrip
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def spot(rule)
|
|
47
|
+
[rule[:file], rule[:line]].compact.join(":")
|
|
48
|
+
end
|
|
49
|
+
end
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
require "json"
|
|
5
|
+
require "stringio"
|
|
6
|
+
require_relative "../../report/coloring"
|
|
7
|
+
require_relative "../../report/formats"
|
|
8
|
+
require_relative "../../report/text"
|
|
9
|
+
|
|
10
|
+
class Kimera::CLI::Run::Emission
|
|
11
|
+
include Kimera::Report::Coloring
|
|
12
|
+
|
|
13
|
+
def initialize(io: $stdout, output: io, color: nil, quiet: false)
|
|
14
|
+
@io = io
|
|
15
|
+
@output = output
|
|
16
|
+
@color = color
|
|
17
|
+
@quiet = quiet
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def emit(report, registry, path: nil, format: "text", metadata: nil, coverage: :hint, log: nil)
|
|
21
|
+
text = rendered(report, registry, coverage, path)
|
|
22
|
+
document = report.document(metadata)
|
|
23
|
+
present(text, format, document)
|
|
24
|
+
save(log, text) if log
|
|
25
|
+
save(path, JSON.pretty_generate(document)) if path
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
private
|
|
29
|
+
|
|
30
|
+
def rendered(report, registry, coverage, path)
|
|
31
|
+
io = StringIO.new
|
|
32
|
+
Kimera::Report::Text.new(registry, io: io, color: color?).report(report, coverage: coverage, path: path)
|
|
33
|
+
io.string
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def present(text, format, document)
|
|
37
|
+
return Kimera::Report::Formats.new(io: @output).render(format, document) unless format == "text"
|
|
38
|
+
@io.write(text) unless @quiet
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def save(file, text)
|
|
42
|
+
FileUtils.mkdir_p(File.dirname(file))
|
|
43
|
+
File.write(file, text)
|
|
44
|
+
end
|
|
45
|
+
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class Kimera::CLI::Run::Gate
|
|
4
|
+
def initialize(report, options)
|
|
5
|
+
@report = report
|
|
6
|
+
@options = options
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def status(stream)
|
|
10
|
+
return 0 if reasons.empty?
|
|
11
|
+
reasons.each { |reason| stream.puts("kimera: gate failed: #{reason}") }
|
|
12
|
+
2
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
private
|
|
16
|
+
|
|
17
|
+
def reasons = @_reasons ||= [survivors, coverage, ignored, unjudged].compact
|
|
18
|
+
|
|
19
|
+
def survivors
|
|
20
|
+
max = @options[:max_survivors]
|
|
21
|
+
count = @report.survived.size
|
|
22
|
+
return unless max ? count > max : count.positive?
|
|
23
|
+
"survivors=#{count} > max_survivors=#{max || 0}"
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def coverage
|
|
27
|
+
uncovered = @report.uncovered.size
|
|
28
|
+
return unless @options[:fail_on_no_coverage] && uncovered.positive?
|
|
29
|
+
"#{uncovered} mutant(s) with no covering test (--fail-on-no-coverage)"
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def ignored
|
|
33
|
+
cap = @options[:max_ignored]
|
|
34
|
+
count = @report.statuses(:ignored).size
|
|
35
|
+
"ignored=#{count} > max_ignored=#{cap}" if cap && count > cap
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def unjudged
|
|
39
|
+
count = @report.errors.size
|
|
40
|
+
cap = Integer(@options[:max_errors] || 0)
|
|
41
|
+
"unjudged=#{count} > max_errors=#{cap}" if count > cap
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../flag"
|
|
4
|
+
|
|
5
|
+
module Kimera
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
class Kimera::CLI
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
module Kimera::CLI::RunOptions
|
|
12
|
+
DEFAULT_TESTS = ["spec/**/*_spec.rb"].freeze
|
|
13
|
+
|
|
14
|
+
OPTIONS = Kimera::FlagTable.new(
|
|
15
|
+
banner: "Usage: kimera run [paths...] [options]\n" \
|
|
16
|
+
"Exit codes: 0 gates passed, 1 usage or setup error, 2 gate failed",
|
|
17
|
+
flags: [
|
|
18
|
+
Kimera::Flag.build("--framework NAME", :framework, "Test framework (rspec)"),
|
|
19
|
+
Kimera::Flag.build(
|
|
20
|
+
"--tests GLOB", :cli_tests,
|
|
21
|
+
"Test file glob (repeatable; replaces config tests:)", collect: true
|
|
22
|
+
),
|
|
23
|
+
Kimera::Flag.build("--source-root DIR", :source_root, "Project root for synthesis"),
|
|
24
|
+
Kimera::REGISTRY_FLAG,
|
|
25
|
+
Kimera::OPERATORS_FLAG,
|
|
26
|
+
Kimera::REQUIRE_FLAG,
|
|
27
|
+
Kimera::Flag.build("--soft-timeout SEC", :soft_timeout, "Per-test soft timeout", type: Float),
|
|
28
|
+
Kimera::Flag.build("--hard-timeout SEC", :hard_timeout, "Per-mutant hard watchdog timeout", type: Float),
|
|
29
|
+
Kimera::Flag.build("--leak-every N", :leak_every, "Re-check a killed mutant every N", type: Integer),
|
|
30
|
+
Kimera::Flag.build("--[no-]coverage", :coverage, "Run each mutant only against covering tests (default: on)"),
|
|
31
|
+
Kimera::Flag.build("--since REF", :since, "Incremental: only mutate lines changed vs REF (git diff)"),
|
|
32
|
+
Kimera::Flag.build("--session FILE", :session, "Persist/resume per-mutant results in FILE"),
|
|
33
|
+
Kimera::Flag.build(
|
|
34
|
+
"--focus ID", :focus, "Evaluate only one or more mutant IDs (repeatable)",
|
|
35
|
+
type: Integer, collect: true
|
|
36
|
+
),
|
|
37
|
+
Kimera::Flag.build("--[no-]gate", :gate, "Return gate status (default: on; use --no-gate for exploration)"),
|
|
38
|
+
Kimera::Flag.build("--max-survivors N", :max_survivors, "Gate: fail only if survivors exceed N", type: Integer),
|
|
39
|
+
Kimera::Flag.build(
|
|
40
|
+
"--max-ignored N", :max_ignored,
|
|
41
|
+
"Gate: fail if more than N mutants are ignore-listed", type: Integer
|
|
42
|
+
),
|
|
43
|
+
Kimera::Flag.build(
|
|
44
|
+
"--max-errors N", :max_errors,
|
|
45
|
+
"Gate: fail if more than N mutants could not be judged (default 0)", type: Integer
|
|
46
|
+
),
|
|
47
|
+
Kimera::Flag.build(
|
|
48
|
+
"--[no-]fail-on-no-coverage", :fail_on_no_coverage,
|
|
49
|
+
"Gate: also fail when any mutant in scope has no covering test"
|
|
50
|
+
),
|
|
51
|
+
Kimera::Flag.build("--jobs N", :jobs, "Concurrent workers (warm pool / isolated mirrors)", type: Integer),
|
|
52
|
+
Kimera::Flag.build(
|
|
53
|
+
"--[no-]progress", :progress, "Live progress bar on stderr (default: on when stderr is a tty)"
|
|
54
|
+
),
|
|
55
|
+
Kimera::Flag.build(
|
|
56
|
+
"--[no-]color", :color,
|
|
57
|
+
"Color text output (default: on when stdout is a tty; respects NO_COLOR)"
|
|
58
|
+
),
|
|
59
|
+
Kimera::Flag.build(["-q", "--quiet"], :quiet, "Suppress the final human-readable report"),
|
|
60
|
+
Kimera::Flag.build(["-v", "--verbose"], :verbose, "Print resolved scope and execution settings"),
|
|
61
|
+
Kimera::Flag.build("--log FILE", :log, "Write the final human-readable report to FILE"),
|
|
62
|
+
Kimera::Flag.build(
|
|
63
|
+
"--[no-]isolate-db", :isolate_db, "Wrap each mutant in a rolled-back ActiveRecord transaction"
|
|
64
|
+
),
|
|
65
|
+
Kimera::Flag.build(
|
|
66
|
+
"--isolated", :isolated,
|
|
67
|
+
"Evaluate every mutant in a fresh subprocess (slow, but trustworthy for " \
|
|
68
|
+
"the runtime selector and harness-critical code)"
|
|
69
|
+
),
|
|
70
|
+
Kimera::Flag.build(
|
|
71
|
+
"--isolate-when-covered-by SUBSTR", :isolate_when_covered_by,
|
|
72
|
+
"Isolate only mutants whose covering set includes a test id matching SUBSTR " \
|
|
73
|
+
"(repeatable) — a fast, targeted alternative to --isolated for pool-unsafe specs",
|
|
74
|
+
collect: true
|
|
75
|
+
),
|
|
76
|
+
Kimera::Flag.build("--exclude GLOB", :exclude, "Exclude matching files (repeatable)", collect: true),
|
|
77
|
+
Kimera::Flag.build(
|
|
78
|
+
"--exclude-test GLOB", :exclude_tests, "Exclude matching test files (repeatable)", collect: true
|
|
79
|
+
),
|
|
80
|
+
Kimera::CONFIG_FLAG,
|
|
81
|
+
Kimera::Flag.build("--report FILE", :report, "Write the canonical JSON report to FILE"),
|
|
82
|
+
Kimera::Flag.build("--format NAME", :format, "Output: text, json, ndjson, github, or sarif (default: text)")
|
|
83
|
+
]
|
|
84
|
+
)
|
|
85
|
+
end
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../../execution/harness"
|
|
4
|
+
require_relative "../../execution/isolated"
|
|
5
|
+
require_relative "../../frameworks/adapter"
|
|
6
|
+
require_relative "../../report/progress"
|
|
7
|
+
require_relative "../../results/run_report"
|
|
8
|
+
require_relative "../../self_protection"
|
|
9
|
+
require_relative "sources"
|
|
10
|
+
|
|
11
|
+
class Kimera::CLI::Run::Pass
|
|
12
|
+
def initialize(registry, options, adapter)
|
|
13
|
+
@registry = registry
|
|
14
|
+
@options = options
|
|
15
|
+
@adapter = adapter
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def call(remaining, session)
|
|
19
|
+
report = with_adapter { perform(remaining) }
|
|
20
|
+
session.merge!(report)
|
|
21
|
+
report
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
private
|
|
25
|
+
|
|
26
|
+
def progress = @_progress ||= Kimera::Report::Progress.new(enabled: @options[:progress])
|
|
27
|
+
|
|
28
|
+
def with_adapter
|
|
29
|
+
yield
|
|
30
|
+
ensure
|
|
31
|
+
@adapter&.finish
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def perform(remaining)
|
|
35
|
+
warm
|
|
36
|
+
run(remaining)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def harness = @_harness ||= Kimera::Execution::Harness.new(**settings)
|
|
40
|
+
|
|
41
|
+
def files
|
|
42
|
+
@_files ||= Kimera::CLI::Run::Sources.new.expand(
|
|
43
|
+
@options[:tests], @options[:exclude_tests], "test", "check --tests"
|
|
44
|
+
)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def settings
|
|
48
|
+
base.merge({ hard_timeout: @options[:hard_timeout] }.compact)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def base
|
|
52
|
+
{ registry: @registry, adapter: @adapter, source_root: @options[:source_root] }
|
|
53
|
+
.merge(soft_timeout: @options[:soft_timeout], leak_every: @options[:leak_every], jobs: @options[:jobs])
|
|
54
|
+
.merge(isolate_db: @options[:isolate_db], progress: progress)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def warm
|
|
58
|
+
if @options[:isolated] || @options[:coverage]
|
|
59
|
+
harness.warm!(files)
|
|
60
|
+
else
|
|
61
|
+
harness.without_coverage!(files)
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def run(remaining)
|
|
66
|
+
isolated = ->(ids) { isolation.run(ids: ids, label: "mutants (isolated)") }
|
|
67
|
+
return isolated.call(remaining) if @options[:isolated]
|
|
68
|
+
pooled(remaining, isolated)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def pooled(remaining, isolated)
|
|
72
|
+
apart, together = split(remaining)
|
|
73
|
+
return harness.run(ids: together, label: "mutants") if apart.empty?
|
|
74
|
+
harness.run(ids: together, label: "mutants (warm)").merge(isolated.call(apart))
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def isolation
|
|
78
|
+
Kimera::Execution::IsolatedExecution.new(**core, **{ hard_timeout: @options[:hard_timeout] }.compact)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def core
|
|
82
|
+
{ registry: @registry, root: @options[:source_root], tests: @adapter.test_ids }
|
|
83
|
+
.merge(coverage: harness.coverage, progress: progress, jobs: @options[:jobs])
|
|
84
|
+
.merge(framework: @options[:framework], test_files: files)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def split(ids)
|
|
88
|
+
patterns = Array(@options[:isolate_when_covered_by])
|
|
89
|
+
ids.partition do |id|
|
|
90
|
+
critical?(id) || patterns.any? { |pattern| matches?(harness.coverage, id, pattern) }
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def critical?(id) = Kimera::SelfProtection.critical?(@registry.index.fetch(id).file)
|
|
95
|
+
|
|
96
|
+
def matches?(coverage, id, pattern)
|
|
97
|
+
tests = coverage.fetch(id) { coverage.fetch(id.to_s, []) }
|
|
98
|
+
tests.any? { |test| test.include?(pattern) }
|
|
99
|
+
end
|
|
100
|
+
end
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../../error"
|
|
4
|
+
require_relative "../../incremental/git_diff"
|
|
5
|
+
require_relative "../../operators"
|
|
6
|
+
require_relative "../../registry/builder"
|
|
7
|
+
require_relative "../../registry/registry"
|
|
8
|
+
require_relative "../../scope/file_set"
|
|
9
|
+
|
|
10
|
+
class Kimera::CLI::Run::Sources
|
|
11
|
+
class << self
|
|
12
|
+
def changed(options)
|
|
13
|
+
Kimera::Incremental::GitDiff.lines(since: options[:since], root: options[:source_root])
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def initialize(errors: $stderr)
|
|
18
|
+
@errors = errors
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def load(options, changed = nil)
|
|
22
|
+
stored = options[:registry]
|
|
23
|
+
return Kimera::Registry.load(stored) if stored
|
|
24
|
+
build(options, files(options, changed))
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def files(options, changed)
|
|
28
|
+
found = expand(options[:paths], options[:exclude], "source", "check paths and --exclude")
|
|
29
|
+
return found unless changed
|
|
30
|
+
root = File.expand_path(options[:source_root])
|
|
31
|
+
found.select { |file| changed.key?(Kimera::FileSet.relative(file, root)) }
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def expand(patterns, exclude, kind, hint)
|
|
35
|
+
notice(patterns, exclude, kind)
|
|
36
|
+
found = Kimera::FileSet.expand(patterns, exclude: exclude)
|
|
37
|
+
raise(Kimera::UsageError, "no #{kind} files matched: #{Array(patterns).join(", ")} (#{hint})") if found.empty?
|
|
38
|
+
found
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def notice(patterns, exclude, kind)
|
|
42
|
+
Kimera::FileSet.unused(patterns, exclude).each do |pattern|
|
|
43
|
+
@errors.puts("kimera: warning: exclude pattern matched no #{kind} files: #{pattern}")
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
private
|
|
48
|
+
|
|
49
|
+
def build(options, files)
|
|
50
|
+
Kimera::RegistryScan.new(
|
|
51
|
+
operators: Kimera::Operators.build(keys: options[:operators]),
|
|
52
|
+
root: options[:source_root],
|
|
53
|
+
shielded: !options[:isolated]
|
|
54
|
+
).build(files)
|
|
55
|
+
end
|
|
56
|
+
end
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../error"
|
|
4
|
+
require_relative "../plugins"
|
|
5
|
+
|
|
6
|
+
module Kimera
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
class Kimera::CLI
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
class Kimera::CLI::Run
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
require_relative "run/arguments"
|
|
16
|
+
require_relative "run/cycle"
|
|
17
|
+
require_relative "run/digest"
|
|
18
|
+
require_relative "run/options"
|
|
19
|
+
require_relative "run/sources"
|
|
20
|
+
|
|
21
|
+
class Kimera::CLI::Run
|
|
22
|
+
include Kimera::CLI::RunOptions
|
|
23
|
+
|
|
24
|
+
def initialize(io: $stdout, errors: $stderr)
|
|
25
|
+
@io = io
|
|
26
|
+
@errors = errors
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def run(argv)
|
|
30
|
+
cycle(parse(argv)).call
|
|
31
|
+
rescue Kimera::Error => error
|
|
32
|
+
@errors.puts("kimera: #{error.message}")
|
|
33
|
+
1
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
def parse(argv) = Kimera::CLI::Run::Arguments.new.parse(argv)
|
|
39
|
+
|
|
40
|
+
def cycle(options)
|
|
41
|
+
Kimera::Plugins.load!(options[:require], root: options[:source_root])
|
|
42
|
+
changed = options[:since] ? Kimera::CLI::Run::Sources.changed(options) : nil
|
|
43
|
+
registry = Kimera::CLI::Run::Sources.new(errors: @errors).load(options, changed)
|
|
44
|
+
Kimera::CLI::Run::Cycle.new(options, registry, changed, digest: digest(options))
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def digest(options = {})
|
|
48
|
+
narration = options.fetch(:format, "text") == "text" ? @io : @errors
|
|
49
|
+
emission = Kimera::CLI::Run::Emission.new(io: narration, output: @io, **options.slice(:color, :quiet))
|
|
50
|
+
Kimera::CLI::Run::Digest.new(io: narration, errors: @errors, emission: emission)
|
|
51
|
+
end
|
|
52
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class Kimera::CLI::Skill
|
|
4
|
+
PATH = File.expand_path("../../../skills/kimera/SKILL.md", __dir__)
|
|
5
|
+
|
|
6
|
+
def initialize(io: $stdout, errors: $stderr)
|
|
7
|
+
@io = io
|
|
8
|
+
@errors = errors
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def run(argv)
|
|
12
|
+
return usage unless argv.empty?
|
|
13
|
+
@io.write(File.read(PATH))
|
|
14
|
+
0
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
private
|
|
18
|
+
|
|
19
|
+
def usage
|
|
20
|
+
@errors.puts("kimera: usage: kimera skill")
|
|
21
|
+
1
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Kimera
|
|
4
|
+
end
|
|
5
|
+
|
|
6
|
+
class Kimera::CLI
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
class Kimera::CLI::Suggestion
|
|
10
|
+
MAX_EDITS = 3
|
|
11
|
+
|
|
12
|
+
def initialize(known)
|
|
13
|
+
@known = known
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def hint(word)
|
|
17
|
+
nearest = @known.min_by { |candidate| distance(word, candidate) }
|
|
18
|
+
nearest && distance(word, nearest) <= MAX_EDITS ? "; did you mean #{nearest.inspect}?" : ""
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
private
|
|
22
|
+
|
|
23
|
+
def distance(left, right)
|
|
24
|
+
left.each_char.with_index(1).reduce(origin(right)) do |above, (char, index)|
|
|
25
|
+
below(above, right.each_char.map { |other| other == char ? 0 : 1 }, index)
|
|
26
|
+
end.last
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def origin(word) = (0..word.length).to_a
|
|
30
|
+
|
|
31
|
+
def below(above, costs, index)
|
|
32
|
+
costs.each_with_index.reduce([index]) do |row, (cost, column)|
|
|
33
|
+
row << [above[column + 1] + 1, row[column] + 1, above[column] + cost].min
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|