kimera 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.kimera.yml.example +59 -0
- data/CHANGELOG.md +36 -0
- data/LICENSE.txt +21 -0
- data/README.md +528 -0
- data/exe/kimera +22 -0
- data/lib/kimera/audit/operator_audit.rb +71 -0
- data/lib/kimera/cli/argv.rb +15 -0
- data/lib/kimera/cli/baseline.rb +87 -0
- data/lib/kimera/cli/changed.rb +27 -0
- data/lib/kimera/cli/ci.rb +27 -0
- data/lib/kimera/cli/completion.rb +37 -0
- data/lib/kimera/cli/doctor.rb +115 -0
- data/lib/kimera/cli/flag.rb +55 -0
- data/lib/kimera/cli/help_text.rb +45 -0
- data/lib/kimera/cli/init.rb +105 -0
- data/lib/kimera/cli/mutant.rb +82 -0
- data/lib/kimera/cli/report_file.rb +20 -0
- data/lib/kimera/cli/run/arguments.rb +58 -0
- data/lib/kimera/cli/run/cycle.rb +78 -0
- data/lib/kimera/cli/run/digest.rb +49 -0
- data/lib/kimera/cli/run/emission.rb +45 -0
- data/lib/kimera/cli/run/gate.rb +43 -0
- data/lib/kimera/cli/run/options.rb +85 -0
- data/lib/kimera/cli/run/pass.rb +100 -0
- data/lib/kimera/cli/run/sources.rb +56 -0
- data/lib/kimera/cli/run.rb +52 -0
- data/lib/kimera/cli/skill.rb +23 -0
- data/lib/kimera/cli/suggestion.rb +36 -0
- data/lib/kimera/cli/survivors/panel.rb +81 -0
- data/lib/kimera/cli/survivors.rb +89 -0
- data/lib/kimera/cli/synthesize.rb +66 -0
- data/lib/kimera/cli/workflows.rb +8 -0
- data/lib/kimera/cli.rb +132 -0
- data/lib/kimera/error.rb +3 -0
- data/lib/kimera/execution/baseline_failure.rb +47 -0
- data/lib/kimera/execution/baseline_pass.rb +123 -0
- data/lib/kimera/execution/boot.rb +50 -0
- data/lib/kimera/execution/callback.rb +33 -0
- data/lib/kimera/execution/child_process.rb +48 -0
- data/lib/kimera/execution/composite_isolation.rb +24 -0
- data/lib/kimera/execution/harness.rb +150 -0
- data/lib/kimera/execution/isolated.rb +64 -0
- data/lib/kimera/execution/isolated_child.rb +15 -0
- data/lib/kimera/execution/isolated_child_command.rb +44 -0
- data/lib/kimera/execution/isolated_child_minitest.rb +34 -0
- data/lib/kimera/execution/isolated_plan.rb +54 -0
- data/lib/kimera/execution/isolated_scheduling.rb +54 -0
- data/lib/kimera/execution/isolated_test_selection.rb +22 -0
- data/lib/kimera/execution/isolated_verdict.rb +72 -0
- data/lib/kimera/execution/isolated_watchdog.rb +59 -0
- data/lib/kimera/execution/isolation.rb +17 -0
- data/lib/kimera/execution/null_progress.rb +17 -0
- data/lib/kimera/execution/overlay_guard_modules.rb +95 -0
- data/lib/kimera/execution/overlay_guards.rb +56 -0
- data/lib/kimera/execution/parallel_test_databases.rb +64 -0
- data/lib/kimera/execution/pool_driver.rb +101 -0
- data/lib/kimera/execution/priority.rb +23 -0
- data/lib/kimera/execution/reload.rb +104 -0
- data/lib/kimera/execution/rig.rb +41 -0
- data/lib/kimera/execution/schedule.rb +105 -0
- data/lib/kimera/execution/schemata.rb +88 -0
- data/lib/kimera/execution/shift/attempt.rb +34 -0
- data/lib/kimera/execution/shift/coverage_channel.rb +43 -0
- data/lib/kimera/execution/shift/killer_memory.rb +20 -0
- data/lib/kimera/execution/shift/leak_guard.rb +34 -0
- data/lib/kimera/execution/shift.rb +137 -0
- data/lib/kimera/execution/sweep.rb +30 -0
- data/lib/kimera/execution/transaction_isolation.rb +32 -0
- data/lib/kimera/execution/trial.rb +16 -0
- data/lib/kimera/execution/verdicts.rb +78 -0
- data/lib/kimera/execution/worker_pool/fleet.rb +104 -0
- data/lib/kimera/execution/worker_pool/stillborn_guard.rb +40 -0
- data/lib/kimera/execution/worker_pool/worker.rb +32 -0
- data/lib/kimera/execution/worker_pool.rb +101 -0
- data/lib/kimera/frameworks/adapter.rb +82 -0
- data/lib/kimera/frameworks/minitest_adapter.rb +102 -0
- data/lib/kimera/frameworks/rspec_adapter.rb +123 -0
- data/lib/kimera/frameworks/rspec_group_index.rb +45 -0
- data/lib/kimera/incremental/git_diff.rb +98 -0
- data/lib/kimera/incremental/selection.rb +27 -0
- data/lib/kimera/incremental/session.rb +109 -0
- data/lib/kimera/memoization.rb +104 -0
- data/lib/kimera/operators/argument_drop.rb +19 -0
- data/lib/kimera/operators/arithmetic.rb +12 -0
- data/lib/kimera/operators/audit.rb +3 -0
- data/lib/kimera/operators/base.rb +40 -0
- data/lib/kimera/operators/binary_swap.rb +23 -0
- data/lib/kimera/operators/boolean.rb +26 -0
- data/lib/kimera/operators/chain_link_deletion.rb +25 -0
- data/lib/kimera/operators/collection_literal.rb +18 -0
- data/lib/kimera/operators/comparison.rb +13 -0
- data/lib/kimera/operators/conditional.rb +36 -0
- data/lib/kimera/operators/default_argument.rb +17 -0
- data/lib/kimera/operators/element_drop.rb +31 -0
- data/lib/kimera/operators/index_fetch.rb +15 -0
- data/lib/kimera/operators/kernel_coercion.rb +17 -0
- data/lib/kimera/operators/method_unwrap.rb +20 -0
- data/lib/kimera/operators/negation.rb +15 -0
- data/lib/kimera/operators/node_swap.rb +20 -0
- data/lib/kimera/operators/numeric_literal.rb +39 -0
- data/lib/kimera/operators/op_assign.rb +18 -0
- data/lib/kimera/operators/plugins.rb +27 -0
- data/lib/kimera/operators/rails_association.rb +24 -0
- data/lib/kimera/operators/rails_callback.rb +49 -0
- data/lib/kimera/operators/rails_declaration.rb +16 -0
- data/lib/kimera/operators/rails_permit.rb +34 -0
- data/lib/kimera/operators/rails_validation.rb +17 -0
- data/lib/kimera/operators/range.rb +15 -0
- data/lib/kimera/operators/regexp_literal.rb +16 -0
- data/lib/kimera/operators/respond_to_guard.rb +24 -0
- data/lib/kimera/operators/return_value.rb +36 -0
- data/lib/kimera/operators/safe_navigation.rb +15 -0
- data/lib/kimera/operators/selector_swap.rb +24 -0
- data/lib/kimera/operators/statement_deletion.rb +26 -0
- data/lib/kimera/operators/string_literal.rb +22 -0
- data/lib/kimera/operators/symbol_literal.rb +26 -0
- data/lib/kimera/operators/vocabulary.rb +82 -0
- data/lib/kimera/operators.rb +141 -0
- data/lib/kimera/plugins.rb +3 -0
- data/lib/kimera/registry/builder.rb +59 -0
- data/lib/kimera/registry/mutation_point.rb +146 -0
- data/lib/kimera/registry/numbering.rb +15 -0
- data/lib/kimera/registry/registry.rb +120 -0
- data/lib/kimera/registry/source_file.rb +71 -0
- data/lib/kimera/registry/tally.rb +11 -0
- data/lib/kimera/registry/walking.rb +55 -0
- data/lib/kimera/report/actions.rb +38 -0
- data/lib/kimera/report/coloring.rb +25 -0
- data/lib/kimera/report/formats.rb +78 -0
- data/lib/kimera/report/live.rb +56 -0
- data/lib/kimera/report/log.rb +46 -0
- data/lib/kimera/report/progress.rb +55 -0
- data/lib/kimera/report/screen.rb +44 -0
- data/lib/kimera/report/sections.rb +50 -0
- data/lib/kimera/report/tally.rb +84 -0
- data/lib/kimera/report/text.rb +104 -0
- data/lib/kimera/results/result.rb +72 -0
- data/lib/kimera/results/run_report.rb +114 -0
- data/lib/kimera/rewrite/ast_walk.rb +20 -0
- data/lib/kimera/rewrite/directive.rb +61 -0
- data/lib/kimera/rewrite/directive_extra_handlers.rb +52 -0
- data/lib/kimera/rewrite/directive_handlers.rb +138 -0
- data/lib/kimera/rewrite/directive_kwarg_handlers.rb +44 -0
- data/lib/kimera/rewrite/numbered_params.rb +50 -0
- data/lib/kimera/runtime.rb +51 -0
- data/lib/kimera/scope/config.rb +90 -0
- data/lib/kimera/scope/file_set.rb +51 -0
- data/lib/kimera/scope/ignore_list.rb +55 -0
- data/lib/kimera/self_protection.rb +56 -0
- data/lib/kimera/support/duration.rb +13 -0
- data/lib/kimera/support/errors.rb +8 -0
- data/lib/kimera/support/operator_protocol.rb +10 -0
- data/lib/kimera/support/syntax.rb +68 -0
- data/lib/kimera/support/syntax_types.rb +20 -0
- data/lib/kimera/support/unparse.rb +15 -0
- data/lib/kimera/support/version.rb +5 -0
- data/lib/kimera/support/warnings.rb +15 -0
- data/lib/kimera/synthesis/file_weave.rb +78 -0
- data/lib/kimera/synthesis/guard_weaver.rb +77 -0
- data/lib/kimera/synthesis/guardrail_dispatch.rb +68 -0
- data/lib/kimera/synthesis/guardrail_interpolation.rb +59 -0
- data/lib/kimera/synthesis/guardrail_value_objects.rb +46 -0
- data/lib/kimera/synthesis/overlay.rb +52 -0
- data/lib/kimera/synthesis/overlay_splice.rb +101 -0
- data/lib/kimera/synthesis/project.rb +58 -0
- data/lib/kimera/synthesis/source_map.rb +42 -0
- data/lib/kimera/version.rb +3 -0
- data/lib/kimera.rb +13 -0
- data/skills/kimera/SKILL.md +118 -0
- metadata +259 -0
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "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,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
|