Class: Necropsy::RuntimeFeedback

Inherits:
Object
  • Object
show all
Defined in:
lib/necropsy/runtime_feedback.rb

Overview

Compares positive runtime target observations with the static target set.

Runtime observations may discover a target that the static resolver did not enumerate. That is a safety signal and a useful regression fixture, but an unobserved static target is never removed from the model.

Constant Summary collapse

SCHEMA_VERSION =
1
DEFAULT_FIXTURE_LIMIT =
32

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(static_targets:, observed_targets:, max_fixtures: DEFAULT_FIXTURE_LIMIT) ⇒ RuntimeFeedback

Returns a new instance of RuntimeFeedback.



16
17
18
19
20
21
22
23
24
25
# File 'lib/necropsy/runtime_feedback.rb', line 16

def initialize(static_targets:, observed_targets:, max_fixtures: DEFAULT_FIXTURE_LIMIT)
  @static_targets = normalize_static_targets(static_targets)
  @observed_targets = normalize_observed_targets(observed_targets)
  @max_fixtures = Integer(max_fixtures)
  raise ArgumentError, 'max_fixtures must be non-negative' if @max_fixtures.negative?
rescue ArgumentError, TypeError => e
  raise e unless e.message.start_with?('invalid value for Integer()', 'no implicit conversion of')

  raise ArgumentError, 'max_fixtures must be a non-negative integer'
end

Class Method Details

.from_graph(graph, observed_targets:, max_fixtures: DEFAULT_FIXTURE_LIMIT) ⇒ Object



78
79
80
81
82
83
# File 'lib/necropsy/runtime_feedback.rb', line 78

def self.from_graph(graph, observed_targets:, max_fixtures: DEFAULT_FIXTURE_LIMIT)
  static_targets = graph.resolution_records.each_with_object(Hash.new { |hash, key| hash[key] = [] }) do |record, result|
    result[record.resolution.call_site_id].concat(record.resolution.target_definition_ids)
  end
  new(static_targets: static_targets, observed_targets: observed_targets, max_fixtures: max_fixtures)
end

Instance Method Details

#callObject



27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
# File 'lib/necropsy/runtime_feedback.rb', line 27

def call
  observed_by_site = @observed_targets.group_by { |target| target.fetch('call_site_id') }
  static_by_site = @static_targets
  missing_static = @observed_targets.filter_map do |target|
    static = static_by_site.fetch(target.fetch('call_site_id'), [])
    next if static.include?(target.fetch('target_definition_id'))

    target.merge(
      'classification' => 'missing_static_target',
      'safety' => 'safety_bug'
    )
  end
  fixtures = missing_static.first(@max_fixtures).map { |target| fixture_candidate(target) }
  unobserved_static = static_by_site.flat_map do |call_site_id, targets|
    observed = observed_by_site.fetch(call_site_id, []).map { |target| target.fetch('target_definition_id') }
    targets.reject { |target| observed.include?(target) }.map do |target|
      { 'call_site_id' => call_site_id, 'target_definition_id' => target, 'informational' => true }
    end
  end

  {
    'schema_version' => SCHEMA_VERSION,
    'policy' => 'positive_only',
    'comparison' => {
      'call_sites' => (static_by_site.keys | observed_by_site.keys).length,
      'static_target_count' => static_by_site.values.sum(&:length),
      'observed_target_count' => @observed_targets.length
    },
    'missing_static_targets' => missing_static,
    'unexpected_targets' => missing_static,
    'unobserved_static_targets' => unobserved_static,
    'fixture_candidates' => fixtures,
    'fixture_exported' => false
  }
end

#write_fixture_candidates(path) ⇒ Object



63
64
65
66
67
68
69
70
71
72
73
74
75
76
# File 'lib/necropsy/runtime_feedback.rb', line 63

def write_fixture_candidates(path)
  payload = call
  candidates = payload.fetch('fixture_candidates')
  destination = File.expand_path(path)
  FileUtils.mkdir_p(File.dirname(destination))
  temporary = "#{destination}.tmp-#{Process.pid}-#{Thread.current.object_id}"
  File.write(temporary, "#{JSON.pretty_generate(candidates)}\n")
  File.rename(temporary, destination)
  payload.merge('fixture_exported' => true, 'fixture_path' => destination)
rescue SystemCallError => e
  raise Error, "Could not export runtime feedback fixtures: #{e.message}"
ensure
  FileUtils.rm_f(temporary) if temporary && File.exist?(temporary)
end