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, untyped]
- #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
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_count ⇒ Integer (readonly)
26 27 28 |
# File 'lib/ibex/coverage/report.rb', line 26 def event_count @event_count end |
#grammar_digest ⇒ String (readonly)
21 22 23 |
# File 'lib/ibex/coverage/report.rb', line 21 def grammar_digest @grammar_digest end |
#production_count ⇒ Integer (readonly)
24 25 26 |
# File 'lib/ibex/coverage/report.rb', line 24 def production_count @production_count end |
#production_hits ⇒ Hash[Integer, Integer] (readonly)
28 29 30 |
# File 'lib/ibex/coverage/report.rb', line 28 def production_hits @production_hits end |
#sessions ⇒ Integer (readonly)
25 26 27 |
# File 'lib/ibex/coverage/report.rb', line 25 def sessions @sessions end |
#state_count ⇒ Integer (readonly)
23 24 25 |
# File 'lib/ibex/coverage/report.rb', line 23 def state_count @state_count end |
#state_hits ⇒ Hash[Integer, Integer] (readonly)
27 28 29 |
# File 'lib/ibex/coverage/report.rb', line 27 def state_hits @state_hits end |
#table_format_version ⇒ Integer (readonly)
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
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.}" end |
.load_file(path) ⇒ Report
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.}" end |
.merge(reports) ⇒ Report
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]]
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
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_h ⇒ 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_json ⇒ 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
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
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_bounds ⇒ void
This method returns an undefined value.
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]
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 |