Class: Ibex::Coverage::Report

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

Overview

Versioned, mergeable runtime coverage result.

Constant Summary collapse

IDENTIFIER =

@rbs! type json_value = String | Integer | Float | bool | nil | Array | Hash[String, json_value]

"runtime-coverage"
SCHEMA_VERSION =

: String

1
MAX_DOCUMENT_BYTES =

: Integer

16_777_216
MAX_TOTAL =

: Integer

1_000_000
MAX_COUNT =

: Integer

9_223_372_036_854_775_807
ROOT_KEYS =

: Integer

%w[events grammar_digest ibex_coverage production_hits schema_version sessions state_hits
table_format_version totals].freeze
TOTAL_KEYS =

: Array

%w[productions states].freeze
HIT_KEYS =

: Array

%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

Raises:

  • (ArgumentError)


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

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_countObject (readonly)

: Integer



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

def event_count
  @event_count
end

#grammar_digestObject (readonly)

: Array



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

def grammar_digest
  @grammar_digest
end

#production_countObject (readonly)

: Integer



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

def production_count
  @production_count
end

#production_hitsObject (readonly)

: Hash[Integer, Integer]



31
32
33
# File 'lib/ibex/coverage/report.rb', line 31

def production_hits
  @production_hits
end

#sessionsObject (readonly)

: Integer



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

def sessions
  @sessions
end

#state_countObject (readonly)

: Integer



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

def state_count
  @state_count
end

#state_hitsObject (readonly)

: Hash[Integer, Integer]



30
31
32
# File 'lib/ibex/coverage/report.rb', line 30

def state_hits
  @state_hits
end

#table_format_versionObject (readonly)

: Integer



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

def table_format_version
  @table_format_version
end

Class Method Details

.from_h(value, source:) ⇒ Object



94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
# File 'lib/ibex/coverage/report.rb', line 94

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

  grammar_digest = document["grammar_digest"] #: String
  table_format_version = document["table_format_version"] #: Integer
  state_count = totals["states"] #: Integer
  production_count = totals["productions"] #: Integer
  sessions = document["sessions"] #: Integer
  event_count = document["events"] #: Integer
  new(
    grammar_digest: grammar_digest,
    table_format_version: table_format_version,
    state_count: state_count,
    production_count: production_count,
    sessions: sessions,
    event_count: event_count,
    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) ⇒ Object



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

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) ⇒ Object

Raises:

  • (ArgumentError)


124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
# File 'lib/ibex/coverage/report.rb', line 124

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

#to_hObject



53
54
55
56
57
58
59
60
61
62
63
64
65
# File 'lib/ibex/coverage/report.rb', line 53

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_jsonObject



68
69
70
# File 'lib/ibex/coverage/report.rb', line 68

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