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,81 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../../support/duration"
|
|
4
|
+
|
|
5
|
+
class Kimera::CLI::Survivors::Panel
|
|
6
|
+
def initialize(io: $stdout)
|
|
7
|
+
@io = io
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def announce(matching, options)
|
|
11
|
+
@io.puts("#{matching.size} #{options[:status]} mutant(s):")
|
|
12
|
+
sorted(matching).each { |row| entry(row) }
|
|
13
|
+
@io.puts
|
|
14
|
+
@io.puts("detail: kimera mutant <N> --report #{options[:report]}")
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def describe(row)
|
|
18
|
+
header(row)
|
|
19
|
+
diff(row, " ")
|
|
20
|
+
extra(row)
|
|
21
|
+
tests("covering tests", row["covering_tests"])
|
|
22
|
+
tests("failing tests", row["failing_tests"])
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
private
|
|
26
|
+
|
|
27
|
+
def entry(row)
|
|
28
|
+
@io.puts
|
|
29
|
+
brief(row)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def sorted(matching)
|
|
33
|
+
matching.sort_by { |file, line,| [file, line] }.map { |_, _, row| row }
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def header(row)
|
|
37
|
+
@io.puts("##{row["mutant_id"]} #{row["status"]} #{location(row)}")
|
|
38
|
+
field("operator: ", row["operator"])
|
|
39
|
+
field("label: ", row["label"])
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def extra(row)
|
|
43
|
+
seconds = row["duration"]
|
|
44
|
+
@io.puts(" duration: #{Kimera::Duration.new(seconds).brief}") if seconds&.positive?
|
|
45
|
+
field("detail: ", row["detail"])
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def field(prefix, value)
|
|
49
|
+
@io.puts(" #{prefix}#{value}") if value
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def brief(row)
|
|
53
|
+
@io.puts(" ##{row["mutant_id"]} #{location(row)} [#{row["label"]}]")
|
|
54
|
+
diff(row, " ")
|
|
55
|
+
covering = Array(row["covering_tests"])
|
|
56
|
+
@io.puts(" covered by #{covering.size} test(s)") unless covering.empty?
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def diff(row, indent)
|
|
60
|
+
source(row["original"], "-", indent)
|
|
61
|
+
mutated = row["mutated"]
|
|
62
|
+
source(mutated, "+", indent) if mutated
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def source(code, sign, indent)
|
|
66
|
+
first, *rest = code.to_s.split("\n")
|
|
67
|
+
@io.puts("#{indent}#{sign} #{first}")
|
|
68
|
+
rest.each { |line| @io.puts("#{indent} #{line}") }
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def tests(title, items)
|
|
72
|
+
items = Array(items)
|
|
73
|
+
return if items.empty?
|
|
74
|
+
@io.puts(" #{title} (#{items.size}):")
|
|
75
|
+
items.each { |test| @io.puts(" #{test}") }
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def location(row)
|
|
79
|
+
[row["file"], row["line"]].compact.join(":")
|
|
80
|
+
end
|
|
81
|
+
end
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "optparse"
|
|
4
|
+
require_relative "../error"
|
|
5
|
+
require_relative "flag"
|
|
6
|
+
require_relative "report_file"
|
|
7
|
+
|
|
8
|
+
class Kimera::CLI::Survivors
|
|
9
|
+
OPTIONS = Kimera::FlagTable.new(
|
|
10
|
+
banner: "Usage: kimera survivors REPORT.json [options] [FILE_SUBSTRING]\n" \
|
|
11
|
+
"Lists a report's surviving mutants; FILE_SUBSTRING keeps only " \
|
|
12
|
+
"mutants whose file path contains it (e.g. models/discount).",
|
|
13
|
+
flags: [
|
|
14
|
+
Kimera::Flag.build("--id N", :id, "Show one mutant in full detail", type: Integer),
|
|
15
|
+
Kimera::Flag.build(
|
|
16
|
+
"--status NAME", :status,
|
|
17
|
+
"List a different status (survived, no_coverage, " \
|
|
18
|
+
"timeout, error, killed, isolated_only)"
|
|
19
|
+
)
|
|
20
|
+
]
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
def initialize(io: $stdout, errors: $stderr)
|
|
24
|
+
@io = io
|
|
25
|
+
@errors = errors
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def run(argv)
|
|
29
|
+
attempt(argv)
|
|
30
|
+
rescue Kimera::UsageError => error
|
|
31
|
+
@errors.puts("kimera: #{error.message}")
|
|
32
|
+
1
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
def attempt(argv)
|
|
38
|
+
options = parse(argv)
|
|
39
|
+
results = load(options[:report]).fetch("results", [])
|
|
40
|
+
id = options[:id]
|
|
41
|
+
id ? show(results, id) : list(results, options)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def parse(argv)
|
|
45
|
+
options = { status: "survived", id: nil }
|
|
46
|
+
report, filter = OPTIONS.parse(argv, options)
|
|
47
|
+
raise(Kimera::UsageError, "survivors: a report file is required (run with --report FILE first)") unless report
|
|
48
|
+
options.merge(report: report, filter: filter)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def load(path)
|
|
52
|
+
raise(Kimera::UsageError, "no such report: #{path}") unless File.file?(path)
|
|
53
|
+
Kimera::CLI::ReportFile.parse(path)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def list(results, options)
|
|
57
|
+
matching = matches(results, options)
|
|
58
|
+
return none(options) if matching.empty?
|
|
59
|
+
Panel.new(io: @io).announce(matching, options)
|
|
60
|
+
0
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def matches(results, options)
|
|
64
|
+
status = options[:status]
|
|
65
|
+
filter = options[:filter]
|
|
66
|
+
found = results.select { |row| row["status"] == status }.map { |row| [row["file"].to_s, row["line"] || 0, row] }
|
|
67
|
+
filter ? found.select { |file,| file.include?(filter) } : found
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def none(options)
|
|
71
|
+
filter = options[:filter]
|
|
72
|
+
@io.puts("no #{options[:status]} mutants#{" matching #{filter}" if filter}")
|
|
73
|
+
0
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def show(results, id)
|
|
77
|
+
row = results.find { |candidate| candidate["mutant_id"] == id }
|
|
78
|
+
return missing(id) unless row
|
|
79
|
+
Panel.new(io: @io).describe(row)
|
|
80
|
+
0
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def missing(id)
|
|
84
|
+
@errors.puts("kimera: no mutant ##{id} in this report")
|
|
85
|
+
1
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
require_relative "survivors/panel"
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "optparse"
|
|
4
|
+
require_relative "../operators"
|
|
5
|
+
require_relative "../registry/builder"
|
|
6
|
+
require_relative "../scope/config"
|
|
7
|
+
require_relative "../scope/file_set"
|
|
8
|
+
require_relative "../synthesis/project"
|
|
9
|
+
require_relative "flag"
|
|
10
|
+
|
|
11
|
+
module Kimera
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
class Kimera::CLI
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
class Kimera::CLI::Synthesize
|
|
18
|
+
OPTIONS = Kimera::FlagTable.new(
|
|
19
|
+
banner: "Usage: kimera synthesize [paths...] [options]",
|
|
20
|
+
flags: [
|
|
21
|
+
Kimera::Flag.build("--out DIR", :out, "Write synthesized mirror tree to DIR"),
|
|
22
|
+
Kimera::REGISTRY_FLAG,
|
|
23
|
+
Kimera::OPERATORS_FLAG,
|
|
24
|
+
Kimera::CONFIG_FLAG
|
|
25
|
+
]
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
def initialize(io: $stdout, errors: $stderr)
|
|
29
|
+
@io = io
|
|
30
|
+
@errors = errors
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def run(argv)
|
|
34
|
+
options = Kimera::Config.scope({ out: nil, registry: nil, operators: Kimera::Operators::DEFAULT_KEYS }, argv)
|
|
35
|
+
paths = OPTIONS.parse(argv, options)
|
|
36
|
+
out = options[:out]
|
|
37
|
+
out ? synthesize(paths, options, out) : missing
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
private
|
|
41
|
+
|
|
42
|
+
def missing
|
|
43
|
+
@errors.puts("kimera synthesize: --out DIR is required")
|
|
44
|
+
1
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def synthesize(paths, options, out)
|
|
48
|
+
registry = load(paths, options)
|
|
49
|
+
report(registry, Kimera::Synthesis::Project.new(registry).write(out), out)
|
|
50
|
+
0
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def report(registry, manifest, out)
|
|
54
|
+
@io.puts("Synthesized #{registry.files.size} files to #{out}.")
|
|
55
|
+
@io.puts(" #{manifest.mutant_files.size} mutants on the schema path.")
|
|
56
|
+
unsafe = manifest.unsafe_mutants
|
|
57
|
+
@io.puts(" #{unsafe.size} mutants routed to reload fallback (memoization).") unless unsafe.empty?
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def load(paths, options)
|
|
61
|
+
stored = options[:registry]
|
|
62
|
+
return Kimera::Registry.load(stored) if stored
|
|
63
|
+
Kimera::RegistryScan.new(operators: Kimera::Operators.build(keys: options[:operators]))
|
|
64
|
+
.build(Kimera::FileSet.scoped(paths, options))
|
|
65
|
+
end
|
|
66
|
+
end
|
data/lib/kimera/cli.rb
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "optparse"
|
|
4
|
+
require_relative "cli/flag"
|
|
5
|
+
require_relative "cli/help_text"
|
|
6
|
+
require_relative "cli/suggestion"
|
|
7
|
+
require_relative "operators"
|
|
8
|
+
require_relative "registry/builder"
|
|
9
|
+
require_relative "scope/config"
|
|
10
|
+
require_relative "scope/file_set"
|
|
11
|
+
require_relative "version"
|
|
12
|
+
|
|
13
|
+
class Kimera::CLI
|
|
14
|
+
REGISTRY_OPTIONS = Kimera::FlagTable.new(
|
|
15
|
+
banner: "Usage: kimera registry [paths...] [options]",
|
|
16
|
+
flags: [
|
|
17
|
+
Kimera::Flag.build(["-o", "--output FILE"], :output, "Write registry JSON to FILE"),
|
|
18
|
+
Kimera::OPERATORS_FLAG,
|
|
19
|
+
Kimera::CONFIG_FLAG,
|
|
20
|
+
Kimera::Flag.build("--json", :json, "Print registry JSON to stdout"),
|
|
21
|
+
Kimera::Flag.build(["-q", "--quiet"], :quiet, "Suppress the summary line")
|
|
22
|
+
]
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
COMMANDS = { "registry" => :registry, "dump" => :registry, "synthesize" => :synthesize }
|
|
26
|
+
.merge("run" => :execute, "survivors" => :survivors, "report" => :report, "mutant" => :mutant)
|
|
27
|
+
.merge("init" => :init, "doctor" => :doctor, "changed" => :changed, "ci" => :ci, "completion" => :completion)
|
|
28
|
+
.merge("baseline" => :baseline, "skill" => :skill, "version" => :version)
|
|
29
|
+
.merge("-v" => :version, "--version" => :version, "help" => :help)
|
|
30
|
+
.merge("-h" => :help, "--help" => :help)
|
|
31
|
+
.freeze
|
|
32
|
+
private_constant(:COMMANDS)
|
|
33
|
+
|
|
34
|
+
COMMAND_NAMES = COMMANDS.keys.grep_v(/\A-/).freeze
|
|
35
|
+
|
|
36
|
+
class << self
|
|
37
|
+
def start(argv)
|
|
38
|
+
new.run(argv)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def initialize(io: $stdout, errors: $stderr)
|
|
43
|
+
@io = io
|
|
44
|
+
@errors = errors
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def run(argv)
|
|
48
|
+
dispatch(argv.dup)
|
|
49
|
+
rescue Kimera::UsageError => error
|
|
50
|
+
@errors.puts("kimera: #{error.message}")
|
|
51
|
+
1
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
private
|
|
55
|
+
|
|
56
|
+
def dispatch(argv)
|
|
57
|
+
command = argv.shift || "help"
|
|
58
|
+
handler = COMMANDS[command]
|
|
59
|
+
return unknown(command, argv) unless handler
|
|
60
|
+
__send__(handler, argv)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def unknown(command, argv)
|
|
64
|
+
hint = Kimera::CLI::Suggestion.new(COMMAND_NAMES).hint(command)
|
|
65
|
+
@errors.puts("kimera: unknown command #{command.inspect}#{hint}\nTry: kimera help")
|
|
66
|
+
dispatch(["help", *argv])
|
|
67
|
+
1
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def registry(argv)
|
|
71
|
+
options = Kimera::Config.scope({ output: nil, operators: Kimera::Operators::DEFAULT_KEYS, quiet: false }, argv)
|
|
72
|
+
paths = REGISTRY_OPTIONS.parse(argv, options)
|
|
73
|
+
files = Kimera::FileSet.scoped(paths, options)
|
|
74
|
+
return missing(paths, options) if files.empty?
|
|
75
|
+
build(files, options)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def missing(paths, options)
|
|
79
|
+
@errors.puts("kimera: no Ruby files matched #{Kimera::FileSet.globs(paths, options).join(", ")}")
|
|
80
|
+
1
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def build(files, options)
|
|
84
|
+
emit(scan(options).build(files), options)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def scan(options)
|
|
88
|
+
Kimera::RegistryScan.new(operators: Kimera::Operators.build(keys: options[:operators]))
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def emit(built, options)
|
|
92
|
+
out, json = options.values_at(:output, :json)
|
|
93
|
+
built.write(out) if out
|
|
94
|
+
@io.puts(built.to_json) if json
|
|
95
|
+
announce(built, out) unless options[:quiet] || json
|
|
96
|
+
0
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def announce(built, out)
|
|
100
|
+
@io.puts(built.summary)
|
|
101
|
+
@io.puts("Wrote registry to #{out}") if out
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def version(*)
|
|
105
|
+
@io.puts("kimera #{Kimera::VERSION}")
|
|
106
|
+
0
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def help(*)
|
|
110
|
+
@io.puts(Kimera::HelpText::HELP)
|
|
111
|
+
0
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def synthesize(argv) = delegate("synthesize", :Synthesize, argv)
|
|
115
|
+
def execute(argv) = delegate("run", :Run, argv)
|
|
116
|
+
def survivors(argv) = delegate("survivors", :Survivors, argv)
|
|
117
|
+
def report(argv) = delegate("survivors", :Survivors, argv)
|
|
118
|
+
def mutant(argv) = delegate("mutant", :Mutant, argv)
|
|
119
|
+
def baseline(argv) = delegate("baseline", :Baseline, argv)
|
|
120
|
+
def init(argv) = workflow(:Init, argv)
|
|
121
|
+
def doctor(argv) = workflow(:Doctor, argv)
|
|
122
|
+
def changed(argv) = workflow(:Changed, argv)
|
|
123
|
+
def ci(argv) = workflow(:CI, argv)
|
|
124
|
+
def completion(argv) = workflow(:Completion, argv)
|
|
125
|
+
def skill(argv) = workflow(:Skill, argv)
|
|
126
|
+
def workflow(name, argv) = delegate("workflows", name, argv)
|
|
127
|
+
|
|
128
|
+
def delegate(file, name, argv)
|
|
129
|
+
require_relative("cli/#{file}")
|
|
130
|
+
Kimera::CLI.const_get(name).new(io: @io, errors: @errors).run(argv)
|
|
131
|
+
end
|
|
132
|
+
end
|
data/lib/kimera/error.rb
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../error"
|
|
4
|
+
|
|
5
|
+
module Kimera
|
|
6
|
+
module Execution
|
|
7
|
+
end
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
class Kimera::Execution::BaselineFailure < Kimera::Error
|
|
11
|
+
MAX_DETAILS = 3
|
|
12
|
+
MAX_MESSAGE = 300
|
|
13
|
+
|
|
14
|
+
class << self
|
|
15
|
+
def build(failed, messages)
|
|
16
|
+
new(summary(failed, messages))
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def summary(failed, messages)
|
|
20
|
+
text = "baseline suite is not green: #{failed.join(", ")}"
|
|
21
|
+
text += appendix(failed, messages)
|
|
22
|
+
"#{text}\n#{reproduce(failed)}"
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def appendix(failed, messages)
|
|
26
|
+
items = details(failed, messages)
|
|
27
|
+
items.empty? ? "" : "\n#{items.join("\n")}"
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def details(failed, messages)
|
|
31
|
+
failed.first(MAX_DETAILS).filter_map { |id| detail(id, messages[id]) }
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def detail(id, message)
|
|
35
|
+
return unless message
|
|
36
|
+
" #{id}:\n #{truncate(message).gsub("\n", "\n ")}"
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def truncate(text)
|
|
40
|
+
text.length > MAX_MESSAGE ? "#{text[0, MAX_MESSAGE]}…" : text
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def reproduce(failed)
|
|
44
|
+
" reproduce without kimera: rspec #{failed.first(MAX_DETAILS).join(" ")} --order defined"
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "stringio"
|
|
4
|
+
require_relative "../error"
|
|
5
|
+
require_relative "../runtime"
|
|
6
|
+
require_relative "baseline_failure"
|
|
7
|
+
require_relative "null_progress"
|
|
8
|
+
|
|
9
|
+
class Kimera::Execution::BaselinePass
|
|
10
|
+
Measured = Struct.new(:coverage, :irrelevant, keyword_init: true)
|
|
11
|
+
Tally = Struct.new(:coverage, :failures, :irrelevant)
|
|
12
|
+
|
|
13
|
+
def initialize(adapter:, registry:, progress: Kimera::Execution::NullProgress)
|
|
14
|
+
@adapter = adapter
|
|
15
|
+
@registry = registry
|
|
16
|
+
@progress = progress
|
|
17
|
+
@messages = {}
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def measure!
|
|
21
|
+
settle { serial }
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def parallel!(&)
|
|
25
|
+
settle { dispatch(&) }
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def check!
|
|
29
|
+
Kimera::Runtime.active = nil
|
|
30
|
+
outcome = quietly { @adapter.run(@adapter.test_ids) }
|
|
31
|
+
return if outcome.passed?
|
|
32
|
+
@messages = outcome.failures
|
|
33
|
+
failure!(outcome.failed_ids)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
def settle
|
|
39
|
+
@progress.start(@adapter.test_ids.size, "baseline")
|
|
40
|
+
yield
|
|
41
|
+
report
|
|
42
|
+
ensure
|
|
43
|
+
@progress.finish
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def tally = @_tally ||= Tally.new(Hash.new { |h, k| h[k] = [] }, [], [])
|
|
47
|
+
|
|
48
|
+
def report
|
|
49
|
+
failures = tally.failures
|
|
50
|
+
failure!(failures) unless failures.empty?
|
|
51
|
+
Measured.new(coverage: tally.coverage, irrelevant: tally.irrelevant)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def serial
|
|
55
|
+
ledger = Kimera::Runtime.start!
|
|
56
|
+
@adapter.test_ids.each { |test_id| attempt(ledger, test_id) }
|
|
57
|
+
ensure
|
|
58
|
+
Kimera::Runtime.stop!(ledger)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def attempt(ledger, test_id)
|
|
62
|
+
Kimera::Runtime.active = nil
|
|
63
|
+
Kimera::Runtime.clear!(ledger)
|
|
64
|
+
record(test_id, quietly { @adapter.run([test_id]) }, Kimera::Runtime.drain!(ledger))
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def record(test_id, outcome, touched)
|
|
68
|
+
touched.each { |id| tally.coverage[id] << test_id }
|
|
69
|
+
bank(test_id, touched, message: outcome.failures[test_id]) unless outcome.passed?
|
|
70
|
+
@progress.tick
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def dispatch
|
|
74
|
+
yield(resolve: resolve, lost: loss)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def resolve
|
|
78
|
+
->(message) { receive(message) }
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def receive(message)
|
|
82
|
+
touched = message["touched"]
|
|
83
|
+
test_id = message["id"]
|
|
84
|
+
touched.each { |id| tally.coverage[id] << test_id }
|
|
85
|
+
bank(test_id, touched, message: message["failure"]) unless message["passed"]
|
|
86
|
+
@progress.tick
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def loss
|
|
90
|
+
method(:lost)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def lost(test_id, _reason)
|
|
94
|
+
tally.failures << test_id
|
|
95
|
+
@progress.tick
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def bank(test_id, touched, message: nil)
|
|
99
|
+
@messages[test_id] = message
|
|
100
|
+
bucket(touched) << test_id
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def bucket(touched)
|
|
104
|
+
Array(touched).any? { |id| @registry.index.key?(id) } ? tally.failures : tally.irrelevant
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def quietly
|
|
108
|
+
swap = [$stdout, $stderr]
|
|
109
|
+
silence
|
|
110
|
+
yield
|
|
111
|
+
ensure
|
|
112
|
+
$stdout, $stderr = swap
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def silence
|
|
116
|
+
$stdout = StringIO.new
|
|
117
|
+
$stderr = StringIO.new
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def failure!(failed)
|
|
121
|
+
raise(Kimera::Execution::BaselineFailure.build(failed, @messages))
|
|
122
|
+
end
|
|
123
|
+
end
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "isolation"
|
|
4
|
+
|
|
5
|
+
class Kimera::Execution::Boot
|
|
6
|
+
def initialize(adapter:, isolate:, errors: nil)
|
|
7
|
+
@adapter = adapter
|
|
8
|
+
@isolate = isolate
|
|
9
|
+
@errors = errors
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def suite(test_files)
|
|
13
|
+
@adapter.source(test_files)
|
|
14
|
+
@adapter.start
|
|
15
|
+
load!
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def load!
|
|
19
|
+
loader&.call
|
|
20
|
+
rescue StandardError => error
|
|
21
|
+
(@errors || $stderr).puts(failure(error))
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def isolate(isolation)
|
|
25
|
+
return isolation unless @isolate
|
|
26
|
+
return isolation unless defined?(ActiveRecord::Base)
|
|
27
|
+
Kimera::Execution::CompositeIsolation.new(
|
|
28
|
+
[Kimera::Execution::TransactionIsolation.new(ActiveRecord::Base), isolation]
|
|
29
|
+
)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
private
|
|
33
|
+
|
|
34
|
+
def application
|
|
35
|
+
Rails.application
|
|
36
|
+
rescue NameError
|
|
37
|
+
nil
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def loader
|
|
41
|
+
application&.method(:eager_load!)
|
|
42
|
+
rescue NameError
|
|
43
|
+
nil
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def failure(error)
|
|
47
|
+
"kimera: eager_load! failed (#{error.class}: #{error.message}); " \
|
|
48
|
+
"lazily-autoloaded files may escape mutation"
|
|
49
|
+
end
|
|
50
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Kimera
|
|
4
|
+
module Execution
|
|
5
|
+
end
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
class Kimera::Execution::Callback
|
|
9
|
+
def initialize(filter)
|
|
10
|
+
@filter = filter
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def duplicate?(existing)
|
|
14
|
+
case @filter
|
|
15
|
+
when Proc then sourced?(existing)
|
|
16
|
+
when Symbol, String then existing == @filter
|
|
17
|
+
else alike?(existing)
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
private
|
|
22
|
+
|
|
23
|
+
def sourced?(existing)
|
|
24
|
+
existing.is_a?(Proc) && existing.source_location&.first == @filter.source_location&.first
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def alike?(existing)
|
|
28
|
+
kind = @filter.class
|
|
29
|
+
return false unless existing.instance_of?(kind)
|
|
30
|
+
return true unless kind.method_defined?(:attributes)
|
|
31
|
+
existing.attributes == @filter.attributes
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "english"
|
|
4
|
+
require "json"
|
|
5
|
+
|
|
6
|
+
module Kimera
|
|
7
|
+
module Execution
|
|
8
|
+
module ChildProcess
|
|
9
|
+
private
|
|
10
|
+
|
|
11
|
+
def silence!
|
|
12
|
+
STDOUT.reopen(File::NULL, "w")
|
|
13
|
+
STDERR.reopen(File::NULL, "w")
|
|
14
|
+
$stdout = STDOUT
|
|
15
|
+
$stderr = STDERR
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def kill(pid)
|
|
19
|
+
Process.kill("KILL", pid)
|
|
20
|
+
rescue Errno::ESRCH
|
|
21
|
+
nil
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def reap(pid)
|
|
25
|
+
Process.wait(pid)
|
|
26
|
+
$CHILD_STATUS
|
|
27
|
+
rescue Errno::ECHILD
|
|
28
|
+
nil
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def shut(io)
|
|
32
|
+
io.close
|
|
33
|
+
rescue IOError
|
|
34
|
+
nil
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def parse(line)
|
|
38
|
+
JSON.parse(line)
|
|
39
|
+
rescue JSON::ParserError
|
|
40
|
+
nil
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def now
|
|
44
|
+
Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|