Class: Ibex::Coverage::Report
- Inherits:
-
Object
- Object
- Ibex::Coverage::Report
- Defined in:
- lib/ibex/coverage/report.rb,
sig/ibex/coverage/report.rbs
Overview
Versioned, mergeable runtime coverage result.
Constant Summary collapse
- IDENTIFIER =
"runtime-coverage"- SCHEMA_VERSION =
1- MAX_DOCUMENT_BYTES =
16_777_216- MAX_TOTAL =
1_000_000- MAX_COUNT =
9_223_372_036_854_775_807- ROOT_KEYS =
%w[events grammar_digest ibex_coverage production_hits schema_version sessions state_hits table_format_version totals].freeze
- TOTAL_KEYS =
%w[productions states].freeze
- HIT_KEYS =
%w[count id].freeze
Instance Attribute Summary collapse
- #event_count ⇒ Integer readonly
- #grammar_digest ⇒ String readonly
- #production_count ⇒ Integer readonly
- #production_hits ⇒ Hash[Integer, Integer] readonly
- #sessions ⇒ Integer readonly
- #state_count ⇒ Integer readonly
- #state_hits ⇒ Hash[Integer, Integer] readonly
- #table_format_version ⇒ Integer readonly
Class Method Summary collapse
Instance Method Summary collapse
- #hit_documents(hits) ⇒ Array[Hash[String, Integer]]
-
#initialize(grammar_digest:, table_format_version:, state_count:, production_count:, sessions:, event_count:, state_hits:, production_hits:) ⇒ Report
constructor
rubocop:disable Layout/LineLength.
- #positive_integer(input, name) ⇒ Integer
- #to_h ⇒ Hash[String, json_value]
- #to_json ⇒ String
- #total_integer(input, name, minimum:) ⇒ Integer
- #validate_digest(input) ⇒ String
- #validate_event_bounds ⇒ void
- #validate_hits(input, total, kind) ⇒ Hash[Integer, Integer]
Constructor Details
#initialize(grammar_digest:, table_format_version:, state_count:, production_count:, sessions:, event_count:, state_hits:, production_hits:) ⇒ Report
rubocop:disable Layout/LineLength
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_count ⇒ Integer (readonly)
29 30 31 |
# File 'lib/ibex/coverage/report.rb', line 29 def event_count @event_count end |
#grammar_digest ⇒ String (readonly)
24 25 26 |
# File 'lib/ibex/coverage/report.rb', line 24 def grammar_digest @grammar_digest end |
#production_count ⇒ Integer (readonly)
27 28 29 |
# File 'lib/ibex/coverage/report.rb', line 27 def production_count @production_count end |
#production_hits ⇒ Hash[Integer, Integer] (readonly)
31 32 33 |
# File 'lib/ibex/coverage/report.rb', line 31 def production_hits @production_hits end |
#sessions ⇒ Integer (readonly)
28 29 30 |
# File 'lib/ibex/coverage/report.rb', line 28 def sessions @sessions end |
#state_count ⇒ Integer (readonly)
26 27 28 |
# File 'lib/ibex/coverage/report.rb', line 26 def state_count @state_count end |
#state_hits ⇒ Hash[Integer, Integer] (readonly)
30 31 32 |
# File 'lib/ibex/coverage/report.rb', line 30 def state_hits @state_hits end |
#table_format_version ⇒ Integer (readonly)
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:) ⇒ Report
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.}" end |
.load_file(path) ⇒ Report
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.}" end |
.merge(reports) ⇒ Report
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
#hit_documents(hits) ⇒ Array[Hash[String, Integer]]
176 177 178 |
# File 'lib/ibex/coverage/report.rb', line 176 def hit_documents(hits) hits.map { |id, count| { "id" => id, "count" => count } } end |
#positive_integer(input, name) ⇒ Integer
151 152 153 154 155 |
# File 'lib/ibex/coverage/report.rb', line 151 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_h ⇒ Hash[String, json_value]
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_json ⇒ String
68 69 70 |
# File 'lib/ibex/coverage/report.rb', line 68 def to_json(*) "#{JSON.pretty_generate(to_h)}\n" end |
#total_integer(input, name, minimum:) ⇒ Integer
158 159 160 161 162 |
# File 'lib/ibex/coverage/report.rb', line 158 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
144 145 146 147 148 |
# File 'lib/ibex/coverage/report.rb', line 144 def validate_digest(input) return input.dup.freeze if input.is_a?(String) && input.match?(/\Asha256:[0-9a-f]{64}\z/) raise ArgumentError, "grammar_digest must be a full lowercase SHA-256 digest" end |
#validate_event_bounds ⇒ void
This method returns an undefined value.
181 182 183 184 185 186 |
# File 'lib/ibex/coverage/report.rb', line 181 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]
165 166 167 168 169 170 171 172 173 |
# File 'lib/ibex/coverage/report.rb', line 165 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 |