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,71 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../support/unparse"
|
|
4
|
+
require_relative "../registry/builder"
|
|
5
|
+
require_relative "../rewrite/directive"
|
|
6
|
+
|
|
7
|
+
class Kimera::Operators::Audit
|
|
8
|
+
OK = :ok
|
|
9
|
+
UNRENDERABLE = :unrenderable
|
|
10
|
+
IDENTICAL = :identical
|
|
11
|
+
|
|
12
|
+
Finding =
|
|
13
|
+
Data.define(:operator, :original, :label, :rendered, :verdict) do
|
|
14
|
+
def ok? = verdict == OK
|
|
15
|
+
|
|
16
|
+
def to_s = "#{operator}: #{label}\n #{original}\n -> #{rendered.inspect} (#{verdict})"
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
Rendering =
|
|
20
|
+
Data.define(:original, :rendered) do
|
|
21
|
+
def verdict
|
|
22
|
+
return UNRENDERABLE unless rendered
|
|
23
|
+
rendered == reprinted ? IDENTICAL : OK
|
|
24
|
+
end
|
|
25
|
+
private
|
|
26
|
+
def reprinted
|
|
27
|
+
Kimera::Unparse.unparse(Kimera::Unparse.parse(original))
|
|
28
|
+
rescue StandardError
|
|
29
|
+
original
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
class << self
|
|
34
|
+
def audit(operators, source, file: "audit.rb")
|
|
35
|
+
new(operators).audit(source, file: file)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def faults(operators, source, file: "audit.rb")
|
|
39
|
+
audit(operators, source, file: file).reject(&:ok?)
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def initialize(operators)
|
|
44
|
+
@operators = operators
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def audit(source, file: "audit.rb")
|
|
48
|
+
scan(source, file).points.flat_map { |point| findings(point) }
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
private
|
|
52
|
+
|
|
53
|
+
def operators = Array(@operators)
|
|
54
|
+
|
|
55
|
+
def scan(source, file)
|
|
56
|
+
Kimera::RegistryScan.new(operators: operators).source(source, file: file)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def findings(point)
|
|
60
|
+
key, original, mutants = point.project(:operator, :original_source, :mutants)
|
|
61
|
+
mutants.map { |mutant| finding(key, original, mutant) }
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def finding(key, original, mutant)
|
|
65
|
+
rendered = Kimera::Rewrite::Directive.render(original, mutant.directive)
|
|
66
|
+
Finding.new(
|
|
67
|
+
operator: key, original: original, label: mutant.label,
|
|
68
|
+
rendered: rendered, verdict: Rendering.new(original, rendered).verdict
|
|
69
|
+
)
|
|
70
|
+
end
|
|
71
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Kimera
|
|
4
|
+
end
|
|
5
|
+
|
|
6
|
+
class Kimera::CLI
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
module Kimera::CLI::Argv
|
|
10
|
+
module_function
|
|
11
|
+
|
|
12
|
+
def option?(args, name) = args.any? { |arg| arg == name || arg.start_with?("#{name}=") }
|
|
13
|
+
|
|
14
|
+
def any?(args, names) = names.any? { |name| option?(args, name) }
|
|
15
|
+
end
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "yaml"
|
|
4
|
+
require_relative "../error"
|
|
5
|
+
require_relative "flag"
|
|
6
|
+
require_relative "report_file"
|
|
7
|
+
|
|
8
|
+
class Kimera::CLI::Baseline
|
|
9
|
+
COMMANDS = { "create" => :create, "review" => :review }.freeze
|
|
10
|
+
ENTRY_KEYS = %w[file line label].freeze
|
|
11
|
+
|
|
12
|
+
CREATE = Kimera::FlagTable.new(
|
|
13
|
+
banner: "Usage: kimera baseline create REPORT.json --reason TEXT [--output FILE]",
|
|
14
|
+
flags: [
|
|
15
|
+
Kimera::Flag.build("--reason TEXT", :reason, "Why this existing mutation debt is being accepted"),
|
|
16
|
+
Kimera::Flag.build("--output FILE", :output, "Baseline file to write (default: .kimera-baseline.yml)"),
|
|
17
|
+
Kimera::FORCE_FLAG.with(help: "Replace an existing baseline file")
|
|
18
|
+
]
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
def initialize(io: $stdout, errors: $stderr)
|
|
22
|
+
@io = io
|
|
23
|
+
@errors = errors
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def run(argv)
|
|
27
|
+
__send__(command(argv.shift), argv)
|
|
28
|
+
rescue Kimera::UsageError => error
|
|
29
|
+
@errors.puts("kimera: #{error.message}")
|
|
30
|
+
1
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
|
|
35
|
+
def command(name) = COMMANDS.fetch(name) { raise(Kimera::UsageError, "usage: kimera baseline <create|review> ...") }
|
|
36
|
+
|
|
37
|
+
def create(argv)
|
|
38
|
+
options = { reason: nil, output: ".kimera-baseline.yml", force: false }
|
|
39
|
+
report, = CREATE.parse(argv, options)
|
|
40
|
+
check(report, options)
|
|
41
|
+
write(report, options[:output], options[:reason])
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def check(report, options)
|
|
45
|
+
raise(Kimera::UsageError, "baseline create needs a report file") unless report
|
|
46
|
+
raise(Kimera::UsageError, "baseline create requires --reason TEXT") if options[:reason].to_s.strip.empty?
|
|
47
|
+
raise(Kimera::UsageError, "no such report: #{report}") unless File.file?(report)
|
|
48
|
+
output = options[:output]
|
|
49
|
+
overwrite(output) if File.exist?(output) && !options[:force]
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def overwrite(output) = raise(Kimera::UsageError, "#{output} already exists (use --force to replace it)")
|
|
53
|
+
|
|
54
|
+
def write(report, output, reason)
|
|
55
|
+
ignored = survivors(report).map { |row| entry(row, reason) }
|
|
56
|
+
File.write(output, YAML.dump("format_version" => 1, "ignore" => ignored))
|
|
57
|
+
@io.puts("Created #{output} with #{ignored.size} accepted survivor(s).")
|
|
58
|
+
@io.puts("Add `baseline: #{output}` to .kimera.yml to apply it.")
|
|
59
|
+
0
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def review(argv)
|
|
63
|
+
file = argv.shift
|
|
64
|
+
raise(Kimera::UsageError, "usage: kimera baseline review BASELINE.yml") unless file && argv.empty?
|
|
65
|
+
raise(Kimera::UsageError, "no such baseline: #{file}") unless File.file?(file)
|
|
66
|
+
list(file, Array((YAML.safe_load_file(file) || {})["ignore"]))
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def list(file, entries)
|
|
70
|
+
@io.puts("#{entries.size} accepted mutant(s) in #{file}:")
|
|
71
|
+
entries.sort_by { |entry| [entry["file"], entry["line"], entry["label"]] }.each { |entry| @io.puts(line(entry)) }
|
|
72
|
+
0
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def line(entry) = " #{entry["file"]}:#{entry["line"]} [#{entry["label"]}] — #{entry["reason"]}"
|
|
76
|
+
|
|
77
|
+
def survivors(path)
|
|
78
|
+
Kimera::CLI::ReportFile.parse(path).fetch("results", []).select { |row| row["status"] == "survived" }
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def entry(row, reason)
|
|
82
|
+
id, *fields = row.values_at("mutant_id", *ENTRY_KEYS)
|
|
83
|
+
ENTRY_KEYS.zip(fields).to_h { |key, value| [key, value || missing(key, id)] }.merge("reason" => reason)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def missing(key, id) = raise(Kimera::UsageError, "report is missing #{key} for mutant ##{id}")
|
|
87
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../error"
|
|
4
|
+
require_relative "argv"
|
|
5
|
+
require_relative "run"
|
|
6
|
+
|
|
7
|
+
class Kimera::CLI::Changed
|
|
8
|
+
def initialize(io: $stdout, errors: $stderr)
|
|
9
|
+
@io = io
|
|
10
|
+
@errors = errors
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def run(argv)
|
|
14
|
+
args = argv.dup
|
|
15
|
+
args.unshift("--since", reference) unless Kimera::CLI::Argv.option?(args, "--since")
|
|
16
|
+
Kimera::CLI::Run.new(io: @io, errors: @errors).run(args)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
private
|
|
20
|
+
|
|
21
|
+
def reference
|
|
22
|
+
%w[origin/main main].find { |name| branch?(name) } ||
|
|
23
|
+
raise(Kimera::UsageError, "cannot find origin/main or main; pass --since REF")
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def branch?(name) = system("git", "rev-parse", "--verify", "--quiet", name, out: File::NULL, err: File::NULL)
|
|
27
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "argv"
|
|
4
|
+
require_relative "run"
|
|
5
|
+
|
|
6
|
+
class Kimera::CLI::CI
|
|
7
|
+
DEFAULTS = [
|
|
8
|
+
[%w[--max-survivors], %w[--max-survivors 0]],
|
|
9
|
+
[%w[--fail-on-no-coverage --no-fail-on-no-coverage], %w[--fail-on-no-coverage]],
|
|
10
|
+
[%w[--report], %w[--report tmp/kimera/report.json]],
|
|
11
|
+
[%w[--format], %w[--format github]]
|
|
12
|
+
].freeze
|
|
13
|
+
|
|
14
|
+
def initialize(io: $stdout, errors: $stderr)
|
|
15
|
+
@io = io
|
|
16
|
+
@errors = errors
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def run(argv)
|
|
20
|
+
args = DEFAULTS.reduce(argv.dup) { |given, (names, extra)| defaulted(given, names, extra) }
|
|
21
|
+
Kimera::CLI::Run.new(io: @io, errors: @errors).run(args)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
private
|
|
25
|
+
|
|
26
|
+
def defaulted(args, names, extra) = Kimera::CLI::Argv.any?(args, names) ? args : args + extra
|
|
27
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Kimera
|
|
4
|
+
end
|
|
5
|
+
|
|
6
|
+
class Kimera::CLI
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
class Kimera::CLI::Completion
|
|
10
|
+
SCRIPTS = {
|
|
11
|
+
"bash" => "_kimera() { COMPREPLY=( $(compgen -W '%s' -- \"${COMP_WORDS[1]}\") ); }\ncomplete -F _kimera kimera",
|
|
12
|
+
"zsh" => "#compdef kimera\n_arguments '1:command:(%s)'",
|
|
13
|
+
"fish" => "complete -c kimera -f -a '%s'"
|
|
14
|
+
}.freeze
|
|
15
|
+
SHELLS = SCRIPTS.keys.freeze
|
|
16
|
+
|
|
17
|
+
def initialize(io: $stdout, errors: $stderr)
|
|
18
|
+
@io = io
|
|
19
|
+
@errors = errors
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def run(argv)
|
|
23
|
+
shell = argv.first
|
|
24
|
+
return usage unless SHELLS.include?(shell) && argv.size == 1
|
|
25
|
+
@io.puts(script(shell))
|
|
26
|
+
0
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
def usage
|
|
32
|
+
@errors.puts("kimera: usage: kimera completion <#{SHELLS.join("|")}>")
|
|
33
|
+
1
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def script(shell) = format(SCRIPTS.fetch(shell), Kimera::CLI::COMMAND_NAMES.join(" "))
|
|
37
|
+
end
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "open3"
|
|
4
|
+
require_relative "../error"
|
|
5
|
+
require_relative "../scope/config"
|
|
6
|
+
require_relative "../scope/file_set"
|
|
7
|
+
require_relative "flag"
|
|
8
|
+
|
|
9
|
+
class Kimera::CLI::Doctor
|
|
10
|
+
OPTIONS = Kimera::FlagTable.new(
|
|
11
|
+
banner: "Usage: kimera doctor [--check-baseline]",
|
|
12
|
+
flags: [
|
|
13
|
+
Kimera::Flag.build("--check-baseline", :check_baseline, "Run the configured test suite and verify it is green")
|
|
14
|
+
]
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
FRAMEWORK_FEATURES = { "rspec" => "rspec/core", "minitest" => "minitest" }.freeze
|
|
18
|
+
|
|
19
|
+
def initialize(io: $stdout, errors: $stderr, root: ".")
|
|
20
|
+
@io = io
|
|
21
|
+
@errors = errors
|
|
22
|
+
@root = root
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def run(argv)
|
|
26
|
+
options = { check_baseline: false }
|
|
27
|
+
OPTIONS.parse(argv, options)
|
|
28
|
+
verdict(checks(Kimera::Config.root(root: @root), options))
|
|
29
|
+
rescue Kimera::UsageError => error
|
|
30
|
+
usage(error)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
|
|
35
|
+
def usage(error)
|
|
36
|
+
@errors.puts("kimera: #{error.message}")
|
|
37
|
+
1
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def checks(config, options)
|
|
41
|
+
all = [configuration(config), framework(config), sources(config), tests(config), git, rails]
|
|
42
|
+
options[:check_baseline] ? all << baseline(config) : all
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def verdict(checks)
|
|
46
|
+
checks.each { |status, text| @io.puts("#{status} #{text}") }
|
|
47
|
+
failed = checks.count { |status,| status == "✗" }
|
|
48
|
+
return 0 if failed.zero?
|
|
49
|
+
@errors.puts("kimera: doctor found #{failed} blocking issue(s)")
|
|
50
|
+
1
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def configuration(config)
|
|
54
|
+
path = File.join(@root, Kimera::Config::DEFAULT_PATH)
|
|
55
|
+
return ["!", "Configuration: none found; run `kimera init`"] unless File.file?(path)
|
|
56
|
+
["✓", "Configuration: #{Kimera::Config::DEFAULT_PATH} (#{config.fetch(:framework, "rspec")})"]
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def framework(config)
|
|
60
|
+
name = config.fetch(:framework, "rspec").to_s
|
|
61
|
+
feature = FRAMEWORK_FEATURES.fetch(name) { return ["✗", "Framework: unknown #{name}; use rspec or minitest"] }
|
|
62
|
+
return ["✓", "Framework: #{name} is installed"] if Gem.find_files(feature).any?
|
|
63
|
+
["✗", "Framework: #{name} is not in this bundle; fix framework: in .kimera.yml or add it to the Gemfile"]
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def sources(config)
|
|
67
|
+
count = glob(config.fetch(:paths, Kimera::FileSet::DEFAULT_GLOBS)).size
|
|
68
|
+
found(count, "Source scope: #{count} Ruby files", "Source scope: no Ruby files found; fix paths: in .kimera.yml")
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def tests(config)
|
|
72
|
+
count = test_files(config).size
|
|
73
|
+
found(count, "Test discovery: #{count} files", "Test discovery: no test files found; fix tests: in .kimera.yml")
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def found(count, present, absent) = count.zero? ? ["✗", absent] : ["✓", present]
|
|
77
|
+
|
|
78
|
+
def git
|
|
79
|
+
return ["✓", "Git: incremental runs are available"] if File.directory?(File.join(@root, ".git"))
|
|
80
|
+
["!", "Git: not a repository; `kimera changed` is unavailable"]
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def rails
|
|
84
|
+
return ["!", "Rails detected: verify parallel database isolation before using --jobs > 1"] if rails?
|
|
85
|
+
["✓", "Runtime: no Rails-specific parallel setup required"]
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def rails? = File.file?(File.join(@root, "config", "application.rb"))
|
|
89
|
+
|
|
90
|
+
def baseline(config)
|
|
91
|
+
output, status = Open3.capture2e(*command(config), chdir: @root)
|
|
92
|
+
return ["✓", "Baseline: configured test suite is green"] if status.success?
|
|
93
|
+
["✗", "Baseline: #{summary(output)} (fix it, then rerun `kimera doctor --check-baseline`)"]
|
|
94
|
+
rescue Errno::ENOENT
|
|
95
|
+
["✗", "Baseline: Bundler is unavailable; run your test suite, then retry"]
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def command(config)
|
|
99
|
+
files = test_files(config)
|
|
100
|
+
minitest?(config) ? ["bundle", "exec", "ruby", "-Itest", *files] : ["bundle", "exec", "rspec", *files]
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def summary(output)
|
|
104
|
+
output.lines.reverse.find { |line| line.match?(/\d+ failures?|\d+ examples?|failed/i) }&.strip ||
|
|
105
|
+
"test command failed"
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def minitest?(config) = config.fetch(:framework, "rspec") == "minitest"
|
|
109
|
+
|
|
110
|
+
def test_files(config) = glob(config.fetch(:tests, tests_for(config)))
|
|
111
|
+
|
|
112
|
+
def tests_for(config) = minitest?(config) ? ["test/**/*_test.rb"] : ["spec/**/*_spec.rb"]
|
|
113
|
+
|
|
114
|
+
def glob(patterns) = Array(patterns).flat_map { |pattern| Dir.glob(File.join(@root, pattern)) }.uniq
|
|
115
|
+
end
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "optparse"
|
|
4
|
+
require_relative "../error"
|
|
5
|
+
|
|
6
|
+
module Kimera
|
|
7
|
+
Flag =
|
|
8
|
+
Data.define(:switch, :key, :help, :type, :collect) do
|
|
9
|
+
def self.build(switch, key, help, type: nil, collect: false)
|
|
10
|
+
new(switch: switch, key: key, help: help, type: type, collect: collect)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def define(parser, options)
|
|
14
|
+
parser.on(*signature) { |value| store(options, value) }
|
|
15
|
+
end
|
|
16
|
+
private
|
|
17
|
+
def signature = [*switch, type, help].compact
|
|
18
|
+
|
|
19
|
+
def store(options, value)
|
|
20
|
+
collect ? options[key] << value : options[key] = value
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
FlagTable =
|
|
25
|
+
Data.define(:banner, :flags) do
|
|
26
|
+
def parse(argv, options)
|
|
27
|
+
build(options).parse(argv)
|
|
28
|
+
rescue OptionParser::ParseError => error
|
|
29
|
+
raise(UsageError, "#{error.message}\n#{banner}")
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def build(options)
|
|
33
|
+
OptionParser.new do |parser|
|
|
34
|
+
parser.banner = banner
|
|
35
|
+
flags.each { |flag| flag.define(parser, options) }
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def keys = flags.map(&:key)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
FORCE_FLAG = Flag.build("--force", :force, "Replace an existing file")
|
|
43
|
+
CONFIG_FLAG = Flag.build("--config FILE", :config, "Load config (default: .kimera.yml)")
|
|
44
|
+
REGISTRY_FLAG = Flag.build("--registry FILE", :registry, "Load registry JSON instead of building")
|
|
45
|
+
REQUIRE_FLAG = Flag.build(
|
|
46
|
+
"--require FILE", :require,
|
|
47
|
+
"Load a Ruby file before scanning, for custom operators (repeatable)",
|
|
48
|
+
collect: true
|
|
49
|
+
)
|
|
50
|
+
OPERATORS_FLAG = Flag.build(
|
|
51
|
+
"--operators a,b,c", :operators,
|
|
52
|
+
"Operator keys, or 'all'/'rails' (default: conservative core)",
|
|
53
|
+
type: Array
|
|
54
|
+
)
|
|
55
|
+
end
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../scope/file_set"
|
|
4
|
+
require_relative "../version"
|
|
5
|
+
|
|
6
|
+
module Kimera
|
|
7
|
+
module HelpText
|
|
8
|
+
HELP = <<~HELP.freeze
|
|
9
|
+
kimera #{Kimera::VERSION} -- schemata-based mutation testing for Ruby
|
|
10
|
+
(every mutant is compiled into one guarded source tree, so the suite
|
|
11
|
+
loads once, not once per mutant)
|
|
12
|
+
|
|
13
|
+
Usage: kimera <command> [options]
|
|
14
|
+
|
|
15
|
+
Start here:
|
|
16
|
+
init Detect this project and create .kimera.yml
|
|
17
|
+
doctor Validate source/test discovery and runtime safety
|
|
18
|
+
run [paths...] Run mutation tests locally
|
|
19
|
+
changed [paths...] Mutate only code changed since origin/main (or main)
|
|
20
|
+
ci [paths...] Strict CI gate; writes a report artifact
|
|
21
|
+
|
|
22
|
+
Investigate:
|
|
23
|
+
report REPORT.json Filter a saved report (survivors, uncovered, errors)
|
|
24
|
+
mutant ID --report R Show one mutant in full detail
|
|
25
|
+
registry [paths...] Inspect what Kimera would mutate
|
|
26
|
+
synthesize [paths...] Write schemata sources for inspection
|
|
27
|
+
|
|
28
|
+
Setup:
|
|
29
|
+
baseline ... Create or review a baseline of accepted survivors
|
|
30
|
+
completion SHELL Print Bash, Zsh, or Fish completion setup
|
|
31
|
+
skill Print the guide for AI agents running Kimera
|
|
32
|
+
version Print version
|
|
33
|
+
help Show this help
|
|
34
|
+
|
|
35
|
+
With no paths, defaults to: #{Kimera::FileSet::DEFAULT_GLOBS.join(", ")}
|
|
36
|
+
|
|
37
|
+
Exit codes: 0 ok, 1 usage or setup error, 2 gate failed (run)
|
|
38
|
+
|
|
39
|
+
Examples:
|
|
40
|
+
kimera init && kimera doctor
|
|
41
|
+
kimera changed --report tmp/kimera/report.json
|
|
42
|
+
kimera report tmp/kimera/report.json --status no_coverage
|
|
43
|
+
HELP
|
|
44
|
+
end
|
|
45
|
+
end
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "etc"
|
|
4
|
+
require "yaml"
|
|
5
|
+
require_relative "../error"
|
|
6
|
+
require_relative "../scope/config"
|
|
7
|
+
require_relative "../scope/file_set"
|
|
8
|
+
require_relative "flag"
|
|
9
|
+
|
|
10
|
+
class Kimera::CLI::Init
|
|
11
|
+
OPTIONS = Kimera::FlagTable.new(
|
|
12
|
+
banner: "Usage: kimera init [options]",
|
|
13
|
+
flags: [
|
|
14
|
+
Kimera::FORCE_FLAG.with(help: "Replace an existing .kimera.yml"),
|
|
15
|
+
Kimera::Flag.build("--dry-run", :dry_run, "Print the detected configuration without writing it")
|
|
16
|
+
]
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
HELPERS = %w[spec/spec_helper.rb spec/rails_helper.rb spec/helper.rb test/test_helper.rb test/helper.rb].freeze
|
|
20
|
+
AGENT_POINTER = "Mutation testing: before running kimera or triaging its results, read `bundle exec kimera skill`.\n"
|
|
21
|
+
|
|
22
|
+
def initialize(io: $stdout, errors: $stderr, root: ".")
|
|
23
|
+
@io = io
|
|
24
|
+
@errors = errors
|
|
25
|
+
@root = root
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def run(argv)
|
|
29
|
+
options = { force: false, dry_run: false }
|
|
30
|
+
OPTIONS.parse(argv, options)
|
|
31
|
+
options[:dry_run] ? preview : write(options)
|
|
32
|
+
rescue Kimera::UsageError => error
|
|
33
|
+
usage(error)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
def usage(error)
|
|
39
|
+
@errors.puts("kimera: #{error.message}")
|
|
40
|
+
1
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def preview
|
|
44
|
+
@io.write(YAML.dump(detected))
|
|
45
|
+
0
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def write(options)
|
|
49
|
+
path = File.join(@root, Kimera::Config::DEFAULT_PATH)
|
|
50
|
+
overwrite(path) if File.exist?(path) && !options[:force]
|
|
51
|
+
File.write(path, YAML.dump(detected))
|
|
52
|
+
announce(*signpost)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def overwrite(path) = raise(Kimera::UsageError, "#{path} already exists (use --force to replace it)")
|
|
56
|
+
|
|
57
|
+
def signpost
|
|
58
|
+
path = File.join(@root, "AGENTS.md")
|
|
59
|
+
existing = File.file?(path) ? File.read(path) : ""
|
|
60
|
+
return [] if existing.include?("kimera skill")
|
|
61
|
+
File.write(path, pointed(existing))
|
|
62
|
+
["Pointed AI agents to `kimera skill` in AGENTS.md."]
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def pointed(text) = [text.rstrip, AGENT_POINTER].reject(&:empty?).join("\n\n")
|
|
66
|
+
|
|
67
|
+
def announce(*notes)
|
|
68
|
+
@io.puts("Created #{Kimera::Config::DEFAULT_PATH} for #{framework}.", *notes, "Next: kimera doctor")
|
|
69
|
+
0
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def detected
|
|
73
|
+
Kimera::Config.document(
|
|
74
|
+
framework: framework, paths: sources, tests: tests,
|
|
75
|
+
jobs: [Etc.nprocessors, 8].min, max_survivors: 0, max_errors: 0, fail_on_no_coverage: false
|
|
76
|
+
)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def framework = @_framework ||= declared || layout
|
|
80
|
+
|
|
81
|
+
def declared
|
|
82
|
+
return "rspec" if File.file?(File.join(@root, ".rspec"))
|
|
83
|
+
helper = HELPERS.map { |path| File.join(@root, path) }.find { |path| File.file?(path) }
|
|
84
|
+
helper && framework_named_in(File.read(helper))
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def framework_named_in(text)
|
|
88
|
+
return "rspec" if text.match?(/\bRSpec\b|require\s*\(?\s*["']rspec/)
|
|
89
|
+
"minitest" if text.match?(/minitest/i)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def layout
|
|
93
|
+
File.directory?(File.join(@root, "test")) && !File.directory?(File.join(@root, "spec")) ? "minitest" : "rspec"
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def sources
|
|
97
|
+
paths = %w[app/**/*.rb lib/**/*.rb].select { |glob| Dir.glob(File.join(@root, glob)).any? }
|
|
98
|
+
paths.empty? ? Kimera::FileSet::DEFAULT_GLOBS : paths
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def tests
|
|
102
|
+
globs = framework == "minitest" ? %w[test/**/*_test.rb spec/**/*_spec.rb] : %w[spec/**/*_spec.rb]
|
|
103
|
+
[globs.find { |glob| Dir.glob(File.join(@root, glob)).any? } || globs.first]
|
|
104
|
+
end
|
|
105
|
+
end
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../error"
|
|
4
|
+
require_relative "flag"
|
|
5
|
+
require_relative "report_file"
|
|
6
|
+
require_relative "run"
|
|
7
|
+
require_relative "survivors"
|
|
8
|
+
|
|
9
|
+
class Kimera::CLI::Mutant
|
|
10
|
+
OPTIONS = Kimera::FlagTable.new(
|
|
11
|
+
banner: "Usage: kimera mutant ID --report REPORT.json",
|
|
12
|
+
flags: [
|
|
13
|
+
Kimera::Flag.build("--report FILE", :report, "Report generated by kimera run"),
|
|
14
|
+
Kimera::Flag.build("--rerun", :rerun, "Re-evaluate only this mutant in the current project")
|
|
15
|
+
]
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
def initialize(io: $stdout, errors: $stderr)
|
|
19
|
+
@io = io
|
|
20
|
+
@errors = errors
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def run(argv)
|
|
24
|
+
options = { report: nil, rerun: false }
|
|
25
|
+
id, = OPTIONS.parse(argv, options)
|
|
26
|
+
show(identify(id, options), options)
|
|
27
|
+
rescue Kimera::UsageError => error
|
|
28
|
+
usage(error)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
def usage(error)
|
|
34
|
+
@errors.puts("kimera: #{error.message}")
|
|
35
|
+
1
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def identify(id, options)
|
|
39
|
+
raise(Kimera::UsageError, "mutant ID is required") unless id
|
|
40
|
+
raise(Kimera::UsageError, "--report REPORT.json is required") unless options[:report]
|
|
41
|
+
Integer(id)
|
|
42
|
+
rescue ArgumentError
|
|
43
|
+
raise(Kimera::UsageError, "mutant ID must be an integer")
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def show(id, options)
|
|
47
|
+
report = options[:report]
|
|
48
|
+
return rerun(id, report) if options[:rerun]
|
|
49
|
+
Kimera::CLI::Survivors.new(io: @io, errors: @errors).run([report, "--id", id.to_s])
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def rerun(id, report)
|
|
53
|
+
document = Kimera::CLI::ReportFile.parse(report)
|
|
54
|
+
row = locate(document, id)
|
|
55
|
+
@io.puts("Re-running mutant ##{id} in #{row["file"]}.")
|
|
56
|
+
Kimera::CLI::Run.new(io: @io, errors: @errors).run(arguments_for(row, id, provenance(document)))
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def locate(document, id)
|
|
60
|
+
row = document.fetch("results", []).find { |result| result["mutant_id"] == id }
|
|
61
|
+
raise(Kimera::UsageError, "no mutant ##{id} in this report") unless row
|
|
62
|
+
raise(Kimera::UsageError, "mutant ##{id} has no source file") unless row["file"]
|
|
63
|
+
row
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def provenance(document)
|
|
67
|
+
document["run"] ||
|
|
68
|
+
raise(Kimera::UsageError, "report has no run provenance; rerun with a report created by this Kimera version")
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def arguments_for(row, id, settings)
|
|
72
|
+
[row.fetch("file"), "--focus", id.to_s, "--framework", settings.fetch("framework", "rspec")]
|
|
73
|
+
.push("--source-root", settings["source_root"])
|
|
74
|
+
.concat(Array(settings["tests"]).flat_map { |glob| ["--tests", glob] })
|
|
75
|
+
.push("--operators", Array(settings["operators"]).join(","))
|
|
76
|
+
.concat(modes(settings))
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def modes(settings)
|
|
80
|
+
[settings["coverage"] == false ? "--no-coverage" : nil, settings["isolated"] ? "--isolated" : nil].compact
|
|
81
|
+
end
|
|
82
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require_relative "../error"
|
|
5
|
+
|
|
6
|
+
module Kimera
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
class Kimera::CLI
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
module Kimera::CLI::ReportFile
|
|
13
|
+
module_function
|
|
14
|
+
|
|
15
|
+
def parse(path)
|
|
16
|
+
JSON.parse(File.read(path))
|
|
17
|
+
rescue JSON::ParserError => error
|
|
18
|
+
raise(Kimera::UsageError, "unreadable report #{path}: #{error.message}")
|
|
19
|
+
end
|
|
20
|
+
end
|