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.
Files changed (171) hide show
  1. checksums.yaml +7 -0
  2. data/.kimera.yml.example +59 -0
  3. data/CHANGELOG.md +36 -0
  4. data/LICENSE.txt +21 -0
  5. data/README.md +528 -0
  6. data/exe/kimera +22 -0
  7. data/lib/kimera/audit/operator_audit.rb +71 -0
  8. data/lib/kimera/cli/argv.rb +15 -0
  9. data/lib/kimera/cli/baseline.rb +87 -0
  10. data/lib/kimera/cli/changed.rb +27 -0
  11. data/lib/kimera/cli/ci.rb +27 -0
  12. data/lib/kimera/cli/completion.rb +37 -0
  13. data/lib/kimera/cli/doctor.rb +115 -0
  14. data/lib/kimera/cli/flag.rb +55 -0
  15. data/lib/kimera/cli/help_text.rb +45 -0
  16. data/lib/kimera/cli/init.rb +105 -0
  17. data/lib/kimera/cli/mutant.rb +82 -0
  18. data/lib/kimera/cli/report_file.rb +20 -0
  19. data/lib/kimera/cli/run/arguments.rb +58 -0
  20. data/lib/kimera/cli/run/cycle.rb +78 -0
  21. data/lib/kimera/cli/run/digest.rb +49 -0
  22. data/lib/kimera/cli/run/emission.rb +45 -0
  23. data/lib/kimera/cli/run/gate.rb +43 -0
  24. data/lib/kimera/cli/run/options.rb +85 -0
  25. data/lib/kimera/cli/run/pass.rb +100 -0
  26. data/lib/kimera/cli/run/sources.rb +56 -0
  27. data/lib/kimera/cli/run.rb +52 -0
  28. data/lib/kimera/cli/skill.rb +23 -0
  29. data/lib/kimera/cli/suggestion.rb +36 -0
  30. data/lib/kimera/cli/survivors/panel.rb +81 -0
  31. data/lib/kimera/cli/survivors.rb +89 -0
  32. data/lib/kimera/cli/synthesize.rb +66 -0
  33. data/lib/kimera/cli/workflows.rb +8 -0
  34. data/lib/kimera/cli.rb +132 -0
  35. data/lib/kimera/error.rb +3 -0
  36. data/lib/kimera/execution/baseline_failure.rb +47 -0
  37. data/lib/kimera/execution/baseline_pass.rb +123 -0
  38. data/lib/kimera/execution/boot.rb +50 -0
  39. data/lib/kimera/execution/callback.rb +33 -0
  40. data/lib/kimera/execution/child_process.rb +48 -0
  41. data/lib/kimera/execution/composite_isolation.rb +24 -0
  42. data/lib/kimera/execution/harness.rb +150 -0
  43. data/lib/kimera/execution/isolated.rb +64 -0
  44. data/lib/kimera/execution/isolated_child.rb +15 -0
  45. data/lib/kimera/execution/isolated_child_command.rb +44 -0
  46. data/lib/kimera/execution/isolated_child_minitest.rb +34 -0
  47. data/lib/kimera/execution/isolated_plan.rb +54 -0
  48. data/lib/kimera/execution/isolated_scheduling.rb +54 -0
  49. data/lib/kimera/execution/isolated_test_selection.rb +22 -0
  50. data/lib/kimera/execution/isolated_verdict.rb +72 -0
  51. data/lib/kimera/execution/isolated_watchdog.rb +59 -0
  52. data/lib/kimera/execution/isolation.rb +17 -0
  53. data/lib/kimera/execution/null_progress.rb +17 -0
  54. data/lib/kimera/execution/overlay_guard_modules.rb +95 -0
  55. data/lib/kimera/execution/overlay_guards.rb +56 -0
  56. data/lib/kimera/execution/parallel_test_databases.rb +64 -0
  57. data/lib/kimera/execution/pool_driver.rb +101 -0
  58. data/lib/kimera/execution/priority.rb +23 -0
  59. data/lib/kimera/execution/reload.rb +104 -0
  60. data/lib/kimera/execution/rig.rb +41 -0
  61. data/lib/kimera/execution/schedule.rb +105 -0
  62. data/lib/kimera/execution/schemata.rb +88 -0
  63. data/lib/kimera/execution/shift/attempt.rb +34 -0
  64. data/lib/kimera/execution/shift/coverage_channel.rb +43 -0
  65. data/lib/kimera/execution/shift/killer_memory.rb +20 -0
  66. data/lib/kimera/execution/shift/leak_guard.rb +34 -0
  67. data/lib/kimera/execution/shift.rb +137 -0
  68. data/lib/kimera/execution/sweep.rb +30 -0
  69. data/lib/kimera/execution/transaction_isolation.rb +32 -0
  70. data/lib/kimera/execution/trial.rb +16 -0
  71. data/lib/kimera/execution/verdicts.rb +78 -0
  72. data/lib/kimera/execution/worker_pool/fleet.rb +104 -0
  73. data/lib/kimera/execution/worker_pool/stillborn_guard.rb +40 -0
  74. data/lib/kimera/execution/worker_pool/worker.rb +32 -0
  75. data/lib/kimera/execution/worker_pool.rb +101 -0
  76. data/lib/kimera/frameworks/adapter.rb +82 -0
  77. data/lib/kimera/frameworks/minitest_adapter.rb +102 -0
  78. data/lib/kimera/frameworks/rspec_adapter.rb +123 -0
  79. data/lib/kimera/frameworks/rspec_group_index.rb +45 -0
  80. data/lib/kimera/incremental/git_diff.rb +98 -0
  81. data/lib/kimera/incremental/selection.rb +27 -0
  82. data/lib/kimera/incremental/session.rb +109 -0
  83. data/lib/kimera/memoization.rb +104 -0
  84. data/lib/kimera/operators/argument_drop.rb +19 -0
  85. data/lib/kimera/operators/arithmetic.rb +12 -0
  86. data/lib/kimera/operators/audit.rb +3 -0
  87. data/lib/kimera/operators/base.rb +40 -0
  88. data/lib/kimera/operators/binary_swap.rb +23 -0
  89. data/lib/kimera/operators/boolean.rb +26 -0
  90. data/lib/kimera/operators/chain_link_deletion.rb +25 -0
  91. data/lib/kimera/operators/collection_literal.rb +18 -0
  92. data/lib/kimera/operators/comparison.rb +13 -0
  93. data/lib/kimera/operators/conditional.rb +36 -0
  94. data/lib/kimera/operators/default_argument.rb +17 -0
  95. data/lib/kimera/operators/element_drop.rb +31 -0
  96. data/lib/kimera/operators/index_fetch.rb +15 -0
  97. data/lib/kimera/operators/kernel_coercion.rb +17 -0
  98. data/lib/kimera/operators/method_unwrap.rb +20 -0
  99. data/lib/kimera/operators/negation.rb +15 -0
  100. data/lib/kimera/operators/node_swap.rb +20 -0
  101. data/lib/kimera/operators/numeric_literal.rb +39 -0
  102. data/lib/kimera/operators/op_assign.rb +18 -0
  103. data/lib/kimera/operators/plugins.rb +27 -0
  104. data/lib/kimera/operators/rails_association.rb +24 -0
  105. data/lib/kimera/operators/rails_callback.rb +49 -0
  106. data/lib/kimera/operators/rails_declaration.rb +16 -0
  107. data/lib/kimera/operators/rails_permit.rb +34 -0
  108. data/lib/kimera/operators/rails_validation.rb +17 -0
  109. data/lib/kimera/operators/range.rb +15 -0
  110. data/lib/kimera/operators/regexp_literal.rb +16 -0
  111. data/lib/kimera/operators/respond_to_guard.rb +24 -0
  112. data/lib/kimera/operators/return_value.rb +36 -0
  113. data/lib/kimera/operators/safe_navigation.rb +15 -0
  114. data/lib/kimera/operators/selector_swap.rb +24 -0
  115. data/lib/kimera/operators/statement_deletion.rb +26 -0
  116. data/lib/kimera/operators/string_literal.rb +22 -0
  117. data/lib/kimera/operators/symbol_literal.rb +26 -0
  118. data/lib/kimera/operators/vocabulary.rb +82 -0
  119. data/lib/kimera/operators.rb +141 -0
  120. data/lib/kimera/plugins.rb +3 -0
  121. data/lib/kimera/registry/builder.rb +59 -0
  122. data/lib/kimera/registry/mutation_point.rb +146 -0
  123. data/lib/kimera/registry/numbering.rb +15 -0
  124. data/lib/kimera/registry/registry.rb +120 -0
  125. data/lib/kimera/registry/source_file.rb +71 -0
  126. data/lib/kimera/registry/tally.rb +11 -0
  127. data/lib/kimera/registry/walking.rb +55 -0
  128. data/lib/kimera/report/actions.rb +38 -0
  129. data/lib/kimera/report/coloring.rb +25 -0
  130. data/lib/kimera/report/formats.rb +78 -0
  131. data/lib/kimera/report/live.rb +56 -0
  132. data/lib/kimera/report/log.rb +46 -0
  133. data/lib/kimera/report/progress.rb +55 -0
  134. data/lib/kimera/report/screen.rb +44 -0
  135. data/lib/kimera/report/sections.rb +50 -0
  136. data/lib/kimera/report/tally.rb +84 -0
  137. data/lib/kimera/report/text.rb +104 -0
  138. data/lib/kimera/results/result.rb +72 -0
  139. data/lib/kimera/results/run_report.rb +114 -0
  140. data/lib/kimera/rewrite/ast_walk.rb +20 -0
  141. data/lib/kimera/rewrite/directive.rb +61 -0
  142. data/lib/kimera/rewrite/directive_extra_handlers.rb +52 -0
  143. data/lib/kimera/rewrite/directive_handlers.rb +138 -0
  144. data/lib/kimera/rewrite/directive_kwarg_handlers.rb +44 -0
  145. data/lib/kimera/rewrite/numbered_params.rb +50 -0
  146. data/lib/kimera/runtime.rb +51 -0
  147. data/lib/kimera/scope/config.rb +90 -0
  148. data/lib/kimera/scope/file_set.rb +51 -0
  149. data/lib/kimera/scope/ignore_list.rb +55 -0
  150. data/lib/kimera/self_protection.rb +56 -0
  151. data/lib/kimera/support/duration.rb +13 -0
  152. data/lib/kimera/support/errors.rb +8 -0
  153. data/lib/kimera/support/operator_protocol.rb +10 -0
  154. data/lib/kimera/support/syntax.rb +68 -0
  155. data/lib/kimera/support/syntax_types.rb +20 -0
  156. data/lib/kimera/support/unparse.rb +15 -0
  157. data/lib/kimera/support/version.rb +5 -0
  158. data/lib/kimera/support/warnings.rb +15 -0
  159. data/lib/kimera/synthesis/file_weave.rb +78 -0
  160. data/lib/kimera/synthesis/guard_weaver.rb +77 -0
  161. data/lib/kimera/synthesis/guardrail_dispatch.rb +68 -0
  162. data/lib/kimera/synthesis/guardrail_interpolation.rb +59 -0
  163. data/lib/kimera/synthesis/guardrail_value_objects.rb +46 -0
  164. data/lib/kimera/synthesis/overlay.rb +52 -0
  165. data/lib/kimera/synthesis/overlay_splice.rb +101 -0
  166. data/lib/kimera/synthesis/project.rb +58 -0
  167. data/lib/kimera/synthesis/source_map.rb +42 -0
  168. data/lib/kimera/version.rb +3 -0
  169. data/lib/kimera.rb +13 -0
  170. data/skills/kimera/SKILL.md +118 -0
  171. 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
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "init"
4
+ require_relative "doctor"
5
+ require_relative "changed"
6
+ require_relative "ci"
7
+ require_relative "completion"
8
+ require_relative "skill"
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
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "support/errors"
@@ -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