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