Class: Ibex::Coverage::Report

Inherits:
Object
  • Object
show all
Defined in:
lib/ibex/coverage/report.rb,
sig/ibex/coverage/report.rbs

Overview

Versioned, mergeable runtime coverage result.

Constant Summary collapse

IDENTIFIER =

Signature:

  • String

Returns:

  • (String)
"runtime-coverage"
SCHEMA_VERSION =

Signature:

  • Integer

Returns:

  • (Integer)
1
MAX_DOCUMENT_BYTES =

Signature:

  • Integer

Returns:

  • (Integer)
16_777_216
MAX_TOTAL =

Signature:

  • Integer

Returns:

  • (Integer)
1_000_000
MAX_COUNT =

Signature:

  • Integer

Returns:

  • (Integer)
9_223_372_036_854_775_807
ROOT_KEYS =

Returns:

  • (Array[String])
%w[events grammar_digest ibex_coverage production_hits schema_version sessions state_hits
table_format_version totals].freeze
TOTAL_KEYS =

Signature:

  • Array[String]

Returns:

  • (Array[String])
%w[productions states].freeze
HIT_KEYS =

Signature:

  • Array[String]

Returns:

  • (Array[String])
%w[count id].freeze

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(grammar_digest:, table_format_version:, state_count:, production_count:, sessions:, event_count:, state_hits:, production_hits:) ⇒ Report

rubocop:disable Layout/LineLength

RBS:

  • (grammar_digest: String, table_format_version: Integer, state_count: Integer, production_count: Integer, sessions: Integer, event_count: Integer, state_hits: Hash[Integer, Integer], production_hits: Hash[Integer, Integer]) -> void

Parameters:

  • grammar_digest: (String)
  • table_format_version: (Integer)
  • state_count: (Integer)
  • production_count: (Integer)
  • sessions: (Integer)
  • event_count: (Integer)
  • state_hits: (Hash[Integer, Integer])
  • production_hits: (Hash[Integer, Integer])


32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
# File 'lib/ibex/coverage/report.rb', line 32

def initialize(grammar_digest:, table_format_version:, state_count:, production_count:, sessions:, event_count:,
               state_hits:, production_hits:)
  @grammar_digest = validate_digest(grammar_digest)
  @table_format_version = positive_integer(table_format_version, "table_format_version")
  @state_count = total_integer(state_count, "state total", minimum: 1)
  @production_count = total_integer(production_count, "production total", minimum: 0)
  @sessions = positive_integer(sessions, "sessions")
  @event_count = positive_integer(event_count, "events")
  raise ArgumentError, "events must be at least sessions" if @event_count < @sessions

  @state_hits = validate_hits(state_hits, @state_count, "state").freeze
  @production_hits = validate_hits(production_hits, @production_count, "production").freeze
  validate_event_bounds
  freeze
end

Instance Attribute Details

#event_countInteger (readonly)

Signature:

  • Integer

Returns:

  • (Integer)


26
27
28
# File 'lib/ibex/coverage/report.rb', line 26

def event_count
  @event_count
end

#grammar_digestString (readonly)

Signature:

  • Array[String]

Returns:

  • (String)


21
22
23
# File 'lib/ibex/coverage/report.rb', line 21

def grammar_digest
  @grammar_digest
end

#production_countInteger (readonly)

Signature:

  • Integer

Returns:

  • (Integer)


24
25
26
# File 'lib/ibex/coverage/report.rb', line 24

def production_count
  @production_count
end

#production_hitsHash[Integer, Integer] (readonly)

Signature:

  • Hash[Integer, Integer]

Returns:

  • (Hash[Integer, Integer])


28
29
30
# File 'lib/ibex/coverage/report.rb', line 28

def production_hits
  @production_hits
end

#sessionsInteger (readonly)

Signature:

  • Integer

Returns:

  • (Integer)


25
26
27
# File 'lib/ibex/coverage/report.rb', line 25

def sessions
  @sessions
end

#state_countInteger (readonly)

Signature:

  • Integer

Returns:

  • (Integer)


23
24
25
# File 'lib/ibex/coverage/report.rb', line 23

def state_count
  @state_count
end

#state_hitsHash[Integer, Integer] (readonly)

Signature:

  • Hash[Integer, Integer]

Returns:

  • (Hash[Integer, Integer])


27
28
29
# File 'lib/ibex/coverage/report.rb', line 27

def state_hits
  @state_hits
end

#table_format_versionInteger (readonly)

Signature:

  • Integer

Returns:

  • (Integer)


22
23
24
# File 'lib/ibex/coverage/report.rb', line 22

def table_format_version
  @table_format_version
end

Class Method Details

.from_h(value, source:) ⇒ Report

RBS:

  • (untyped value, source: String) -> Report

Parameters:

  • value (Object)
  • source: (String)

Returns:



91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
# File 'lib/ibex/coverage/report.rb', line 91

def self.from_h(value, source:)
  document = string_hash(value, source, "coverage document")
  invalid(source, "coverage object has unknown or missing fields") unless document.keys.sort == ROOT_KEYS
  valid_schema = document["ibex_coverage"] == IDENTIFIER &&
                 document["schema_version"] == SCHEMA_VERSION
  invalid(source, "unsupported coverage schema") unless valid_schema
  totals = string_hash(document["totals"], source, "coverage totals")
  invalid(source, "coverage totals have unknown or missing fields") unless totals.keys.sort == TOTAL_KEYS

  state_count = totals["states"]
  production_count = totals["productions"]
  new(
    grammar_digest: document["grammar_digest"],
    table_format_version: document["table_format_version"],
    state_count: state_count,
    production_count: production_count,
    sessions: document["sessions"],
    event_count: document["events"],
    state_hits: parse_hits(document["state_hits"], state_count, source, "state"),
    production_hits: parse_hits(document["production_hits"], production_count, source, "production")
  )
rescue ArgumentError => e
  raise Ibex::Error, "#{source}:1:1: #{e.message}"
end

.load_file(path) ⇒ Report

RBS:

  • (String path) -> Report

Parameters:

  • path (String)

Returns:



70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
# File 'lib/ibex/coverage/report.rb', line 70

def self.load_file(path)
  size = File.size(path)
  if size > MAX_DOCUMENT_BYTES
    raise Ibex::Error, "#{path}:1:1: coverage document exceeds #{MAX_DOCUMENT_BYTES} bytes"
  end

  source = File.binread(path)
  if source.bytesize > MAX_DOCUMENT_BYTES
    raise Ibex::Error, "#{path}:1:1: coverage document exceeds #{MAX_DOCUMENT_BYTES} bytes"
  end

  source.force_encoding(Encoding::UTF_8)
  raise Ibex::Error, "#{path}:1:1: coverage document is not valid UTF-8" unless source.valid_encoding?

  value = JSON.parse(source, max_nesting: 16, allow_nan: false)
  from_h(value, source: path)
rescue JSON::ParserError => e
  raise Ibex::Error, "#{path}:1:1: invalid coverage JSON: #{e.message}"
end

.merge(reports) ⇒ Report

RBS:

  • (Array[Report] reports) -> Report

Parameters:

Returns:



117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
# File 'lib/ibex/coverage/report.rb', line 117

def self.merge(reports)
  raise ArgumentError, "at least one coverage report is required" if reports.empty?

  first = reports.first
  reports.drop(1).each { |report| ensure_compatible(first, report) }
  new(
    grammar_digest: first.grammar_digest,
    table_format_version: first.table_format_version,
    state_count: first.state_count,
    production_count: first.production_count,
    sessions: checked_sum(reports.map(&:sessions), "sessions"),
    event_count: checked_sum(reports.map(&:event_count), "events"),
    state_hits: merge_hits(reports.map(&:state_hits), "state"),
    production_hits: merge_hits(reports.map(&:production_hits), "production")
  )
end

Instance Method Details

#hit_documents(hits) ⇒ Array[Hash[String, Integer]]

RBS:

  • (Hash[Integer, Integer] hits) -> Array[Hash[String, Integer]]

Parameters:

  • hits (Hash[Integer, Integer])

Returns:

  • (Array[Hash[String, Integer]])


170
171
172
# File 'lib/ibex/coverage/report.rb', line 170

def hit_documents(hits)
  hits.map { |id, count| { "id" => id, "count" => count } }
end

#positive_integer(input, name) ⇒ Integer

RBS:

  • (untyped input, String name) -> Integer

Parameters:

  • input (Object)
  • name (String)

Returns:

  • (Integer)


145
146
147
148
149
# File 'lib/ibex/coverage/report.rb', line 145

def positive_integer(input, name)
  return input if input.is_a?(Integer) && input.positive? && input <= MAX_COUNT

  raise ArgumentError, "#{name} must be a bounded positive integer"
end

#to_hHash[String, untyped]

RBS:

  • () -> Hash[String, untyped]

Returns:

  • (Hash[String, untyped])


50
51
52
53
54
55
56
57
58
59
60
61
62
# File 'lib/ibex/coverage/report.rb', line 50

def to_h
  {
    "ibex_coverage" => IDENTIFIER,
    "schema_version" => SCHEMA_VERSION,
    "grammar_digest" => @grammar_digest,
    "table_format_version" => @table_format_version,
    "totals" => { "states" => @state_count, "productions" => @production_count },
    "sessions" => @sessions,
    "events" => @event_count,
    "state_hits" => hit_documents(@state_hits),
    "production_hits" => hit_documents(@production_hits)
  }
end

#to_jsonString

RBS:

  • (*untyped) -> String

Parameters:

  • (Object)

Returns:

  • (String)


65
66
67
# File 'lib/ibex/coverage/report.rb', line 65

def to_json(*)
  "#{JSON.pretty_generate(to_h)}\n"
end

#total_integer(input, name, minimum:) ⇒ Integer

RBS:

  • (untyped input, String name, minimum: Integer) -> Integer

Parameters:

  • input (Object)
  • name (String)
  • minimum: (Integer)

Returns:

  • (Integer)


152
153
154
155
156
# File 'lib/ibex/coverage/report.rb', line 152

def total_integer(input, name, minimum:)
  return input if input.is_a?(Integer) && input >= minimum && input <= MAX_TOTAL

  raise ArgumentError, "#{name} must be between #{minimum} and #{MAX_TOTAL}"
end

#validate_digest(input) ⇒ String

RBS:

  • (untyped input) -> String

Parameters:

  • input (Object)

Returns:

  • (String)


137
138
139
140
141
142
# File 'lib/ibex/coverage/report.rb', line 137

def validate_digest(input)
  valid = input.is_a?(String) && input.match?(/\Asha256:[0-9a-f]{64}\z/)
  return input.dup.freeze if valid

  raise ArgumentError, "grammar_digest must be a full lowercase SHA-256 digest"
end

#validate_event_boundsvoid

This method returns an undefined value.

RBS:

  • () -> void



175
176
177
178
179
180
# File 'lib/ibex/coverage/report.rb', line 175

def validate_event_bounds
  raise ArgumentError, "state hit counts exceed events" if @state_hits.values.sum > @event_count
  return unless @production_hits.values.sum > @event_count

  raise ArgumentError, "production hit counts exceed events"
end

#validate_hits(input, total, kind) ⇒ Hash[Integer, Integer]

RBS:

  • (Hash[Integer, Integer] input, Integer total, String kind) -> Hash[Integer, Integer]

Parameters:

  • input (Hash[Integer, Integer])
  • total (Integer)
  • kind (String)

Returns:

  • (Hash[Integer, Integer])


159
160
161
162
163
164
165
166
167
# File 'lib/ibex/coverage/report.rb', line 159

def validate_hits(input, total, kind)
  input.to_h do |id, count|
    unless id.is_a?(Integer) && id >= 0 && id < total
      raise ArgumentError, "#{kind} hit id #{id.inspect} is outside 0...#{total}"
    end

    [id, positive_integer(count, "#{kind} hit count")]
  end.sort.to_h
end