is-module-reset 0.8.0 → 0.8.0.2

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c285525e3c4f98d207ff16fe510941254976d5985445b93a9442bb5edc7af372
4
- data.tar.gz: 795a153090769391780c48225be7c84faccad4df1ffa374c71f3317f38557121
3
+ metadata.gz: 4c3492581888c7c5515f550a39afb1b9760551aa0ab212e60a263f36cc1d88d7
4
+ data.tar.gz: 3003eee4ccc3ca344e1d61d14eab567935ddfc6b2a8d91c7ec86a202f1708a0b
5
5
  SHA512:
6
- metadata.gz: 9adadbc9707da9b5c56f2a5d276342f1816a40d63c7c9645d016c7532b12664f6b409b6c9922c1812edeb8f323914e2071adcd7855051a41725dea7e7808ed61
7
- data.tar.gz: d6a7a5f655b0cfef7a4480056d316157d03a4cdb4a43285f4fa05a4af00dfe3f271bc4d27e99a00afe0838760cb062a8e86db77cfdda1015d821c92f3920d88a
6
+ metadata.gz: fb1def03555aaab2ac065cd94c9746c3d475afefa94e233d9fddb829a7455bc83aa37ec715963dac24a23477edd5d5cd1ce02fb8b8f207d25841e6a5db8e8b1a
7
+ data.tar.gz: 96d73839eb00418981af55744d585b1bb5c7693ccc47fb323848d6182262d32fc0e99d765a3636fd0793ec728af1138b71c5911602fad0ad754b1e0f9fe5ca7b
data/README.md CHANGED
@@ -1 +1,194 @@
1
- # is-module-reset
1
+ # is-module-reset
2
+
3
+ [![GitHub License](https://img.shields.io/github/license/jekyll-is/is-module-reset)](LICENSE)
4
+ [![Gem Version](https://badge.fury.io/rb/is-module-reset.svg)](https://badge.fury.io/rb/is-module-reset)
5
+ ![Coverage](coverage-badge.svg)
6
+
7
+ Save and restore the state of Ruby modules and classes **in tests**: class
8
+ variables (`@@var`) and instance variables of the modules and classes
9
+ themselves (`@var`). It keeps tests that touch global state (singletons,
10
+ registries, caches, settings stored in modules) from affecting each other.
11
+
12
+ > **Not intended for production use.** The gem does not create a truly
13
+ > isolated environment: it only puts values back into the same objects.
14
+
15
+ Русская версия: [README-ru.md](README-ru.md)
16
+
17
+ ## Installation
18
+
19
+ Add to your Gemfile:
20
+
21
+ ```ruby
22
+ gem 'is-module-reset', group: :test
23
+ ```
24
+
25
+ Or install directly:
26
+
27
+ ```bash
28
+ gem install is-module-reset
29
+ ```
30
+
31
+ ## Quick start
32
+
33
+ ```ruby
34
+ require 'is-module-reset'
35
+
36
+ module App
37
+ @@requests = 0
38
+ @settings = { debug: false }
39
+
40
+ def self.requests
41
+ @@requests
42
+ end
43
+
44
+ def self.hit!
45
+ @@requests += 1
46
+ end
47
+
48
+ def self.settings
49
+ @settings
50
+ end
51
+ end
52
+ ```
53
+
54
+ Run a block and roll everything back afterwards:
55
+
56
+ ```ruby
57
+ IS::ModuleReset.isolate(App) do
58
+ App.hit!
59
+ App.settings[:debug] = true
60
+ App.requests # => 1
61
+ end
62
+
63
+ App.requests # => 0
64
+ App.settings # => { debug: false }
65
+ ```
66
+
67
+ Or take a snapshot and restore it manually:
68
+
69
+ ```ruby
70
+ state = IS::ModuleReset.dump(App)
71
+
72
+ App.hit!
73
+ App.settings[:debug] = true
74
+
75
+ IS::ModuleReset.undump(state)
76
+
77
+ App.requests # => 0
78
+ App.settings # => { debug: false }
79
+ ```
80
+
81
+ ## What is saved
82
+
83
+ For every module or class you pass in, and for every module or class
84
+ reachable from it through constants (recursively, using only the module's own
85
+ constants, i.e. `Module#constants(false)`), the gem saves:
86
+
87
+ - the class variables defined by that module itself (inherited ones are not
88
+ included);
89
+ - the instance variables of the module object itself (`@var` set at module or
90
+ class level, including those created by `attr_accessor` in `class << self`).
91
+
92
+ When restoring, all such variables are removed and the saved ones are set
93
+ again. This means that variables created after the snapshot disappear, changed
94
+ ones return to their old values, and deleted ones come back.
95
+
96
+ Values are copied with `deep_dup` from [is-deep](https://github.com/inat-get/is-deep):
97
+ `Hash` and `Array` are copied recursively (shared and circular references are
98
+ preserved), modules and classes are kept as they are, and other objects are
99
+ copied with `dup` (if an object cannot be duplicated, the same object is kept).
100
+ Restoring also works on a copy, so the same snapshot can be restored any
101
+ number of times.
102
+
103
+ ## API
104
+
105
+ ### `IS::ModuleReset.dump(*mods) → Hash`
106
+
107
+ Takes a snapshot of the given modules and everything reachable from them via
108
+ constants. The returned hash is an internal representation; treat it as an
109
+ opaque value and pass it to `undump`.
110
+
111
+ ### `IS::ModuleReset.undump(state)`
112
+
113
+ Restores the state from a snapshot returned by `dump`. May be called multiple
114
+ times with the same snapshot.
115
+
116
+ ### `IS::ModuleReset.isolate(*mods) { ... }`
117
+
118
+ Takes a snapshot, runs the block, and restores the snapshot afterwards, even if
119
+ the block raised an exception. Returns the value of the block. Raises
120
+ `ArgumentError` if no block is given.
121
+
122
+ ## Test framework integration
123
+
124
+ ### RSpec
125
+
126
+ Isolate every example:
127
+
128
+ ```ruby
129
+ RSpec.configure do |config|
130
+ config.around do |example|
131
+ IS::ModuleReset.isolate(MyApp) { example.run }
132
+ end
133
+ end
134
+ ```
135
+
136
+ `around` hooks wrap `before` and `after` hooks, so changes made there are
137
+ rolled back too.
138
+
139
+ Or restore the state captured when the suite started:
140
+
141
+ ```ruby
142
+ RSpec.configure do |config|
143
+ baseline = nil
144
+ config.before(:suite) { baseline = IS::ModuleReset.dump(MyApp) }
145
+ config.after(:each) { IS::ModuleReset.undump(baseline) }
146
+ end
147
+ ```
148
+
149
+ ### Minitest
150
+
151
+ ```ruby
152
+ class Minitest::Test
153
+ def setup
154
+ super
155
+ @module_reset_state = IS::ModuleReset.dump(MyApp)
156
+ end
157
+
158
+ def teardown
159
+ IS::ModuleReset.undump(@module_reset_state)
160
+ super
161
+ end
162
+ end
163
+ ```
164
+
165
+ If your test classes define their own `setup` and `teardown`, call `super`.
166
+
167
+ ## Limitations
168
+
169
+ - **Constants are not restored.** Neither reassigned, added, or removed
170
+ constants, nor the contents of mutable constants (`CONFIG = {}`).
171
+ - **Only module-level state is saved.** Global variables (`$var`), `ENV`,
172
+ thread-local variables, instance variables of ordinary objects, and anything
173
+ outside the namespaces you pass in are not touched.
174
+ - **Values are copies.** Objects other than `Hash` and `Array` are copied with
175
+ a shallow `dup`: their inner mutable state is shared with the original, and
176
+ after restoring the module holds the copy rather than the original instance.
177
+ If you store, say, a singleton object in `@instance`, it may become a
178
+ different object after restoring.
179
+ - **Copies are not frozen.** A frozen value becomes mutable after restoring.
180
+ - **Constants are followed blindly.** An alias to a module from another
181
+ namespace (`Log = ::Logger`) is included in the snapshot and rolled back too.
182
+ Traversal may also trigger loading of `autoload` constants.
183
+ - **Not thread-safe.** The gem changes global state, so do not use it with
184
+ tests running in parallel threads of the same process. Process-based
185
+ parallelism is fine.
186
+
187
+ ## Requirements
188
+
189
+ - Ruby >= 3.4
190
+ - [is-deep](https://github.com/inat-get/is-deep)
191
+
192
+ ## License
193
+
194
+ LGPL-3.0-or-later. See the [LICENSE](LICENSE) file.
@@ -6,7 +6,7 @@ module IS::ModuleReset; end
6
6
  module IS::ModuleReset::Info
7
7
 
8
8
  NAME = 'is-module-reset'
9
- VERSION = '0.8.0'
9
+ VERSION = '0.8.0.2'
10
10
  SUMMARY = 'Reset singletons data for testing'
11
11
  LICENSE = 'LGPL-3.0-or-later'
12
12
  AUTHOR = 'Ivan Shikhalev'
@@ -17,8 +17,8 @@ module IS::ModuleReset
17
17
  end
18
18
 
19
19
  def undump state
20
- state.each do |mod, mod_state|
21
- mod.class_variables.each { mod.remove_class_variable it }
20
+ state.deep_dup.each do |mod, mod_state|
21
+ mod.class_variables(false).each { mod.remove_class_variable it }
22
22
  mod_state[:class_vars].each do |var, value|
23
23
  mod.class_variable_set var, value
24
24
  end
@@ -32,16 +32,18 @@ module IS::ModuleReset
32
32
  def isolate *mods
33
33
  raise ArgumentError, 'Block is required!' unless block_given?
34
34
  state = dump(*mods)
35
- yield
36
- ensure
37
- undump state
35
+ begin
36
+ yield
37
+ ensure
38
+ undump state
39
+ end
38
40
  end
39
41
 
40
42
  private
41
43
 
42
44
  def dump_module state, mod
43
45
  mod_state = { instance_vars: {}, class_vars: {} }
44
- mod.class_variables.each do |var|
46
+ mod.class_variables(false).each do |var|
45
47
  mod_state[:class_vars][var] = mod.class_variable_get(var)
46
48
  end
47
49
  mod.instance_variables.each do |var|
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: is-module-reset
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.8.0
4
+ version: 0.8.0.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ivan Shikhalev
@@ -16,6 +16,9 @@ dependencies:
16
16
  - - "~>"
17
17
  - !ruby/object:Gem::Version
18
18
  version: '0.8'
19
+ - - ">="
20
+ - !ruby/object:Gem::Version
21
+ version: 0.8.0.4
19
22
  type: :runtime
20
23
  prerelease: false
21
24
  version_requirements: !ruby/object:Gem::Requirement
@@ -23,6 +26,9 @@ dependencies:
23
26
  - - "~>"
24
27
  - !ruby/object:Gem::Version
25
28
  version: '0.8'
29
+ - - ">="
30
+ - !ruby/object:Gem::Version
31
+ version: 0.8.0.4
26
32
  - !ruby/object:Gem::Dependency
27
33
  name: rspec
28
34
  requirement: !ruby/object:Gem::Requirement