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 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,6 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require 'oselvar/var/runner/cli'
5
+
6
+ exit Oselvar::Var::Runner::CLI.main(ARGV)
@@ -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: []