necropsy 0.1.0 → 0.3.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 +4 -4
- data/.rubocop.yml +33 -0
- data/CHANGELOG.md +44 -0
- data/MEASUREMENTS.md +112 -0
- data/README.md +233 -16
- data/Rakefile +3 -1
- data/bench/README.md +92 -0
- data/bench/audit.rb +113 -0
- data/bench/audits/0.2.1/audit.json +501 -0
- data/bench/audits/0.2.1/audit.md +55 -0
- data/bench/audits/0.2.1/baseline_performance.yml +16 -0
- data/bench/audits/0.2.1/config.yml +42 -0
- data/bench/audits/0.2.1/review.yml +56 -0
- data/bench/audits/0.2.1/review_queue.yml +3651 -0
- data/bench/corpora/v1/README.md +30 -0
- data/bench/corpora/v1/labels.yml +37 -0
- data/bench/corpora/v1/manifest.yml +63 -0
- data/bench/corpora/v1/rubocop.necropsy.yml +7 -0
- data/bench/corpora/v1/self.necropsy.yml +8 -0
- data/bench/corpora/v1/tools/debride.yml +42 -0
- data/bench/corpora/v1/tools/spoom.yml +12 -0
- data/bench/corpora/v1/tools/type_aware.yml +11 -0
- data/bench/golden/v1/candidate_union.json +21918 -0
- data/bench/golden/v1/metadata.json +12 -0
- data/bench/golden/v1/reports/dynamic_evidence.json +146 -0
- data/bench/golden/v1/reports/plain_ruby.json +135 -0
- data/bench/golden/v1/reports/rails.json +176 -0
- data/bench/golden/v1/reports/rubocop_1_75_0.json +26848 -0
- data/bench/golden/v1/reports/self.json +3449 -0
- data/bench/review_queue.rb +35 -0
- data/bench/run.rb +31 -0
- data/bench/schema/candidate-union-v1.schema.json +70 -0
- data/docs/impv_implementation_matrix.md +179 -0
- data/docs/migrations/0.2.1.md +57 -0
- data/docs/migrations/0.3.0.md +207 -0
- data/docs/migrations/0.4.0.md +13 -0
- data/docs/necropsy_performance_adr.md +43 -0
- data/docs/necropsy_scope_decisions.md +58 -0
- data/docs/necropsy_type_facts_adr.md +22 -0
- data/gemfiles/prism_min.gemfile +9 -0
- data/gemfiles/prism_min.gemfile.lock +50 -0
- data/lib/necropsy/analyzer.rb +121 -2
- data/lib/necropsy/analyzers/dynamic/coverage_collector.rb +81 -19
- data/lib/necropsy/analyzers/dynamic/coverage_importer.rb +104 -10
- data/lib/necropsy/analyzers/dynamic/coverband_importer.rb +74 -250
- data/lib/necropsy/analyzers/dynamic/coverband_payload_set.rb +149 -0
- data/lib/necropsy/analyzers/dynamic/observation_policy.rb +96 -0
- data/lib/necropsy/analyzers/dynamic/redis_input_limits.rb +121 -0
- data/lib/necropsy/analyzers/dynamic/redis_nonblocking_io.rb +94 -0
- data/lib/necropsy/analyzers/dynamic/redis_payload_loader.rb +165 -0
- data/lib/necropsy/analyzers/dynamic/redis_transport.rb +217 -0
- data/lib/necropsy/analyzers/dynamic/runtime_reference.rb +96 -0
- data/lib/necropsy/analyzers/dynamic/trace_point_collector.rb +225 -20
- data/lib/necropsy/analyzers/dynamic/trace_point_importer.rb +3 -1
- data/lib/necropsy/analyzers/legacy_result_adapter.rb +226 -0
- data/lib/necropsy/analyzers/static/cha.rb +33 -69
- data/lib/necropsy/analyzers/static/name_resolution.rb +151 -21
- data/lib/necropsy/analyzers/static/rta.rb +249 -25
- data/lib/necropsy/ast_scanner/call_recording.rb +275 -0
- data/lib/necropsy/ast_scanner/call_site_creation.rb +54 -0
- data/lib/necropsy/ast_scanner/definition_creation.rb +43 -0
- data/lib/necropsy/ast_scanner/dsl_macros.rb +532 -0
- data/lib/necropsy/ast_scanner/method_definitions.rb +358 -0
- data/lib/necropsy/ast_scanner/references.rb +115 -0
- data/lib/necropsy/ast_scanner/ruby_semantics.rb +263 -0
- data/lib/necropsy/ast_scanner/traversal.rb +347 -0
- data/lib/necropsy/ast_scanner/value_definitions.rb +78 -0
- data/lib/necropsy/ast_scanner.rb +89 -564
- data/lib/necropsy/bench/candidate_union.rb +555 -0
- data/lib/necropsy/bench/claim_gate.rb +112 -0
- data/lib/necropsy/bench/evaluator.rb +356 -24
- data/lib/necropsy/bench/finding_facts.rb +152 -0
- data/lib/necropsy/bench/precision_gate.rb +144 -0
- data/lib/necropsy/bench/release_audit/adversarial_runner.rb +56 -0
- data/lib/necropsy/bench/release_audit/artifact_writer.rb +112 -0
- data/lib/necropsy/bench/release_audit/config_validator.rb +112 -0
- data/lib/necropsy/bench/release_audit/git_snapshot.rb +36 -0
- data/lib/necropsy/bench/release_audit/performance_gate.rb +165 -0
- data/lib/necropsy/bench/release_audit/run_provenance.rb +133 -0
- data/lib/necropsy/bench/release_audit.rb +360 -0
- data/lib/necropsy/bench/report_normalizer.rb +140 -0
- data/lib/necropsy/bench/review_queue.rb +154 -0
- data/lib/necropsy/bench/safety_mutation_harness.rb +59 -0
- data/lib/necropsy/bench/seed_runner.rb +408 -0
- data/lib/necropsy/bounded_canonicalizer.rb +218 -0
- data/lib/necropsy/cache/scan_cache.rb +94 -18
- data/lib/necropsy/call_site_identity.rb +54 -0
- data/lib/necropsy/cli.rb +296 -53
- data/lib/necropsy/clock.rb +40 -0
- data/lib/necropsy/confidence/scorer.rb +181 -58
- data/lib/necropsy/configuration.rb +339 -18
- data/lib/necropsy/convention_rules.rb +138 -0
- data/lib/necropsy/definition_identity/canonical_digest.rb +278 -0
- data/lib/necropsy/definition_identity.rb +37 -0
- data/lib/necropsy/diagnostics.rb +342 -0
- data/lib/necropsy/embedded_ruby.rb +55 -0
- data/lib/necropsy/entry_points/plain.rb +125 -7
- data/lib/necropsy/entry_points/rails.rb +422 -105
- data/lib/necropsy/entry_points/test.rb +6 -1
- data/lib/necropsy/flow_interpreter.rb +460 -0
- data/lib/necropsy/graph/blocker_matching.rb +338 -0
- data/lib/necropsy/graph/call_graph.rb +1234 -62
- data/lib/necropsy/graph/definition_index.rb +149 -0
- data/lib/necropsy/graph/dynamic_evidence_tracking.rb +206 -0
- data/lib/necropsy/graph/evidence_store.rb +213 -0
- data/lib/necropsy/graph/resolution_store.rb +497 -0
- data/lib/necropsy/graph_self_check.rb +79 -0
- data/lib/necropsy/guardrail/baseline.rb +359 -13
- data/lib/necropsy/guardrail/diff.rb +5 -2
- data/lib/necropsy/guardrail/quarantine.rb +101 -10
- data/lib/necropsy/load_graph.rb +206 -0
- data/lib/necropsy/models.rb +894 -21
- data/lib/necropsy/performance_profiler.rb +108 -0
- data/lib/necropsy/project.rb +361 -13
- data/lib/necropsy/reachability/engine.rb +80 -15
- data/lib/necropsy/reference_barrier.rb +458 -0
- data/lib/necropsy/report.rb +153 -14
- data/lib/necropsy/reporter.rb +441 -19
- data/lib/necropsy/runner.rb +258 -14
- data/lib/necropsy/runtime_feedback.rb +136 -0
- data/lib/necropsy/semantics_matrix.rb +153 -0
- data/lib/necropsy/trace_point_runtime.rb +19 -0
- data/lib/necropsy/type_facts.rb +53 -0
- data/lib/necropsy/version.rb +1 -1
- data/lib/necropsy/why_not_explanation.rb +436 -0
- data/lib/necropsy/why_not_renderer.rb +197 -0
- data/lib/necropsy/world_policy.rb +90 -0
- data/lib/necropsy.rb +39 -2
- data/schema/necropsy-report-v2.schema.json +366 -0
- data/script/measure.rb +20 -0
- metadata +106 -2
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require 'json'
|
|
4
4
|
require 'yaml'
|
|
5
|
+
require_relative 'runtime_reference'
|
|
5
6
|
|
|
6
7
|
module Necropsy
|
|
7
8
|
module Analyzers
|
|
@@ -16,29 +17,64 @@ module Necropsy
|
|
|
16
17
|
return AnalyzerResult.empty unless source
|
|
17
18
|
|
|
18
19
|
payload = load_payload(File.expand_path(source, project.root))
|
|
19
|
-
|
|
20
|
+
observation = ObservationPolicy.metadata(payload, expected_revision: config['expected_source_revision'])
|
|
21
|
+
malformed = []
|
|
22
|
+
alive = node_references(payload).map do |raw_reference|
|
|
23
|
+
node_id = normalized_or_raw(raw_reference, malformed, :node)
|
|
20
24
|
AliveEvidence.new(
|
|
21
25
|
node_id: node_id,
|
|
22
|
-
evidence: evidence(
|
|
23
|
-
|
|
26
|
+
evidence: evidence(
|
|
27
|
+
kind: :alive,
|
|
28
|
+
details: "Coverage marked #{reference_label(node_id)} as executed",
|
|
29
|
+
metadata: observation.merge('node_reference' => node_id),
|
|
30
|
+
grade: :observed,
|
|
31
|
+
relation: :execution,
|
|
32
|
+
source: { 'type' => profile.name.to_s, 'node_reference' => node_id },
|
|
33
|
+
scope: ObservationPolicy.evidence_scope(observation).merge('node_reference' => node_id)
|
|
34
|
+
)
|
|
24
35
|
)
|
|
25
36
|
end
|
|
26
37
|
|
|
27
|
-
edge_evidences =
|
|
28
|
-
|
|
29
|
-
|
|
38
|
+
edge_evidences = edge_references(payload).filter_map do |edge|
|
|
39
|
+
unless edge.is_a?(Hash)
|
|
40
|
+
malformed << malformed_reference(:edge, edge)
|
|
41
|
+
next
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
caller = edge['caller_id'] || edge[:caller_id]
|
|
45
|
+
callee = edge['callee_id'] || edge[:callee_id]
|
|
46
|
+
caller_id = normalized_or_raw(caller, malformed, :edge_caller)
|
|
47
|
+
callee_id = normalized_or_raw(callee, malformed, :edge_callee)
|
|
30
48
|
EdgeEvidence.new(
|
|
31
49
|
caller_id: caller_id,
|
|
32
50
|
callee_id: callee_id,
|
|
33
|
-
evidence: evidence(
|
|
51
|
+
evidence: evidence(
|
|
52
|
+
kind: :call_edge,
|
|
53
|
+
details: "Coverage observed #{reference_label(caller_id)} -> #{reference_label(callee_id)}",
|
|
54
|
+
metadata: { 'caller_reference' => caller_id, 'callee_reference' => callee_id },
|
|
55
|
+
grade: :observed,
|
|
56
|
+
relation: :observed_call,
|
|
57
|
+
source: {
|
|
58
|
+
'type' => profile.name.to_s,
|
|
59
|
+
'caller_reference' => caller_id,
|
|
60
|
+
'callee_reference' => callee_id
|
|
61
|
+
},
|
|
62
|
+
scope: ObservationPolicy.evidence_scope(observation).merge(
|
|
63
|
+
'caller_reference' => caller_id,
|
|
64
|
+
'callee_reference' => callee_id
|
|
65
|
+
)
|
|
66
|
+
)
|
|
34
67
|
)
|
|
35
68
|
end
|
|
69
|
+
observation = observation.merge('malformed_references' => malformed) unless malformed.empty?
|
|
36
70
|
|
|
37
71
|
AnalyzerResult.new(
|
|
38
72
|
edge_evidences: edge_evidences,
|
|
39
73
|
alive_evidences: alive,
|
|
40
74
|
uncertainties: {},
|
|
41
|
-
observation: { 'coverage' =>
|
|
75
|
+
observation: { 'coverage' => observation },
|
|
76
|
+
resolutions: [],
|
|
77
|
+
evidences: result_evidences(edge_evidences, alive)
|
|
42
78
|
)
|
|
43
79
|
end
|
|
44
80
|
|
|
@@ -47,7 +83,9 @@ module Necropsy
|
|
|
47
83
|
name: :coverage,
|
|
48
84
|
kind: :dynamic,
|
|
49
85
|
soundness: :observational,
|
|
50
|
-
description: 'Imports method execution and observed edges from Ruby Coverage output.'
|
|
86
|
+
description: 'Imports method execution and observed edges from Ruby Coverage output.',
|
|
87
|
+
version: Necropsy::VERSION,
|
|
88
|
+
assumptions: ['positive_observations_only']
|
|
51
89
|
)
|
|
52
90
|
end
|
|
53
91
|
|
|
@@ -55,13 +93,69 @@ module Necropsy
|
|
|
55
93
|
|
|
56
94
|
attr_reader :config
|
|
57
95
|
|
|
96
|
+
def node_references(payload)
|
|
97
|
+
structured = payload['node_references'] if payload.key?('node_references')
|
|
98
|
+
RuntimeReference.preferred(structured: structured, legacy: payload['executed'] || payload['nodes'])
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def edge_references(payload)
|
|
102
|
+
structured = payload['edge_references'] if payload.key?('edge_references')
|
|
103
|
+
return Array(payload['edges']) if structured.nil?
|
|
104
|
+
|
|
105
|
+
structured_edges = Array(structured)
|
|
106
|
+
structured_pairs = structured_edges.filter_map { |edge| edge_symbol_pair(edge) }.to_set
|
|
107
|
+
structured_edges + Array(payload['edges']).reject do |edge|
|
|
108
|
+
structured_pairs.include?(edge_symbol_pair(edge))
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def edge_symbol_pair(edge)
|
|
113
|
+
return unless edge.is_a?(Hash)
|
|
114
|
+
|
|
115
|
+
caller = RuntimeReference.normalize(edge['caller_id'] || edge[:caller_id])
|
|
116
|
+
callee = RuntimeReference.normalize(edge['callee_id'] || edge[:callee_id])
|
|
117
|
+
return unless caller && callee
|
|
118
|
+
|
|
119
|
+
[reference_symbol(caller), reference_symbol(callee)]
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def reference_symbol(reference)
|
|
123
|
+
reference.is_a?(Hash) ? reference['symbol_id'] : reference
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def normalized_or_raw(reference, malformed, kind)
|
|
127
|
+
normalized = RuntimeReference.normalize(reference)
|
|
128
|
+
return normalized if normalized
|
|
129
|
+
|
|
130
|
+
malformed << malformed_reference(kind, reference)
|
|
131
|
+
reference
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def malformed_reference(kind, reference)
|
|
135
|
+
{ 'kind' => kind.to_s, 'reference' => reference }
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def reference_label(reference)
|
|
139
|
+
return reference.to_s unless reference.is_a?(Hash)
|
|
140
|
+
|
|
141
|
+
symbol = reference['symbol_id'] || reference[:symbol_id] || '<missing symbol>'
|
|
142
|
+
file = reference['file'] || reference[:file]
|
|
143
|
+
line = reference['line'] || reference[:line]
|
|
144
|
+
location = [file, line].compact.join(':')
|
|
145
|
+
location.empty? ? symbol.to_s : "#{symbol} at #{location}"
|
|
146
|
+
end
|
|
147
|
+
|
|
58
148
|
def load_payload(path)
|
|
149
|
+
raise Error, "Coverage source does not exist: #{path}" unless File.file?(path)
|
|
150
|
+
|
|
59
151
|
case File.extname(path)
|
|
60
152
|
when '.json'
|
|
61
153
|
JSON.parse(File.read(path))
|
|
62
154
|
else
|
|
63
|
-
YAML.
|
|
155
|
+
YAML.safe_load_file(path, aliases: false) || {}
|
|
64
156
|
end
|
|
157
|
+
rescue JSON::ParserError, Psych::Exception => e
|
|
158
|
+
raise Error, "Could not parse coverage source #{path}: #{e.message}"
|
|
65
159
|
end
|
|
66
160
|
end
|
|
67
161
|
end
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require 'json'
|
|
4
|
-
require 'openssl'
|
|
5
|
-
require 'socket'
|
|
6
|
-
require 'uri'
|
|
7
4
|
require 'yaml'
|
|
8
5
|
|
|
6
|
+
require_relative 'redis_payload_loader'
|
|
7
|
+
require_relative 'runtime_reference'
|
|
8
|
+
|
|
9
9
|
module Necropsy
|
|
10
10
|
module Analyzers
|
|
11
11
|
module Dynamic
|
|
@@ -19,11 +19,23 @@ module Necropsy
|
|
|
19
19
|
return AnalyzerResult.empty unless source
|
|
20
20
|
|
|
21
21
|
payload = load_payload(source, project.root)
|
|
22
|
-
|
|
22
|
+
observation = ObservationPolicy.metadata(payload, expected_revision: config['expected_source_revision'])
|
|
23
|
+
node_ids, line_diagnostics = alive_ids_from_payload(graph, payload)
|
|
24
|
+
observation = observation.merge(line_diagnostics) unless line_diagnostics.empty?
|
|
25
|
+
alive = node_ids.map do |node_id|
|
|
26
|
+
node = graph.nodes.lookup(node_id).node
|
|
27
|
+
details = node ? "Coverband executed #{node.file}:#{node.line}" : "Coverband marked #{node_id} as executed"
|
|
23
28
|
AliveEvidence.new(
|
|
24
|
-
node_id:
|
|
25
|
-
evidence: evidence(
|
|
26
|
-
|
|
29
|
+
node_id: node_id,
|
|
30
|
+
evidence: evidence(
|
|
31
|
+
kind: :alive,
|
|
32
|
+
details: details,
|
|
33
|
+
metadata: observation.merge('node_reference' => node_id),
|
|
34
|
+
grade: :observed,
|
|
35
|
+
relation: :execution,
|
|
36
|
+
source: { 'type' => 'coverband', 'node_reference' => node_id },
|
|
37
|
+
scope: ObservationPolicy.evidence_scope(observation).merge('node_reference' => node_id)
|
|
38
|
+
)
|
|
27
39
|
)
|
|
28
40
|
end
|
|
29
41
|
|
|
@@ -31,7 +43,9 @@ module Necropsy
|
|
|
31
43
|
edge_evidences: [],
|
|
32
44
|
alive_evidences: alive,
|
|
33
45
|
uncertainties: {},
|
|
34
|
-
observation: { 'coverband' =>
|
|
46
|
+
observation: { 'coverband' => observation },
|
|
47
|
+
resolutions: [],
|
|
48
|
+
evidences: result_evidences(alive)
|
|
35
49
|
)
|
|
36
50
|
end
|
|
37
51
|
|
|
@@ -40,7 +54,9 @@ module Necropsy
|
|
|
40
54
|
name: :coverband,
|
|
41
55
|
kind: :dynamic,
|
|
42
56
|
soundness: :observational,
|
|
43
|
-
description: 'Imports file and line execution data compatible with coverband-style exports.'
|
|
57
|
+
description: 'Imports file and line execution data compatible with coverband-style exports.',
|
|
58
|
+
version: Necropsy::VERSION,
|
|
59
|
+
assumptions: %w[line_execution_mapping positive_observations_only]
|
|
44
60
|
)
|
|
45
61
|
end
|
|
46
62
|
|
|
@@ -52,29 +68,41 @@ module Necropsy
|
|
|
52
68
|
return load_redis_payload(source) if source.start_with?('redis://', 'rediss://')
|
|
53
69
|
|
|
54
70
|
path = File.expand_path(source, root)
|
|
71
|
+
raise Error, "Coverband source does not exist: #{path}" unless File.file?(path)
|
|
72
|
+
|
|
55
73
|
case File.extname(path)
|
|
56
74
|
when '.json'
|
|
57
75
|
JSON.parse(File.read(path))
|
|
58
76
|
else
|
|
59
|
-
YAML.
|
|
77
|
+
YAML.safe_load_file(path, aliases: true) || {}
|
|
60
78
|
end
|
|
79
|
+
rescue JSON::ParserError, Psych::Exception => e
|
|
80
|
+
raise Error, "Could not parse Coverband source #{path}: #{e.message}"
|
|
61
81
|
end
|
|
62
82
|
|
|
63
83
|
def load_redis_payload(source)
|
|
64
84
|
RedisPayloadLoader.new(source: source, config: config).load
|
|
65
85
|
end
|
|
66
86
|
|
|
67
|
-
def
|
|
68
|
-
|
|
69
|
-
|
|
87
|
+
def alive_ids_from_payload(graph, payload)
|
|
88
|
+
structured = payload['node_references'] if payload.key?('node_references')
|
|
89
|
+
malformed = []
|
|
90
|
+
executed_nodes = RuntimeReference.preferred(
|
|
91
|
+
structured: structured,
|
|
92
|
+
legacy: payload['executed'] || payload['nodes']
|
|
93
|
+
).map do |reference|
|
|
94
|
+
normalized = RuntimeReference.normalize(reference)
|
|
95
|
+
next normalized if normalized
|
|
70
96
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
executed_lines = lines_for(files, node.file)
|
|
74
|
-
executed_lines.any? { |line| line.between?(node.line, node.end_line) }
|
|
97
|
+
malformed << { 'kind' => 'node', 'reference' => reference }
|
|
98
|
+
reference
|
|
75
99
|
end
|
|
76
100
|
|
|
77
|
-
|
|
101
|
+
files = coverage_files(payload)
|
|
102
|
+
by_line, diagnostics = line_references(graph, files)
|
|
103
|
+
diagnostics['malformed_references'] = malformed unless malformed.empty?
|
|
104
|
+
|
|
105
|
+
[(executed_nodes + by_line).uniq, diagnostics]
|
|
78
106
|
end
|
|
79
107
|
|
|
80
108
|
def coverage_files(payload)
|
|
@@ -131,244 +159,40 @@ module Necropsy
|
|
|
131
159
|
left.merge(right) { |_file, left_lines, right_lines| (left_lines + right_lines).uniq }
|
|
132
160
|
end
|
|
133
161
|
|
|
134
|
-
def
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
end
|
|
154
|
-
|
|
155
|
-
def load
|
|
156
|
-
connect
|
|
157
|
-
authenticate
|
|
158
|
-
select_database
|
|
159
|
-
merge_payloads(keys.filter_map { |key| payload_for_key(key) })
|
|
160
|
-
ensure
|
|
161
|
-
socket&.close
|
|
162
|
-
end
|
|
163
|
-
|
|
164
|
-
private
|
|
165
|
-
|
|
166
|
-
attr_reader :uri, :config, :socket
|
|
167
|
-
|
|
168
|
-
def connect
|
|
169
|
-
tcp_socket = TCPSocket.new(uri.host, uri.port || 6379)
|
|
170
|
-
@socket = uri.scheme == 'rediss' ? tls_socket(tcp_socket) : tcp_socket
|
|
171
|
-
end
|
|
172
|
-
|
|
173
|
-
def tls_socket(tcp_socket)
|
|
174
|
-
context = OpenSSL::SSL::SSLContext.new
|
|
175
|
-
socket = OpenSSL::SSL::SSLSocket.new(tcp_socket, context)
|
|
176
|
-
socket.hostname = uri.host if socket.respond_to?(:hostname=)
|
|
177
|
-
socket.connect
|
|
178
|
-
socket
|
|
179
|
-
end
|
|
180
|
-
|
|
181
|
-
def authenticate
|
|
182
|
-
return unless uri.password
|
|
183
|
-
|
|
184
|
-
command('AUTH', uri.password)
|
|
185
|
-
end
|
|
186
|
-
|
|
187
|
-
def select_database
|
|
188
|
-
db = uri.path.to_s.delete_prefix('/')
|
|
189
|
-
command('SELECT', db) unless db.empty?
|
|
190
|
-
end
|
|
191
|
-
|
|
192
|
-
def keys
|
|
193
|
-
configured = Array(config['keys'] || config['key'] || query['key']).compact
|
|
194
|
-
return configured unless configured.empty?
|
|
195
|
-
|
|
196
|
-
scan(pattern: config['pattern'] || query['pattern'] || DEFAULT_PATTERN)
|
|
197
|
-
end
|
|
198
|
-
|
|
199
|
-
def query
|
|
200
|
-
@query ||= URI.decode_www_form(uri.query.to_s).to_h
|
|
201
|
-
end
|
|
202
|
-
|
|
203
|
-
def scan(pattern:)
|
|
204
|
-
cursor = '0'
|
|
205
|
-
found = []
|
|
206
|
-
loop do
|
|
207
|
-
cursor, keys = command('SCAN', cursor, 'MATCH', pattern)
|
|
208
|
-
found.concat(keys)
|
|
209
|
-
break if cursor == '0'
|
|
210
|
-
end
|
|
211
|
-
found
|
|
212
|
-
end
|
|
213
|
-
|
|
214
|
-
def command(*parts)
|
|
215
|
-
socket.write(redis_command(parts))
|
|
216
|
-
read_response
|
|
217
|
-
end
|
|
218
|
-
|
|
219
|
-
def payload_for_key(key)
|
|
220
|
-
parse_payload(command('GET', key))
|
|
221
|
-
rescue Error => e
|
|
222
|
-
raise unless e.message.include?('WRONGTYPE')
|
|
223
|
-
|
|
224
|
-
parse_hash_payload(command('HGETALL', key))
|
|
225
|
-
end
|
|
226
|
-
|
|
227
|
-
def redis_command(parts)
|
|
228
|
-
["*#{parts.length}", *parts.flat_map { |part| ["$#{part.to_s.bytesize}", part.to_s] }].join("\r\n") + "\r\n"
|
|
229
|
-
end
|
|
230
|
-
|
|
231
|
-
def read_response
|
|
232
|
-
prefix = socket.read(1)
|
|
233
|
-
case prefix
|
|
234
|
-
when '+'
|
|
235
|
-
socket.gets("\r\n").delete_suffix("\r\n")
|
|
236
|
-
when '-'
|
|
237
|
-
raise Error, socket.gets("\r\n").delete_suffix("\r\n")
|
|
238
|
-
when ':'
|
|
239
|
-
socket.gets("\r\n").to_i
|
|
240
|
-
when '$'
|
|
241
|
-
read_bulk_string
|
|
242
|
-
when '*'
|
|
243
|
-
Array.new(socket.gets("\r\n").to_i) { read_response }
|
|
244
|
-
else
|
|
245
|
-
raise Error, "Unsupported Redis response prefix #{prefix.inspect}"
|
|
246
|
-
end
|
|
247
|
-
end
|
|
248
|
-
|
|
249
|
-
def read_bulk_string
|
|
250
|
-
length = socket.gets("\r\n").to_i
|
|
251
|
-
return nil if length.negative?
|
|
252
|
-
|
|
253
|
-
value = socket.read(length)
|
|
254
|
-
socket.read(2)
|
|
255
|
-
value
|
|
256
|
-
end
|
|
257
|
-
|
|
258
|
-
def parse_payload(value)
|
|
259
|
-
return nil if value.nil? || value.empty?
|
|
260
|
-
|
|
261
|
-
JSON.parse(value)
|
|
262
|
-
rescue JSON::ParserError
|
|
263
|
-
YAML.load(value)
|
|
264
|
-
rescue Psych::SyntaxError
|
|
265
|
-
Marshal.load(value)
|
|
266
|
-
end
|
|
267
|
-
|
|
268
|
-
def parse_hash_payload(entries)
|
|
269
|
-
hash = Array(entries).each_slice(2).to_h
|
|
270
|
-
return coverband_hash_payload(hash) if coverband_hash_payload?(hash)
|
|
271
|
-
|
|
272
|
-
payloads = []
|
|
273
|
-
files = {}
|
|
274
|
-
hash.each do |field, value|
|
|
275
|
-
parsed = parse_payload(value)
|
|
276
|
-
if payload_shape?(parsed)
|
|
277
|
-
payloads << parsed
|
|
278
|
-
else
|
|
279
|
-
files[field.to_s] = parsed
|
|
280
|
-
end
|
|
281
|
-
end
|
|
282
|
-
|
|
283
|
-
payloads << { 'files' => files } unless files.empty?
|
|
284
|
-
merge_payloads(payloads)
|
|
285
|
-
end
|
|
286
|
-
|
|
287
|
-
def coverband_hash_payload?(hash)
|
|
288
|
-
hash.key?('file') && hash.key?('file_length')
|
|
289
|
-
end
|
|
290
|
-
|
|
291
|
-
def coverband_hash_payload(hash)
|
|
292
|
-
{
|
|
293
|
-
'files' => { hash.fetch('file') => coverband_line_counts(hash) },
|
|
294
|
-
'observation' => coverband_observation(hash)
|
|
295
|
-
}
|
|
296
|
-
end
|
|
297
|
-
|
|
298
|
-
def coverband_line_counts(hash)
|
|
299
|
-
length = hash.fetch('file_length').to_i
|
|
300
|
-
Array.new(length) do |index|
|
|
301
|
-
count = hash[index.to_s]
|
|
302
|
-
count&.to_i
|
|
303
|
-
end
|
|
304
|
-
end
|
|
305
|
-
|
|
306
|
-
def coverband_observation(hash)
|
|
307
|
-
%w[first_updated_at last_updated_at file_hash].each_with_object({}) do |key, observation|
|
|
308
|
-
observation[key] = hash[key] if hash[key]
|
|
309
|
-
end
|
|
310
|
-
end
|
|
311
|
-
|
|
312
|
-
def payload_shape?(value)
|
|
313
|
-
value.is_a?(Hash) && value.keys.any? do |key|
|
|
314
|
-
%w[files coverage runtime production nodes executed observation].include?(key.to_s)
|
|
315
|
-
end
|
|
316
|
-
end
|
|
317
|
-
|
|
318
|
-
def merge_payloads(payloads)
|
|
319
|
-
payloads.compact.reduce({}) { |merged, payload| merge_payload(merged, payload) }
|
|
320
|
-
end
|
|
321
|
-
|
|
322
|
-
def merge_payload(merged, payload)
|
|
323
|
-
merged.merge(payload) do |key, left, right|
|
|
324
|
-
case key
|
|
325
|
-
when 'files', 'coverage', 'runtime', 'production'
|
|
326
|
-
merge_files(left, right)
|
|
327
|
-
when 'nodes', 'executed'
|
|
328
|
-
(Array(left) + Array(right)).uniq
|
|
329
|
-
when 'edges'
|
|
330
|
-
Array(left) + Array(right)
|
|
331
|
-
when 'observation'
|
|
332
|
-
left.merge(right)
|
|
333
|
-
else
|
|
334
|
-
right
|
|
162
|
+
def line_references(graph, files)
|
|
163
|
+
definition_ids = []
|
|
164
|
+
line_ambiguities = []
|
|
165
|
+
path_ambiguities = []
|
|
166
|
+
graph.method_nodes.group_by(&:file).sort.each do |relative_file, definitions|
|
|
167
|
+
paths = matching_coverage_paths(files, relative_file)
|
|
168
|
+
next if paths.empty?
|
|
169
|
+
|
|
170
|
+
path_ambiguities << { 'file' => relative_file, 'coverage_paths' => paths } if paths.length > 1
|
|
171
|
+
paths.flat_map { |path| files.fetch(path) }.uniq.sort.each do |line|
|
|
172
|
+
matches = definitions.select { |definition| line.between?(definition.line, definition.end_line) }
|
|
173
|
+
definition_ids.concat(matches.map(&:graph_id))
|
|
174
|
+
next unless matches.length > 1
|
|
175
|
+
|
|
176
|
+
line_ambiguities << {
|
|
177
|
+
'file' => relative_file,
|
|
178
|
+
'line' => line,
|
|
179
|
+
'definition_ids' => matches.map(&:graph_id).sort
|
|
180
|
+
}
|
|
335
181
|
end
|
|
336
182
|
end
|
|
337
|
-
end
|
|
338
183
|
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
left.merge(right) do |_file, left_lines, right_lines|
|
|
344
|
-
merge_file_coverage(left_lines, right_lines)
|
|
345
|
-
end
|
|
346
|
-
end
|
|
347
|
-
|
|
348
|
-
def merge_file_coverage(left, right)
|
|
349
|
-
return merge_count_hashes(left, right) if left.is_a?(Hash) && right.is_a?(Hash)
|
|
350
|
-
return merge_count_arrays(left, right) if count_array?(left) || count_array?(right)
|
|
351
|
-
|
|
352
|
-
(Array(left) + Array(right)).uniq
|
|
184
|
+
diagnostics = {}
|
|
185
|
+
diagnostics['path_ambiguities'] = path_ambiguities unless path_ambiguities.empty?
|
|
186
|
+
diagnostics['line_ambiguities'] = line_ambiguities unless line_ambiguities.empty?
|
|
187
|
+
[definition_ids.uniq.sort, diagnostics]
|
|
353
188
|
end
|
|
354
189
|
|
|
355
|
-
def
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
def merge_count_arrays(left, right)
|
|
360
|
-
length = [Array(left).length, Array(right).length].max
|
|
361
|
-
Array.new(length) do |index|
|
|
362
|
-
left_value = Array(left)[index]
|
|
363
|
-
right_value = Array(right)[index]
|
|
364
|
-
next if left_value.nil? && right_value.nil?
|
|
365
|
-
|
|
366
|
-
left_value.to_i + right_value.to_i
|
|
367
|
-
end
|
|
368
|
-
end
|
|
190
|
+
def matching_coverage_paths(files, relative_file)
|
|
191
|
+
exact = [relative_file, File.join('.', relative_file)].select { |path| files.key?(path) }
|
|
192
|
+
return exact.sort unless exact.empty?
|
|
369
193
|
|
|
370
|
-
|
|
371
|
-
|
|
194
|
+
suffix = "/#{relative_file}"
|
|
195
|
+
files.keys.select { |path| path.end_with?(suffix) }.sort
|
|
372
196
|
end
|
|
373
197
|
end
|
|
374
198
|
end
|