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,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "prism"
4
+ require_relative "../memoization"
5
+ require_relative "mutation_point"
6
+ require_relative "walking"
7
+
8
+ class Kimera::RegistryScan::SourceFile
9
+ include Kimera::RegistryWalking
10
+
11
+ def initialize(source, file:, operators:, numbering:)
12
+ @source = source
13
+ @file = file
14
+ @operators = operators
15
+ @numbering = numbering
16
+ end
17
+
18
+ def points
19
+ parsed = Kimera::Syntax.parse(@source)
20
+ return [] if parsed.failure?
21
+ mined(parsed.value)
22
+ end
23
+
24
+ private
25
+
26
+ def tainted = @_tainted ||= Kimera::Memoization.ranges(@source)
27
+
28
+ def mined(root)
29
+ found = []
30
+ walk(root, origin) { |node, cursor| bank(found, node, cursor) }
31
+ found
32
+ end
33
+
34
+ def origin
35
+ Kimera::RegistryWalking::Cursor.new(position: nil, inside_def: nil, in_pattern: nil, def_body: nil, defname: nil)
36
+ end
37
+
38
+ def bank(found, node, cursor)
39
+ pairs = harvest(applicable(cursor), node, cursor.position)
40
+ return if pairs.empty?
41
+ found << assemble(node, pairs, cursor).taint!(tainted)
42
+ end
43
+
44
+ def applicable(cursor)
45
+ chosen = cursor.inside_def ? @operators : @operators.select(&:body?)
46
+ cursor.position ? chosen : chosen.reject(&:statement?)
47
+ end
48
+
49
+ def harvest(operators, node, position)
50
+ operators.flat_map { |operator| operator.pairs(node, position: position) }
51
+ end
52
+
53
+ def assemble(node, pairs, cursor)
54
+ point = Kimera::MutationPoint.new(**attrs(node, distinct(pairs), cursor.defname))
55
+ point.unsafe!(Kimera::MutationPoint::CLASS_BODY_REASON) unless cursor.inside_def
56
+ point
57
+ end
58
+
59
+ def distinct(pairs) = pairs.uniq { |(_key, variant)| variant.directive }
60
+
61
+ def attrs(node, pairs, name)
62
+ location = node.location
63
+ { point_id: @numbering.point, file: @file, operator: pairs.map(&:first).uniq.join("+") }
64
+ .merge(node_type: node.type.to_s, location: Kimera::Location.parse(location), original_source: location.slice)
65
+ .merge(method_name: name, mutants: mint(pairs))
66
+ end
67
+
68
+ def mint(pairs)
69
+ pairs.map { |(_key, variant)| Kimera::Mutant.of(@numbering.mutant, variant) }
70
+ end
71
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Kimera::RegistryScan::Tally
4
+ def initialize
5
+ @value = 0
6
+ end
7
+
8
+ def next
9
+ @value += 1
10
+ end
11
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kimera::RegistryWalking
4
+ PATTERN_HOLDERS = [
5
+ Kimera::SyntaxTypes::InNode,
6
+ Kimera::SyntaxTypes::MatchRequiredNode,
7
+ Kimera::SyntaxTypes::MatchPredicateNode
8
+ ].freeze
9
+
10
+ Cursor = Data.define(:position, :inside_def, :in_pattern, :def_body, :defname)
11
+ Shape =
12
+ Data.define(:statements, :tail, :matchnode, :defbody, :insidedef) do
13
+ def advance(child, cursor)
14
+ cursor.with(
15
+ position: position(child), inside_def: insidedef,
16
+ in_pattern: cursor.in_pattern || child.equal?(matchnode), def_body: child.equal?(defbody)
17
+ )
18
+ end
19
+
20
+ def position(child)
21
+ return :tail if child.equal?(tail)
22
+ statements&.include?(child) || false
23
+ end
24
+ end
25
+
26
+ private
27
+
28
+ def walk(node, cursor, &)
29
+ return unless node.is_a?(Kimera::SyntaxTypes::Node)
30
+ cursor = cursor.with(defname: node.name.to_s) if node.is_a?(Kimera::SyntaxTypes::DefNode)
31
+ yield(node, cursor) unless cursor.in_pattern
32
+ shape = shape(node, cursor)
33
+ node.compact_child_nodes.each { |child| walk(child, shape.advance(child, cursor), &) }
34
+ end
35
+
36
+ def shape(node, cursor)
37
+ statements = node.is_a?(Kimera::SyntaxTypes::StatementsNode) ? node.body : nil
38
+ Shape.new(
39
+ statements: statements, tail: cursor.def_body ? statements&.last : nil, matchnode: pattern(node),
40
+ defbody: body(node), insidedef: cursor.inside_def || reexecuted?(node)
41
+ )
42
+ end
43
+
44
+ def pattern(node)
45
+ PATTERN_HOLDERS.any? { |k| node.is_a?(k) } ? node.pattern : nil
46
+ end
47
+
48
+ def body(node)
49
+ node.is_a?(Kimera::SyntaxTypes::DefNode) ? node.body : nil
50
+ end
51
+
52
+ def reexecuted?(node)
53
+ node.is_a?(Kimera::SyntaxTypes::DefNode) || node.is_a?(Kimera::SyntaxTypes::LambdaNode)
54
+ end
55
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kimera
4
+ module Report
5
+ end
6
+ end
7
+
8
+ class Kimera::Report::Actions
9
+ def initialize(io: $stdout)
10
+ @io = io
11
+ end
12
+
13
+ def show(report, path: nil)
14
+ lines = [survivors(report.survived, path), uncovered(report.uncovered, path), unjudged(report.errors)].compact
15
+ @io.puts("", "Next actions:", *lines) unless lines.empty?
16
+ end
17
+
18
+ private
19
+
20
+ def survivors(found, path)
21
+ first = found.min_by(&:mutant_id)
22
+ first && advice(found, "surviving mutant(s): inspect ##{first.mutant_id} with `#{inspection(first, path)}`")
23
+ end
24
+
25
+ def inspection(first, path)
26
+ path ? "kimera mutant #{first.mutant_id} --report #{path}" : "kimera run --report tmp/kimera.json"
27
+ end
28
+
29
+ def uncovered(found, path) = advice(found, "uncovered mutant(s): #{listing(path)}")
30
+
31
+ def listing(path) = path ? "kimera report #{path} --status no_coverage" : "rerun with --report tmp/kimera.json"
32
+
33
+ def unjudged(found) = advice(found, "unjudged mutant(s): retry with `kimera run --isolated`")
34
+
35
+ def advice(found, text)
36
+ " #{found.size} #{text}" unless found.empty?
37
+ end
38
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "screen"
4
+
5
+ module Kimera
6
+ module Report
7
+ end
8
+ end
9
+
10
+ module Kimera::Report::Coloring
11
+ private
12
+
13
+ def bold(text) = colorize(text, "1")
14
+ def red(text) = colorize(text, "31")
15
+ def green(text) = colorize(text, "32")
16
+ def yellow(text) = colorize(text, "33")
17
+
18
+ def colorize(text, code)
19
+ color? ? "\e[#{code}m#{text}\e[0m" : text
20
+ end
21
+
22
+ def color? = Kimera::Report::Screen::MODES.key?(@color) ? @color : screen.colored?
23
+
24
+ def screen = @_screen ||= Kimera::Report::Screen.for(@io, @color)
25
+ end
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module Kimera
6
+ module Report
7
+ end
8
+ end
9
+
10
+ class Kimera::Report::Formats
11
+ NAMES = %w[text json ndjson github sarif].freeze
12
+ REPORTED = %w[survived no_coverage harness_error].freeze
13
+ SUMMARY = %w[schema_version counts mutation_score run].freeze
14
+ RECORDS = { "result" => "results", "leak" => "leaks" }.freeze
15
+
16
+ def initialize(io: $stdout)
17
+ @io = io
18
+ end
19
+
20
+ def emit(name, report, metadata: nil) = render(name, report.document(metadata))
21
+
22
+ def render(name, document) = __send__(normalize(name), document)
23
+
24
+ def normalize(name)
25
+ format = String(name)
26
+ return format if NAMES.include?(format)
27
+ raise(Kimera::UsageError, "unknown report format #{format.inspect} (choose: #{NAMES.join(", ")})")
28
+ end
29
+
30
+ private
31
+
32
+ def json(document) = write(JSON.pretty_generate(document))
33
+
34
+ def ndjson(document)
35
+ write(JSON.generate(type: "summary", **document.slice(*SUMMARY)))
36
+ RECORDS.each { |type, key| document.fetch(key).each { |row| write(JSON.generate(type: type, **row)) } }
37
+ end
38
+
39
+ def github(document)
40
+ findings(document).each { |result| write("::#{level(result)} #{fields(result)}::#{escaped(message(result))}") }
41
+ end
42
+
43
+ def sarif(document)
44
+ write(JSON.pretty_generate(envelope(findings(document).map { |result| finding(result) })))
45
+ end
46
+
47
+ def findings(document) = document.fetch("results").select { |result| REPORTED.include?(result.fetch("status")) }
48
+
49
+ def level(result) = result["status"] == "survived" ? "error" : "warning"
50
+
51
+ def fields(result) = "file=#{property(result.fetch("file"))},line=#{result["line"] || 1}"
52
+
53
+ def envelope(results)
54
+ { "$schema" => "https://json.schemastore.org/sarif-2.1.0.json", "version" => "2.1.0" }
55
+ .merge("runs" => [{ "tool" => { "driver" => { "name" => "Kimera" } }, "results" => results }])
56
+ end
57
+
58
+ def finding(result)
59
+ { "ruleId" => "kimera/#{result["operator"] || result["status"]}", "level" => level(result) }
60
+ .merge("message" => { "text" => message(result) }, "locations" => [location(result)])
61
+ end
62
+
63
+ def location(result)
64
+ region = { "startLine" => result["line"] || 1 }
65
+ { "physicalLocation" => { "artifactLocation" => { "uri" => result["file"] }, "region" => region } }
66
+ end
67
+
68
+ def message(result)
69
+ location = [result["file"], result["line"]].compact.join(":")
70
+ "#{result["status"]} mutant ##{result["mutant_id"]} at #{location}: #{result["label"] || result["detail"]}".strip
71
+ end
72
+
73
+ def property(value) = value.to_s.gsub(/[%\r\n,:]/) { |char| "%#{char.ord.to_s(16).upcase.rjust(2, "0")}" }
74
+
75
+ def escaped(value) = value.to_s.gsub("%", "%25").gsub("\r", "%0D").gsub("\n", "%0A")
76
+
77
+ def write(text) = @io.puts(text)
78
+ end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "log"
4
+ require "io/console"
5
+
6
+ class Kimera::Report::Live < Kimera::Report::Log
7
+ REDRAW_EVERY = 0.1
8
+ ERASE = "\r\e[K"
9
+ CURSOR_UP = "\e[1A"
10
+
11
+ class << self
12
+ def for(io) = new(io, columns: width(io))
13
+
14
+ private
15
+
16
+ def width(io)
17
+ io.winsize.fetch(1)
18
+ rescue NoMethodError, SystemCallError
19
+ nil
20
+ end
21
+ end
22
+
23
+ def initialize(io, columns: nil)
24
+ super(io)
25
+ @columns = columns
26
+ @rows = 1
27
+ end
28
+
29
+ def cadence = REDRAW_EVERY
30
+
31
+ def commit
32
+ return unless @drawn
33
+ write("\n")
34
+ @drawn = false
35
+ @rows = 1
36
+ end
37
+
38
+ private
39
+
40
+ def frame(text)
41
+ prefix = erasure
42
+ @rows = rows_for(text)
43
+ "#{prefix}#{text}"
44
+ end
45
+
46
+ def wipe = write(erasure)
47
+
48
+ def erasure
49
+ ERASE + ((CURSOR_UP + ERASE) * (@rows - 1))
50
+ end
51
+
52
+ def rows_for(text)
53
+ return 1 unless @columns&.positive?
54
+ [(text.length + @columns - 1) / @columns, 1].max
55
+ end
56
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kimera
4
+ module Report
5
+ end
6
+ end
7
+
8
+ class Kimera::Report::Log
9
+ HEARTBEAT_EVERY = 10.0
10
+ NEVER = -Float::INFINITY
11
+
12
+ def initialize(io)
13
+ @io = io
14
+ @last = NEVER
15
+ @drawn = false
16
+ end
17
+
18
+ def cadence = HEARTBEAT_EVERY
19
+
20
+ def due?(now, cadence) = now - @last >= cadence
21
+
22
+ def paint(text, now)
23
+ write(frame(text))
24
+ @drawn = true
25
+ @last = now
26
+ end
27
+
28
+ def clear
29
+ return unless @drawn
30
+ wipe
31
+ @drawn = false
32
+ end
33
+
34
+ def commit = clear
35
+
36
+ private
37
+
38
+ def frame(text) = "#{text}\n"
39
+
40
+ def wipe = @io.flush
41
+
42
+ def write(text)
43
+ @io.print(text)
44
+ @io.flush
45
+ end
46
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "screen"
4
+ require_relative "tally"
5
+
6
+ module Kimera
7
+ module Report
8
+ end
9
+ end
10
+
11
+ class Kimera::Report::Progress
12
+ MONOTONIC = -> { Process.clock_gettime(Process::CLOCK_MONOTONIC) }
13
+
14
+ def initialize(io: $stderr, enabled: nil, interval: nil, clock: MONOTONIC)
15
+ @io = io
16
+ @enabled = enabled
17
+ @redraw = interval
18
+ @clock = clock
19
+ end
20
+
21
+ def enabled? = screen.enabled?
22
+
23
+ def start(total, label = "mutants")
24
+ tally.begin!(Integer(total), label, monotonic)
25
+ emit if visible?
26
+ end
27
+
28
+ def tick(status = nil)
29
+ tally.count!(status)
30
+ emit if visible? && (tally.complete? || due?)
31
+ end
32
+
33
+ def finish
34
+ return surface.clear unless tally.complete?
35
+ surface.commit
36
+ end
37
+
38
+ private
39
+
40
+ def screen = @_screen ||= Kimera::Report::Screen.for(@io, @enabled)
41
+
42
+ def surface = @_surface ||= screen.surface
43
+
44
+ def tally = @_tally ||= Kimera::Report::Tally.new
45
+
46
+ def visible? = enabled? && tally.running?
47
+
48
+ def due? = surface.due?(monotonic, cadence)
49
+
50
+ def cadence = @redraw || surface.cadence
51
+
52
+ def emit = surface.paint(tally.line(monotonic), monotonic)
53
+
54
+ def monotonic = @clock.call
55
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "live"
4
+ require_relative "log"
5
+
6
+ module Kimera
7
+ module Report
8
+ end
9
+ end
10
+
11
+ class Kimera::Report::Screen
12
+ MODES = { true => :on, false => :off }.freeze
13
+
14
+ class << self
15
+ def for(io, preference) = public_send(MODES.fetch(preference, :auto), io)
16
+
17
+ def on(io) = new(io, enabled: true, interactive: tty?(io))
18
+
19
+ def off(io) = new(io, enabled: false, interactive: tty?(io))
20
+
21
+ def auto(io)
22
+ tty = tty?(io)
23
+ new(io, enabled: tty, interactive: tty)
24
+ end
25
+
26
+ private
27
+
28
+ def tty?(io) = io.tty?
29
+ end
30
+
31
+ def initialize(io, enabled:, interactive:)
32
+ @io = io
33
+ @enabled = enabled
34
+ @interactive = interactive
35
+ end
36
+
37
+ def enabled? = @enabled
38
+
39
+ def colored? = @enabled && !ENV.key?("NO_COLOR")
40
+
41
+ def surface
42
+ @interactive ? Kimera::Report::Live.for(@io) : Kimera::Report::Log.new(@io)
43
+ end
44
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kimera
4
+ module Report
5
+ end
6
+ end
7
+
8
+ module Kimera::Report::Sections
9
+ private
10
+
11
+ def unjudged(report) = section("Mutants Kimera could not judge", report.errors) { |r| unjudge(r) }
12
+
13
+ def unjudge(result)
14
+ id = result.mutant_id
15
+ @io.puts(" #{red("unjudged")} ##{id} #{where(@registry.index[id], result)} #{result.detail}")
16
+ end
17
+
18
+ def where(point, result)
19
+ point ? "#{point.file}:#{point.location.start_line}" : result.file.to_s
20
+ end
21
+
22
+ def hint(report, path)
23
+ count = report.uncovered.size
24
+ return if count.zero?
25
+ @io.puts
26
+ @io.puts("#{count} mutant(s) have no covering test; #{command(path)}")
27
+ end
28
+
29
+ def command(path)
30
+ return "list them with: kimera report #{path} --status no_coverage" if path
31
+ "re-run with --report FILE, then `kimera report FILE --status no_coverage` to list them"
32
+ end
33
+
34
+ def missing(report, _path = nil) = section("Mutants with no covering test", report.uncovered) { |r| uncovered(r) }
35
+
36
+ def uncovered(result)
37
+ resolved(result) { |id, mutant, point| spotted("no coverage", id, mutant, point) }
38
+ end
39
+
40
+ def spotted(status, id, mutant, point)
41
+ @io.puts(" #{yellow(status)} ##{id} #{point.file}:#{point.location.start_line} [#{mutant.label}]")
42
+ end
43
+
44
+ def leaks(leaks)
45
+ return if leaks.empty?
46
+ @io.puts
47
+ @io.puts(yellow("State-leak warnings (#{leaks.size}):"))
48
+ leaks.each { |leak| @io.puts(" - #{leak.detail}") }
49
+ end
50
+ end
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../support/duration"
4
+
5
+ module Kimera
6
+ module Report
7
+ end
8
+ end
9
+
10
+ class Kimera::Report::Tally
11
+ BAR_WIDTH = 24
12
+ ALWAYS_SHOWN = %i[killed survived].freeze
13
+
14
+ def initialize
15
+ @total = 0
16
+ @counts = {}
17
+ @label = nil
18
+ @started = 0.0
19
+ end
20
+
21
+ def begin!(total, label, now)
22
+ @total = total
23
+ @label = label
24
+ @started = now
25
+ @counts = {}
26
+ end
27
+
28
+ def count!(status)
29
+ @counts[status] = @counts.fetch(status, 0) + 1
30
+ end
31
+
32
+ def done = @counts.values.sum
33
+
34
+ def complete? = done >= @total
35
+
36
+ def running? = @total.positive?
37
+
38
+ def line(now)
39
+ ["#{@label} #{bar}", "#{done}/#{@total}", "#{percent}%", *pace(now), elapsed(now)].join(" ")
40
+ end
41
+
42
+ private
43
+
44
+ def statuses = @counts.keys.compact
45
+
46
+ def pace(now)
47
+ parts = statuses.empty? ? [] : [breakdown]
48
+ parts << rate(now) if done.positive?
49
+ parts << eta(now) unless complete? || done.zero?
50
+ parts
51
+ end
52
+
53
+ def bar
54
+ filled = (done * BAR_WIDTH) / @total
55
+ head = filled.between?(1, BAR_WIDTH - 1) ? ">" : ""
56
+ "[#{"=" * [filled - head.size, 0].max}#{head}#{" " * (BAR_WIDTH - filled)}]"
57
+ end
58
+
59
+ def percent = (done * 100) / @total
60
+
61
+ def breakdown
62
+ (ALWAYS_SHOWN + statuses).uniq.filter_map { |status| counted(status) }.join(" ")
63
+ end
64
+
65
+ def counted(status)
66
+ "#{status}=#{@counts.fetch(status, 0)}"
67
+ end
68
+
69
+ def elapsed(now) = clock(Integer(now - @started))
70
+
71
+ def rate(now)
72
+ seconds = now - @started
73
+ return "rate=—" if seconds <= 0
74
+ format("%.1f/s", done / seconds)
75
+ end
76
+
77
+ def eta(now)
78
+ seconds = now - @started
79
+ return "ETA —" if seconds <= 0
80
+ "ETA #{clock(Integer((@total - done) / (done / seconds)))}"
81
+ end
82
+
83
+ def clock(seconds) = Kimera::Duration.new(seconds).clock
84
+ end