rspec_in_context 1.1.0.3 → 1.2.1

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.
@@ -0,0 +1,23 @@
1
+ # Example: Using :interactor_expect for contract validation
2
+ #
3
+ # Tests that the interactor fails when any required field is nil,
4
+ # then tests the happy path separately.
5
+
6
+ require "rails_helper"
7
+
8
+ RSpec.describe CreateInvoice do
9
+ subject do
10
+ described_class.call(amount: amount, client: client, due_date: due_date)
11
+ end
12
+
13
+ let(:amount) { 100 }
14
+ let(:client) { create(:client) }
15
+ let(:due_date) { Date.tomorrow }
16
+
17
+ in_context :interactor_expect, %i[amount client due_date]
18
+
19
+ it "creates the invoice" do
20
+ expect(subject).to be_success
21
+ expect(subject.invoice).to be_persisted
22
+ end
23
+ end
@@ -0,0 +1,30 @@
1
+ # Example: Nesting multiple contexts together
2
+ #
3
+ # Combines :with_frozen_time and :with_inline_mailer
4
+ # to test time-sensitive email delivery.
5
+
6
+ require "rails_helper"
7
+
8
+ RSpec.describe PasswordReset do
9
+ in_context :with_frozen_time do
10
+ in_context :with_inline_mailer do
11
+ it "sends the reset email" do
12
+ user = create(:user)
13
+
14
+ PasswordReset.call(user)
15
+
16
+ expect(ActionMailer::Base.deliveries.size).to eq(1)
17
+ end
18
+
19
+ it "includes the current timestamp" do
20
+ user = create(:user)
21
+
22
+ PasswordReset.call(user)
23
+
24
+ expect(ActionMailer::Base.deliveries.last.body.to_s).to include(
25
+ Time.current.iso8601,
26
+ )
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,29 @@
1
+ # Example: Using :authenticated_request in a request spec
2
+ #
3
+ # The context handles authentication setup and tests the unauthenticated case.
4
+ # Your tests inside the block run in an authenticated state.
5
+
6
+ require "rails_helper"
7
+
8
+ RSpec.describe "Projects", type: :request do
9
+ let(:http_method) { :get }
10
+ let(:endpoint_path) { projects_path }
11
+
12
+ in_context :authenticated_request do
13
+ describe "GET /projects" do
14
+ it "returns 200" do
15
+ get projects_path
16
+
17
+ expect(response).to have_http_status(:ok)
18
+ end
19
+
20
+ it "lists the user's projects" do
21
+ project = create(:project, account: account)
22
+
23
+ get projects_path
24
+
25
+ expect(response.body).to include(project.name)
26
+ end
27
+ end
28
+ end
29
+ end
@@ -1,12 +1,10 @@
1
- # frozen_string_literal: true
2
-
3
1
  module RspecInContext
4
2
  # Allow context to be scoped inside a block
5
3
  module ContextManagement
6
4
  # @api private
7
5
  # prepending a RSpec method so we can know when a describe/context block finish
8
6
  # its reading
9
- def subclass(parent, description, args, registration_collection, &example_group_block)
7
+ def subclass(parent, description, args, registration_collection, &)
10
8
  subclass = super
11
9
  RspecInContext::InContext.remove_context(subclass)
12
10
  subclass
@@ -1,14 +1,29 @@
1
- # frozen_string_literal: true
2
-
3
1
  # Base module
4
2
  module RspecInContext
5
- # Error type when no context is find from its name (and eventualy namespace)
6
- class NoContextFound < StandardError; end
3
+ # Base error class for all gem errors
4
+ class Error < StandardError
5
+ end
6
+
7
+ # Error type when no context is found from its name (and eventually namespace)
8
+ class NoContextFound < Error
9
+ end
10
+
11
+ # Error type when define_context is called without a block
12
+ class MissingDefinitionBlock < ArgumentError
13
+ end
14
+
15
+ # Error type when context_name is nil or empty
16
+ class InvalidContextName < ArgumentError
17
+ end
18
+
19
+ # Error type when multiple namespaces contain a context with the same name
20
+ class AmbiguousContextName < Error
21
+ end
7
22
 
8
23
  # Context struct
9
24
  # @attr [Proc] block what will be executed in the test context
10
25
  # @attr [Class] owner current rspec context class. This will be used to know where a define_context has been defined
11
- # @attr [String | Symbol] name represent the name by which the context can be find.
26
+ # @attr [String | Symbol] name represent the name by which the context can be found.
12
27
  # @attr [String | Symbol] namespace namespace for context names to avoid collisions
13
28
  # @attr [Boolean] silent does the in_context wrap itself into a context with its name or an anonymous context
14
29
  Context = Struct.new(:block, :owner, :name, :namespace, :silent)
@@ -17,6 +32,9 @@ module RspecInContext
17
32
  module InContext
18
33
  # Name of the Global context
19
34
  GLOBAL_CONTEXT = :global_context
35
+ # Mutex protecting @contexts for thread-safety (e.g. parallel_tests in thread mode)
36
+ @contexts_mutex = Mutex.new
37
+
20
38
  class << self
21
39
  # Hook for easier inclusion of the gem in RSpec
22
40
  # @api private
@@ -25,50 +43,105 @@ module RspecInContext
25
43
  end
26
44
 
27
45
  # Contexts container + creation
46
+ # Keys are normalized to strings so symbols and strings are interchangeable.
28
47
  # @api private
29
48
  def contexts
30
- @contexts ||= HashWithIndifferentAccess.new { |hash, key| hash[key] = HashWithIndifferentAccess.new }
49
+ @contexts_mutex.synchronize do
50
+ @contexts ||= Hash.new { |hash, key| hash[key.to_s] = {} }
51
+ end
52
+ end
53
+
54
+ # Remove all stored contexts (both scoped and global).
55
+ # Useful for memory cleanup in long-running test suites with
56
+ # dynamically generated contexts.
57
+ def clear_all_contexts!
58
+ @contexts_mutex.synchronize { @contexts = nil }
31
59
  end
32
60
 
33
61
  # Meta method to add a new context
34
62
  # @api private
35
63
  #
36
- # @note Will warn if a context is overriden
37
- def add_context(context_name, owner = nil, namespace = nil, silent = true, &block)
64
+ # @note Will warn if a context is overridden
65
+ # @raise [InvalidContextName] if context_name is nil or empty
66
+ # @raise [MissingDefinitionBlock] if no block is provided
67
+ def add_context(
68
+ context_name,
69
+ owner = nil,
70
+ namespace = nil,
71
+ silent = true,
72
+ &block
73
+ )
74
+ if context_name.nil? ||
75
+ (context_name.respond_to?(:empty?) && context_name.empty?)
76
+ raise InvalidContextName, "context_name cannot be nil or empty"
77
+ end
78
+ unless block
79
+ raise MissingDefinitionBlock, "define_context requires a block"
80
+ end
81
+
38
82
  namespace ||= GLOBAL_CONTEXT
39
- warn("Overriding an existing context: #{context_name}@#{namespace}") if contexts[namespace][context_name]
40
- contexts[namespace][context_name] = Context.new(block, owner, context_name, namespace, silent)
83
+ ns_key = namespace.to_s
84
+ name_key = context_name.to_s
85
+ if contexts.dig(ns_key, name_key)
86
+ warn("Overriding an existing context: #{context_name}@#{namespace}")
87
+ end
88
+ contexts[ns_key][name_key] = Context.new(
89
+ block,
90
+ owner,
91
+ context_name,
92
+ namespace,
93
+ silent,
94
+ )
41
95
  end
42
96
 
43
97
  # Find a context.
44
98
  # @api private
99
+ # @raise [NoContextFound] if no context is found
100
+ # @raise [AmbiguousContextName] if multiple namespaces contain the same context name
45
101
  def find_context(context_name, namespace = nil)
46
- if namespace&.present?
47
- contexts[namespace][context_name]
48
- else
49
- contexts[GLOBAL_CONTEXT][context_name] || find_context_in_any_namespace(context_name)
50
- end || (raise NoContextFound, "No context found with name #{context_name}")
102
+ name_key = context_name.to_s
103
+ result =
104
+ if namespace && !namespace.to_s.empty?
105
+ contexts.dig(namespace.to_s, name_key)
106
+ else
107
+ find_context_across_all_namespaces(name_key)
108
+ end
109
+ result ||
110
+ (raise NoContextFound, "No context found with name #{context_name}")
51
111
  end
52
112
 
53
113
  # Look into every namespace to find the context
114
+ # Uses dig to avoid auto-vivifying empty namespace entries
54
115
  # @api private
55
- def find_context_in_any_namespace(context_name)
56
- valid_namespace = contexts.find { |_, namespaced_contexts| namespaced_contexts[context_name] }&.last
57
- valid_namespace[context_name] if valid_namespace
116
+ # @raise [AmbiguousContextName] if multiple namespaces contain the same context name
117
+ def find_context_across_all_namespaces(name_key)
118
+ matching_namespaces =
119
+ contexts.select do |_, namespaced_contexts|
120
+ namespaced_contexts[name_key]
121
+ end
122
+ if matching_namespaces.size > 1
123
+ namespace_names = matching_namespaces.keys.join(", ")
124
+ raise AmbiguousContextName,
125
+ "Context '#{name_key}' exists in multiple namespaces (#{namespace_names}). " \
126
+ "Please specify a namespace."
127
+ end
128
+ matching_namespaces.values.first&.[](name_key)
58
129
  end
59
130
 
60
131
  # @api private
61
132
  # Delete a context
62
133
  def remove_context(current_class)
63
134
  contexts.each_value do |namespaced_contexts|
64
- namespaced_contexts.delete_if { |_, context| context.owner == current_class }
135
+ namespaced_contexts.delete_if do |_, context|
136
+ context.owner == current_class
137
+ end
65
138
  end
66
139
  end
67
140
 
68
141
  # @api private
69
142
  # Define a context from outside a RSpec.describe block
70
- def outside_define_context(context_name, namespace, silent, &block)
71
- InContext.add_context(context_name, nil, namespace, silent, &block)
143
+ def outside_define_context(context_name, namespace, silent, &)
144
+ InContext.add_context(context_name, nil, namespace, silent, &)
72
145
  end
73
146
  end
74
147
 
@@ -83,40 +156,69 @@ module RspecInContext
83
156
  # @param block Content that will be re-injected (see #execute_tests)
84
157
  def in_context(context_name, *args, namespace: nil, ns: nil, &block)
85
158
  namespace ||= ns
86
- Thread.current[:test_block] = block
87
159
  context_to_exec = InContext.find_context(context_name, namespace)
88
- return context { instance_exec(*args, &context_to_exec.block) } if context_to_exec.silent
89
-
90
- context(context_name.to_s) { instance_exec(*args, &context_to_exec.block) }
160
+ Thread.current[:test_block_stack] ||= []
161
+ Thread.current[:test_block_stack].push(block)
162
+ begin
163
+ if context_to_exec.silent
164
+ context { instance_exec(*args, &context_to_exec.block) }
165
+ else
166
+ context(
167
+ context_name.to_s,
168
+ ) { instance_exec(*args, &context_to_exec.block) }
169
+ end
170
+ ensure
171
+ Thread.current[:test_block_stack].pop
172
+ end
91
173
  end
92
174
 
93
175
  # Used in context definition
94
176
  # Place where you want to re-inject code passed in argument of in_context
95
- #
96
- # For convenience and readability, a `instanciate_context` alias have been defined
97
177
  # (for more examples look at tests)
98
178
  def execute_tests
99
- instance_exec(&Thread.current[:test_block]) if Thread.current[:test_block]
179
+ current_block = Thread.current[:test_block_stack]&.last
180
+ instance_exec(&current_block) if current_block
181
+ end
182
+ alias instantiate_context execute_tests
183
+
184
+ # @deprecated Use {#instantiate_context} or {#execute_tests} instead
185
+ def instanciate_context
186
+ warn(
187
+ "DEPRECATION: `instanciate_context` is deprecated due to a typo. " \
188
+ "Use `instantiate_context` or `execute_tests` instead.",
189
+ uplevel: 1,
190
+ )
191
+ execute_tests
100
192
  end
101
- alias instanciate_context execute_tests
102
193
 
103
194
  # Let you define a context that can be reused later
104
195
  #
105
196
  # @param context_name [String, Symbol] The name of the context that will be re-used later
106
197
  # @param namespace [String, Symbol] namespace name where the context will be stored.
107
- # It helps reducing colisions when you define "global" contexts
198
+ # It helps reducing collisions when you define "global" contexts
108
199
  # @param ns [String, Symbol] Alias of namespace
109
200
  # @param block [Proc] Contain the code that will be injected with #in_context later
110
201
  # @param silent [Boolean] Does the in_context wrap itself into a context with its name or an anonymous context
111
202
  # @param print_context [Boolean] Reverse alias of silent
112
203
  #
113
204
  # @note contexts are scoped to the block they are defined in.
114
- def define_context(context_name, namespace: nil, ns: nil, silent: true, print_context: nil, &block)
205
+ def define_context(
206
+ context_name,
207
+ namespace: nil,
208
+ ns: nil,
209
+ silent: true,
210
+ print_context: nil,
211
+ &
212
+ )
115
213
  namespace ||= ns
116
- silent = print_context.nil? ? silent : !print_context
117
- instance_exec do
118
- InContext.add_context(context_name, hooks.instance_variable_get(:@owner), namespace, silent, &block)
119
- end
214
+ silent = !print_context unless print_context.nil?
215
+ InContext.add_context(
216
+ context_name,
217
+ hooks.instance_variable_get(:@owner),
218
+ namespace,
219
+ silent,
220
+ &
221
+ )
120
222
  end
121
223
  end
122
224
  end
@@ -1,6 +1,4 @@
1
- # frozen_string_literal: true
2
-
3
1
  module RspecInContext
4
2
  # Version of the gem
5
- VERSION = '1.1.0.3'
3
+ VERSION = "1.2.1".freeze
6
4
  end
@@ -1,9 +1,6 @@
1
- # frozen_string_literal: true
2
-
3
- require 'active_support/all'
4
- require 'rspec_in_context/version'
5
- require 'rspec_in_context/in_context'
6
- require 'rspec_in_context/context_management'
1
+ require "rspec_in_context/version"
2
+ require "rspec_in_context/in_context"
3
+ require "rspec_in_context/context_management"
7
4
 
8
5
  # Main wrapping module
9
6
  module RspecInContext
@@ -24,9 +21,16 @@ module RSpec
24
21
  # @param silent [Boolean] Does the in_context should wrap itself into a context block with its name
25
22
  # @param print_context [Boolean] Reverse alias of silent
26
23
  # @param block [Proc] code that will be injected later
27
- def self.define_context(name, namespace: nil, ns: nil, silent: true, print_context: nil, &block)
24
+ def self.define_context(
25
+ name,
26
+ namespace: nil,
27
+ ns: nil,
28
+ silent: true,
29
+ print_context: nil,
30
+ &
31
+ )
28
32
  namespace ||= ns
29
- silent = print_context.nil? ? silent : !print_context
30
- RspecInContext::InContext.outside_define_context(name, namespace, silent, &block)
33
+ silent = !print_context unless print_context.nil?
34
+ RspecInContext::InContext.outside_define_context(name, namespace, silent, &)
31
35
  end
32
36
  end
data/package.json CHANGED
@@ -6,7 +6,21 @@
6
6
  "author": "Denis <Zaratan> Pasin <zaratan@hey.com>",
7
7
  "license": "MIT",
8
8
  "devDependencies": {
9
- "@prettier/plugin-ruby": "^1.3.0",
10
- "prettier": "^2.2.1"
9
+ "@prettier/plugin-ruby": "^4.0",
10
+ "prettier": "^3.8.1"
11
+ },
12
+ "prettier": {
13
+ "trailingComma": "es5",
14
+ "plugins": [
15
+ "@prettier/plugin-ruby"
16
+ ],
17
+ "overrides": [
18
+ {
19
+ "files": "*.rb",
20
+ "options": {
21
+ "parser": "ruby"
22
+ }
23
+ }
24
+ ]
11
25
  }
12
26
  }
@@ -1,45 +1,46 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- lib = File.expand_path('lib', __dir__)
3
+ lib = File.expand_path("lib", __dir__)
4
4
  $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
5
5
  require "rspec_in_context/version"
6
6
 
7
7
  Gem::Specification.new do |spec|
8
- spec.name = "rspec_in_context"
9
- spec.version = RspecInContext::VERSION
10
- spec.authors = ["Denis <Zaratan> Pasin"]
11
- spec.email = ["denis@pasin.fr"]
8
+ spec.name = "rspec_in_context"
9
+ spec.version = RspecInContext::VERSION
10
+ spec.authors = ["Denis <Zaratan> Pasin"]
11
+ spec.email = ["denis@pasin.fr"]
12
12
 
13
- spec.summary = 'This gem is here to help DRYing your tests cases by giving a better "shared_examples".'
14
- spec.homepage = "https://github.com/denispasin/rspec_in_context"
13
+ spec.summary =
14
+ 'This gem is here to help DRYing your tests cases by giving a better "shared_examples".'
15
+ spec.homepage = "https://github.com/denispasin/rspec_in_context"
15
16
 
16
17
  # Specify which files should be added to the gem when it is released.
17
18
  # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
18
- spec.files =
19
+ spec.files =
19
20
  Dir.chdir(File.expand_path(__dir__)) do
20
- `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
21
+ `git ls-files -z`.split("\x0")
22
+ .reject { |f| f.match(%r{^(test|spec|features)/}) }
21
23
  end
22
- spec.bindir = "exe"
23
- spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
24
+ spec.bindir = "exe"
25
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
24
26
  spec.require_paths = ["lib"]
25
- spec.required_ruby_version = '>= 2.5.8' # rubocop:disable Gemspec/RequiredRubyVersion
26
- spec.license = 'MIT'
27
+ spec.required_ruby_version = ">= 3.2.0"
28
+ spec.license = "MIT"
27
29
 
28
- spec.add_dependency "activesupport", "> 2.0"
29
- spec.add_dependency "rspec", "> 3.0"
30
+ spec.add_dependency "rspec", "~> 3.0"
30
31
 
31
32
  spec.add_development_dependency "bundler"
32
33
  spec.add_development_dependency "bundler-audit", "> 0.6.0"
33
- spec.add_development_dependency "codacy-coverage", '>= 2.1.0'
34
- spec.add_development_dependency "faker", "> 1.8"
34
+
35
35
  spec.add_development_dependency "guard-rspec", "> 4.7"
36
- spec.add_development_dependency "overcommit", '> 0.46'
36
+ spec.add_development_dependency "overcommit", "> 0.46"
37
37
  spec.add_development_dependency "prettier"
38
- spec.add_development_dependency "rake", "~> 12.0"
38
+ spec.add_development_dependency "rake", "~> 13.0"
39
39
  spec.add_development_dependency "rspec_junit_formatter", "~> 0.4.1"
40
- spec.add_development_dependency "rubocop", "> 0.58"
40
+ spec.add_development_dependency "rubocop", "~> 1.82"
41
41
  spec.add_development_dependency "rubocop-performance"
42
42
  spec.add_development_dependency "simplecov", "> 0.16"
43
43
  spec.add_development_dependency "solargraph"
44
44
  spec.add_development_dependency "yard"
45
+ spec.metadata["rubygems_mfa_required"] = "true"
45
46
  end
data/yarn.lock CHANGED
@@ -2,14 +2,12 @@
2
2
  # yarn lockfile v1
3
3
 
4
4
 
5
- "@prettier/plugin-ruby@^1.3.0":
6
- version "1.3.0"
7
- resolved "https://registry.yarnpkg.com/@prettier/plugin-ruby/-/plugin-ruby-1.3.0.tgz#38ec1447ca43121cfe72961ed974038cbcb57ff7"
8
- integrity sha512-8MHLLdHpb0MDmkh+GZa+MQcQVQzYc34Nv5/I5yQv4n14ogLRMs3oi5C9kQSG2PAYLhAwF8EU36OczzVaHrPHoA==
9
- dependencies:
10
- prettier ">=1.10"
5
+ "@prettier/plugin-ruby@^4.0":
6
+ version "4.0.4"
7
+ resolved "https://registry.yarnpkg.com/@prettier/plugin-ruby/-/plugin-ruby-4.0.4.tgz#73d85fc2a1731a3f62b57ac3116cf1c234027cb6"
8
+ integrity sha512-lCpvfS/dQU5WrwN3AQ5vR8qrvj2h5gE41X08NNzAAXvHdM4zwwGRcP2sHSxfu6n6No+ljWCVx95NvJPFTTjCTg==
11
9
 
12
- prettier@>=1.10, prettier@^2.2.1:
13
- version "2.2.1"
14
- resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.2.1.tgz#795a1a78dd52f073da0cd42b21f9c91381923ff5"
15
- integrity sha512-PqyhM2yCjg/oKkFPtTGUojv7gnZAoG80ttl45O6x2Ug/rMJw4wcc9k6aaf2hibP7BGVCCM33gZoGjyvt9mm16Q==
10
+ prettier@^3.8.1:
11
+ version "3.8.1"
12
+ resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.8.1.tgz#edf48977cf991558f4fcbd8a3ba6015ba2a3a173"
13
+ integrity sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==