polyrun 2.2.9 → 3.0.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/CHANGELOG.md +17 -8
- data/README.md +7 -6
- data/docs/SETUP_PROFILE.md +2 -1
- data/lib/polyrun/cli/ci_shard_run_command.rb +31 -0
- data/lib/polyrun/cli/default_run.rb +10 -3
- data/lib/polyrun/cli/help.rb +5 -3
- data/lib/polyrun/cli/run_shards_command.rb +17 -0
- data/lib/polyrun/cli.rb +5 -2
- data/lib/polyrun/coverage/collector.rb +21 -49
- data/lib/polyrun/coverage/collector_blob.rb +53 -0
- data/lib/polyrun/data/parallel_provisioning.rb +2 -2
- data/lib/polyrun/hooks/dsl.rb +6 -6
- data/lib/polyrun/{minitest.rb → minitest_support.rb} +4 -4
- data/lib/polyrun/partition/paths.rb +95 -48
- data/lib/polyrun/partition/suite.rb +10 -6
- data/lib/polyrun/quick/config.rb +56 -0
- data/lib/polyrun/quick/example_group.rb +49 -4
- data/lib/polyrun/quick/example_runner.rb +30 -7
- data/lib/polyrun/quick/matchers.rb +6 -3
- data/lib/polyrun/quick/matchers_predicates.rb +122 -0
- data/lib/polyrun/quick/reporter.rb +40 -6
- data/lib/polyrun/quick/runner.rb +30 -16
- data/lib/polyrun/reporting/rspec_failure_fragment_formatter.rb +1 -1
- data/lib/polyrun/reporting/rspec_junit.rb +2 -2
- data/lib/polyrun/{rspec → rspec_support}/active_record_timeout_recovery.rb +1 -1
- data/lib/polyrun/{rspec → rspec_support}/example_debug.rb +1 -1
- data/lib/polyrun/{rspec → rspec_support}/example_debug_instrumentation.rb +1 -1
- data/lib/polyrun/{rspec → rspec_support}/sharded_formatter_compat.rb +1 -1
- data/lib/polyrun/{rspec.rb → rspec_support.rb} +12 -12
- data/lib/polyrun/sus_support.rb +55 -0
- data/lib/polyrun/templates/POLYRUN.md +1 -1
- data/lib/polyrun/timing/rspec_example_formatter.rb +2 -2
- data/lib/polyrun/version.rb +1 -1
- data/lib/polyrun/worker_ping.rb +9 -6
- data/sig/polyrun/{minitest.rbs → minitest_support.rbs} +1 -1
- data/sig/polyrun/{rspec.rbs → rspec_support.rbs} +1 -1
- data/sig/polyrun/sus_support.rbs +7 -0
- metadata +14 -9
|
@@ -2,18 +2,24 @@ module Polyrun
|
|
|
2
2
|
module Partition
|
|
3
3
|
# Shared spec path listing for run-shards, queue init, and plan helpers.
|
|
4
4
|
module Paths
|
|
5
|
+
SUS_CONFIG_REL = "config/sus.rb"
|
|
6
|
+
SUS_TEST_GLOB = "test/**/*.rb"
|
|
7
|
+
|
|
5
8
|
module_function
|
|
6
9
|
|
|
7
10
|
def read_lines(path)
|
|
8
11
|
File.read(File.expand_path(path.to_s, Dir.pwd)).split("\n").map(&:strip).reject(&:empty?)
|
|
9
12
|
end
|
|
10
13
|
|
|
11
|
-
# Prefer +spec/+ RSpec files, then +test
|
|
12
|
-
#
|
|
14
|
+
# Prefer +spec/+ RSpec files, then Sus (+config/sus.rb+ + +test/**/*.rb+), then +test/+ Minitest,
|
|
15
|
+
# then Polyrun Quick files (same globs as +polyrun quick+).
|
|
13
16
|
def detect_auto_suite(cwd = Dir.pwd)
|
|
14
17
|
base = File.expand_path(cwd)
|
|
15
18
|
return :rspec if Dir.glob(File.join(base, "spec/**/*_spec.rb")).any?
|
|
16
19
|
|
|
20
|
+
sus = sus_test_paths(base)
|
|
21
|
+
return :sus if sus_configured?(base) && sus.any?
|
|
22
|
+
|
|
17
23
|
return :minitest if Dir.glob(File.join(base, "test/**/*_test.rb")).any?
|
|
18
24
|
|
|
19
25
|
quick = quick_parallel_default_paths(base)
|
|
@@ -22,32 +28,25 @@ module Polyrun
|
|
|
22
28
|
nil
|
|
23
29
|
end
|
|
24
30
|
|
|
25
|
-
# Infer parallel suite from explicit paths
|
|
31
|
+
# Infer parallel suite from explicit paths.
|
|
26
32
|
# Recognizes RSpec-style +path:line+ locators (strips a trailing numeric locator before classifying).
|
|
27
|
-
# Returns +:rspec+, +:minitest+, +:quick+, +:invalid+ (mixed
|
|
33
|
+
# Returns +:rspec+, +:minitest+, +:sus+, +:quick+, +:invalid+ (mixed families), or +nil+ (empty).
|
|
28
34
|
def infer_suite_from_paths(paths)
|
|
29
|
-
|
|
30
|
-
return nil if
|
|
31
|
-
|
|
32
|
-
specs = paths.count { |p| File.basename(p).end_with?("_spec.rb") }
|
|
33
|
-
tests = paths.count { |p| File.basename(p).end_with?("_test.rb") }
|
|
34
|
-
return :invalid if specs.positive? && tests.positive?
|
|
35
|
-
|
|
36
|
-
return :rspec if specs.positive?
|
|
37
|
-
return :minitest if tests.positive?
|
|
35
|
+
families = Array(paths).map { |p| suite_family_for_path(p) }.compact
|
|
36
|
+
return nil if families.empty?
|
|
38
37
|
|
|
39
|
-
|
|
40
|
-
return :
|
|
38
|
+
uniq = families.uniq
|
|
39
|
+
return :invalid if uniq.size > 1
|
|
41
40
|
|
|
42
|
-
|
|
41
|
+
uniq.first
|
|
43
42
|
end
|
|
44
43
|
|
|
45
44
|
# When +paths_file+ is set but missing, returns +{ error: "..." }+.
|
|
46
45
|
# Otherwise returns +{ items:, source: }+ (human-readable source label).
|
|
47
46
|
#
|
|
48
|
-
# +partition.suite+ (optional): +auto+ (default), +rspec+, +minitest+, +quick+ — used when resolving
|
|
47
|
+
# +partition.suite+ (optional): +auto+ (default), +rspec+, +minitest+, +sus+, +quick+ — used when resolving
|
|
49
48
|
# from globs. Legacy +spec/spec_paths.txt+ is used only for +auto+/+rspec+ (RSpec-oriented); an
|
|
50
|
-
# explicit +minitest+/+quick+ suite skips it and globs instead. Bare +polyrun+ suite selection is
|
|
49
|
+
# explicit +minitest+/+sus+/+quick+ suite skips it and globs instead. Bare +polyrun+ suite selection is
|
|
51
50
|
# {Suite.resolve_default} (explicit suite → paths_file inference → {detect_auto_suite}).
|
|
52
51
|
def resolve_run_shard_items(paths_file: nil, cwd: Dir.pwd, partition: {})
|
|
53
52
|
if paths_file
|
|
@@ -64,50 +63,88 @@ module Polyrun
|
|
|
64
63
|
end
|
|
65
64
|
|
|
66
65
|
def resolve_run_shard_items_glob(cwd:, partition: {})
|
|
67
|
-
suite = (partition
|
|
68
|
-
|
|
66
|
+
suite = partition_suite_name(partition)
|
|
67
|
+
catalogs = shard_path_catalogs(File.expand_path(cwd))
|
|
68
|
+
case suite
|
|
69
|
+
when "rspec", "minitest", "sus", "quick"
|
|
70
|
+
resolve_named_suite_items(suite, catalogs)
|
|
71
|
+
when "auto"
|
|
72
|
+
resolve_auto_suite_items(catalogs)
|
|
73
|
+
else
|
|
74
|
+
{error: "unknown partition.suite: #{suite.inspect} (expected auto, rspec, minitest, sus, quick)"}
|
|
75
|
+
end
|
|
76
|
+
end
|
|
69
77
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
78
|
+
def partition_suite_name(partition)
|
|
79
|
+
suite = (partition["suite"] || partition[:suite] || "auto").to_s.downcase
|
|
80
|
+
suite.empty? ? "auto" : suite
|
|
81
|
+
end
|
|
82
|
+
private_class_method :partition_suite_name
|
|
83
|
+
|
|
84
|
+
def shard_path_catalogs(base)
|
|
85
|
+
{
|
|
86
|
+
base: base,
|
|
87
|
+
spec: Dir.glob(File.join(base, "spec/**/*_spec.rb")).sort,
|
|
88
|
+
test: Dir.glob(File.join(base, "test/**/*_test.rb")).sort,
|
|
89
|
+
sus: sus_test_paths(base),
|
|
90
|
+
quick: quick_parallel_default_paths(base)
|
|
91
|
+
}
|
|
92
|
+
end
|
|
93
|
+
private_class_method :shard_path_catalogs
|
|
74
94
|
|
|
95
|
+
def resolve_named_suite_items(suite, catalogs)
|
|
75
96
|
case suite
|
|
76
97
|
when "rspec"
|
|
77
|
-
return {error: "partition.suite is rspec but no spec/**/*_spec.rb files"} if spec.empty?
|
|
98
|
+
return {error: "partition.suite is rspec but no spec/**/*_spec.rb files"} if catalogs[:spec].empty?
|
|
78
99
|
|
|
79
|
-
{items: spec, source: "spec/**/*_spec.rb glob"}
|
|
100
|
+
{items: catalogs[:spec], source: "spec/**/*_spec.rb glob"}
|
|
80
101
|
when "minitest"
|
|
81
|
-
return {error: "partition.suite is minitest but no test/**/*_test.rb files"} if test.empty?
|
|
102
|
+
return {error: "partition.suite is minitest but no test/**/*_test.rb files"} if catalogs[:test].empty?
|
|
82
103
|
|
|
83
|
-
{items: test, source: "test/**/*_test.rb glob"}
|
|
84
|
-
when "
|
|
85
|
-
return {error: "partition.suite is
|
|
104
|
+
{items: catalogs[:test], source: "test/**/*_test.rb glob"}
|
|
105
|
+
when "sus"
|
|
106
|
+
return {error: "partition.suite is sus but no test/**/*.rb files"} if catalogs[:sus].empty?
|
|
86
107
|
|
|
87
|
-
{items:
|
|
88
|
-
|
|
89
|
-
if
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
108
|
+
{items: catalogs[:sus], source: "test/**/*.rb glob (sus)"}
|
|
109
|
+
else
|
|
110
|
+
return {error: "partition.suite is quick but no Polyrun quick files under spec/ or test/"} if catalogs[:quick].empty?
|
|
111
|
+
|
|
112
|
+
{items: catalogs[:quick], source: "Polyrun quick glob"}
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
private_class_method :resolve_named_suite_items
|
|
116
|
+
|
|
117
|
+
def resolve_auto_suite_items(catalogs)
|
|
118
|
+
if catalogs[:spec].any?
|
|
119
|
+
{items: catalogs[:spec], source: "spec/**/*_spec.rb glob"}
|
|
120
|
+
elsif sus_configured?(catalogs[:base]) && catalogs[:sus].any?
|
|
121
|
+
{items: catalogs[:sus], source: "test/**/*.rb glob (sus)"}
|
|
122
|
+
elsif catalogs[:test].any?
|
|
123
|
+
{items: catalogs[:test], source: "test/**/*_test.rb glob"}
|
|
124
|
+
elsif catalogs[:quick].any?
|
|
125
|
+
{items: catalogs[:quick], source: "Polyrun quick glob"}
|
|
101
126
|
else
|
|
102
|
-
{
|
|
127
|
+
{
|
|
128
|
+
error: "no spec paths (spec/spec_paths.txt, partition.paths_file, or spec/**/*_spec.rb); " \
|
|
129
|
+
"no Sus test/**/*.rb (with config/sus.rb); no test/**/*_test.rb; no Polyrun quick files"
|
|
130
|
+
}
|
|
103
131
|
end
|
|
104
132
|
end
|
|
133
|
+
private_class_method :resolve_auto_suite_items
|
|
105
134
|
|
|
106
135
|
def quick_parallel_default_paths(base)
|
|
107
136
|
require_relative "../quick/runner"
|
|
108
137
|
Polyrun::Quick::Runner.parallel_default_paths(base)
|
|
109
138
|
end
|
|
110
139
|
|
|
140
|
+
def sus_configured?(base)
|
|
141
|
+
File.file?(File.join(base, SUS_CONFIG_REL))
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def sus_test_paths(base)
|
|
145
|
+
Dir.glob(File.join(base, SUS_TEST_GLOB)).sort
|
|
146
|
+
end
|
|
147
|
+
|
|
111
148
|
# Strip trailing numeric locators (+path:line+ or +path:line:column+), then expand.
|
|
112
149
|
def suite_classify_path(path)
|
|
113
150
|
raw = path.to_s.sub(/(?::\d+)+\z/, "")
|
|
@@ -115,12 +152,22 @@ module Polyrun
|
|
|
115
152
|
end
|
|
116
153
|
private_class_method :suite_classify_path
|
|
117
154
|
|
|
155
|
+
def suite_family_for_path(path)
|
|
156
|
+
abs = suite_classify_path(path)
|
|
157
|
+
base = File.basename(abs)
|
|
158
|
+
return :rspec if base.end_with?("_spec.rb")
|
|
159
|
+
return :minitest if base.end_with?("_test.rb")
|
|
160
|
+
return :quick if abs.include?("polyrun_quick")
|
|
161
|
+
return :sus if abs.match?(%r{(?:^|/)test/})
|
|
162
|
+
|
|
163
|
+
:quick
|
|
164
|
+
end
|
|
165
|
+
private_class_method :suite_family_for_path
|
|
166
|
+
|
|
118
167
|
def use_legacy_spec_paths?(cwd, partition)
|
|
119
168
|
return false unless File.file?(File.join(cwd, "spec", "spec_paths.txt"))
|
|
120
169
|
|
|
121
|
-
|
|
122
|
-
suite = "auto" if suite.empty?
|
|
123
|
-
%w[auto rspec].include?(suite)
|
|
170
|
+
%w[auto rspec].include?(partition_suite_name(partition))
|
|
124
171
|
end
|
|
125
172
|
private_class_method :use_legacy_spec_paths?
|
|
126
173
|
end
|
|
@@ -2,15 +2,17 @@ module Polyrun
|
|
|
2
2
|
module Partition
|
|
3
3
|
# Chooses the parallel runner suite for bare +polyrun+ and validates command vs path lists.
|
|
4
4
|
module Suite
|
|
5
|
+
ALLOWED = %w[auto rspec minitest sus quick].freeze
|
|
6
|
+
|
|
5
7
|
module_function
|
|
6
8
|
|
|
7
9
|
# Resolve suite for default CLI dispatch.
|
|
8
10
|
# Order: explicit +partition.suite+ (not auto) → infer from +paths_file+ → +detect_auto_suite+.
|
|
9
|
-
# Returns +{ suite: :rspec|:minitest|:quick }+ or +{ error: "..." }+.
|
|
11
|
+
# Returns +{ suite: :rspec|:minitest|:sus|:quick }+ or +{ error: "..." }+.
|
|
10
12
|
def resolve_default(cwd: Dir.pwd, partition: {})
|
|
11
13
|
configured = configured_suite(partition)
|
|
12
|
-
return {error: "unknown partition.suite: #{configured.inspect} (expected auto, rspec, minitest, quick)"} unless
|
|
13
|
-
|
|
14
|
+
return {error: "unknown partition.suite: #{configured.inspect} (expected auto, rspec, minitest, sus, quick)"} unless
|
|
15
|
+
ALLOWED.include?(configured)
|
|
14
16
|
|
|
15
17
|
inferred = infer_from_paths_file(cwd, partition)
|
|
16
18
|
return inferred if inferred.is_a?(Hash) && inferred[:error]
|
|
@@ -32,7 +34,8 @@ module Polyrun
|
|
|
32
34
|
return {suite: auto} if auto
|
|
33
35
|
|
|
34
36
|
{
|
|
35
|
-
error: "no tests found (spec/**/*_spec.rb,
|
|
37
|
+
error: "no tests found (spec/**/*_spec.rb, Sus config/sus.rb + test/**/*.rb, " \
|
|
38
|
+
"test/**/*_test.rb, or Polyrun quick files). See polyrun help."
|
|
36
39
|
}
|
|
37
40
|
end
|
|
38
41
|
|
|
@@ -41,6 +44,7 @@ module Polyrun
|
|
|
41
44
|
return nil if tokens.empty?
|
|
42
45
|
|
|
43
46
|
return :rspec if tokens.any? { |t| File.basename(t) == "rspec" }
|
|
47
|
+
return :sus if tokens.any? { |t| %w[sus sus-parallel].include?(File.basename(t)) }
|
|
44
48
|
return :quick if tokens.include?("quick") && tokens.any? { |t| File.basename(t).include?("polyrun") }
|
|
45
49
|
return :minitest if minitest_command?(tokens)
|
|
46
50
|
|
|
@@ -54,7 +58,7 @@ module Polyrun
|
|
|
54
58
|
if path_suite == :invalid
|
|
55
59
|
return nil unless cmd_suite
|
|
56
60
|
|
|
57
|
-
return "mixing
|
|
61
|
+
return "mixing path families is not supported with a #{cmd_suite} command " \
|
|
58
62
|
"(use a suite-agnostic custom command, or split the path list)"
|
|
59
63
|
end
|
|
60
64
|
|
|
@@ -81,7 +85,7 @@ module Polyrun
|
|
|
81
85
|
|
|
82
86
|
items = Paths.read_lines(abs)
|
|
83
87
|
inferred = Paths.infer_suite_from_paths(items)
|
|
84
|
-
return {error: "paths_file mixes
|
|
88
|
+
return {error: "paths_file mixes path families (not supported in one run)"} if inferred == :invalid
|
|
85
89
|
|
|
86
90
|
inferred
|
|
87
91
|
end
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
module Polyrun
|
|
2
|
+
module Quick
|
|
3
|
+
# Optional host file +config/polyrun_quick.rb+ evaluated before Quick examples run.
|
|
4
|
+
class Config
|
|
5
|
+
PATH = "config/polyrun_quick.rb"
|
|
6
|
+
|
|
7
|
+
attr_reader :before_suite_hooks, :after_suite_hooks, :load_paths
|
|
8
|
+
|
|
9
|
+
def self.load!(root: Dir.pwd)
|
|
10
|
+
cfg = new
|
|
11
|
+
path = File.join(File.expand_path(root), PATH)
|
|
12
|
+
return cfg unless File.file?(path)
|
|
13
|
+
|
|
14
|
+
cfg.instance_eval(File.read(path), path)
|
|
15
|
+
cfg
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def initialize
|
|
19
|
+
@before_suite_hooks = []
|
|
20
|
+
@after_suite_hooks = []
|
|
21
|
+
@load_paths = []
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def before_suite(&block)
|
|
25
|
+
@before_suite_hooks << block if block
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def after_suite(&block)
|
|
29
|
+
@after_suite_hooks << block if block
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def add_load_path(path)
|
|
33
|
+
@load_paths << path.to_s
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def apply_load_paths!(root:)
|
|
37
|
+
base = File.expand_path(root)
|
|
38
|
+
@load_paths.each do |rel|
|
|
39
|
+
abs = File.expand_path(rel, base)
|
|
40
|
+
next unless File.directory?(abs)
|
|
41
|
+
next if $LOAD_PATH.include?(abs)
|
|
42
|
+
|
|
43
|
+
$LOAD_PATH.unshift(abs)
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def run_before_suite!
|
|
48
|
+
@before_suite_hooks.each(&:call)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def run_after_suite!(status)
|
|
52
|
+
@after_suite_hooks.each { |h| h.call(status) }
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
@@ -1,18 +1,22 @@
|
|
|
1
1
|
module Polyrun
|
|
2
2
|
module Quick
|
|
3
|
-
# One +describe+ block (possibly nested). Holds
|
|
3
|
+
# One +describe+ / +with+ block (possibly nested). Holds examples and hooks.
|
|
4
4
|
class ExampleGroup
|
|
5
|
-
attr_reader :name, :parent, :children, :examples, :before_hooks, :after_hooks, :
|
|
5
|
+
attr_reader :name, :parent, :children, :examples, :before_hooks, :after_hooks, :around_hooks,
|
|
6
|
+
:lets, :let_bang_order, :described_target
|
|
6
7
|
|
|
7
|
-
def initialize(name, parent: nil)
|
|
8
|
-
@name = name
|
|
8
|
+
def initialize(name, parent: nil, described_target: nil)
|
|
9
|
+
@name = name_label(name)
|
|
10
|
+
@described_target = described_target.nil? ? infer_described_target(name) : described_target
|
|
9
11
|
@parent = parent
|
|
10
12
|
@children = []
|
|
11
13
|
@examples = []
|
|
12
14
|
@before_hooks = []
|
|
13
15
|
@after_hooks = []
|
|
16
|
+
@around_hooks = []
|
|
14
17
|
@lets = {}
|
|
15
18
|
@let_bang_order = []
|
|
19
|
+
maybe_default_subject!
|
|
16
20
|
end
|
|
17
21
|
|
|
18
22
|
def full_name
|
|
@@ -28,6 +32,8 @@ module Polyrun
|
|
|
28
32
|
child
|
|
29
33
|
end
|
|
30
34
|
|
|
35
|
+
alias_method :with, :describe
|
|
36
|
+
|
|
31
37
|
def it(description, &block)
|
|
32
38
|
@examples << [description.to_s, block]
|
|
33
39
|
end
|
|
@@ -42,6 +48,10 @@ module Polyrun
|
|
|
42
48
|
@after_hooks << block
|
|
43
49
|
end
|
|
44
50
|
|
|
51
|
+
def around(&block)
|
|
52
|
+
@around_hooks << block
|
|
53
|
+
end
|
|
54
|
+
|
|
45
55
|
def let(name, &block)
|
|
46
56
|
@lets[name.to_sym] = block
|
|
47
57
|
end
|
|
@@ -52,6 +62,15 @@ module Polyrun
|
|
|
52
62
|
@let_bang_order << sym
|
|
53
63
|
end
|
|
54
64
|
|
|
65
|
+
def subject(&block)
|
|
66
|
+
if block
|
|
67
|
+
let(:subject, &block)
|
|
68
|
+
else
|
|
69
|
+
# Used from examples via ExampleRunner method; group-level call without block is invalid.
|
|
70
|
+
raise ArgumentError, "subject requires a block at describe time"
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
55
74
|
def each_example_with_ancestors(ancestors = [], &visitor)
|
|
56
75
|
chain = ancestors + [self]
|
|
57
76
|
@examples.each do |desc, block|
|
|
@@ -61,6 +80,32 @@ module Polyrun
|
|
|
61
80
|
child.each_example_with_ancestors(chain, &visitor)
|
|
62
81
|
end
|
|
63
82
|
end
|
|
83
|
+
|
|
84
|
+
private
|
|
85
|
+
|
|
86
|
+
def name_label(name)
|
|
87
|
+
return name.to_s unless name.is_a?(Module)
|
|
88
|
+
|
|
89
|
+
name.name || name.to_s
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def infer_described_target(name)
|
|
93
|
+
name.is_a?(Module) ? name : nil
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def maybe_default_subject!
|
|
97
|
+
return unless @described_target
|
|
98
|
+
return if @lets.key?(:subject)
|
|
99
|
+
|
|
100
|
+
target = @described_target
|
|
101
|
+
@lets[:subject] = -> {
|
|
102
|
+
if target.respond_to?(:new)
|
|
103
|
+
target.new
|
|
104
|
+
else
|
|
105
|
+
target
|
|
106
|
+
end
|
|
107
|
+
}
|
|
108
|
+
end
|
|
64
109
|
end
|
|
65
110
|
end
|
|
66
111
|
end
|
|
@@ -26,17 +26,21 @@ module Polyrun
|
|
|
26
26
|
qloc = quick_example_location(block)
|
|
27
27
|
Polyrun::WorkerPing.ping!(location: qloc)
|
|
28
28
|
Polyrun::SpecQuality.start_example!(location: qloc) if Polyrun::SpecQuality.started?
|
|
29
|
+
error = nil
|
|
30
|
+
started = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
29
31
|
begin
|
|
30
32
|
run_before_hooks_from_chain(ancestor_chain)
|
|
31
|
-
instance_eval(&block)
|
|
32
|
-
@reporter.pass(group_name, description)
|
|
33
|
+
invoke_arounds(ancestor_chain.flat_map(&:around_hooks), 0) { instance_eval(&block) }
|
|
34
|
+
@reporter.pass(group_name, description, duration_seconds: elapsed_since(started))
|
|
33
35
|
rescue AssertionFailed => e
|
|
34
|
-
|
|
36
|
+
error = e
|
|
37
|
+
@reporter.fail(group_name, description, e, duration_seconds: elapsed_since(started))
|
|
35
38
|
rescue => e
|
|
36
|
-
|
|
39
|
+
error = e
|
|
40
|
+
@reporter.error(group_name, description, e, duration_seconds: elapsed_since(started))
|
|
37
41
|
ensure
|
|
38
42
|
Polyrun::SpecQuality.finish_example!(location: qloc) if Polyrun::SpecQuality.started?
|
|
39
|
-
run_after_hooks_from_chain(ancestor_chain)
|
|
43
|
+
run_after_hooks_from_chain(ancestor_chain, error: error)
|
|
40
44
|
reset_capybara_if_enabled!
|
|
41
45
|
@_let_cache = {}
|
|
42
46
|
Polyrun::WorkerPing.ping!(location: qloc)
|
|
@@ -45,6 +49,10 @@ module Polyrun
|
|
|
45
49
|
|
|
46
50
|
private
|
|
47
51
|
|
|
52
|
+
def elapsed_since(started)
|
|
53
|
+
Process.clock_gettime(Process::CLOCK_MONOTONIC) - started
|
|
54
|
+
end
|
|
55
|
+
|
|
48
56
|
def quick_example_location(block)
|
|
49
57
|
loc = block&.source_location
|
|
50
58
|
loc ? "#{loc[0]}:#{loc[1]}" : nil
|
|
@@ -79,10 +87,25 @@ module Polyrun
|
|
|
79
87
|
end
|
|
80
88
|
end
|
|
81
89
|
|
|
82
|
-
def run_after_hooks_from_chain(ancestor_chain)
|
|
90
|
+
def run_after_hooks_from_chain(ancestor_chain, error: nil)
|
|
83
91
|
ancestor_chain.reverse_each do |g|
|
|
84
|
-
g.after_hooks.reverse_each { |h|
|
|
92
|
+
g.after_hooks.reverse_each { |h| instance_exec(error, &h) }
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def invoke_arounds(arounds, index, &body)
|
|
97
|
+
if index >= arounds.size
|
|
98
|
+
body.call
|
|
99
|
+
return
|
|
85
100
|
end
|
|
101
|
+
|
|
102
|
+
called = false
|
|
103
|
+
run = lambda {
|
|
104
|
+
called = true
|
|
105
|
+
invoke_arounds(arounds, index + 1, &body)
|
|
106
|
+
}
|
|
107
|
+
instance_exec(run, &arounds[index])
|
|
108
|
+
raise AssertionFailed, "around hook did not run the example" unless called
|
|
86
109
|
end
|
|
87
110
|
|
|
88
111
|
def extend_capybara_if_enabled!
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
require_relative "errors"
|
|
2
|
+
require_relative "matchers_predicates"
|
|
2
3
|
|
|
3
4
|
module Polyrun
|
|
4
5
|
module Quick
|
|
5
|
-
# Minimal +expect(x).to …+ chain
|
|
6
|
+
# Minimal +expect(x).to …+ / +expect { }.to …+ chain without external gems.
|
|
6
7
|
class Expectation
|
|
7
8
|
def initialize(actual)
|
|
8
9
|
@actual = actual
|
|
@@ -22,8 +23,10 @@ module Polyrun
|
|
|
22
23
|
end
|
|
23
24
|
|
|
24
25
|
module Matchers
|
|
25
|
-
|
|
26
|
-
|
|
26
|
+
include MatchersPredicates
|
|
27
|
+
|
|
28
|
+
def expect(actual = nil, &block)
|
|
29
|
+
Expectation.new(block_given? ? block : actual)
|
|
27
30
|
end
|
|
28
31
|
|
|
29
32
|
def eq(expected)
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
module Polyrun
|
|
2
|
+
module Quick
|
|
3
|
+
# Predicate-style matchers (+be ==+, +be_a+, +raise_exception+) for Quick expectations.
|
|
4
|
+
module MatchersPredicates
|
|
5
|
+
def be
|
|
6
|
+
BeMatcher.new
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def be_a(klass)
|
|
10
|
+
BeAMatcher.new(klass)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
alias_method :be_kind_of, :be_a
|
|
14
|
+
|
|
15
|
+
def raise_exception(exception_class = StandardError, message: nil)
|
|
16
|
+
RaiseExceptionMatcher.new(exception_class, message: message)
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
class BeMatcher
|
|
21
|
+
def ==(other)
|
|
22
|
+
EqMatcher.new(other)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def >(other)
|
|
26
|
+
CompareMatcher.new(:>, other)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def >=(other)
|
|
30
|
+
CompareMatcher.new(:>=, other)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def <(other)
|
|
34
|
+
CompareMatcher.new(:<, other)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def <=(other)
|
|
38
|
+
CompareMatcher.new(:<=, other)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
class CompareMatcher
|
|
43
|
+
def initialize(op, expected)
|
|
44
|
+
@op = op
|
|
45
|
+
@expected = expected
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def matches?(actual)
|
|
49
|
+
actual.public_send(@op, @expected)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def does_not_match?(actual)
|
|
53
|
+
!matches?(actual)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def failure_message(actual)
|
|
57
|
+
"expected #{actual.inspect} #{@op} #{@expected.inspect}"
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def failure_message_when_negated(actual)
|
|
61
|
+
"expected #{actual.inspect} not #{@op} #{@expected.inspect}"
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
class BeAMatcher
|
|
66
|
+
def initialize(klass)
|
|
67
|
+
@klass = klass
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def matches?(actual)
|
|
71
|
+
actual.is_a?(@klass)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def does_not_match?(actual)
|
|
75
|
+
!matches?(actual)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def failure_message(actual)
|
|
79
|
+
"expected #{actual.inspect} to be a #{@klass}"
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def failure_message_when_negated(actual)
|
|
83
|
+
"expected #{actual.inspect} not to be a #{@klass}"
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
class RaiseExceptionMatcher
|
|
88
|
+
def initialize(exception_class, message: nil)
|
|
89
|
+
@exception_class = exception_class
|
|
90
|
+
@message = message
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def matches?(actual)
|
|
94
|
+
return false unless actual.respond_to?(:call)
|
|
95
|
+
|
|
96
|
+
begin
|
|
97
|
+
actual.call
|
|
98
|
+
false
|
|
99
|
+
rescue @exception_class => e
|
|
100
|
+
return true if @message.nil?
|
|
101
|
+
return true if @message.is_a?(Regexp) && @message.match?(e.message)
|
|
102
|
+
|
|
103
|
+
e.message == @message.to_s
|
|
104
|
+
rescue
|
|
105
|
+
false
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def does_not_match?(actual)
|
|
110
|
+
!matches?(actual)
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def failure_message(actual)
|
|
114
|
+
"expected block to raise #{@exception_class}"
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def failure_message_when_negated(actual)
|
|
118
|
+
"expected block not to raise #{@exception_class}"
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
end
|