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,82 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../error"
4
+
5
+ module Kimera
6
+ module Frameworks
7
+ RunOutcome =
8
+ Struct.new(:passed, :failed_ids, :failures, keyword_init: true) do
9
+ def passed?
10
+ passed
11
+ end
12
+
13
+ def failures
14
+ self[:failures] || {}
15
+ end
16
+
17
+ def verdict
18
+ passed? ? [:survived, []] : [:killed, failed_ids]
19
+ end
20
+ end
21
+ end
22
+ end
23
+
24
+ class Kimera::Frameworks::Adapter
25
+ def source(files)
26
+ raise(NotImplementedError)
27
+ end
28
+
29
+ def test_ids
30
+ raise(NotImplementedError)
31
+ end
32
+
33
+ def start; end
34
+
35
+ def finish; end
36
+
37
+ def run(ids)
38
+ raise(NotImplementedError)
39
+ end
40
+
41
+ def describe(id)
42
+ id
43
+ end
44
+
45
+ @registry = {}
46
+
47
+ NO_LOADER = -> {}
48
+ ADAPTER_LOADERS = {
49
+ "rspec" => lambda do
50
+ require_relative "rspec_adapter"
51
+ end,
52
+ "minitest" => lambda do
53
+ require_relative "minitest_adapter"
54
+ end
55
+ }.freeze
56
+
57
+ class << self
58
+ attr_reader :registry
59
+
60
+ def load(name)
61
+ ADAPTER_LOADERS.fetch(name.to_s, NO_LOADER).call
62
+ fetch(name).build
63
+ rescue LoadError => error
64
+ raise(Kimera::Error, unloadable(name, error))
65
+ end
66
+
67
+ def unloadable(name, error)
68
+ "framework #{name} is configured but cannot be loaded (#{error.message}); " \
69
+ "set framework: rspec or minitest in .kimera.yml, or add it to the Gemfile"
70
+ end
71
+
72
+ def register(name, klass)
73
+ registry[name.to_s] = klass
74
+ end
75
+
76
+ def fetch(name)
77
+ registry.fetch(name.to_s) do
78
+ raise(Kimera::Error, "unknown test framework adapter: #{name}")
79
+ end
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,102 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "adapter"
4
+
5
+ module Kimera
6
+ module Frameworks
7
+ end
8
+ end
9
+
10
+ class Kimera::Frameworks::MinitestAdapter < Kimera::Frameworks::Adapter
11
+ TESTROOTS = %w[test spec].freeze
12
+ INSTALLED = true
13
+
14
+ class << self
15
+ def build
16
+ require("minitest")
17
+ Minitest.seed ||= 1
18
+ disable
19
+ new
20
+ end
21
+
22
+ private
23
+
24
+ def disable
25
+ Minitest.class_variable_set(:@@installed_at_exit, INSTALLED)
26
+ rescue NameError, ArgumentError
27
+ Minitest.define_singleton_method(:run) { |*| true }
28
+ end
29
+ end
30
+
31
+ def initialize
32
+ super
33
+ @methods = {}
34
+ end
35
+
36
+ def source(files)
37
+ paths(files)
38
+ Array(files).each { |f| load(File.expand_path(f)) }
39
+ reindex
40
+ self
41
+ end
42
+
43
+ def test_ids
44
+ @methods.keys
45
+ end
46
+
47
+ def run(ids)
48
+ failed = []
49
+ failures = {}
50
+ ids.each { |id| record(id, failed, failures) }
51
+ Kimera::Frameworks::RunOutcome.new(passed: failed.empty?, failed_ids: failed, failures: failures)
52
+ end
53
+
54
+ private
55
+
56
+ def record(id, failed, failures)
57
+ klass, name = @methods[id]
58
+ return unless klass
59
+ result = klass.new(name).run
60
+ return unless killing?(result)
61
+ failure(id, result, failed, failures)
62
+ end
63
+
64
+ def failure(id, result, failed, failures)
65
+ failed << id
66
+ failures[id] = message(result)
67
+ end
68
+
69
+ def paths(files)
70
+ Array(files).each do |file|
71
+ parts = File.expand_path(file).split(File::SEPARATOR)
72
+ TESTROOTS.each { |root| prepend(parts, root) }
73
+ end
74
+ end
75
+
76
+ def prepend(parts, root)
77
+ index = parts.rindex(root)
78
+ return unless index
79
+ dir = parts[0..index].join(File::SEPARATOR)
80
+ $LOAD_PATH.unshift(dir) unless $LOAD_PATH.include?(dir)
81
+ end
82
+
83
+ def reindex
84
+ @methods = {}
85
+ Minitest::Runnable.runnables.each { |runnable| index(runnable) }
86
+ end
87
+
88
+ def index(runnable)
89
+ runnable.runnable_methods.each { |name| @methods["#{runnable}##{name}"] = [runnable, name] }
90
+ end
91
+
92
+ def killing?(result)
93
+ !result.passed? && !result.skipped?
94
+ end
95
+
96
+ def message(result)
97
+ assertion = result.failures.first
98
+ assertion && "#{assertion.class.name.split("::").last}: #{assertion.message}"
99
+ end
100
+ end
101
+
102
+ Kimera::Frameworks::Adapter.register(:minitest, Kimera::Frameworks::MinitestAdapter)
@@ -0,0 +1,123 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rspec/core"
4
+ require_relative "adapter"
5
+ require_relative "rspec_group_index"
6
+
7
+ module Kimera
8
+ module Frameworks
9
+ end
10
+ end
11
+
12
+ class Kimera::Frameworks::RSpecAdapter < Kimera::Frameworks::Adapter
13
+ class << self
14
+ def build
15
+ configure
16
+ new
17
+ end
18
+
19
+ private
20
+
21
+ def configure
22
+ RSpec::Core::ConfigurationOptions.new([]).configure(RSpec.configuration)
23
+ rescue StandardError, ScriptError
24
+ nil
25
+ end
26
+ end
27
+
28
+ def initialize
29
+ super
30
+ @examples = {}
31
+ @suite = nil
32
+ end
33
+
34
+ def source(files)
35
+ Array(files).each { |file| load(file) }
36
+ reindex
37
+ self
38
+ end
39
+
40
+ def test_ids
41
+ @examples.keys
42
+ end
43
+
44
+ def describe(id)
45
+ ex = @examples[id]
46
+ ex ? ex.full_description : id
47
+ end
48
+
49
+ def start
50
+ pid = Process.pid
51
+ return if @suite == pid
52
+ @suite = pid
53
+ hooks(:@before_suite_hooks)
54
+ end
55
+
56
+ def finish
57
+ return unless @suite == Process.pid
58
+ hooks(:@after_suite_hooks)
59
+ end
60
+
61
+ def run(ids)
62
+ selected = ids.filter_map { |id| @examples[id] }
63
+ reset
64
+ clear(selected)
65
+ execute(selected)
66
+ outcome(selected)
67
+ end
68
+
69
+ private
70
+
71
+ def outcome(selected)
72
+ failed = selected.select { |ex| ex.execution_result.status == :failed }
73
+ Kimera::Frameworks::RunOutcome.new(
74
+ passed: failed.empty?, failed_ids: failed.map(&:id),
75
+ failures: failed.to_h { |example| [example.id, failure(example)] }
76
+ )
77
+ end
78
+
79
+ def hooks(store)
80
+ config = RSpec.configuration
81
+ context = RSpec::Core::SuiteHookContext.new(store.to_s, config.reporter)
82
+ config.instance_variable_get(store).each { |hook| hook.run(context) }
83
+ end
84
+
85
+ def failure(example)
86
+ exception = example.execution_result.exception
87
+ exception && "#{exception.class}: #{exception.message}"
88
+ end
89
+
90
+ def reset
91
+ world = RSpec.world
92
+ world.wants_to_quit = false
93
+ world.non_example_failure = false
94
+ end
95
+
96
+ def load(file) = Kernel.load(File.expand_path(file))
97
+
98
+ def reindex
99
+ @examples = {}
100
+ RSpec.world.example_groups.each { |top| index(top) }
101
+ end
102
+
103
+ def index(top)
104
+ Kimera::Frameworks::RSpecGroupIndex.examples(top).each { |example| @examples[example.id] = example }
105
+ end
106
+
107
+ def clear(examples)
108
+ examples.each { |ex| ex.instance_variable_set(:@exception, nil) }
109
+ end
110
+
111
+ def execute(selected)
112
+ reporter = RSpec::Core::Reporter.new(RSpec.configuration)
113
+ filtered = RSpec.world.filtered_examples
114
+ wanted = selected.to_a
115
+ Kimera::Frameworks::RSpecGroupIndex.top(selected).each { |top| perform(top, reporter, filtered, wanted) }
116
+ end
117
+
118
+ def perform(top, reporter, filtered, wanted)
119
+ Kimera::Frameworks::RSpecGroupIndex.narrow(filtered, top, wanted) { top.run(reporter) }
120
+ end
121
+ end
122
+
123
+ Kimera::Frameworks::Adapter.register(:rspec, Kimera::Frameworks::RSpecAdapter)
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kimera
4
+ module Frameworks
5
+ end
6
+ end
7
+
8
+ class Kimera::Frameworks::RSpecGroupIndex
9
+ class << self
10
+ def top(examples)
11
+ examples.map { |example| group(example) }.uniq
12
+ end
13
+
14
+ def tree(group, collected = [])
15
+ collected << group
16
+ group.children.each { |child| tree(child, collected) }
17
+ collected
18
+ end
19
+
20
+ def filtered(group)
21
+ RSpec.configuration.filter_manager.prune(group.examples)
22
+ end
23
+
24
+ def examples(top)
25
+ tree(top).flat_map { |group| filtered(group) }
26
+ end
27
+
28
+ def narrow(filtered, top, wanted, &)
29
+ swap(filtered, tree(top).to_h { |group| [group, group.examples & wanted] }, &)
30
+ end
31
+
32
+ private
33
+
34
+ def swap(filtered, scoped)
35
+ filtered.merge!(scoped)
36
+ yield
37
+ ensure
38
+ scoped.each_key { |group| filtered.delete(group) }
39
+ end
40
+
41
+ def group(example)
42
+ example.example_group.parent_groups.last
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,98 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "open3"
4
+ require_relative "../error"
5
+
6
+ module Kimera
7
+ module Incremental
8
+ end
9
+ end
10
+
11
+ class Kimera::Incremental::DiffError < Kimera::Error; end
12
+
13
+ class Kimera::Incremental::GitDiff
14
+ FILE_HEADER = %r{\A\+\+\+ b/(.*)\n?\z}
15
+ HUNK_HEADER = /\A@@ -\d+(?:,\d+)? \+(\d+)(?:,(\d+))? @@/
16
+ BASEFLAGS = %w[diff --relative --unified=0 --no-color --no-ext-diff --src-prefix=a/ --dst-prefix=b/].freeze
17
+
18
+ class << self
19
+ def lines(**scope)
20
+ new(**scope).lines
21
+ end
22
+
23
+ def files(**scope)
24
+ new(**scope).files
25
+ end
26
+
27
+ def parse(text)
28
+ files = {}
29
+ current = nil
30
+ text.each_line { |line| current = advance(files, current, line) }
31
+ files
32
+ end
33
+
34
+ private
35
+
36
+ def advance(files, current, line)
37
+ header = line.match(FILE_HEADER)
38
+ return named(header) if header
39
+ hunk = line.match(HUNK_HEADER)
40
+ collect(files, current, hunk) if current && hunk
41
+ current
42
+ end
43
+
44
+ def named(header)
45
+ path = header[1]
46
+ path unless path == File::NULL
47
+ end
48
+
49
+ def collect(files, file, match)
50
+ span = match[2]
51
+ count = span ? Integer(span, 10) : 1
52
+ return if count.zero?
53
+ range(files, file, Integer(match[1], 10), count)
54
+ end
55
+
56
+ def range(files, file, start, count)
57
+ set = (files[file] ||= Set.new)
58
+ (start...(start + count)).each { |line| set << line }
59
+ end
60
+ end
61
+
62
+ def initialize(since:, root: ".", paths: nil)
63
+ @since = since
64
+ @root = root
65
+ @paths = paths
66
+ end
67
+
68
+ def lines
69
+ out, ok = read
70
+ raise(failure) unless ok
71
+ self.class.parse(out)
72
+ end
73
+
74
+ def files
75
+ lines.keys
76
+ end
77
+
78
+ private
79
+
80
+ def failure
81
+ Kimera::Incremental::DiffError.new(
82
+ "git diff against #{@since.inspect} failed (unknown ref, not a git repository, or git missing)"
83
+ )
84
+ end
85
+
86
+ def read
87
+ out, _error, status = Open3.capture3(*command)
88
+ [out, status.success?]
89
+ rescue Errno::ENOENT
90
+ ["", false]
91
+ end
92
+
93
+ def command
94
+ cmd = ["git", "-C", @root.to_s, *BASEFLAGS, @since.to_s]
95
+ cmd += ["--", *Array(@paths)] if @paths
96
+ cmd
97
+ end
98
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "git_diff"
4
+
5
+ module Kimera
6
+ module Incremental
7
+ module Selection
8
+ module_function
9
+
10
+ def select(registry, changes)
11
+ ids = []
12
+ registry.points.each { |point| ids.concat(ids(point, changes)) }
13
+ ids
14
+ end
15
+
16
+ def ids(point, changes)
17
+ lines = changes[point.file]
18
+ return [] unless lines && point.location.range.any? { |line| lines.include?(line) }
19
+ point.ids
20
+ end
21
+
22
+ def changed(registry, since:, root: ".")
23
+ select(registry, GitDiff.lines(since: since, root: root))
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,109 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require_relative "../results/result"
5
+ require_relative "../results/run_report"
6
+
7
+ module Kimera
8
+ module Incremental
9
+ end
10
+ end
11
+
12
+ class Kimera::Incremental::Session
13
+ VERSION = 2
14
+
15
+ attr_reader :results, :leaks
16
+
17
+ def initialize(results: {}, leaks: [], meta: {}, fingerprints: {})
18
+ @results = results
19
+ @leaks = leaks
20
+ @meta = meta
21
+ @fingerprints = fingerprints
22
+ end
23
+
24
+ class << self
25
+ def load(path, registry: nil)
26
+ return new unless path && File.exist?(path)
27
+ session = decode(JSON.parse(File.read(path)))
28
+ session.prune!(registry) if registry
29
+ session
30
+ end
31
+
32
+ def decode(data)
33
+ new(**attributes(data))
34
+ end
35
+
36
+ def attributes(data)
37
+ attrs = { leaks: leaks(data["leaks"]), meta: data["meta"] || {} }
38
+ attrs[:results] = keys(data["results"]) { |hash| Kimera::MutantResult.from_h(hash) }
39
+ attrs[:fingerprints] = keys(data["fingerprints"]) { |fingerprint| fingerprint }
40
+ attrs
41
+ end
42
+
43
+ def leaks(raw)
44
+ Array(raw).map { |hash| Kimera::LeakReport.new(mutant_id: hash["mutant_id"], detail: hash["detail"]) }
45
+ end
46
+
47
+ def keys(hash)
48
+ Hash(hash).to_h { |id, value| [Integer(id, 10), yield(value)] }
49
+ end
50
+
51
+ def fingerprint(registry, id)
52
+ pair = registry.point(id)
53
+ return unless pair
54
+ mutant, point = pair
55
+ [point.file, point.location.start_line, point.original_source, mutant.label].join(" ")
56
+ end
57
+ end
58
+
59
+ def prune!(registry)
60
+ @results.reject! { |id, _| stale?(id, registry) }
61
+ @leaks.select! { |leak| @results.key?(leak.mutant_id) }
62
+ self
63
+ end
64
+
65
+ def done?(id)
66
+ @results.key?(id)
67
+ end
68
+
69
+ def report(evaluated, waived, registry)
70
+ results = evaluated.filter_map { |id| @results[id] }
71
+ results += waived.map { |id| Kimera::MutantResult.waived(id, registry.index[id]&.file) }
72
+ Kimera::RunReport.new(results: results, leaks: @leaks, registry: registry)
73
+ end
74
+
75
+ def merge!(report)
76
+ report.results.each { |r| @results[r.mutant_id] = r }
77
+ @leaks.concat(report.leaks)
78
+ self
79
+ end
80
+
81
+ def save(path, registry: nil, meta: {})
82
+ @meta = @meta.merge(meta)
83
+ @fingerprints = fingerprints(registry) if registry
84
+ File.write(path, JSON.pretty_generate(payload))
85
+ path
86
+ end
87
+
88
+ private
89
+
90
+ def stale?(id, registry)
91
+ stored = @fingerprints[id]
92
+ !stored || stored != self.class.fingerprint(registry, id)
93
+ end
94
+
95
+ def payload
96
+ base.merge("fingerprints" => @fingerprints.transform_keys(&:to_s), "leaks" => @leaks.map(&:to_h))
97
+ end
98
+
99
+ def base
100
+ { "version" => VERSION, "meta" => @meta, "results" => @results.transform_keys(&:to_s).transform_values(&:to_h) }
101
+ end
102
+
103
+ def fingerprints(registry)
104
+ @results.keys.each_with_object({}) do |id, hash|
105
+ fingerprint = self.class.fingerprint(registry, id)
106
+ hash[id] = fingerprint if fingerprint
107
+ end
108
+ end
109
+ end
@@ -0,0 +1,104 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "support/syntax"
4
+ require_relative "support/syntax_types"
5
+
6
+ module Kimera
7
+ module Memoization
8
+ OR_WRITE_NODES = [
9
+ SyntaxTypes::InstanceVariableOrWriteNode,
10
+ SyntaxTypes::ClassVariableOrWriteNode,
11
+ SyntaxTypes::GlobalVariableOrWriteNode
12
+ ].freeze
13
+
14
+ CONST_WRITE_NODES = [SyntaxTypes::ConstantWriteNode, SyntaxTypes::ConstantPathWriteNode].freeze
15
+
16
+ SELF_OR_WRITE = {
17
+ SyntaxTypes::InstanceVariableWriteNode => SyntaxTypes::InstanceVariableReadNode,
18
+ SyntaxTypes::ClassVariableWriteNode => SyntaxTypes::ClassVariableReadNode,
19
+ SyntaxTypes::GlobalVariableWriteNode => SyntaxTypes::GlobalVariableReadNode
20
+ }.freeze
21
+
22
+ Range =
23
+ Struct.new(:start_offset, :end_offset, :reason, :holes) do
24
+ def contains?(start, finish)
25
+ return false unless start >= start_offset && finish <= end_offset
26
+ Array(holes).none? { |first, last| start >= first && finish <= last }
27
+ end
28
+ end
29
+
30
+ module_function
31
+
32
+ def ranges(source)
33
+ result = Kimera::Syntax.parse(source)
34
+ return [] if result.failure?
35
+ ranges = []
36
+ visit(result.value, ranges)
37
+ ranges
38
+ end
39
+
40
+ def visit(node, ranges)
41
+ return unless node.is_a?(SyntaxTypes::Node)
42
+ taint = taint(node)
43
+ ranges << taint if taint
44
+ node.compact_child_nodes.each { |child| visit(child, ranges) }
45
+ end
46
+
47
+ def taint(node)
48
+ case node
49
+ when *OR_WRITE_NODES then value(node, "memoized via ||=")
50
+ when *CONST_WRITE_NODES then value(node, "constant assignment (computed once)")
51
+ else cached(node)
52
+ end
53
+ end
54
+
55
+ def value(node, reason)
56
+ expression = node.value
57
+ range(expression, reason) if expression
58
+ end
59
+
60
+ def cached(node)
61
+ reader = SELF_OR_WRITE[node.class]
62
+ return unless reader
63
+ memoized(node, reader)
64
+ end
65
+
66
+ def memoized(node, reader)
67
+ value = node.value
68
+ return unless value.is_a?(SyntaxTypes::OrNode)
69
+ left = value.left
70
+ return unless left.is_a?(reader) && same?(node, left)
71
+ range(value, "memoized via `x = x || ...`")
72
+ end
73
+
74
+ def same?(write, read)
75
+ write.name == read.name
76
+ end
77
+
78
+ def range(node, reason)
79
+ start, finish = span(node)
80
+ Range.new(start, finish, reason, holes(node))
81
+ end
82
+
83
+ HOLE_NODES = [SyntaxTypes::DefNode, SyntaxTypes::LambdaNode].freeze
84
+
85
+ def holes(node, holes = [])
86
+ return holes unless node.is_a?(SyntaxTypes::Node)
87
+ return body(node, holes) if HOLE_NODES.any? { |kind| node.is_a?(kind) }
88
+ node.compact_child_nodes.each { |child| holes(child, holes) }
89
+ holes
90
+ end
91
+
92
+ def body(node, holes)
93
+ value = node.body
94
+ holes << span(value) if value
95
+ holes
96
+ end
97
+
98
+ def span(node)
99
+ location = node.location
100
+ start = location.start_offset
101
+ [start, start + location.length]
102
+ end
103
+ end
104
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "prism"
4
+ require_relative "base"
5
+
6
+ class Kimera::Operators::ArgumentDrop < Kimera::Operators::Base
7
+ SKIP_KINDS = [SplatNode, KeywordHashNode, BlockArgumentNode, ForwardingArgumentsNode].freeze
8
+
9
+ class << self
10
+ def key = "argument_drop"
11
+ end
12
+
13
+ def variants(node, **)
14
+ return unless identifier?(node)
15
+ args = arguments(node)
16
+ return if args.any? { |a| SKIP_KINDS.any? { |k| a.is_a?(k) } }
17
+ drops(args, "drop_argument", label: "drop arg")
18
+ end
19
+ end