Class: SimpleCov::ContextMap

Inherits:
Object
  • Object
show all
Defined in:
lib/simplecov/context_map.rb,
lib/simplecov/context_map/union.rb

Overview

Records which context covered which line: a list of context ids and, per source file, a bitmap of covered lines for each context that touched the file. A context is any labeled region of execution, and the vocabulary is deliberately the general one (coverage.py calls the same idea dynamic contexts), so the stored format doesn't bake simplecov's current use into a name.

The naive shape of this data, a list of ids on every line, is O(contexts x lines) strings. Context ids are interned instead, and each context's covered lines within a file are a single Integer bitmap with bit N set when line N+1 was executed, which packs a thousand-line file into ~125 bytes and makes the union of two recordings a bitwise OR.

Serialized into .resultset.json as {"version" => 1, "contexts" => [...ids...], "files" => {path => {context index => bitmap as hex}}}. The format is tolerated, not trusted: .from_hash returns nil for anything malformed or future-versioned, which the merge treats the same as a map that was never recorded.

Defined Under Namespace

Classes: Union

Constant Summary collapse

VERSION =

Bumped on any change an older reader could misread, so .from_hash can treat a future format as absent instead of answering from it wrongly.

1

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initializeContextMap

Returns a new instance of ContextMap.



29
30
31
32
33
# File 'lib/simplecov/context_map.rb', line 29

def initialize
  @contexts = [] #: Array[String]
  @indices = {} #: Hash[String, Integer]
  @files = {} #: Hash[String, Hash[Integer, Integer]]
end

Class Method Details

.from_hash(data) ⇒ Object

nil when the data is not a well-formed map of this format version. All-or-nothing on purpose: a partially salvaged map would answer covering queries with silent gaps, and the merge already drops an absent map consistently.



119
120
121
122
123
124
125
126
127
128
129
# File 'lib/simplecov/context_map.rb', line 119

def from_hash(data)
  # `eql?` rather than `==`: a version written as 1.0 is not this format,
  # and reading it as if it were is what the version gate exists to stop.
  return nil unless data.instance_of?(Hash) && data["version"].eql?(VERSION)

  contexts = data["contexts"]
  files = data["files"]
  return nil unless contexts.instance_of?(Array) && contexts.all?(String) && files.instance_of?(Hash)

  build(contexts, files)
end

Instance Method Details

#absorb(other) ⇒ Object

Other's contexts are re-interned, so maps recorded by different processes merge by id, and a context both sides saw contributes one entry.



74
75
76
77
78
79
80
81
82
83
84
# File 'lib/simplecov/context_map.rb', line 74

def absorb(other)
  remap = other.interned_contexts.map { |context_id| intern(context_id) }
  other.file_tables.each do |path, table|
    target = (@files[path] ||= {})
    table.each do |index, bitmap|
      key = remap.fetch(index)
      target[key] = (target[key] || 0) | bitmap
    end
  end
  self
end

#contextsObject



64
65
66
# File 'lib/simplecov/context_map.rb', line 64

def contexts
  @contexts.dup
end

#covering(path, line) ⇒ Object

Sorted for stable output, since recording order varies between runs and merges. The path is resolved against SimpleCov.root, so callers can pass either an absolute or a project-relative one.

A line number below 1 needs no guard of its own: it shifts the probe bit right off the end of the bitmap, leaving a mask no line can match.



56
57
58
59
60
61
62
# File 'lib/simplecov/context_map.rb', line 56

def covering(path, line)
  table = @files[File.expand_path(path, SimpleCov.root)]
  return [] unless table

  bit = 1 << (line - 1)
  table.filter_map { |index, bitmap| @contexts.fetch(index) if bitmap.anybits?(bit) }.sort
end

#empty?Boolean

Returns:

  • (Boolean)


68
69
70
# File 'lib/simplecov/context_map.rb', line 68

def empty?
  @contexts.empty?
end

#intern(context_id) ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.



107
108
109
110
111
112
# File 'lib/simplecov/context_map.rb', line 107

def intern(context_id)
  @indices[context_id] ||= begin
    @contexts << context_id
    @contexts.size - 1
  end
end

#record(context_id, lines_by_file) ⇒ Object

lines_by_file maps a source path to the bitmap of lines the context executed there. The id is interned even when the delta is empty: the context ran, and keeping it distinguishes "covered nothing of its own" from "never recorded".



39
40
41
42
43
44
45
46
47
48
# File 'lib/simplecov/context_map.rb', line 39

def record(context_id, lines_by_file)
  index = intern(context_id)
  lines_by_file.each do |path, bitmap|
    next if bitmap.zero?

    table = (@files[path] ||= {})
    table[index] = (table[index] || 0) | bitmap
  end
  self
end

#serialized_bitmaps_for(path) ⇒ Object

Index string => hex bitmap, the wire encoding to_h writes. coverage.json shares the resultset's encoding through this, so the format has one owner.



102
103
104
# File 'lib/simplecov/context_map.rb', line 102

def serialized_bitmaps_for(path)
  serialize_table(@files[File.expand_path(path, SimpleCov.root)] || {})
end

#to_h(only: nil) ⇒ Object

only: restricts the files to a given set of paths, so the map follows the same universe as the coverage it sits beside. The context list is never restricted: which contexts ran is true regardless of which files survived filtering.



90
91
92
93
94
95
96
97
# File 'lib/simplecov/context_map.rb', line 90

def to_h(only: nil)
  # Selected rather than sliced: `only` is a Set, and splatting one into
  # `slice` is a call `to_a` answers for, which makes dropping the `to_a` a
  # mutation nothing can observe.
  tables = only ? @files.select { |path, _table| only.include?(path) } : @files # rubocop:disable Style/HashSlice
  serialized = tables.transform_values { |table| serialize_table(table) }
  {"version" => VERSION, "contexts" => @contexts.dup, "files" => serialized}
end