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 +7 -0
- data/exe/varar +6 -0
- data/lib/varar/runner/baseline_store.rb +28 -0
- data/lib/varar/runner/cli.rb +126 -0
- data/lib/varar/runner/discovery.rb +71 -0
- data/lib/varar/runner/render.rb +33 -0
- data/lib/varar/runner/run.rb +33 -0
- data/lib/varar/runner/steps.rb +25 -0
- data/lib/varar/runner.rb +20 -0
- metadata +79 -0
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,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
|
data/lib/varar/runner.rb
ADDED
|
@@ -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: []
|