Class: Kensho::RSpec::Formatter

Inherits:
RSpec::Core::Formatters::BaseFormatter
  • Object
show all
Defined in:
lib/kensho/rspec/formatter.rb

Constant Summary collapse

SEVERITY_KEYS =
Kensho::Schema::SEVERITY.map(&:to_sym).freeze
META_BLOCKLIST =

Metadata keys that aren’t user tags. We prune these from the tag list so RSpec’s housekeeping (location, type, file_path, etc.) doesn’t pollute case.tags.

%i[
  absolute_file_path block default_path described_class description
  described_class_name described_class_name described_at description_args
  execution_result file_path file_path_proc full_description
  kensho_feature kensho_epic kensho_story
  last_run_status line_number location parent_example_group
  rerun_file_path scoped_id severity shared_group_inclusions
  shared_group_metadata shared_group_inclusion_backtrace
  skip stack_frames type variants
].to_set

Instance Method Summary collapse

Constructor Details

#initialize(output = nil) ⇒ Formatter

Returns a new instance of Formatter.



65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
# File 'lib/kensho/rspec/formatter.rb', line 65

def initialize(output = nil)
  # RSpec's BaseFormatter wants an output IO; we never write to it,
  # but we still hand one up so RSpec doesn't blow up.
  super(output || StringIO.new)
  @output_dir = File.expand_path(ENV['KENSHO_OUTPUT'] || (defined?(Kensho::RSpec::DEFAULT_OUTPUT) ? Kensho::RSpec::DEFAULT_OUTPUT : 'kensho-results'))
  @cases_dir = File.join(@output_dir, 'cases')
  @attachments_dir = File.join(@output_dir, 'attachments')
  @project = {
    'name' => ENV['KENSHO_PROJECT_NAME'] || 'Unknown project',
    'slug' => ENV['KENSHO_PROJECT_SLUG'] || Kensho::Schema.slugify(ENV['KENSHO_PROJECT_NAME'] || 'unknown')
  }
  @run_id = ENV['KENSHO_RUN_ID'] || Kensho::Schema.default_run_id
  @severity_from_meta = ENV['KENSHO_NO_SEVERITY_FROM_META'].to_s.empty?
  @started_at = Kensho::Schema.iso_now
  @started_perf = monotonic_now
  @rootpath = Dir.pwd

  @cases_by_id = {}
  @ids_seen = Hash.new(0)
  @groups = {}

  FileUtils.mkdir_p(@cases_dir)
  FileUtils.mkdir_p(@attachments_dir)

  Kensho::RSpec::State.formatter = self
  install_capture_hook!
end

Instance Method Details

#example_failed(notification) ⇒ Object



161
162
163
164
165
166
167
168
169
170
171
172
# File 'lib/kensho/rspec/formatter.rb', line 161

def example_failed(notification)
  ex = notification.example.execution_result.exception
  status =
    if ex && (ex.class.name == 'RSpec::Expectations::ExpectationNotMetError')
      'fail'
    elsif ex
      'broken'
    else
      'fail'
    end
  finalize(notification.example, status, ex)
end

#example_group_started(notification) ⇒ Object



136
137
138
139
# File 'lib/kensho/rspec/formatter.rb', line 136

def example_group_started(notification)
  group = notification.group
  @groups[group.[:scoped_id] || group.object_id] = collect_group_meta(group)
end

#example_passed(notification) ⇒ Object



157
158
159
# File 'lib/kensho/rspec/formatter.rb', line 157

def example_passed(notification)
  finalize(notification.example, 'pass', nil)
end

#example_pending(notification) ⇒ Object



174
175
176
# File 'lib/kensho/rspec/formatter.rb', line 174

def example_pending(notification)
  finalize(notification.example, 'skip', nil)
end

#example_started(notification) ⇒ Object



141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
# File 'lib/kensho/rspec/formatter.rb', line 141

def example_started(notification)
  example = notification.example
  case_obj = build_case(example)
  scratch = CaseScratch.new(
    case_id: case_obj['id'],
    example_id: example.id,
    started_at_ms: (Time.now.to_f * 1000.0)
  )
  scratch.instance_variable_set(:@case_obj, case_obj)
  scratch.instance_variable_set(:@stdout_capture, StringIO.new)
  scratch.instance_variable_set(:@stderr_capture, StringIO.new)
  State.current = scratch

  @cases_by_id[case_obj['id']] = case_obj
end

#install_capture_hook!Object

Tee stdout/stderr around each example into the per-case scratch so case.logs[] picks up ‘puts` calls without the user having to do anything. RSpec doesn’t expose a “captured output” API for arbitrary formatters, so we install a global before/after hook.



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
122
123
124
125
126
127
128
129
130
# File 'lib/kensho/rspec/formatter.rb', line 97

def install_capture_hook!
  return if @capture_installed

  @capture_installed = true
  ::RSpec.configure do |config|
    config.before(:each) do
      scratch = Kensho::RSpec::State.current
      next unless scratch

      stdout_io = StringIO.new
      stderr_io = StringIO.new
      scratch.instance_variable_set(:@orig_stdout, $stdout)
      scratch.instance_variable_set(:@orig_stderr, $stderr)
      scratch.instance_variable_set(:@stdout_io, stdout_io)
      scratch.instance_variable_set(:@stderr_io, stderr_io)
      $stdout = stdout_io
      $stderr = stderr_io
    end

    config.after(:each) do
      scratch = Kensho::RSpec::State.current
      next unless scratch

      stdout_io = scratch.instance_variable_get(:@stdout_io)
      stderr_io = scratch.instance_variable_get(:@stderr_io)
      orig_out = scratch.instance_variable_get(:@orig_stdout)
      orig_err = scratch.instance_variable_get(:@orig_stderr)
      $stdout = orig_out if orig_out
      $stderr = orig_err if orig_err
      scratch.instance_variable_set(:@stdout_text, stdout_io.string) if stdout_io
      scratch.instance_variable_set(:@stderr_text, stderr_io.string) if stderr_io
    end
  end
end

#register_attachment(scratch, src_path, kind:, name:, mime_type:) ⇒ Object

—– internals —– #



186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
# File 'lib/kensho/rspec/formatter.rb', line 186

def register_attachment(scratch, src_path, kind:, name:, mime_type:)
  return nil unless File.file?(src_path)

  attachments_root = File.join(@attachments_dir, scratch.case_id)
  FileUtils.mkdir_p(attachments_root)
  att_id = "att_#{SecureRandom.hex(4)}"
  dest_name = name || File.basename(src_path)
  dest = File.join(attachments_root, "#{att_id}_#{dest_name}")
  begin
    FileUtils.cp(src_path, dest)
  rescue StandardError => e
    warn "[kensho] failed to copy #{src_path}: #{e.message}"
    return nil
  end

  guessed_kind, guessed_mime = Kensho::Schema.kind_and_mime_for(src_path)
  rel = relpath(dest, @output_dir)
  record = {
    'id'           => att_id,
    'kind'         => kind || guessed_kind,
    'relativePath' => rel,
    'mimeType'     => mime_type || guessed_mime
  }
  begin
    record['sizeBytes'] = File.size(dest)
  rescue StandardError
    # best-effort
  end
  record
end

#start(_notification) ⇒ Object

—– lifecycle hooks —– #



134
# File 'lib/kensho/rspec/formatter.rb', line 134

def start(_notification); end

#stop(_notification) ⇒ Object



178
179
180
181
182
# File 'lib/kensho/rspec/formatter.rb', line 178

def stop(_notification)
  write_run_json
ensure
  State.formatter = nil
end