rspec-sleeping_king_studios 2.7.0 → 2.8.0.rc.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.
Files changed (36) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +25 -0
  3. data/README.md +228 -9
  4. data/config/rubocop-rspec.yml +41 -0
  5. data/lib/rspec/sleeping_king_studios/concerns/example_constants.rb +107 -74
  6. data/lib/rspec/sleeping_king_studios/concerns/memoized_helpers.rb +19 -0
  7. data/lib/rspec/sleeping_king_studios/concerns/shared_example_group.rb +5 -2
  8. data/lib/rspec/sleeping_king_studios/concerns.rb +8 -3
  9. data/lib/rspec/sleeping_king_studios/configuration.rb +45 -37
  10. data/lib/rspec/sleeping_king_studios/deferred/call.rb +74 -0
  11. data/lib/rspec/sleeping_king_studios/deferred/calls/example.rb +42 -0
  12. data/lib/rspec/sleeping_king_studios/deferred/calls/example_group.rb +42 -0
  13. data/lib/rspec/sleeping_king_studios/deferred/calls/hook.rb +64 -0
  14. data/lib/rspec/sleeping_king_studios/deferred/calls/included_examples.rb +34 -0
  15. data/lib/rspec/sleeping_king_studios/deferred/calls/shared_examples.rb +41 -0
  16. data/lib/rspec/sleeping_king_studios/deferred/calls.rb +19 -0
  17. data/lib/rspec/sleeping_king_studios/deferred/consumer.rb +159 -0
  18. data/lib/rspec/sleeping_king_studios/deferred/definitions.rb +42 -0
  19. data/lib/rspec/sleeping_king_studios/deferred/dependencies.rb +138 -0
  20. data/lib/rspec/sleeping_king_studios/deferred/dsl/example_constants.rb +72 -0
  21. data/lib/rspec/sleeping_king_studios/deferred/dsl/example_groups.rb +69 -0
  22. data/lib/rspec/sleeping_king_studios/deferred/dsl/examples.rb +84 -0
  23. data/lib/rspec/sleeping_king_studios/deferred/dsl/hooks.rb +125 -0
  24. data/lib/rspec/sleeping_king_studios/deferred/dsl/memoized_helpers.rb +123 -0
  25. data/lib/rspec/sleeping_king_studios/deferred/dsl/shared_examples.rb +128 -0
  26. data/lib/rspec/sleeping_king_studios/deferred/dsl.rb +26 -0
  27. data/lib/rspec/sleeping_king_studios/deferred/examples.rb +83 -0
  28. data/lib/rspec/sleeping_king_studios/deferred/missing.rb +46 -0
  29. data/lib/rspec/sleeping_king_studios/deferred/provider.rb +164 -0
  30. data/lib/rspec/sleeping_king_studios/deferred.rb +142 -0
  31. data/lib/rspec/sleeping_king_studios/matchers/built_in/include_matcher.rb +85 -70
  32. data/lib/rspec/sleeping_king_studios/matchers/core/deep_matcher.rb +28 -23
  33. data/lib/rspec/sleeping_king_studios/sandbox.rb +105 -0
  34. data/lib/rspec/sleeping_king_studios/version.rb +4 -3
  35. data/lib/rspec/sleeping_king_studios.rb +10 -4
  36. metadata +36 -141
@@ -0,0 +1,128 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rspec/sleeping_king_studios/deferred/calls/included_examples'
4
+ require 'rspec/sleeping_king_studios/deferred/calls/shared_examples'
5
+ require 'rspec/sleeping_king_studios/deferred/dsl'
6
+
7
+ module RSpec::SleepingKingStudios::Deferred::Dsl # rubocop:disable Style/Documentation
8
+ # Methods for defining and including deferred shared example groups.
9
+ module SharedExamples
10
+ # Meta-methods for defining deferred examples.
11
+ module Macros
12
+ # Registers a method for deferring including a shared example group.
13
+ #
14
+ # @param method_name [String, Symbol] the name of the deferred method.
15
+ #
16
+ # @return [void]
17
+ def define_included_examples_method(method_name) # rubocop:disable Metrics/MethodLength
18
+ define_method(method_name) do |name, *args, **kwargs, &block|
19
+ deferred_calls <<
20
+ RSpec::SleepingKingStudios::Deferred::Calls::IncludedExamples.new(
21
+ method_name,
22
+ name,
23
+ *args,
24
+ **kwargs,
25
+ &block
26
+ )
27
+
28
+ nil
29
+ end
30
+ end
31
+
32
+ # Registers a method for deferring a shared example group.
33
+ #
34
+ # @param method_name [String, Symbol] the name of the deferred method.
35
+ #
36
+ # @return [void]
37
+ def define_shared_examples_method(method_name) # rubocop:disable Metrics/MethodLength
38
+ define_method(method_name) do |name, *args, **kwargs, &block|
39
+ deferred_calls <<
40
+ RSpec::SleepingKingStudios::Deferred::Calls::SharedExamples.new(
41
+ method_name,
42
+ name,
43
+ *args,
44
+ **kwargs,
45
+ &block
46
+ )
47
+
48
+ nil
49
+ end
50
+ end
51
+ end
52
+
53
+ extend Macros
54
+
55
+ # @!macro [new] define_included_examples_method
56
+ # @!method $1(name, *flags, **metadata, &block)
57
+ # Defines a deferred included example group using the $1 method.
58
+ #
59
+ # @param name [String, Symbol, Module] the name for the included example
60
+ # group.
61
+ # @param flags [Array<Symbol>] metadata flags for the included example
62
+ # group.
63
+ # @param metadata [Hash] metadata for the included example group.
64
+ # @param block [Proc] the implementation of the included example group.
65
+ #
66
+ # @return [void]
67
+
68
+ # @!macro [new] define_shared_examples_method
69
+ # @!method $1(name, *flags, **metadata, &block)
70
+ # Defines a deferred shared example group using the $1 method.
71
+ #
72
+ # @param name [String, Symbol, Module] the name for the shared example
73
+ # group.
74
+ # @param flags [Array<Symbol>] metadata flags for the shared example
75
+ # group.
76
+ # @param metadata [Hash] metadata for the shared example group.
77
+ # @param block [Proc] the implementation of the shared example group.
78
+ #
79
+ # @return [void]
80
+
81
+ # @!macro define_included_examples_method
82
+ define_included_examples_method :finclude_examples
83
+
84
+ # @!macro define_included_examples_method
85
+ define_included_examples_method :fwrap_context
86
+
87
+ # @!macro define_included_examples_method
88
+ define_included_examples_method :fwrap_examples
89
+
90
+ # @!macro define_included_examples_method
91
+ define_included_examples_method :include_context
92
+
93
+ # @!macro define_included_examples_method
94
+ define_included_examples_method :include_examples
95
+
96
+ # @!macro define_included_examples_method
97
+ define_included_examples_method :it_behaves_like
98
+
99
+ # @!macro define_included_examples_method
100
+ define_included_examples_method :it_should_behave_like
101
+
102
+ # @!macro define_shared_examples_method
103
+ define_shared_examples_method :shared_context
104
+
105
+ # @!macro define_shared_examples_method
106
+ define_shared_examples_method :shared_examples
107
+
108
+ # @!macro define_shared_examples_method
109
+ define_shared_examples_method :shared_examples_for
110
+
111
+ # @!macro define_included_examples_method
112
+ define_included_examples_method :wrap_context
113
+
114
+ # @!macro define_included_examples_method
115
+ define_included_examples_method :wrap_examples
116
+
117
+ # @!macro define_included_examples_method
118
+ define_included_examples_method :xinclude_examples
119
+
120
+ # @!macro define_included_examples_method
121
+ define_included_examples_method :xwrap_context
122
+
123
+ # @!macro define_included_examples_method
124
+ define_included_examples_method :xwrap_examples
125
+ end
126
+
127
+ include SharedExamples
128
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rspec/sleeping_king_studios/deferred'
4
+
5
+ module RSpec::SleepingKingStudios::Deferred
6
+ # Domain-specific language for defining deferred examples.
7
+ module Dsl
8
+ # Callback invoked when the module is extended into another module or class.
9
+ #
10
+ # Delegates to child module #extended methods.
11
+ #
12
+ # @param other [Module] the other module or class.
13
+ def self.extended(other)
14
+ super
15
+
16
+ other.extend(RSpec::SleepingKingStudios::Deferred::Dsl::MemoizedHelpers)
17
+ end
18
+ end
19
+ end
20
+
21
+ require 'rspec/sleeping_king_studios/deferred/dsl/examples'
22
+ require 'rspec/sleeping_king_studios/deferred/dsl/example_constants'
23
+ require 'rspec/sleeping_king_studios/deferred/dsl/example_groups'
24
+ require 'rspec/sleeping_king_studios/deferred/dsl/hooks'
25
+ require 'rspec/sleeping_king_studios/deferred/dsl/memoized_helpers'
26
+ require 'rspec/sleeping_king_studios/deferred/dsl/shared_examples'
@@ -0,0 +1,83 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rspec/sleeping_king_studios/deferred'
4
+ require 'rspec/sleeping_king_studios/deferred/definitions'
5
+ require 'rspec/sleeping_king_studios/deferred/dsl'
6
+
7
+ module RSpec::SleepingKingStudios::Deferred
8
+ # Defines a deferred example group for declaring shared tests.
9
+ module Examples
10
+ # Class methods for deferred examples.
11
+ module ClassMethods
12
+ # @return [RSpec::SleepingKingStudios::Deferred::Examples] the deferred
13
+ # example group where this example group was included, if any.
14
+ attr_accessor :parent_group
15
+
16
+ # @return [Array<String, Integer>] the Ruby source filename and line
17
+ # number where the deferred example group was defined.
18
+ attr_accessor :source_location
19
+
20
+ # @return [String] the description for the deferred examples. By default,
21
+ # formats the last segment of the module name in lowercase words,
22
+ # excepting any trailing "Context" or "Examples".
23
+ def description
24
+ return @description if @description
25
+
26
+ return @description = '(anonymous examples)' if name.nil?
27
+
28
+ @description = format_description
29
+ end
30
+
31
+ # @param value [String] the description for the deferred examples.
32
+ def description=(value)
33
+ tools.assertions.validate_name(value, as: 'description')
34
+
35
+ @description = value.to_s.tr('_', ' ')
36
+ end
37
+
38
+ # @return [Boolean] flag indicating that the included module has deferred
39
+ # examples, rather than including another deferred examples module.
40
+ def deferred_examples?
41
+ true
42
+ end
43
+
44
+ private
45
+
46
+ def format_description
47
+ name
48
+ .split('::')
49
+ .last
50
+ .gsub(/(Context|Examples?)\z/, '')
51
+ .then { |str| tools.string_tools.underscore(str) }
52
+ .tr('_', ' ')
53
+ end
54
+
55
+ def tools
56
+ SleepingKingStudios::Tools::Toolbelt.instance
57
+ end
58
+ end
59
+
60
+ # Callback invoked when the module is included in another module or class.
61
+ #
62
+ # Extends the class or module with the Deferred::Definitions
63
+ # and Deferred::Examples::DSL modules.
64
+ #
65
+ # @param other [Module] the other module or class.
66
+ #
67
+ # @see RSpec::SleepingKingStudios::Deferred::Definitions.
68
+ # @see RSpec::SleepingKingStudios::Deferred::Examples::Dsl.
69
+ def self.included(other)
70
+ super
71
+
72
+ other.extend ClassMethods
73
+ other.extend RSpec::SleepingKingStudios::Deferred::Definitions
74
+ other.extend RSpec::SleepingKingStudios::Deferred::Dsl
75
+ other.include RSpec::SleepingKingStudios::Deferred::Provider
76
+ other.include RSpec::SleepingKingStudios::Deferred::Consumer
77
+
78
+ location = caller_locations(1, 1).first
79
+
80
+ other.source_location = [location.path, location.lineno]
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rspec/sleeping_king_studios/deferred/call'
4
+ require 'rspec/sleeping_king_studios/deferred/definitions'
5
+ require 'rspec/sleeping_king_studios/deferred'
6
+
7
+ module RSpec::SleepingKingStudios::Deferred
8
+ # Optional support for deferring unrecognized methods.
9
+ module Missing
10
+ # Methods extended into the class when included in a class or module.
11
+ module ClassMethods
12
+ include RSpec::SleepingKingStudios::Deferred::Definitions
13
+
14
+ private
15
+
16
+ def method_missing(...)
17
+ deferred_calls << RSpec::SleepingKingStudios::Deferred::Call.new(...)
18
+
19
+ nil
20
+ end
21
+
22
+ def respond_to_missing?(name, include_private = false)
23
+ return true if super
24
+
25
+ return false if !include_private && super(name, true)
26
+
27
+ true
28
+ end
29
+ end
30
+
31
+ # Callback invoked when the module is included in another module or class.
32
+ #
33
+ # Extends the class or module with the ClassMethods module.
34
+ #
35
+ # @param other [Module] the other module or class.
36
+ #
37
+ # @see RSpec::SleepingKingStudios::Deferred::Missing::ClassMethods.
38
+ def self.included(other)
39
+ super
40
+
41
+ other.extend(
42
+ RSpec::SleepingKingStudios::Deferred::Missing::ClassMethods
43
+ )
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,164 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'sleeping_king_studios/tools/toolbelt'
4
+ require 'sleeping_king_studios/tools/toolbox/mixin'
5
+
6
+ require 'rspec/sleeping_king_studios/deferred'
7
+
8
+ module RSpec::SleepingKingStudios::Deferred
9
+ # Methods for registering deferred examples.
10
+ module Provider
11
+ extend SleepingKingStudios::Tools::Toolbox::Mixin
12
+
13
+ # Exception raised when the requested deferred examples are not defined.
14
+ class DeferredExamplesNotFoundError < StandardError; end
15
+
16
+ # Class methods for registering deferred examples.
17
+ module ClassMethods
18
+ # Defines deferred examples in the current context.
19
+ #
20
+ # @param description [String] the name of the deferred examples.
21
+ #
22
+ # @yield [*arguments, **keywords, &block] the definition for the deferred
23
+ # examples. Supports the same DSL as an RSpec::Core::ExampleGroup. If
24
+ # the block takes parameters, these can be used to customize the
25
+ # behavior of the deferred examples when they are included in an example
26
+ # group.
27
+ # @yieldparam arguments [Array] arguments passed to the deferred examples.
28
+ # @yieldparam keywords [Hash] keywords passed to the deferred examples.
29
+ # @yieldparam block [Block] a block passed to the deferred examples.
30
+ #
31
+ # @example Defining Deferred Examples
32
+ # deferred_examples 'should be a Rocket' do
33
+ # it { expect(subject).to be_a Rocket }
34
+ # end
35
+ #
36
+ # @example Defining Parameterized Examples
37
+ # deferred_examples 'should be a Vehicle' do |expected_type:|
38
+ # it { expect(subject).to be_a Vehicle }
39
+ #
40
+ # it { expect(subject.tyoe).to be == expected_type }
41
+ # end
42
+ def deferred_examples(description, &block)
43
+ raise ArgumentError, 'block is required' unless block_given?
44
+
45
+ tools.assertions.validate_name(description, as: 'description')
46
+
47
+ defined_deferred_examples[description.to_s] = block
48
+
49
+ nil
50
+ end
51
+ alias deferred_context deferred_examples
52
+
53
+ # @private
54
+ def defined_deferred_examples
55
+ @defined_deferred_examples ||= {}
56
+ end
57
+
58
+ # Checks if the given deferred example group is defined.
59
+ #
60
+ # @param description [String] the name of the deferred examples.
61
+ #
62
+ # @return [true, false] true if a deferred example group with the given
63
+ # description is defined in the current context; otherwise false.
64
+ def defined_deferred_examples?(description)
65
+ ancestors.any? do |ancestor|
66
+ next false unless ancestor.respond_to?(:defined_deferred_examples)
67
+
68
+ ancestor.deferred_definition_exists?(description) ||
69
+ ancestor.deferred_module_exists?(description)
70
+ end
71
+ end
72
+ alias defined_deferred_context? defined_deferred_examples?
73
+
74
+ # @api private
75
+ def find_deferred_examples(description)
76
+ tools.assertions.validate_name(description, as: 'description')
77
+
78
+ deferred = find_deferred_by_description(description.to_s)
79
+
80
+ return deferred if deferred
81
+
82
+ message =
83
+ 'deferred examples not found with description ' \
84
+ "#{description.to_s.inspect}"
85
+
86
+ raise DeferredExamplesNotFoundError, message
87
+ end
88
+
89
+ protected
90
+
91
+ def deferred_definition_exists?(description)
92
+ defined_deferred_examples.key?(description)
93
+ end
94
+
95
+ def deferred_module_exists?(description)
96
+ constants(false)
97
+ .any? do |const_name|
98
+ value = const_get(const_name)
99
+
100
+ next false unless module_is_deferred_examples?(value)
101
+
102
+ return true if matches_description?(description, const_name, value)
103
+ end
104
+ end
105
+
106
+ def find_deferred_definition(description)
107
+ defined_deferred_examples.fetch(description, nil)
108
+ end
109
+
110
+ def find_deferred_module(description)
111
+ constants(false)
112
+ .each do |const_name|
113
+ value = const_get(const_name)
114
+
115
+ next false unless module_is_deferred_examples?(value)
116
+
117
+ return value if matches_description?(description, const_name, value)
118
+ end
119
+
120
+ nil
121
+ end
122
+
123
+ private
124
+
125
+ def find_deferred_by_description(description)
126
+ ancestors.each do |ancestor|
127
+ next unless ancestor.respond_to?(:defined_deferred_examples)
128
+
129
+ deferred =
130
+ ancestor.find_deferred_definition(description) ||
131
+ ancestor.find_deferred_module(description)
132
+
133
+ return deferred if deferred
134
+ end
135
+
136
+ nil
137
+ end
138
+
139
+ def matches_description?(description, const_name, value)
140
+ return true if value.description == description
141
+
142
+ const_name = const_name.to_s
143
+
144
+ return true if const_name == description
145
+
146
+ const_name = const_name.gsub(/(Context|Examples?)\z/, '')
147
+
148
+ const_name == description
149
+ end
150
+
151
+ def module_is_deferred_examples?(value)
152
+ return false unless value.is_a?(Module)
153
+
154
+ return false unless value.respond_to?(:deferred_examples?)
155
+
156
+ value.deferred_examples?
157
+ end
158
+
159
+ def tools
160
+ SleepingKingStudios::Tools::Toolbelt.instance
161
+ end
162
+ end
163
+ end
164
+ end
@@ -0,0 +1,142 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rspec/sleeping_king_studios'
4
+
5
+ module RSpec::SleepingKingStudios
6
+ # Namespace for deferred example functionality.
7
+ module Deferred
8
+ autoload :Call, 'rspec/sleeping_king_studios/deferred/call'
9
+ autoload :Calls, 'rspec/sleeping_king_studios/deferred/calls'
10
+ autoload :Consumer, 'rspec/sleeping_king_studios/deferred/consumer'
11
+ autoload :Definitions, 'rspec/sleeping_king_studios/deferred/definitions'
12
+ autoload :Dependencies, 'rspec/sleeping_king_studios/deferred/dependencies'
13
+ autoload :Dsl, 'rspec/sleeping_king_studios/deferred/dsl'
14
+ autoload :Examples, 'rspec/sleeping_king_studios/deferred/examples'
15
+ autoload :Missing, 'rspec/sleeping_king_studios/deferred/missing'
16
+ autoload :Provider, 'rspec/sleeping_king_studios/deferred/provider'
17
+
18
+ class << self
19
+ # Returns the full path of an example, including deferred example groups.
20
+ #
21
+ # By default, returns the path in a single-line format similar to an
22
+ # example group description. Deferred example groups are parenthesized.
23
+ # When the :source_locations flag is set to true, it instead returns each
24
+ # example group or deferred group on its own line, along with the source
25
+ # location for that group.
26
+ #
27
+ # @param example [RSpec::Core::Example] the example to examine.
28
+ # @param source_locations [true, false] if true, returns the path in a
29
+ # multi-line format including the source location for each group.
30
+ #
31
+ # @return [String] the generated example path.
32
+ #
33
+ # @example Displaying the full path of failing specs:
34
+ # config.after(:example) do |example|
35
+ # next unless ENV['REFLECT_ON_FAILURE']
36
+ # next unless example.metadata[:last_run_status] == 'failed'
37
+ #
38
+ # STDERR.puts "\nFailing spec at:"
39
+ #
40
+ # path =
41
+ # RSpec::SleepingKingStudios::Deferred
42
+ # .reflect(example, source_locations: true)
43
+ # path =
44
+ # SleepingKingStudios::Tools::Toolbelt
45
+ # .instance
46
+ # .string_tools
47
+ # .indent(path)
48
+ #
49
+ # STDERR.puts path
50
+ # end
51
+ def reflect(example, source_locations: false)
52
+ return short_description_for(example) unless source_locations
53
+
54
+ each_ancestor_group_for(example)
55
+ .reverse_each
56
+ .reduce([]) do |lines, group|
57
+ lines << format_full_description(group)
58
+ end
59
+ .join("\n")
60
+ end
61
+
62
+ private
63
+
64
+ def constant_or_method?(description)
65
+ description.start_with?('::') ||
66
+ description.start_with?('#') ||
67
+ description.start_with?('.')
68
+ end
69
+
70
+ def deferred_group?(example_group)
71
+ return false unless example_group.is_a?(Module)
72
+ return false if example_group.is_a?(Class)
73
+
74
+ example_group < RSpec::SleepingKingStudios::Deferred::Consumer
75
+ end
76
+
77
+ def each_ancestor_group_for(example, &) # rubocop:disable Metrics/MethodLength
78
+ return enum_for(:each_ancestor_group_for, example) unless block_given?
79
+
80
+ each_parent_group_for(example.metadata[:deferred_example_group], &)
81
+
82
+ example
83
+ .example_group
84
+ .ancestors
85
+ .select do |ancestor|
86
+ ancestor.is_a?(Class) && ancestor < RSpec::Core::ExampleGroup
87
+ end # rubocop:disable Style/MultilineBlockChain
88
+ .each do |ancestor|
89
+ yield ancestor
90
+
91
+ each_parent_group_for(ancestor.metadata[:deferred_example_group], &)
92
+ end
93
+ end
94
+
95
+ def each_parent_group_for(maybe_deferred, &)
96
+ unless block_given?
97
+ return enum_for(:each_parent_group_for, maybe_deferred)
98
+ end
99
+
100
+ loop do
101
+ break unless deferred_group?(maybe_deferred)
102
+
103
+ yield maybe_deferred
104
+
105
+ maybe_deferred = maybe_deferred.parent_group
106
+ end
107
+ end
108
+
109
+ def format_description(group)
110
+ return group.description unless deferred_group?(group)
111
+
112
+ "(#{group.description})"
113
+ end
114
+
115
+ def format_full_description(group)
116
+ "#{format_description(group)} at #{format_source_location(group)}"
117
+ end
118
+
119
+ def format_source_location(group)
120
+ source_location =
121
+ if group < RSpec::Core::ExampleGroup
122
+ group.metadata[:block].source_location
123
+ else
124
+ group.source_location
125
+ end
126
+
127
+ source_location.join(':')
128
+ end
129
+
130
+ def short_description_for(example)
131
+ each_ancestor_group_for(example)
132
+ .reverse_each
133
+ .reduce('') do |description, group|
134
+ description += ' ' unless constant_or_method?(group.description)
135
+
136
+ description + format_description(group)
137
+ end
138
+ .strip
139
+ end
140
+ end
141
+ end
142
+ end