oselvar-var-core 0.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/lib/oselvar/var/core/ast.rb +49 -0
- data/lib/oselvar/var/core/canonical_json.rb +49 -0
- data/lib/oselvar/var/core/cell_diff.rb +107 -0
- data/lib/oselvar/var/core/conformance.rb +264 -0
- data/lib/oselvar/var/core/deep_freeze.rb +27 -0
- data/lib/oselvar/var/core/diagnostics.rb +50 -0
- data/lib/oselvar/var/core/doc_string_diff.rb +38 -0
- data/lib/oselvar/var/core/drift.rb +187 -0
- data/lib/oselvar/var/core/execute.rb +189 -0
- data/lib/oselvar/var/core/failure_anchor.rb +30 -0
- data/lib/oselvar/var/core/hash.rb +29 -0
- data/lib/oselvar/var/core/matcher.rb +134 -0
- data/lib/oselvar/var/core/param_diff.rb +54 -0
- data/lib/oselvar/var/core/parse.rb +20 -0
- data/lib/oselvar/var/core/plan.rb +290 -0
- data/lib/oselvar/var/core/registry.rb +76 -0
- data/lib/oselvar/var/core/scanner.rb +348 -0
- data/lib/oselvar/var/core/sentences.rb +116 -0
- data/lib/oselvar/var/core/span.rb +86 -0
- data/lib/oselvar/var/core/step_role.rb +25 -0
- data/lib/oselvar/var/core/structurer.rb +86 -0
- data/lib/oselvar/var/core/table_cells.rb +46 -0
- data/lib/oselvar/var/core.rb +34 -0
- metadata +82 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: c98c97c8b2591024987fbb163ba752459045e7fd3b51cb711567909bff8d70bc
|
|
4
|
+
data.tar.gz: 012bb71e5e6974d00b37df2b3ab2c380c4446d9301bd887b7871ae262847b803
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 8b0b30035ea20fb0a36214099c3c18455024dea9fc8c999a072cdbfed6b16cb14313a5aa3b45cc7c87b979fbc28971698f29aa0e211335f64bd0fe5c51341bf5
|
|
7
|
+
data.tar.gz: 7bf02659040e7c302225da45c0385f86e12f591212e373eb70ce4d6aa2ab63d4962a4288252d668b817096836b5e696a56896cdb77e042abe7fb42c51d5f36d5
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'oselvar/var/core/span'
|
|
4
|
+
|
|
5
|
+
module Oselvar
|
|
6
|
+
module Var
|
|
7
|
+
module Core
|
|
8
|
+
# Maps a block-text offset to its source offset. Block text is the raw
|
|
9
|
+
# source minus BLOCK markers only (list bullets, blockquote `>` prefixes);
|
|
10
|
+
# inline markup is never stripped. A paragraph/list item has a single
|
|
11
|
+
# entry; a blockquote one entry per quoted line.
|
|
12
|
+
SegmentOffset = Data.define(:text_offset, :source_offset)
|
|
13
|
+
|
|
14
|
+
Heading = Data.define(:level, :text, :span) do
|
|
15
|
+
def kind = 'heading'
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
Paragraph = Data.define(:text, :span, :segment_map) do
|
|
19
|
+
def kind = 'paragraph'
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
ListItem = Data.define(:text, :span, :segment_map, :ordered, :marker_span) do
|
|
23
|
+
def kind = 'list_item'
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
Blockquote = Data.define(:text, :span, :segment_map) do
|
|
27
|
+
def kind = 'blockquote'
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
Row = Data.define(:cells, :cell_spans, :span)
|
|
31
|
+
|
|
32
|
+
Table = Data.define(:span, :header, :rows) do
|
|
33
|
+
def kind = 'table'
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
Fence = Data.define(:span, :info, :body, :body_span) do
|
|
37
|
+
def kind = 'fence'
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
ThematicBreak = Data.define(:span) do
|
|
41
|
+
def kind = 'thematic_break'
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
Example = Data.define(:scope_stack, :span, :body)
|
|
45
|
+
|
|
46
|
+
VarDoc = Data.define(:path, :source, :examples, :orphan_attachments)
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
|
|
5
|
+
module Oselvar
|
|
6
|
+
module Var
|
|
7
|
+
module Core
|
|
8
|
+
# JSON serializers byte-for-byte compatible with JS `JSON.stringify(v, null, 2)`:
|
|
9
|
+
# 2-space indent, LF, trailing newline, non-ASCII raw, empty containers as
|
|
10
|
+
# {}/[]. `canonical_stringify` recursively sorts object keys (the goldens);
|
|
11
|
+
# `ordered_stringify` preserves insertion order (var.lock.json).
|
|
12
|
+
#
|
|
13
|
+
# The container layout is hand-rolled because Ruby's JSON.pretty_generate
|
|
14
|
+
# renders empty arrays/objects as "[\n\n]". Scalar encoding is delegated to
|
|
15
|
+
# the stdlib, which matches JS (escapes " \ control chars, keeps non-ASCII raw).
|
|
16
|
+
module CanonicalJson
|
|
17
|
+
module_function
|
|
18
|
+
|
|
19
|
+
def canonical_stringify(value)
|
|
20
|
+
"#{encode(value, '', sort_keys: true)}\n"
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def ordered_stringify(value)
|
|
24
|
+
"#{encode(value, '', sort_keys: false)}\n"
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def encode(value, indent, sort_keys:)
|
|
28
|
+
case value
|
|
29
|
+
when Hash
|
|
30
|
+
return '{}' if value.empty?
|
|
31
|
+
|
|
32
|
+
keys = sort_keys ? value.keys.sort : value.keys
|
|
33
|
+
inner = "#{indent} "
|
|
34
|
+
items = keys.map { |key| "#{inner}#{key.to_s.to_json}: #{encode(value[key], inner, sort_keys: sort_keys)}" }
|
|
35
|
+
"{\n#{items.join(",\n")}\n#{indent}}"
|
|
36
|
+
when Array
|
|
37
|
+
return '[]' if value.empty?
|
|
38
|
+
|
|
39
|
+
inner = "#{indent} "
|
|
40
|
+
items = value.map { |element| "#{inner}#{encode(element, inner, sort_keys: sort_keys)}" }
|
|
41
|
+
"[\n#{items.join(",\n")}\n#{indent}]"
|
|
42
|
+
else
|
|
43
|
+
value.to_json
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Oselvar
|
|
4
|
+
module Var
|
|
5
|
+
module Core
|
|
6
|
+
# One checked column of one header-bound row: the cell text and its span.
|
|
7
|
+
RowCheck = Data.define(:column, :value, :span)
|
|
8
|
+
|
|
9
|
+
# The verdict for one checked column after comparing against the table.
|
|
10
|
+
# expected_value/actual_value/formatted are adapter-facing, never serialized.
|
|
11
|
+
CellDiff = Data.define(:column, :span, :expected, :actual, :ok,
|
|
12
|
+
:expected_value, :actual_value, :formatted) do
|
|
13
|
+
def initialize(column:, span:, expected:, actual:, ok:,
|
|
14
|
+
expected_value: nil, actual_value: nil, formatted: false)
|
|
15
|
+
super
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# The step returned the wrong type/shape — an author mistake, not a value diff.
|
|
20
|
+
class ReturnShapeError < StandardError; end
|
|
21
|
+
|
|
22
|
+
# Raised when a header-bound row's / a table's returned columns don't match.
|
|
23
|
+
class CellMismatchError < StandardError
|
|
24
|
+
attr_reader :cells
|
|
25
|
+
|
|
26
|
+
def initialize(cells)
|
|
27
|
+
@cells = cells
|
|
28
|
+
super(cells.map { |c| "#{c.column}: expected #{c.expected} but was #{c.actual}" }.join('; '))
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Pure comparison of row/table step returns against the authored cells.
|
|
33
|
+
# Port of cell-diff.ts.
|
|
34
|
+
module CellDiffs
|
|
35
|
+
module_function
|
|
36
|
+
|
|
37
|
+
# Display rules 2-4 of the mismatch-rendering chain (rule 1, the
|
|
38
|
+
# parameter type's `format`, is applied in param_diff). A string renders
|
|
39
|
+
# as-is, other primitives via to_s, anything else via inspect. The
|
|
40
|
+
# inspect fallback is port-native and deliberately outside conformance.
|
|
41
|
+
def render_cell_value(value)
|
|
42
|
+
return value if value.is_a?(String)
|
|
43
|
+
return value.to_s if value.nil? || value == true || value == false ||
|
|
44
|
+
value.is_a?(Integer) || value.is_a?(Float)
|
|
45
|
+
|
|
46
|
+
value.inspect
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Compare a row step's returned Hash against the row's cells. Only columns
|
|
50
|
+
# present on +returned+ are checked; a non-Hash return checks nothing.
|
|
51
|
+
def compare_row(returned, checks)
|
|
52
|
+
return [] unless returned.is_a?(Hash)
|
|
53
|
+
|
|
54
|
+
checks.filter_map do |check|
|
|
55
|
+
next unless returned.key?(check.column)
|
|
56
|
+
|
|
57
|
+
actual = render_cell_value(returned[check.column])
|
|
58
|
+
CellDiff.new(column: check.column, span: check.span, expected: check.value,
|
|
59
|
+
actual: actual, ok: actual == check.value)
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Compare a whole-table step's returned table against the input table.
|
|
64
|
+
# +returned+: nil (no checks), Array of Arrays (positional), or Array of
|
|
65
|
+
# Hashes (keyed by header). Cells compare as exact strings.
|
|
66
|
+
def compare_table(returned, input_table)
|
|
67
|
+
return [] if returned.nil?
|
|
68
|
+
raise ReturnShapeError, "expected a table (array of rows), got #{returned.class}" unless returned.is_a?(Array)
|
|
69
|
+
|
|
70
|
+
columns = input_table.header.cells
|
|
71
|
+
data_rows = input_table.rows
|
|
72
|
+
if returned.length != data_rows.length
|
|
73
|
+
raise ReturnShapeError, "expected #{data_rows.length} row(s), got #{returned.length}"
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
all_arrays = returned.all?(Array)
|
|
77
|
+
all_records = returned.all?(Hash)
|
|
78
|
+
raise ReturnShapeError, 'table rows must be all arrays or all objects' if !all_arrays && !all_records
|
|
79
|
+
|
|
80
|
+
diffs = []
|
|
81
|
+
data_rows.each_with_index do |row, i|
|
|
82
|
+
ret = returned[i]
|
|
83
|
+
if all_arrays && ret.length != columns.length
|
|
84
|
+
raise ReturnShapeError, "row #{i}: expected #{columns.length} column(s), got #{ret.length}"
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
columns.each_with_index do |column, j|
|
|
88
|
+
if all_arrays
|
|
89
|
+
actual_value = ret[j]
|
|
90
|
+
else
|
|
91
|
+
raise ReturnShapeError, "row #{i}: missing column \"#{column}\"" unless ret.key?(column)
|
|
92
|
+
|
|
93
|
+
actual_value = ret[column]
|
|
94
|
+
end
|
|
95
|
+
expected = j < row.cells.length ? row.cells[j] : ''
|
|
96
|
+
actual = render_cell_value(actual_value)
|
|
97
|
+
span = j < row.cell_spans.length ? row.cell_spans[j] : row.span
|
|
98
|
+
diffs << CellDiff.new(column: column, span: span, expected: expected, actual: actual,
|
|
99
|
+
ok: actual == expected)
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
diffs
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'oselvar/var/core/ast'
|
|
4
|
+
require 'oselvar/var/core/plan'
|
|
5
|
+
require 'oselvar/var/core/execute'
|
|
6
|
+
require 'oselvar/var/core/failure_anchor'
|
|
7
|
+
|
|
8
|
+
module Oselvar
|
|
9
|
+
module Var
|
|
10
|
+
module Core
|
|
11
|
+
# Projections from the internal pipeline values to the camelCase wire
|
|
12
|
+
# dicts compared against golden/*.json. Port of conformance.ts. (var-doc
|
|
13
|
+
# stage; registry/plan/trace projections are added in later stages.)
|
|
14
|
+
module Conformance
|
|
15
|
+
module_function
|
|
16
|
+
|
|
17
|
+
def span_hash(span)
|
|
18
|
+
{
|
|
19
|
+
'startOffset' => span.start_offset,
|
|
20
|
+
'endOffset' => span.end_offset,
|
|
21
|
+
'startLine' => span.start_line,
|
|
22
|
+
'startCol' => span.start_col,
|
|
23
|
+
'endLine' => span.end_line,
|
|
24
|
+
'endCol' => span.end_col
|
|
25
|
+
}
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def segment_hash(segment_offset)
|
|
29
|
+
{
|
|
30
|
+
'textOffset' => segment_offset.text_offset,
|
|
31
|
+
'sourceOffset' => segment_offset.source_offset
|
|
32
|
+
}
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def row_hash(row)
|
|
36
|
+
{
|
|
37
|
+
'cells' => row.cells,
|
|
38
|
+
'cellSpans' => row.cell_spans.map { |cs| span_hash(cs) },
|
|
39
|
+
'span' => span_hash(row.span)
|
|
40
|
+
}
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def block_hash(block)
|
|
44
|
+
case block.kind
|
|
45
|
+
when 'paragraph', 'blockquote'
|
|
46
|
+
{
|
|
47
|
+
'kind' => block.kind,
|
|
48
|
+
'text' => block.text,
|
|
49
|
+
'span' => span_hash(block.span),
|
|
50
|
+
'segmentMap' => block.segment_map.map { |so| segment_hash(so) }
|
|
51
|
+
}
|
|
52
|
+
when 'heading'
|
|
53
|
+
{
|
|
54
|
+
'kind' => block.kind,
|
|
55
|
+
'level' => block.level,
|
|
56
|
+
'text' => block.text,
|
|
57
|
+
'span' => span_hash(block.span)
|
|
58
|
+
}
|
|
59
|
+
when 'list_item'
|
|
60
|
+
{
|
|
61
|
+
'kind' => block.kind,
|
|
62
|
+
'text' => block.text,
|
|
63
|
+
'span' => span_hash(block.span),
|
|
64
|
+
'segmentMap' => block.segment_map.map { |so| segment_hash(so) },
|
|
65
|
+
'ordered' => block.ordered,
|
|
66
|
+
'markerSpan' => span_hash(block.marker_span)
|
|
67
|
+
}
|
|
68
|
+
when 'table'
|
|
69
|
+
{
|
|
70
|
+
'kind' => block.kind,
|
|
71
|
+
'span' => span_hash(block.span),
|
|
72
|
+
'header' => row_hash(block.header),
|
|
73
|
+
'rows' => block.rows.map { |r| row_hash(r) }
|
|
74
|
+
}
|
|
75
|
+
when 'fence'
|
|
76
|
+
{
|
|
77
|
+
'kind' => block.kind,
|
|
78
|
+
'span' => span_hash(block.span),
|
|
79
|
+
'info' => block.info,
|
|
80
|
+
'body' => block.body,
|
|
81
|
+
'bodySpan' => span_hash(block.body_span)
|
|
82
|
+
}
|
|
83
|
+
when 'thematic_break'
|
|
84
|
+
{
|
|
85
|
+
'kind' => block.kind,
|
|
86
|
+
'span' => span_hash(block.span)
|
|
87
|
+
}
|
|
88
|
+
else
|
|
89
|
+
raise "Unknown block kind: #{block.kind}"
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def example_hash(example)
|
|
94
|
+
{
|
|
95
|
+
'scopeStack' => example.scope_stack,
|
|
96
|
+
'span' => span_hash(example.span),
|
|
97
|
+
'body' => example.body.map { |b| block_hash(b) }
|
|
98
|
+
}
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# Project a VarDoc to the wire dict for the var-doc artifact.
|
|
102
|
+
def to_var_doc_artifact(doc)
|
|
103
|
+
{
|
|
104
|
+
'path' => doc.path,
|
|
105
|
+
'examples' => doc.examples.map { |ex| example_hash(ex) },
|
|
106
|
+
'orphanAttachments' => doc.orphan_attachments.map { |b| block_hash(b) }
|
|
107
|
+
}
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# Parameter-type names in source order from a compiled CucumberExpression.
|
|
111
|
+
# The Ruby gem populates @parameter_types in source order during
|
|
112
|
+
# construction (it has no public reader), mirroring the TS AST walk.
|
|
113
|
+
def parameter_type_names(compiled)
|
|
114
|
+
compiled.instance_variable_get(:@parameter_types).map(&:name)
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# Project a Registry to the wire dict for the registry artifact.
|
|
118
|
+
# +parameter_types+ is the custom-type list ({"name","regexp"}).
|
|
119
|
+
def to_registry_artifact(registry, parameter_types = [])
|
|
120
|
+
{
|
|
121
|
+
'steps' => registry.steps.map do |s|
|
|
122
|
+
{ 'expression' => s.expression, 'parameterTypeNames' => parameter_type_names(s.compiled) }
|
|
123
|
+
end,
|
|
124
|
+
'parameterTypes' => parameter_types.map do |p|
|
|
125
|
+
{ 'name' => p['name'], 'regexp' => p['regexp'] }
|
|
126
|
+
end
|
|
127
|
+
}
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def doc_string_hash(doc_string)
|
|
131
|
+
{
|
|
132
|
+
'content' => doc_string.content,
|
|
133
|
+
'contentType' => doc_string.content_type,
|
|
134
|
+
'span' => span_hash(doc_string.span)
|
|
135
|
+
}
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# Project an ExecutionPlan to the wire dict for the plan artifact.
|
|
139
|
+
def to_plan_artifact(plan)
|
|
140
|
+
source = plan.var_doc.source
|
|
141
|
+
{
|
|
142
|
+
'examples' => plan.examples.map { |ex| planned_example_hash(ex, source) },
|
|
143
|
+
'diagnostics' => plan.diagnostics.map do |d|
|
|
144
|
+
{ 'code' => d.code, 'severity' => d.severity, 'span' => span_hash(d.span) }
|
|
145
|
+
end
|
|
146
|
+
}
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def planned_example_hash(example, source)
|
|
150
|
+
result = {
|
|
151
|
+
'name' => example.name,
|
|
152
|
+
'scopeStack' => example.scope_stack,
|
|
153
|
+
'span' => span_hash(example.span),
|
|
154
|
+
'expectedOutcome' => example.expected_outcome || 'pass'
|
|
155
|
+
}
|
|
156
|
+
result['expectedErrorMessage'] = example.expected_error_message if example.expected_error_message
|
|
157
|
+
result['steps'] = example.steps.map { |s| planned_step_hash(s, source) }
|
|
158
|
+
result
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def planned_step_hash(step, source)
|
|
162
|
+
step_names = parameter_type_names(step.step_def.compiled)
|
|
163
|
+
result = {
|
|
164
|
+
'text' => step.text,
|
|
165
|
+
'matchSpan' => span_hash(step.match_span),
|
|
166
|
+
'paramSpans' => step.param_spans.map { |s| span_hash(s) },
|
|
167
|
+
'matchedExpression' => step.step_def.expression,
|
|
168
|
+
'args' => step.param_spans.each_with_index.map do |s, i|
|
|
169
|
+
{
|
|
170
|
+
'value' => Offsets.utf16_slice(source, s.start_offset, s.end_offset),
|
|
171
|
+
'parameterType' => i < step_names.length ? step_names[i] : nil
|
|
172
|
+
}
|
|
173
|
+
end
|
|
174
|
+
}
|
|
175
|
+
result['dataTable'] = block_hash(step.data_table) if step.data_table
|
|
176
|
+
result['docString'] = doc_string_hash(step.doc_string) if step.doc_string
|
|
177
|
+
result
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
# Return the file stem: "path/to/foo.steps.rb" -> "foo.steps".
|
|
181
|
+
def file_stem(path)
|
|
182
|
+
File.basename(path, '.*')
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
# Project an execution error to a FailureArtifact dict. line and anchor
|
|
186
|
+
# are deterministic source positions (never scraped from a backtrace).
|
|
187
|
+
def to_failure_artifact(error, match_span)
|
|
188
|
+
line = match_span.start_line
|
|
189
|
+
anchor = span_hash(FailureAnchor.failure_anchor(error, match_span))
|
|
190
|
+
case error
|
|
191
|
+
when CellMismatchError
|
|
192
|
+
{
|
|
193
|
+
'kind' => 'cell-mismatch', 'line' => line, 'anchor' => anchor,
|
|
194
|
+
'cells' => error.cells.reject(&:ok).map do |c|
|
|
195
|
+
{ 'column' => c.column, 'expected' => c.expected, 'actual' => c.actual, 'span' => span_hash(c.span) }
|
|
196
|
+
end
|
|
197
|
+
}
|
|
198
|
+
when DocStringMismatchError
|
|
199
|
+
{
|
|
200
|
+
'kind' => 'doc-string-mismatch', 'line' => line, 'anchor' => anchor,
|
|
201
|
+
'diff' => {
|
|
202
|
+
'expected' => error.diff.expected,
|
|
203
|
+
'actual' => error.diff.actual,
|
|
204
|
+
'span' => span_hash(error.diff.span)
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
when ReturnShapeError
|
|
208
|
+
{ 'kind' => 'return-shape', 'line' => line, 'anchor' => anchor }
|
|
209
|
+
when UnexpectedPassError
|
|
210
|
+
{ 'kind' => 'unexpected-pass', 'line' => line, 'anchor' => anchor }
|
|
211
|
+
else
|
|
212
|
+
{ 'kind' => 'thrown', 'line' => line, 'anchor' => anchor }
|
|
213
|
+
end
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
# Run all examples and return the four-artifact bundle. Port of runConformance.
|
|
217
|
+
def run_conformance(var_doc, registry, create_context, parameter_types = [])
|
|
218
|
+
execution = Plan.plan(var_doc, registry)
|
|
219
|
+
observed = Hash.new { |h, k| h[k] = [] }
|
|
220
|
+
observer = ->(o) { observed[o.example_index] << o }
|
|
221
|
+
queue = Execute.collect_examples(execution, create_context: create_context, observer: observer)
|
|
222
|
+
|
|
223
|
+
trace_examples = queue.each_with_index.map do |queued, k|
|
|
224
|
+
outcome = 'pass'
|
|
225
|
+
begin
|
|
226
|
+
queued.run.call
|
|
227
|
+
rescue StandardError
|
|
228
|
+
outcome = 'fail'
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
planned = execution.examples[k]
|
|
232
|
+
obs_list = observed[k]
|
|
233
|
+
steps = planned.steps.each_with_index.map do |step, i|
|
|
234
|
+
ordinal = i + 1
|
|
235
|
+
matches = obs_list.select { |x| x.ordinal == ordinal }
|
|
236
|
+
observation = matches.find { |m| m.outcome == 'fail' } || matches.last
|
|
237
|
+
step_outcome = observation ? observation.outcome : 'skipped'
|
|
238
|
+
step_dict = {
|
|
239
|
+
'exampleName' => queued.name,
|
|
240
|
+
'ordinal' => ordinal,
|
|
241
|
+
'stepText' => step.text,
|
|
242
|
+
'matchedExpression' => step.step_def.expression,
|
|
243
|
+
'contextKey' => { 'exampleName' => queued.name,
|
|
244
|
+
'stepFile' => file_stem(step.step_def.expression_source_file) },
|
|
245
|
+
'outcome' => step_outcome
|
|
246
|
+
}
|
|
247
|
+
step_dict['failure'] = to_failure_artifact(observation&.error, step.match_span) if step_outcome == 'fail'
|
|
248
|
+
step_dict
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
{ 'name' => queued.name, 'outcome' => outcome, 'steps' => steps }
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
{
|
|
255
|
+
var_doc: to_var_doc_artifact(var_doc),
|
|
256
|
+
registry: to_registry_artifact(registry, parameter_types),
|
|
257
|
+
plan: to_plan_artifact(execution),
|
|
258
|
+
trace: { 'examples' => trace_examples }
|
|
259
|
+
}
|
|
260
|
+
end
|
|
261
|
+
end
|
|
262
|
+
end
|
|
263
|
+
end
|
|
264
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Oselvar
|
|
4
|
+
module Var
|
|
5
|
+
module Core
|
|
6
|
+
# Recursively freeze plain Hash/Array so handler code mutating state raises
|
|
7
|
+
# FrozenError. Other objects (class instances, primitives, nil) pass
|
|
8
|
+
# through. Assumes acyclic input. Port of deep-freeze.ts.
|
|
9
|
+
module DeepFreeze
|
|
10
|
+
module_function
|
|
11
|
+
|
|
12
|
+
def deep_freeze(value)
|
|
13
|
+
case value
|
|
14
|
+
when Hash
|
|
15
|
+
value.each_value { |v| deep_freeze(v) }
|
|
16
|
+
value.freeze
|
|
17
|
+
when Array
|
|
18
|
+
value.each { |v| deep_freeze(v) }
|
|
19
|
+
value.freeze
|
|
20
|
+
else
|
|
21
|
+
value
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Oselvar
|
|
4
|
+
module Var
|
|
5
|
+
module Core
|
|
6
|
+
# A planning/run diagnostic on the shared rail. code is one of
|
|
7
|
+
# "ambiguous-match", "error-fence-without-step", "drift". Port of
|
|
8
|
+
# diagnostics.ts.
|
|
9
|
+
Diagnostic = Data.define(:code, :severity, :message, :span)
|
|
10
|
+
Candidate = Data.define(:expression, :source_file, :source_line)
|
|
11
|
+
AmbiguousInput = Data.define(:text, :span, :candidates)
|
|
12
|
+
|
|
13
|
+
module Diagnostics
|
|
14
|
+
module_function
|
|
15
|
+
|
|
16
|
+
def ambiguous_match(input)
|
|
17
|
+
lines = input.candidates.map do |c|
|
|
18
|
+
" '#{c.expression}' at #{c.source_file}:#{c.source_line}"
|
|
19
|
+
end.join("\n")
|
|
20
|
+
Diagnostic.new(
|
|
21
|
+
severity: 'error',
|
|
22
|
+
code: 'ambiguous-match',
|
|
23
|
+
message: "Ambiguous step: \"#{input.text}\"\nMatched by:\n#{lines}",
|
|
24
|
+
span: input.span
|
|
25
|
+
)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def drift_detected(name, span)
|
|
29
|
+
Diagnostic.new(
|
|
30
|
+
severity: 'error',
|
|
31
|
+
code: 'drift',
|
|
32
|
+
message: "This paragraph was an example and no longer matches any step (drift): \"#{name}\".\n" \
|
|
33
|
+
'Fix the step so it matches again, or accept it as prose (run in update mode).',
|
|
34
|
+
span: span
|
|
35
|
+
)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def error_fence_without_step(span)
|
|
39
|
+
Diagnostic.new(
|
|
40
|
+
severity: 'error',
|
|
41
|
+
code: 'error-fence-without-step',
|
|
42
|
+
message: 'This `error` fence marks the example as expected-to-fail, ' \
|
|
43
|
+
'but the example has no step to run.',
|
|
44
|
+
span: span
|
|
45
|
+
)
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'oselvar/var/core/cell_diff'
|
|
4
|
+
|
|
5
|
+
module Oselvar
|
|
6
|
+
module Var
|
|
7
|
+
module Core
|
|
8
|
+
# A doc-string content difference: fence body span, expected, actual.
|
|
9
|
+
DocStringDiff = Data.define(:span, :expected, :actual)
|
|
10
|
+
|
|
11
|
+
# Raised when a doc-string step's returned string differs from the content.
|
|
12
|
+
class DocStringMismatchError < StandardError
|
|
13
|
+
attr_reader :diff
|
|
14
|
+
|
|
15
|
+
def initialize(diff)
|
|
16
|
+
@diff = diff
|
|
17
|
+
super("doc string: expected #{diff.expected.inspect} but was #{diff.actual.inspect}")
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Pure comparison of a doc-string step's return against the fence body.
|
|
22
|
+
# Port of doc-string-diff.ts.
|
|
23
|
+
module DocStringDiffs
|
|
24
|
+
module_function
|
|
25
|
+
|
|
26
|
+
# nil → no check; equal string → nil (pass); unequal → DocStringDiff;
|
|
27
|
+
# non-string → ReturnShapeError.
|
|
28
|
+
def compare_doc_string(returned, content, span)
|
|
29
|
+
return nil if returned.nil?
|
|
30
|
+
raise ReturnShapeError, "expected a doc string (string), got #{returned.class}" unless returned.is_a?(String)
|
|
31
|
+
return nil if returned == content
|
|
32
|
+
|
|
33
|
+
DocStringDiff.new(span: span, expected: content, actual: returned)
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|