varar-core 0.7.0 → 0.8.0
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 +4 -4
- data/lib/varar/core/ast.rb +1 -1
- data/lib/varar/core/canonical_json.rb +15 -15
- data/lib/varar/core/conformance.rb +6 -6
- data/lib/varar/core/drift.rb +71 -38
- data/lib/varar/core/execute.rb +13 -10
- data/lib/varar/core/failure.rb +55 -0
- data/lib/varar/core/failure_anchor.rb +20 -0
- data/lib/varar/core/parse.rb +1 -1
- data/lib/varar/core/plan.rb +11 -11
- data/lib/varar/core/registry.rb +1 -1
- data/lib/varar/core/result.rb +78 -0
- data/lib/varar/core/structurer.rb +1 -1
- data/lib/varar/core.rb +3 -1
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 4e14558a60af014ad7a1e7edfea61644a415408c5da1e384d6c34d4f191a2743
|
|
4
|
+
data.tar.gz: f91c617e21f8affcd745676fffba8be6b1ab484ec9c2c4e17b7173d6d07e1733
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 4a89ff743d9d00027565cad5933ad28f2bc05af437a773feeac38f9c7c640ee25d548af761852343259afed64654205360e5e2983df9fbd82bc14a1167746013
|
|
7
|
+
data.tar.gz: 66c9b893012bd279f337db321fbae0262802bf579f627a7d7e0db37ff4183b3ec3112c413af5614481521d61543ca2e634b966d54260032615cf0c97057fd89f
|
data/lib/varar/core/ast.rb
CHANGED
|
@@ -46,6 +46,6 @@ module Varar
|
|
|
46
46
|
# one example. See ADR 0012.
|
|
47
47
|
Example = Data.define(:scope_stack, :span, :body, :preceded_by_delimiter)
|
|
48
48
|
|
|
49
|
-
|
|
49
|
+
Doc = Data.define(:path, :source, :examples, :orphan_attachments)
|
|
50
50
|
end
|
|
51
51
|
end
|
|
@@ -4,39 +4,39 @@ require 'json'
|
|
|
4
4
|
|
|
5
5
|
module Varar
|
|
6
6
|
module Core
|
|
7
|
-
#
|
|
7
|
+
# Writes varar.lock.json the way JS `JSON.stringify(v, null, 2)` does:
|
|
8
8
|
# 2-space indent, LF, trailing newline, non-ASCII raw, empty containers as
|
|
9
|
-
# {}/[]
|
|
10
|
-
# `ordered_stringify` preserves insertion order (varar.lock.json).
|
|
9
|
+
# {}/[], keys in insertion order.
|
|
11
10
|
#
|
|
12
|
-
#
|
|
13
|
-
# renders empty arrays/objects as
|
|
14
|
-
#
|
|
11
|
+
# A committed, language-shared file, so the layout is hand-rolled rather
|
|
12
|
+
# than left to JSON.pretty_generate, which renders empty arrays/objects as
|
|
13
|
+
# "[\n\n]" — a Ruby run would otherwise churn the file against every other
|
|
14
|
+
# port's. Scalar encoding is delegated to the stdlib, which matches JS
|
|
15
|
+
# (escapes " \ control chars, keeps non-ASCII raw).
|
|
16
|
+
#
|
|
17
|
+
# Conformance goldens are NOT compared through here: a port has to agree
|
|
18
|
+
# with the goldens' CONTENT, and every spec parses them and compares deep
|
|
19
|
+
# equality.
|
|
15
20
|
module CanonicalJson
|
|
16
21
|
module_function
|
|
17
22
|
|
|
18
|
-
def canonical_stringify(value)
|
|
19
|
-
"#{encode(value, '', sort_keys: true)}\n"
|
|
20
|
-
end
|
|
21
|
-
|
|
22
23
|
def ordered_stringify(value)
|
|
23
|
-
"#{encode(value, ''
|
|
24
|
+
"#{encode(value, '')}\n"
|
|
24
25
|
end
|
|
25
26
|
|
|
26
|
-
def encode(value, indent
|
|
27
|
+
def encode(value, indent)
|
|
27
28
|
case value
|
|
28
29
|
when Hash
|
|
29
30
|
return '{}' if value.empty?
|
|
30
31
|
|
|
31
|
-
keys = sort_keys ? value.keys.sort : value.keys
|
|
32
32
|
inner = "#{indent} "
|
|
33
|
-
items = keys.map { |key| "#{inner}#{key.to_s.to_json}: #{encode(value[key], inner
|
|
33
|
+
items = value.keys.map { |key| "#{inner}#{key.to_s.to_json}: #{encode(value[key], inner)}" }
|
|
34
34
|
"{\n#{items.join(",\n")}\n#{indent}}"
|
|
35
35
|
when Array
|
|
36
36
|
return '[]' if value.empty?
|
|
37
37
|
|
|
38
38
|
inner = "#{indent} "
|
|
39
|
-
items = value.map { |element| "#{inner}#{encode(element, inner
|
|
39
|
+
items = value.map { |element| "#{inner}#{encode(element, inner)}" }
|
|
40
40
|
"[\n#{items.join(",\n")}\n#{indent}]"
|
|
41
41
|
else
|
|
42
42
|
value.to_json
|
|
@@ -98,8 +98,8 @@ module Varar
|
|
|
98
98
|
}
|
|
99
99
|
end
|
|
100
100
|
|
|
101
|
-
# Project a
|
|
102
|
-
def
|
|
101
|
+
# Project a Doc to the wire dict for the var-doc artifact.
|
|
102
|
+
def to_doc_artifact(doc)
|
|
103
103
|
{
|
|
104
104
|
'path' => doc.path,
|
|
105
105
|
'examples' => doc.examples.map { |ex| example_hash(ex) },
|
|
@@ -137,7 +137,7 @@ module Varar
|
|
|
137
137
|
|
|
138
138
|
# Project an ExecutionPlan to the wire dict for the plan artifact.
|
|
139
139
|
def to_plan_artifact(plan)
|
|
140
|
-
source = plan.
|
|
140
|
+
source = plan.doc.source
|
|
141
141
|
{
|
|
142
142
|
'examples' => plan.examples.map { |ex| planned_example_hash(ex, source) },
|
|
143
143
|
'diagnostics' => plan.diagnostics.map do |d|
|
|
@@ -206,8 +206,8 @@ module Varar
|
|
|
206
206
|
end
|
|
207
207
|
|
|
208
208
|
# Run all examples and return the four-artifact bundle. Port of runConformance.
|
|
209
|
-
def run_conformance(
|
|
210
|
-
execution = Plan.plan(
|
|
209
|
+
def run_conformance(doc, registry, create_context, parameter_types = [])
|
|
210
|
+
execution = Plan.plan(doc, registry)
|
|
211
211
|
observed = Hash.new { |h, k| h[k] = [] }
|
|
212
212
|
observer = ->(o) { observed[o.example_index] << o }
|
|
213
213
|
queue = Execute.collect_examples(execution, create_context: create_context, observer: observer)
|
|
@@ -244,7 +244,7 @@ module Varar
|
|
|
244
244
|
end
|
|
245
245
|
|
|
246
246
|
{
|
|
247
|
-
|
|
247
|
+
doc: to_doc_artifact(doc),
|
|
248
248
|
registry: to_registry_artifact(registry, parameter_types),
|
|
249
249
|
plan: to_plan_artifact(execution),
|
|
250
250
|
trace: { 'examples' => trace_examples }
|
data/lib/varar/core/drift.rb
CHANGED
|
@@ -10,14 +10,14 @@ module Varar
|
|
|
10
10
|
module Core
|
|
11
11
|
# One example-producing paragraph, as recorded in the baseline.
|
|
12
12
|
BaselineExample = Data.define(:name, :line)
|
|
13
|
-
# The committed baseline for one
|
|
14
|
-
|
|
15
|
-
# The whole varar.lock.json: every
|
|
16
|
-
|
|
13
|
+
# The committed baseline for one oath file.
|
|
14
|
+
OathBaseline = Data.define(:source_hash, :examples)
|
|
15
|
+
# The whole varar.lock.json: every oath keyed by its POSIX path.
|
|
16
|
+
LockFile = Data.define(:version, :oaths)
|
|
17
17
|
# A paragraph the baseline says was an example and now matches no step.
|
|
18
18
|
Drift = Data.define(:name, :line, :span)
|
|
19
19
|
|
|
20
|
-
#
|
|
20
|
+
# Oath drift detection: a paragraph the committed varar.lock.json baseline
|
|
21
21
|
# recorded as an example that now matches no step. Pure, byte-identical to
|
|
22
22
|
# the TS port so varar.lock.json is shared across languages. Port of drift.ts.
|
|
23
23
|
#
|
|
@@ -62,25 +62,25 @@ module Varar
|
|
|
62
62
|
end
|
|
63
63
|
|
|
64
64
|
# The current example-producing paragraphs, in document order.
|
|
65
|
-
def live_examples(
|
|
66
|
-
|
|
65
|
+
def live_examples(doc, plan)
|
|
66
|
+
doc.examples.filter_map do |candidate|
|
|
67
67
|
next unless live?(candidate.span, plan)
|
|
68
68
|
|
|
69
69
|
BaselineExample.new(name: Plan.derive_example_name(candidate.body), line: candidate.span.start_line)
|
|
70
70
|
end
|
|
71
71
|
end
|
|
72
72
|
|
|
73
|
-
def
|
|
74
|
-
|
|
73
|
+
def derive_oath_baseline(source, doc, plan)
|
|
74
|
+
OathBaseline.new(source_hash: Hash32.hash_source(source), examples: live_examples(doc, plan))
|
|
75
75
|
end
|
|
76
76
|
|
|
77
77
|
# Paragraphs the baseline recorded as examples that now match zero steps.
|
|
78
78
|
# Each re-identified by the most word-similar current paragraph at/above
|
|
79
79
|
# the threshold (exact name scores 1; ties break toward the nearest line).
|
|
80
|
-
def detect_drift(baseline,
|
|
80
|
+
def detect_drift(baseline, doc, plan)
|
|
81
81
|
return [] if baseline.nil?
|
|
82
82
|
|
|
83
|
-
candidates =
|
|
83
|
+
candidates = doc.examples
|
|
84
84
|
tokens = candidates.map { |c| tokenize(Plan.derive_example_name(c.body)) }
|
|
85
85
|
live = candidates.map { |c| live?(c.span, plan) }
|
|
86
86
|
|
|
@@ -111,43 +111,76 @@ module Varar
|
|
|
111
111
|
drifts.map { |d| Diagnostics.drift_detected(d.name, d.span) }
|
|
112
112
|
end
|
|
113
113
|
|
|
114
|
-
# One
|
|
114
|
+
# One oath's baseline reconciliation against a BaselineStore. In update
|
|
115
115
|
# mode, accept all drift (re-record, report nothing); otherwise detect
|
|
116
116
|
# drift and rewrite the baseline only on a clean run, so an unacknowledged
|
|
117
117
|
# drift keeps its old entry (and stays red).
|
|
118
|
-
def reconcile_drift(store,
|
|
118
|
+
def reconcile_drift(store, oath_path, source, doc, plan, update: false)
|
|
119
119
|
text = store.read
|
|
120
|
-
lock = text ?
|
|
121
|
-
baseline = lock ? lock.
|
|
122
|
-
drifts = update ? [] : detect_drift(baseline,
|
|
120
|
+
lock = text ? parse_lock_file(text) : nil
|
|
121
|
+
baseline = lock ? lock.oaths[oath_path] : nil
|
|
122
|
+
drifts = update ? [] : detect_drift(baseline, doc, plan)
|
|
123
123
|
if update || drifts.empty?
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
store.write(
|
|
124
|
+
oaths = lock ? lock.oaths.dup : {}
|
|
125
|
+
oaths[oath_path] = derive_oath_baseline(source, doc, plan)
|
|
126
|
+
store.write(stringify_lock_file(LockFile.new(version: 2, oaths: oaths)))
|
|
127
127
|
end
|
|
128
128
|
drifts
|
|
129
129
|
end
|
|
130
130
|
|
|
131
|
-
|
|
131
|
+
# Drop every baseline whose oath path is not in +keep_paths+ — the entries
|
|
132
|
+
# left behind when an oath is deleted or moved. Pure counterpart of
|
|
133
|
+
# parse_lock_file / stringify_lock_file; the caller decides what "still
|
|
134
|
+
# exists" means.
|
|
135
|
+
def prune_lock_file(lock, keep_paths)
|
|
136
|
+
keep = keep_paths.to_a
|
|
137
|
+
LockFile.new(version: 2, oaths: lock.oaths.slice(*keep))
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# The whole-lock counterpart of reconcile_drift, run ONCE per run rather
|
|
141
|
+
# than per oath: reconciliation cannot see paths that no longer exist, so
|
|
142
|
+
# without this the lock silently accumulates dead entries and stops being
|
|
143
|
+
# a faithful inventory of the oath set (#70).
|
|
144
|
+
#
|
|
145
|
+
# +keep_paths+ MUST be everything the +docs+ globs currently match — never
|
|
146
|
+
# the set the run happened to execute. Runs are routinely filtered, and
|
|
147
|
+
# pruning against a filtered set would delete live baselines.
|
|
148
|
+
#
|
|
149
|
+
# Removal is still not *gated*: a deleted oath is a different signal from
|
|
150
|
+
# drift and stays ungated (ADR 0002). This only stops preserving dead
|
|
151
|
+
# state, and only under +update+. Returns the paths removed (or, without
|
|
152
|
+
# +update+, the ones that would be).
|
|
153
|
+
def prune_baselines(store, keep_paths, update: false)
|
|
154
|
+
text = store.read
|
|
155
|
+
lock = text ? parse_lock_file(text) : nil
|
|
156
|
+
return [] unless lock
|
|
157
|
+
|
|
158
|
+
keep = keep_paths.to_a
|
|
159
|
+
stale = lock.oaths.keys.reject { |path| keep.include?(path) }
|
|
160
|
+
store.write(stringify_lock_file(prune_lock_file(lock, keep))) if update && !stale.empty?
|
|
161
|
+
stale
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def parse_lock_file(text)
|
|
132
165
|
parsed = JSON.parse(text)
|
|
133
|
-
return nil unless parsed.is_a?(::Hash) && parsed['version'] ==
|
|
166
|
+
return nil unless parsed.is_a?(::Hash) && parsed['version'] == 2
|
|
134
167
|
|
|
135
|
-
|
|
136
|
-
return nil unless
|
|
168
|
+
oaths_raw = parsed['oaths']
|
|
169
|
+
return nil unless oaths_raw.is_a?(::Hash)
|
|
137
170
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
baseline =
|
|
171
|
+
oaths = {}
|
|
172
|
+
oaths_raw.each do |path, value|
|
|
173
|
+
baseline = parse_oath_baseline(value)
|
|
141
174
|
return nil if baseline.nil?
|
|
142
175
|
|
|
143
|
-
|
|
176
|
+
oaths[path] = baseline
|
|
144
177
|
end
|
|
145
|
-
|
|
178
|
+
LockFile.new(version: 2, oaths: oaths)
|
|
146
179
|
rescue JSON::ParserError, TypeError
|
|
147
180
|
nil
|
|
148
181
|
end
|
|
149
182
|
|
|
150
|
-
def
|
|
183
|
+
def parse_oath_baseline(value)
|
|
151
184
|
return nil unless value.is_a?(::Hash)
|
|
152
185
|
|
|
153
186
|
source_hash = value['sourceHash']
|
|
@@ -161,7 +194,7 @@ module Varar
|
|
|
161
194
|
|
|
162
195
|
examples << parsed
|
|
163
196
|
end
|
|
164
|
-
|
|
197
|
+
OathBaseline.new(source_hash: source_hash, examples: examples)
|
|
165
198
|
end
|
|
166
199
|
|
|
167
200
|
def parse_baseline_example(value)
|
|
@@ -174,19 +207,19 @@ module Varar
|
|
|
174
207
|
BaselineExample.new(name: name, line: line)
|
|
175
208
|
end
|
|
176
209
|
|
|
177
|
-
# Serialize varar.lock.json deterministically:
|
|
178
|
-
# in document order, insertion-order keys otherwise (version,
|
|
210
|
+
# Serialize varar.lock.json deterministically: oath paths sorted, examples
|
|
211
|
+
# in document order, insertion-order keys otherwise (version, oaths;
|
|
179
212
|
# sourceHash, examples; name, line) — NOT canonical JSON's key sort.
|
|
180
|
-
def
|
|
181
|
-
|
|
182
|
-
lock.
|
|
183
|
-
baseline = lock.
|
|
184
|
-
|
|
213
|
+
def stringify_lock_file(lock)
|
|
214
|
+
oaths = {}
|
|
215
|
+
lock.oaths.keys.sort.each do |path|
|
|
216
|
+
baseline = lock.oaths[path]
|
|
217
|
+
oaths[path] = {
|
|
185
218
|
'sourceHash' => baseline.source_hash,
|
|
186
219
|
'examples' => baseline.examples.map { |e| { 'name' => e.name, 'line' => e.line } }
|
|
187
220
|
}
|
|
188
221
|
end
|
|
189
|
-
CanonicalJson.ordered_stringify({ 'version' =>
|
|
222
|
+
CanonicalJson.ordered_stringify({ 'version' => 2, 'oaths' => oaths })
|
|
190
223
|
end
|
|
191
224
|
end
|
|
192
225
|
end
|
data/lib/varar/core/execute.rb
CHANGED
|
@@ -42,17 +42,17 @@ module Varar
|
|
|
42
42
|
def execute_plan(plan, sink:, create_context:, observer: nil, reporter: nil)
|
|
43
43
|
plan.diagnostics.each { |d| reporter.call(d) } if reporter
|
|
44
44
|
create_ctx = create_context || ->(_file) { {} }
|
|
45
|
-
|
|
45
|
+
oath_path = plan.doc.path
|
|
46
46
|
|
|
47
47
|
plan.examples.each_with_index do |ex, example_index|
|
|
48
48
|
seen_lines = {}
|
|
49
49
|
ex.steps.each { |s| seen_lines[s.match_span.start_line] = true }
|
|
50
50
|
info = { lines: seen_lines.keys }
|
|
51
|
-
sink.call(ex.name, build_run(plan, ex, example_index, create_ctx, observer,
|
|
51
|
+
sink.call(ex.name, build_run(plan, ex, example_index, create_ctx, observer, oath_path), info)
|
|
52
52
|
end
|
|
53
53
|
end
|
|
54
54
|
|
|
55
|
-
def build_run(plan, ex, example_index, create_ctx, observer,
|
|
55
|
+
def build_run(plan, ex, example_index, create_ctx, observer, oath_path)
|
|
56
56
|
lambda do
|
|
57
57
|
state_by_file = {}
|
|
58
58
|
last_return = nil
|
|
@@ -97,7 +97,7 @@ module Varar
|
|
|
97
97
|
raise ReturnShapeError, "unknown step kind: #{step.step_def.kind}"
|
|
98
98
|
end
|
|
99
99
|
rescue StandardError => e
|
|
100
|
-
augmented = augment_stack(e, step,
|
|
100
|
+
augmented = augment_stack(e, step, oath_path)
|
|
101
101
|
observer&.call(observation(ex, example_index, i + 1, file, 'fail', augmented))
|
|
102
102
|
thrown = augmented
|
|
103
103
|
break
|
|
@@ -117,7 +117,7 @@ module Varar
|
|
|
117
117
|
bad = CellDiffs.compare_row(last_return, ex.row_checks).reject(&:ok)
|
|
118
118
|
if row_error || !bad.empty?
|
|
119
119
|
last_step = ex.steps.last
|
|
120
|
-
augmented = augment_stack(row_error || CellMismatchError.new(bad), last_step,
|
|
120
|
+
augmented = augment_stack(row_error || CellMismatchError.new(bad), last_step, oath_path)
|
|
121
121
|
observer&.call(observation(ex, example_index, ex.steps.length,
|
|
122
122
|
last_step.step_def.expression_source_file, 'fail', augmented))
|
|
123
123
|
thrown = augmented
|
|
@@ -129,7 +129,7 @@ module Varar
|
|
|
129
129
|
if thrown.nil?
|
|
130
130
|
error = UnexpectedPassError.new
|
|
131
131
|
last = ex.steps.last
|
|
132
|
-
raise(last ? augment_stack(error, last,
|
|
132
|
+
raise(last ? augment_stack(error, last, oath_path) : error)
|
|
133
133
|
end
|
|
134
134
|
raise thrown if ex.expected_error_message && !thrown.message.include?(ex.expected_error_message)
|
|
135
135
|
|
|
@@ -177,7 +177,7 @@ module Varar
|
|
|
177
177
|
|
|
178
178
|
inline_returned = slots[0...step.args.length]
|
|
179
179
|
source_texts = step.param_spans.map do |s|
|
|
180
|
-
Offsets.utf16_slice(plan.
|
|
180
|
+
Offsets.utf16_slice(plan.doc.source, s.start_offset, s.end_offset)
|
|
181
181
|
end
|
|
182
182
|
param_diffs = ParamDiff.compare_params(inline_returned, step.args, step.param_spans, source_texts,
|
|
183
183
|
step.formats).reject(&:ok)
|
|
@@ -199,9 +199,12 @@ module Varar
|
|
|
199
199
|
end
|
|
200
200
|
|
|
201
201
|
# In TS this injects a synthetic `at <text> (path:line:col)` frame for
|
|
202
|
-
# editor navigation
|
|
203
|
-
#
|
|
204
|
-
|
|
202
|
+
# editor navigation. Ruby has no writable stack text to splice into, so
|
|
203
|
+
# it records the anchor structurally instead — the failing step's span
|
|
204
|
+
# (or the first mismatched cell's), which Failures.to_failure reads back
|
|
205
|
+
# so a renderer underlines the step and not its whole line.
|
|
206
|
+
def augment_stack(error, step, _var_path)
|
|
207
|
+
FailureAnchor.attach_anchor(error, FailureAnchor.failure_anchor(error, step.match_span))
|
|
205
208
|
error
|
|
206
209
|
end
|
|
207
210
|
end
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'varar/core/cell_diff'
|
|
4
|
+
require 'varar/core/failure_anchor'
|
|
5
|
+
require 'varar/core/result'
|
|
6
|
+
|
|
7
|
+
module Varar
|
|
8
|
+
module Core
|
|
9
|
+
# Converts a caught step error into the structured ExampleFailure payload —
|
|
10
|
+
# port of failure.ts / failure.py / Failure.java / failure.rs. Shared by
|
|
11
|
+
# every producer so failures are byte-identical across ports.
|
|
12
|
+
#
|
|
13
|
+
# Where TS scrapes an injected `<path>:line:col` stack frame for the failing
|
|
14
|
+
# line, Ruby reads it off the anchor the executor attached (the Rust port
|
|
15
|
+
# does the same): a Ruby backtrace has no synthetic frame to scrape, and the
|
|
16
|
+
# anchor already carries the line the frame would have named.
|
|
17
|
+
module Failures
|
|
18
|
+
module_function
|
|
19
|
+
|
|
20
|
+
# A caught step error → the ExampleResult.failure payload.
|
|
21
|
+
#
|
|
22
|
+
# `fallback_line` is used when the error carries no anchor, i.e. it never
|
|
23
|
+
# passed through a step.
|
|
24
|
+
def to_failure(error, _oath_path, fallback_line)
|
|
25
|
+
anchor = FailureAnchor.attached_anchor(error)
|
|
26
|
+
|
|
27
|
+
ExampleFailure.new(
|
|
28
|
+
line: anchor ? anchor.start_line : fallback_line,
|
|
29
|
+
message: error.message,
|
|
30
|
+
stack: render_stack(error),
|
|
31
|
+
cells: failing_cells(error),
|
|
32
|
+
anchor: anchor && AnchorRange.new(from: anchor.start_offset, to: anchor.end_offset)
|
|
33
|
+
)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Every mismatched cell — table, header-bound row, inline capture or doc
|
|
37
|
+
# string. nil (not an empty array) when the error is not a mismatch, so
|
|
38
|
+
# the key stays absent in the serialized payload.
|
|
39
|
+
def failing_cells(error)
|
|
40
|
+
return nil unless error.is_a?(CellMismatchError)
|
|
41
|
+
|
|
42
|
+
failing = error.cells.reject(&:ok).map do |c|
|
|
43
|
+
CellFailure.new(from: c.span.start_offset, to: c.span.end_offset, actual: c.actual)
|
|
44
|
+
end
|
|
45
|
+
failing.empty? ? nil : failing
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Display-only: the message plus Ruby's own backtrace. Runtime-shaped by
|
|
49
|
+
# design (ADR 0014) — no consumer parses it.
|
|
50
|
+
def render_stack(error)
|
|
51
|
+
([error.message] + Array(error.backtrace)).join("\n")
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
@@ -12,6 +12,13 @@ module Varar
|
|
|
12
12
|
module FailureAnchor
|
|
13
13
|
module_function
|
|
14
14
|
|
|
15
|
+
# The anchor travels with the raised error, from the executor (which knows
|
|
16
|
+
# the step) to Failures.to_failure (which only sees the error) — the same
|
|
17
|
+
# job TS does with a global symbol on the Error. An instance variable on
|
|
18
|
+
# the exception, so it never shows up in `inspect` output the way an
|
|
19
|
+
# extra attribute would.
|
|
20
|
+
ANCHOR_IVAR = :@varar_failure_anchor
|
|
21
|
+
|
|
15
22
|
def failure_anchor(error, fallback)
|
|
16
23
|
case error
|
|
17
24
|
when CellMismatchError
|
|
@@ -21,6 +28,19 @@ module Varar
|
|
|
21
28
|
fallback
|
|
22
29
|
end
|
|
23
30
|
end
|
|
31
|
+
|
|
32
|
+
# Record on the error itself where the failure points.
|
|
33
|
+
def attach_anchor(error, anchor)
|
|
34
|
+
error.instance_variable_set(ANCHOR_IVAR, anchor) if error.respond_to?(:instance_variable_set)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# The anchor the executor attached, or nil if there is none — then a
|
|
38
|
+
# renderer only has the failing line to go on.
|
|
39
|
+
def attached_anchor(error)
|
|
40
|
+
return nil unless error.respond_to?(:instance_variable_get)
|
|
41
|
+
|
|
42
|
+
error.instance_variable_get(ANCHOR_IVAR)
|
|
43
|
+
end
|
|
24
44
|
end
|
|
25
45
|
end
|
|
26
46
|
end
|
data/lib/varar/core/parse.rb
CHANGED
data/lib/varar/core/plan.rb
CHANGED
|
@@ -29,9 +29,9 @@ module Varar
|
|
|
29
29
|
end
|
|
30
30
|
end
|
|
31
31
|
|
|
32
|
-
ExecutionPlan = Data.define(:
|
|
32
|
+
ExecutionPlan = Data.define(:doc, :examples, :diagnostics)
|
|
33
33
|
|
|
34
|
-
# Produce an ExecutionPlan from a
|
|
34
|
+
# Produce an ExecutionPlan from a Doc + Registry: match step expressions
|
|
35
35
|
# against every text block, attach trailing tables/fences, detect
|
|
36
36
|
# header-bound tables, and collect diagnostics. Port of plan.ts.
|
|
37
37
|
module Plan
|
|
@@ -52,11 +52,11 @@ module Varar
|
|
|
52
52
|
|
|
53
53
|
module_function
|
|
54
54
|
|
|
55
|
-
def plan(
|
|
55
|
+
def plan(doc, registry)
|
|
56
56
|
diagnostics = []
|
|
57
57
|
|
|
58
58
|
# Phase 1: plan each candidate paragraph independently into a "unit".
|
|
59
|
-
units =
|
|
59
|
+
units = doc.examples.map { |ex| plan_candidate(ex, doc, registry, diagnostics) }
|
|
60
60
|
|
|
61
61
|
# Phase 2: group adjacent candidates into examples. A matching candidate
|
|
62
62
|
# continues the open example when no delimiter (heading / `---`) precedes
|
|
@@ -66,7 +66,7 @@ module Varar
|
|
|
66
66
|
examples = []
|
|
67
67
|
open = nil
|
|
68
68
|
flush = lambda do
|
|
69
|
-
examples << finish_merged(open,
|
|
69
|
+
examples << finish_merged(open, doc.source) if open
|
|
70
70
|
open = nil
|
|
71
71
|
end
|
|
72
72
|
units.each do |unit|
|
|
@@ -89,7 +89,7 @@ module Varar
|
|
|
89
89
|
end
|
|
90
90
|
flush.call
|
|
91
91
|
|
|
92
|
-
ExecutionPlan.new(
|
|
92
|
+
ExecutionPlan.new(doc: doc, examples: examples, diagnostics: diagnostics)
|
|
93
93
|
end
|
|
94
94
|
|
|
95
95
|
def start_merged(unit)
|
|
@@ -124,7 +124,7 @@ module Varar
|
|
|
124
124
|
|
|
125
125
|
# Plan a single candidate paragraph (plus attached tables/fences) in
|
|
126
126
|
# isolation. Emits ambiguity / error-fence diagnostics into +diagnostics+.
|
|
127
|
-
def plan_candidate(ex,
|
|
127
|
+
def plan_candidate(ex, doc, registry, diagnostics)
|
|
128
128
|
had_ambiguous = false
|
|
129
129
|
steps_by_block = {}
|
|
130
130
|
|
|
@@ -135,7 +135,7 @@ module Varar
|
|
|
135
135
|
result = plan_block(block.text, registry)
|
|
136
136
|
|
|
137
137
|
result.ambiguities.each do |collision|
|
|
138
|
-
span = lift_span(
|
|
138
|
+
span = lift_span(doc.source, block, collision.match_start, collision.match_end)
|
|
139
139
|
cp_start = Offsets.cp_index_for_utf16(block.text, collision.match_start)
|
|
140
140
|
cp_end = Offsets.cp_index_for_utf16(block.text, collision.match_end)
|
|
141
141
|
diagnostics << Diagnostics.ambiguous_match(
|
|
@@ -159,8 +159,8 @@ module Varar
|
|
|
159
159
|
steps_by_block[idx] = result.steps.map do |hit|
|
|
160
160
|
PlannedStep.new(
|
|
161
161
|
text: Offsets.utf16_slice(block.text, hit.match_start, hit.match_end),
|
|
162
|
-
match_span: lift_span(
|
|
163
|
-
param_spans: hit.param_spans.map { |p| lift_span(
|
|
162
|
+
match_span: lift_span(doc.source, block, hit.match_start, hit.match_end),
|
|
163
|
+
param_spans: hit.param_spans.map { |p| lift_span(doc.source, block, p.start, p.end) },
|
|
164
164
|
step_def: hit.step_def,
|
|
165
165
|
args: hit.args,
|
|
166
166
|
formats: hit.formats
|
|
@@ -169,7 +169,7 @@ module Varar
|
|
|
169
169
|
end
|
|
170
170
|
|
|
171
171
|
# Header-bound table detection.
|
|
172
|
-
bound = had_ambiguous ? nil : detect_header_bound(ex, steps_by_block,
|
|
172
|
+
bound = had_ambiguous ? nil : detect_header_bound(ex, steps_by_block, doc.source)
|
|
173
173
|
if bound
|
|
174
174
|
table, binding_step, header_spans = bound
|
|
175
175
|
header_binding = HeaderBinding.new(
|
data/lib/varar/core/registry.rb
CHANGED
|
@@ -39,7 +39,7 @@ module Varar
|
|
|
39
39
|
end
|
|
40
40
|
|
|
41
41
|
# Seed Varar's own built-in parameter types (beyond cucumber-expressions'
|
|
42
|
-
# int/float/string/word). Shared by every port so
|
|
42
|
+
# int/float/string/word). Shared by every port so oaths match
|
|
43
43
|
# identically. Built-ins are NOT tracked as custom parameter types, so
|
|
44
44
|
# they never appear in the conformance registry.json projection.
|
|
45
45
|
def seed_builtins(registry)
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Varar
|
|
4
|
+
module Core
|
|
5
|
+
# Run-result records — port of result.ts / result.rb's siblings in every
|
|
6
|
+
# other port. The persisted .varar/<oath_path>.json file is a serialized
|
|
7
|
+
# OathResults, read by the language server to place run diagnostics in the
|
|
8
|
+
# editor (ADR 0014).
|
|
9
|
+
|
|
10
|
+
# One mismatched CELL as a source-offset range plus the runtime value.
|
|
11
|
+
# `from`/`to` are absolute UTF-16 source offsets; `to` is exclusive.
|
|
12
|
+
CellFailure = Data.define(:from, :to, :actual)
|
|
13
|
+
|
|
14
|
+
# Where a failure points in the source: an offset range, `to` exclusive.
|
|
15
|
+
# The failing step's match span, or the first mismatched cell's span (the
|
|
16
|
+
# failure_anchor rule) — what lets a renderer underline the step that
|
|
17
|
+
# failed rather than the whole line it sits on.
|
|
18
|
+
AnchorRange = Data.define(:from, :to)
|
|
19
|
+
|
|
20
|
+
# The failure payload of a failed ExampleResult. `cells` and `anchor` are
|
|
21
|
+
# nil when they do not apply, and serialize as absent (not null), so a
|
|
22
|
+
# reader that predates them still parses the file. `stack` is deliberately
|
|
23
|
+
# runtime-shaped — no consumer parses it.
|
|
24
|
+
ExampleFailure = Data.define(:line, :message, :stack, :cells, :anchor) do
|
|
25
|
+
def initialize(line:, message:, stack:, cells: nil, anchor: nil)
|
|
26
|
+
super
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# The run result for one BDD example. `lines` are the 1-based source lines
|
|
31
|
+
# of its steps (the editor's line-wash anchors).
|
|
32
|
+
ExampleResult = Data.define(:name, :status, :lines, :failure) do
|
|
33
|
+
def initialize(name:, status:, lines:, failure: nil)
|
|
34
|
+
super
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# The persisted run result for one oath file. `oath_path` uses POSIX
|
|
39
|
+
# separators and is relative to the workspace root; `source_hash` is
|
|
40
|
+
# Hashing.hash_source over the oath as it was run, so a reader can tell
|
|
41
|
+
# whether the offsets still apply to the buffer in front of it.
|
|
42
|
+
OathResults = Data.define(:version, :oath_path, :source_hash, :examples)
|
|
43
|
+
|
|
44
|
+
# Projection of OathResults onto the JSON shape of .varar/<oath_path>.json.
|
|
45
|
+
#
|
|
46
|
+
# The wire format is the TypeScript one (ADR 0014): camelCase names,
|
|
47
|
+
# declaration order, and the optional members absent rather than null so a
|
|
48
|
+
# reader that predates them still parses the file. Pure — writing the file
|
|
49
|
+
# is the shell's job.
|
|
50
|
+
module Results
|
|
51
|
+
module_function
|
|
52
|
+
|
|
53
|
+
def to_wire(results)
|
|
54
|
+
{
|
|
55
|
+
'version' => results.version,
|
|
56
|
+
'oathPath' => results.oath_path,
|
|
57
|
+
'sourceHash' => results.source_hash,
|
|
58
|
+
'examples' => results.examples.map { |e| example_to_wire(e) }
|
|
59
|
+
}
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def example_to_wire(example)
|
|
63
|
+
out = { 'name' => example.name, 'status' => example.status, 'lines' => example.lines.to_a }
|
|
64
|
+
out['failure'] = failure_to_wire(example.failure) if example.failure
|
|
65
|
+
out
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def failure_to_wire(failure)
|
|
69
|
+
out = { 'line' => failure.line, 'message' => failure.message, 'stack' => failure.stack }
|
|
70
|
+
if failure.cells && !failure.cells.empty?
|
|
71
|
+
out['cells'] = failure.cells.map { |c| { 'from' => c.from, 'to' => c.to, 'actual' => c.actual } }
|
|
72
|
+
end
|
|
73
|
+
out['anchor'] = { 'from' => failure.anchor.from, 'to' => failure.anchor.to } if failure.anchor
|
|
74
|
+
out
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
data/lib/varar/core.rb
CHANGED
|
@@ -4,7 +4,7 @@ module Varar
|
|
|
4
4
|
# The pure functional core: parse, match, plan, execute, diffs, drift, and
|
|
5
5
|
# the conformance projections. No filesystem, network, globals, or time.
|
|
6
6
|
module Core
|
|
7
|
-
VERSION = '0.
|
|
7
|
+
VERSION = '0.8.0'
|
|
8
8
|
end
|
|
9
9
|
end
|
|
10
10
|
|
|
@@ -24,6 +24,8 @@ require 'varar/core/plan'
|
|
|
24
24
|
require 'varar/core/doc_string_diff'
|
|
25
25
|
require 'varar/core/param_diff'
|
|
26
26
|
require 'varar/core/failure_anchor'
|
|
27
|
+
require 'varar/core/result'
|
|
28
|
+
require 'varar/core/failure'
|
|
27
29
|
require 'varar/core/execute'
|
|
28
30
|
require 'varar/core/hash'
|
|
29
31
|
require 'varar/core/drift'
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: varar-core
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.8.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Aslak Hellesøy
|
|
@@ -40,6 +40,7 @@ files:
|
|
|
40
40
|
- lib/varar/core/doc_string_diff.rb
|
|
41
41
|
- lib/varar/core/drift.rb
|
|
42
42
|
- lib/varar/core/execute.rb
|
|
43
|
+
- lib/varar/core/failure.rb
|
|
43
44
|
- lib/varar/core/failure_anchor.rb
|
|
44
45
|
- lib/varar/core/hash.rb
|
|
45
46
|
- lib/varar/core/matcher.rb
|
|
@@ -47,6 +48,7 @@ files:
|
|
|
47
48
|
- lib/varar/core/parse.rb
|
|
48
49
|
- lib/varar/core/plan.rb
|
|
49
50
|
- lib/varar/core/registry.rb
|
|
51
|
+
- lib/varar/core/result.rb
|
|
50
52
|
- lib/varar/core/scanner.rb
|
|
51
53
|
- lib/varar/core/sentences.rb
|
|
52
54
|
- lib/varar/core/span.rb
|