oselvar-var-runner 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/exe/var +6 -0
- data/lib/oselvar/var/runner/baseline_store.rb +30 -0
- data/lib/oselvar/var/runner/cli.rb +128 -0
- data/lib/oselvar/var/runner/discovery.rb +73 -0
- data/lib/oselvar/var/runner/render.rb +35 -0
- data/lib/oselvar/var/runner/run.rb +35 -0
- data/lib/oselvar/var/runner/steps.rb +27 -0
- data/lib/oselvar/var/runner.rb +22 -0
- metadata +82 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: e42590bbc4c12415be6ca340782799736446b9b4726bcfeb267b739299857374
|
|
4
|
+
data.tar.gz: 1662affcd72ef5dceda9fe1943b83f70c96bd16031f84a969ddd60424c813b61
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: b9c8dbc815fe1b16987c83b8ed26e180453c7b814088c5fc299d6c2d2048640767d44965b9bfd9aa89246c220d51b343377e7fada72d17211d4cc4d058bb192f
|
|
7
|
+
data.tar.gz: 5c12fd37536504b8c0cdb3303ec86b8971d7132023be2d107c2b495ce2d0915d42d64f369fce7e72f4dea70db53fbdb174961c9310868d75bf2987839b163b56
|
data/exe/var
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Oselvar
|
|
4
|
+
module Var
|
|
5
|
+
module Runner
|
|
6
|
+
# The filesystem BaselineStore: the committed drift baseline lives at the
|
|
7
|
+
# project root as var.lock.json. The core owns the format; this adapter
|
|
8
|
+
# only moves raw text. Port of baseline_store.py.
|
|
9
|
+
class FileBaselineStore
|
|
10
|
+
def initialize(root)
|
|
11
|
+
@path = File.join(root.to_s, 'var.lock.json')
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def read
|
|
15
|
+
File.exist?(@path) ? File.read(@path, encoding: 'UTF-8') : nil
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def write(contents)
|
|
19
|
+
File.write(@path, contents)
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
module_function
|
|
24
|
+
|
|
25
|
+
def create_file_baseline_store(root)
|
|
26
|
+
FileBaselineStore.new(root)
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'fileutils'
|
|
4
|
+
|
|
5
|
+
module Oselvar
|
|
6
|
+
module Var
|
|
7
|
+
module Runner
|
|
8
|
+
# The `var` command-line entry point (exposed by the `exe/var`
|
|
9
|
+
# executable). Today it offers a single sub-command, `var init`, which
|
|
10
|
+
# scaffolds a new project: a `var.config.json`, one Markdown spec, its
|
|
11
|
+
# step definitions, and a framework bridge that turns the specs into
|
|
12
|
+
# RSpec examples or Minitest tests.
|
|
13
|
+
#
|
|
14
|
+
# The config, spec and steps mirror the TypeScript CLI (`@oselvar/var-cli`)
|
|
15
|
+
# so a project started with `var init` looks the same in every language;
|
|
16
|
+
# only the bridge is Ruby-specific, because RSpec/Minitest — unlike
|
|
17
|
+
# pytest — need an explicit generator call to discover the specs.
|
|
18
|
+
module CLI
|
|
19
|
+
CONFIG = <<~JSON
|
|
20
|
+
{
|
|
21
|
+
"docs": { "include": ["var-examples/**/*.md"], "exclude": [] },
|
|
22
|
+
"steps": ["var-examples/**/*.steps.rb"]
|
|
23
|
+
}
|
|
24
|
+
JSON
|
|
25
|
+
|
|
26
|
+
EXAMPLE_MD = <<~MARKDOWN
|
|
27
|
+
# Hello, BDD
|
|
28
|
+
|
|
29
|
+
Given I greet "world"
|
|
30
|
+
Then the greeting is "Hello, world!"
|
|
31
|
+
MARKDOWN
|
|
32
|
+
|
|
33
|
+
EXAMPLE_STEPS = <<~RUBY
|
|
34
|
+
# frozen_string_literal: true
|
|
35
|
+
|
|
36
|
+
require 'oselvar/var'
|
|
37
|
+
|
|
38
|
+
steps(greeting: '') do
|
|
39
|
+
stimulus('I greet {string}') { |_state, name| { greeting: "Hello, \#{name}!" } }
|
|
40
|
+
sensor('the greeting is {string}') { |state, _expected| state[:greeting] }
|
|
41
|
+
end
|
|
42
|
+
RUBY
|
|
43
|
+
|
|
44
|
+
RSPEC_BRIDGE = <<~RUBY
|
|
45
|
+
# frozen_string_literal: true
|
|
46
|
+
|
|
47
|
+
# Turn every Markdown spec matched by var.config.json into RSpec examples —
|
|
48
|
+
# one `it` per Markdown example, discovered when this file loads.
|
|
49
|
+
require 'oselvar/var/rspec'
|
|
50
|
+
|
|
51
|
+
# var.config.json lives at the project root (the parent of spec/).
|
|
52
|
+
Oselvar::Var::RSpec.generate(root: File.expand_path('..', __dir__))
|
|
53
|
+
RUBY
|
|
54
|
+
|
|
55
|
+
MINITEST_BRIDGE = <<~RUBY
|
|
56
|
+
# frozen_string_literal: true
|
|
57
|
+
|
|
58
|
+
require 'minitest/autorun'
|
|
59
|
+
require 'oselvar/var/minitest'
|
|
60
|
+
|
|
61
|
+
# Turn every Markdown spec matched by var.config.json into Minitest tests —
|
|
62
|
+
# var.config.json lives at the project root (the parent of test/).
|
|
63
|
+
Oselvar::Var::Minitest.generate_tests(Object, root: File.expand_path('..', __dir__))
|
|
64
|
+
RUBY
|
|
65
|
+
|
|
66
|
+
USAGE = <<~TEXT
|
|
67
|
+
var — scaffold and run Markdown specs
|
|
68
|
+
|
|
69
|
+
Usage:
|
|
70
|
+
var init scaffold a new project
|
|
71
|
+
TEXT
|
|
72
|
+
|
|
73
|
+
def self.main(argv, cwd: Dir.pwd, out: $stdout)
|
|
74
|
+
case argv.first
|
|
75
|
+
when 'init'
|
|
76
|
+
run_init(cwd, out)
|
|
77
|
+
else
|
|
78
|
+
out.print(USAGE)
|
|
79
|
+
argv.empty? || %w[help -h --help].include?(argv.first) ? 0 : 1
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Write the scaffold into +cwd+, skipping any file that already exists.
|
|
84
|
+
# The framework bridge matches whichever adapter gem is installed
|
|
85
|
+
# (RSpec by default).
|
|
86
|
+
def self.run_init(cwd, out, framework: detect_framework)
|
|
87
|
+
files = [
|
|
88
|
+
['var.config.json', CONFIG],
|
|
89
|
+
['var-examples/01-hello.md', EXAMPLE_MD],
|
|
90
|
+
['var-examples/steps/01-hello.steps.rb', EXAMPLE_STEPS]
|
|
91
|
+
]
|
|
92
|
+
files << if framework == :minitest
|
|
93
|
+
['test/var_test.rb', MINITEST_BRIDGE]
|
|
94
|
+
else
|
|
95
|
+
['spec/var_spec.rb', RSPEC_BRIDGE]
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
files.each do |rel, content|
|
|
99
|
+
target = File.join(cwd, rel)
|
|
100
|
+
if File.exist?(target)
|
|
101
|
+
out.puts "skipped #{rel} (already exists)"
|
|
102
|
+
next
|
|
103
|
+
end
|
|
104
|
+
FileUtils.mkdir_p(File.dirname(target))
|
|
105
|
+
File.write(target, content)
|
|
106
|
+
out.puts "created #{rel}"
|
|
107
|
+
end
|
|
108
|
+
0
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# RSpec when its adapter is installed, Minitest when only that one is,
|
|
112
|
+
# RSpec as the fallback (matching the tutorial's default track).
|
|
113
|
+
def self.detect_framework
|
|
114
|
+
return :rspec if gem_present?('oselvar-var-rspec')
|
|
115
|
+
return :minitest if gem_present?('oselvar-var-minitest')
|
|
116
|
+
|
|
117
|
+
:rspec
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def self.gem_present?(name)
|
|
121
|
+
Gem::Specification.find_all_by_name(name).any?
|
|
122
|
+
rescue StandardError
|
|
123
|
+
false
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
end
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'pathname'
|
|
4
|
+
|
|
5
|
+
module Oselvar
|
|
6
|
+
module Var
|
|
7
|
+
module Runner
|
|
8
|
+
module_function
|
|
9
|
+
|
|
10
|
+
# Translate a glob with **, *, ? to an anchored regex (PEP 428 / pathlib
|
|
11
|
+
# full_match semantics), matching the other ports' hand-rolled compiler
|
|
12
|
+
# rather than Ruby's Dir glob. Port of _glob_to_regex.
|
|
13
|
+
def glob_to_regex(pattern)
|
|
14
|
+
result = +''
|
|
15
|
+
i = 0
|
|
16
|
+
n = pattern.length
|
|
17
|
+
while i < n
|
|
18
|
+
c = pattern[i]
|
|
19
|
+
if c == '/' && pattern[i, 4] == '/**/'
|
|
20
|
+
result << '/(?:.+/)?'
|
|
21
|
+
i += 4
|
|
22
|
+
elsif c == '/' && pattern[i, 3] == '/**' && i + 3 == n
|
|
23
|
+
result << '(?:/.*)?'
|
|
24
|
+
i += 3
|
|
25
|
+
elsif c == '*' && pattern[i, 3] == '**/'
|
|
26
|
+
result << '(?:.*/)?'
|
|
27
|
+
i += 3
|
|
28
|
+
elsif c == '*' && pattern[i, 2] == '**'
|
|
29
|
+
result << '.*'
|
|
30
|
+
i += 2
|
|
31
|
+
elsif c == '*'
|
|
32
|
+
result << '[^/]*'
|
|
33
|
+
i += 1
|
|
34
|
+
elsif c == '?'
|
|
35
|
+
result << '[^/]'
|
|
36
|
+
i += 1
|
|
37
|
+
else
|
|
38
|
+
result << Regexp.escape(c)
|
|
39
|
+
i += 1
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
/\A#{result}\z/
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Relative POSIX path of +path+ within +root+, without dereferencing
|
|
46
|
+
# symlinks; yields a ../ prefix when +path+ is outside +root+.
|
|
47
|
+
def rel_posix(path, root)
|
|
48
|
+
Pathname.new(File.expand_path(path))
|
|
49
|
+
.relative_path_from(Pathname.new(File.expand_path(root))).to_s
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def matches_any?(rel, globs)
|
|
53
|
+
globs.any? { |g| glob_to_regex(g).match?(rel) }
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# True iff +path+ matches an include glob and no exclude glob.
|
|
57
|
+
def match_spec?(path, include, exclude, root)
|
|
58
|
+
rel = rel_posix(path, root)
|
|
59
|
+
matches_any?(rel, include) && !matches_any?(rel, exclude)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Existing files under +root+ matching any include glob, minus excludes; sorted.
|
|
63
|
+
def find_specs(include, exclude, root)
|
|
64
|
+
out = []
|
|
65
|
+
include.each do |g|
|
|
66
|
+
out.concat(Dir.glob(g, base: root).map { |rel| File.join(root, rel) })
|
|
67
|
+
end
|
|
68
|
+
out = out.select { |p| File.file?(p) }.uniq
|
|
69
|
+
out.reject { |p| matches_any?(rel_posix(p, root), exclude) }.sort
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'oselvar/var/core'
|
|
4
|
+
|
|
5
|
+
module Oselvar
|
|
6
|
+
module Var
|
|
7
|
+
module Runner
|
|
8
|
+
module_function
|
|
9
|
+
|
|
10
|
+
# Render a step failure as a human-readable, markdown-anchored string,
|
|
11
|
+
# dispatching on the concrete error type. Port of render.py.
|
|
12
|
+
def render_failure(error, _source, path)
|
|
13
|
+
case error
|
|
14
|
+
when Core::CellMismatchError
|
|
15
|
+
lines = ["Cell mismatch in #{path}:"]
|
|
16
|
+
failing = error.cells.reject(&:ok)
|
|
17
|
+
lines << ' (no failing cells)' if failing.empty?
|
|
18
|
+
failing.each do |cell|
|
|
19
|
+
lines << " line #{cell.span.start_line} | column '#{cell.column}' — " \
|
|
20
|
+
"expected: #{cell.expected.inspect}, actual: #{cell.actual.inspect}"
|
|
21
|
+
end
|
|
22
|
+
lines.join("\n")
|
|
23
|
+
when Core::DocStringMismatchError
|
|
24
|
+
diff = error.diff
|
|
25
|
+
"Doc string mismatch at line #{diff.span.start_line}:\n " \
|
|
26
|
+
"expected: #{diff.expected.inspect}\n actual: #{diff.actual.inspect}"
|
|
27
|
+
when Core::ReturnShapeError
|
|
28
|
+
error.message
|
|
29
|
+
else
|
|
30
|
+
"#{error.class}: #{error.message}"
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'oselvar/var/core'
|
|
4
|
+
|
|
5
|
+
module Oselvar
|
|
6
|
+
module Var
|
|
7
|
+
module Runner
|
|
8
|
+
# Collects diagnostics emitted during planning/execution.
|
|
9
|
+
class RecordingReporter
|
|
10
|
+
attr_reader :diagnostics
|
|
11
|
+
|
|
12
|
+
def initialize
|
|
13
|
+
@diagnostics = []
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def diagnostic(diagnostic)
|
|
17
|
+
@diagnostics << diagnostic
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
module_function
|
|
22
|
+
|
|
23
|
+
def plan_spec(path, source, registry)
|
|
24
|
+
Core::Plan.plan(Core::Parse.parse(path, source), registry)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Pair each PlannedExample with its lazy run closure, in plan order.
|
|
28
|
+
def examples_with_runs(execution_plan, create_context, reporter)
|
|
29
|
+
reporter_cb = ->(d) { reporter.diagnostic(d) }
|
|
30
|
+
queue = Core::Execute.collect_examples(execution_plan, create_context: create_context, reporter: reporter_cb)
|
|
31
|
+
execution_plan.examples.zip(queue).map { |example, queued| [example, queued.run] }
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'oselvar/var'
|
|
4
|
+
require 'oselvar/var/registry'
|
|
5
|
+
|
|
6
|
+
module Oselvar
|
|
7
|
+
module Var
|
|
8
|
+
module Runner
|
|
9
|
+
# The registry + per-file context factory built from loaded step files.
|
|
10
|
+
LoadedSteps = Data.define(:registry, :create_context)
|
|
11
|
+
|
|
12
|
+
module_function
|
|
13
|
+
|
|
14
|
+
# Reset the accumulator, load (execute) every step file matching
|
|
15
|
+
# +step_globs+ under +root+, and build the registry + context factory.
|
|
16
|
+
def load_steps(step_globs, root)
|
|
17
|
+
RegistryGlue.reset_builder
|
|
18
|
+
files = []
|
|
19
|
+
step_globs.each do |g|
|
|
20
|
+
files.concat(Dir.glob(g, base: root).map { |rel| File.join(root, rel) })
|
|
21
|
+
end
|
|
22
|
+
files.select { |p| File.file?(p) }.uniq.sort.each { |path| load path }
|
|
23
|
+
LoadedSteps.new(registry: RegistryGlue.build_registry, create_context: RegistryGlue.context_factory)
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'oselvar/var'
|
|
4
|
+
require 'oselvar/var/config'
|
|
5
|
+
require 'oselvar/var/core'
|
|
6
|
+
|
|
7
|
+
module Oselvar
|
|
8
|
+
module Var
|
|
9
|
+
# The imperative shell: discovery, step loading, planning, failure
|
|
10
|
+
# rendering, and the filesystem drift baseline store. Depends on the facade
|
|
11
|
+
# and config; never on a test framework. Port of var-runner.
|
|
12
|
+
module Runner
|
|
13
|
+
VERSION = '0.3.2'
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
require 'oselvar/var/runner/discovery'
|
|
19
|
+
require 'oselvar/var/runner/steps'
|
|
20
|
+
require 'oselvar/var/runner/run'
|
|
21
|
+
require 'oselvar/var/runner/render'
|
|
22
|
+
require 'oselvar/var/runner/baseline_store'
|
metadata
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: oselvar-var-runner
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.3.2
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Aslak Hellesøy
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: exe
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2026-07-07 00:00:00.000000000 Z
|
|
12
|
+
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: oselvar-var
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - '='
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: 0.3.2
|
|
20
|
+
type: :runtime
|
|
21
|
+
prerelease: false
|
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
+
requirements:
|
|
24
|
+
- - '='
|
|
25
|
+
- !ruby/object:Gem::Version
|
|
26
|
+
version: 0.3.2
|
|
27
|
+
- !ruby/object:Gem::Dependency
|
|
28
|
+
name: oselvar-var-config
|
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
|
30
|
+
requirements:
|
|
31
|
+
- - '='
|
|
32
|
+
- !ruby/object:Gem::Version
|
|
33
|
+
version: 0.3.2
|
|
34
|
+
type: :runtime
|
|
35
|
+
prerelease: false
|
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
37
|
+
requirements:
|
|
38
|
+
- - '='
|
|
39
|
+
- !ruby/object:Gem::Version
|
|
40
|
+
version: 0.3.2
|
|
41
|
+
description: Spec/step discovery, step loading, planning, failure rendering, and the
|
|
42
|
+
drift baseline store.
|
|
43
|
+
email:
|
|
44
|
+
- aslak@oselvar.com
|
|
45
|
+
executables:
|
|
46
|
+
- var
|
|
47
|
+
extensions: []
|
|
48
|
+
extra_rdoc_files: []
|
|
49
|
+
files:
|
|
50
|
+
- exe/var
|
|
51
|
+
- lib/oselvar/var/runner.rb
|
|
52
|
+
- lib/oselvar/var/runner/baseline_store.rb
|
|
53
|
+
- lib/oselvar/var/runner/cli.rb
|
|
54
|
+
- lib/oselvar/var/runner/discovery.rb
|
|
55
|
+
- lib/oselvar/var/runner/render.rb
|
|
56
|
+
- lib/oselvar/var/runner/run.rb
|
|
57
|
+
- lib/oselvar/var/runner/steps.rb
|
|
58
|
+
homepage: https://var.oselvar.com
|
|
59
|
+
licenses:
|
|
60
|
+
- MIT
|
|
61
|
+
metadata:
|
|
62
|
+
rubygems_mfa_required: 'true'
|
|
63
|
+
post_install_message:
|
|
64
|
+
rdoc_options: []
|
|
65
|
+
require_paths:
|
|
66
|
+
- lib
|
|
67
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
68
|
+
requirements:
|
|
69
|
+
- - ">="
|
|
70
|
+
- !ruby/object:Gem::Version
|
|
71
|
+
version: '3.2'
|
|
72
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
73
|
+
requirements:
|
|
74
|
+
- - ">="
|
|
75
|
+
- !ruby/object:Gem::Version
|
|
76
|
+
version: '0'
|
|
77
|
+
requirements: []
|
|
78
|
+
rubygems_version: 3.4.10
|
|
79
|
+
signing_key:
|
|
80
|
+
specification_version: 4
|
|
81
|
+
summary: Markdown-native BDD — imperative shell (discovery, loading, drift)
|
|
82
|
+
test_files: []
|