Class: RSpec::Signal::HtmlSummary

Inherits:
Object
  • Object
show all
Defined in:
lib/rspec/signal/html_summary.rb

Overview

Recognises bulk HTML inside a failure message and replaces it with the handful of facts that actually diagnose it.

A request spec that expects one sentence and receives a Rails exception page produces a diff several thousand lines long whose opening hundred lines are the exception page's own CSS. Printing the start of that is worse than useless: it is the one part of the response guaranteed to be identical for every failure in the suite. So the expected value is left exactly as it was, the actual value is named as HTML and measured, and the title, headings and leading visible text are pulled out -- which on a Rails error page is the exception class and its message.

Regex only, deliberately. A DOM parser would be a new hard dependency for something that never has to be correct, only useful, and which is handed broken markup by definition.

Constant Summary collapse

MARKER =
"[HTML document]"
MIN_TAGS =
5
SCAN_WINDOW =
4_000
MAX_FACT_CHARS =
160
MAX_FACTS =
4
DOCUMENT_START =
/\A\s*(?:<!doctype\s+html|<html[\s>])/i
TAG =
%r{</?[a-z][a-z0-9]*(?:\s[^<>]*?)?/?>}im
STRUCTURAL =
/<(?:html|head|body|div|table|section|main|article|p)\b/i
QUOTED =
/"(?:[^"\\]|\\.)*"/m
DIFF_LINE =
/\A(\s*)([-+])(.*)\z/m
NOISE =
%r{<(script|style)\b[^>]*>.*?</\1>|<!--.*?-->}im
TITLE =
%r{<title[^>]*>(.*?)</title>}im
H1 =
%r{<h1[^>]*>(.*?)</h1>}im
H2 =
%r{<h2[^>]*>(.*?)</h2>}im
PRE =
%r{<pre[^>]*>(.*?)</pre>}im
ESCAPES =
{ "n" => "\n", "t" => "\t", "r" => "\r", "e" => "\e", '"' => '"', "\\" => "\\" }.freeze
ENTITIES =
{ "amp" => "&", "lt" => "<", "gt" => ">", "quot" => '"',
"apos" => "'", "#39" => "'", "nbsp" => " " }.freeze

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(html) ⇒ HtmlSummary

Returns a new instance of HtmlSummary.



151
152
153
154
155
# File 'lib/rspec/signal/html_summary.rb', line 151

def initialize(html)
  @html = html
  @line_count = html.count("\n") + 1
  @byte_size = html.bytesize
end

Instance Attribute Details

#byte_sizeObject (readonly)

Returns the value of attribute byte_size.



149
150
151
# File 'lib/rspec/signal/html_summary.rb', line 149

def byte_size
  @byte_size
end

#line_countObject (readonly)

Returns the value of attribute line_count.



149
150
151
# File 'lib/rspec/signal/html_summary.rb', line 149

def line_count
  @line_count
end

Class Method Details

.html?(text) ⇒ Boolean

Returns:

  • (Boolean)


61
62
63
64
65
66
# File 'lib/rspec/signal/html_summary.rb', line 61

def html?(text)
  return true if DOCUMENT_START.match?(text)

  window = text[0, SCAN_WINDOW].to_s
  STRUCTURAL.match?(window) && window.scan(TAG).size >= MIN_TAGS
end

.reduce(lines, threshold:) ⇒ Array<String>

Returns the same lines with bulk HTML replaced.

Parameters:

  • lines (Array<String>)

    message lines

  • threshold (Integer)

    smallest blob worth summarising

Returns:

  • (Array<String>)

    the same lines with bulk HTML replaced



47
48
49
# File 'lib/rspec/signal/html_summary.rb', line 47

def reduce(lines, threshold:)
  reduce_diff_runs(reduce_inline(lines, threshold), threshold)
end

.summarise(text, threshold) ⇒ HtmlSummary?

Returns:



52
53
54
55
56
57
58
59
# File 'lib/rspec/signal/html_summary.rb', line 52

def summarise(text, threshold)
  return nil if text.length < threshold

  html = unescape(text)
  return nil unless html?(html)

  new(html)
end

Instance Method Details

#factsObject

[["Title", "Action Controller: Exception caught"], ...]



158
159
160
# File 'lib/rspec/signal/html_summary.rb', line 158

def facts
  @facts ||= build_facts
end

#to_hObject



168
169
170
# File 'lib/rspec/signal/html_summary.rb', line 168

def to_h
  { lines: line_count, bytes: byte_size }.merge(facts.to_h { |(label, value)| [label.downcase.to_sym, value] })
end

#to_linesObject



162
163
164
165
166
# File 'lib/rspec/signal/html_summary.rb', line 162

def to_lines
  ["[HTML document: #{number(line_count)} #{plural(line_count, "line")}, #{human_size} " \
   "-- markup omitted]",
   *facts.map { |(label, value)| "  #{label}: #{value}" }]
end