with_model 2.3.1 → 2.4.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e161c7fe750e7eb3b78e178a14b2e23ef88b0fa4612abfb22e3920be9eda614e
4
- data.tar.gz: fb6d9310626f6a006463782d179406e1a2af10b6672899fb57c2bf26f6df61a0
3
+ metadata.gz: f788d32a4cd252a5254b82928cab9b62b66cfd80e00dd010bdb901b60a6ddc6c
4
+ data.tar.gz: 2c44d64d8f3225e809c113894ec77df2e64c6dae513a12065e171bd892c80c7d
5
5
  SHA512:
6
- metadata.gz: 7d80585ba48b4b87593f9b7f18b8c4674391e51458b5e273ee121e4962bf4b479000a178a83db14cfa38a1eb185c8f09ac01d727ce6f79afe39ce5e45686ca70
7
- data.tar.gz: 3e241e4d17f7b5d889e665e5cb17e5d66de1ca7fdd3b3cbd4e73615421561f735aa3d3f40176b27c1be32cfc9e1db7059d642662ad35fc786fc45620b8a8146f
6
+ metadata.gz: bbf514f03d362582bf15cd38301a9f2929c559ea702c283bae2c01720f48e3f613e93ce1580906983d567048d7241d70af92b94f54f4d992da06f3497c9afc8e
7
+ data.tar.gz: 66200b79760795f6d3e622b4b29c13285fb042e4e168f1fea34ac38161bca41e4330a451add6f9a0db777dd20cbde97567cd687c811dff1675a1090a24dc7704
@@ -29,8 +29,6 @@ jobs:
29
29
  matrix:
30
30
  ruby-version: ["3.3", "3.4", "4.0"]
31
31
  active-record:
32
- - label: AR 7.2
33
- env: ACTIVE_RECORD_VERSION=~> 7.2.0
34
32
  - label: AR 8.0
35
33
  env: ACTIVE_RECORD_VERSION=~> 8.0.0
36
34
  - label: AR 8.1
@@ -52,11 +50,6 @@ jobs:
52
50
  label: AR 8-0-stable
53
51
  env: ACTIVE_RECORD_BRANCH=8-0-stable
54
52
  allow-failure: true
55
- - ruby-version: "4.0"
56
- active-record:
57
- label: AR 7-2-stable
58
- env: ACTIVE_RECORD_BRANCH=7-2-stable
59
- allow-failure: true
60
53
  # A canary for the next Ruby, which cannot fail the build.
61
54
  - ruby-version: ruby-head
62
55
  active-record:
@@ -78,11 +71,6 @@ jobs:
78
71
  ruby-version: ${{ matrix.ruby-version }}
79
72
  bundler-cache: true
80
73
  cache-version: ${{ matrix.active-record.env }}
81
- # Gemfile.lock is not checked in, so this resolves the newest gems that
82
- # satisfy the Gemfile on each run rather than a recorded set. `--all` is
83
- # what Bundler 4, shipped with Ruby 4.0, wants for that.
84
- - name: Update bundle
85
- run: bundle update --all
86
74
  # Not bin/rake: bin/ holds generated binstubs and is not checked in, and
87
75
  # whether bundling writes them there varies by Bundler version - Bundler 4
88
76
  # does not, which left Ruby 4.0 with no binstub to run.
data/CHANGELOG.md CHANGED
@@ -1,5 +1,15 @@
1
1
  ### Unreleased
2
2
 
3
+ ### 2.4.0
4
+
5
+ - Add Test::Unit support, including lifecycle integration for dynamic models and
6
+ tables.
7
+ - Add `with_model/rspec`, `with_model/minitest`, and `with_model/test_unit`
8
+ convenience entrypoints.
9
+ - Add runner integration guides for RSpec, Minitest, and Test::Unit.
10
+ - Skip temporary-table teardown when the table has already disappeared.
11
+ - Require Active Record 8.0 or later.
12
+
3
13
  ### 2.3.1
4
14
 
5
15
  - Honor Active Record destruction callbacks and dependent associations when
data/CONTRIBUTING.md CHANGED
@@ -24,12 +24,22 @@ avoid anything that only works on the newest of either.
24
24
 
25
25
  ## Running the tests
26
26
 
27
- The suite runs against both supported test harnesses, so `with_model` has to work
28
- under each:
27
+ The suite runs against all three supported test harnesses, so `with_model` has
28
+ to work under each:
29
29
 
30
30
  - `spec/` covers behavior under RSpec.
31
- - `test/` covers the minitest life cycle — the setup and teardown hooks, and
31
+ - `test/` covers the Minitest life cycle — the setup and teardown hooks, and
32
32
  their ordering — rather than repeating the specs.
33
+ - `test_unit/` covers Test::Unit integration in a dedicated process, including
34
+ native fixture callback ordering.
35
+
36
+ Run one harness with its scoped Rake task:
37
+
38
+ ```sh
39
+ bundle exec rake spec
40
+ bundle exec rake test
41
+ bundle exec rake test_unit
42
+ ```
33
43
 
34
44
  Run everything, including the linter, with:
35
45
 
@@ -37,6 +47,13 @@ Run everything, including the linter, with:
37
47
  bundle exec rake
38
48
  ```
39
49
 
50
+ Validate the README and runner guides with RDoc before submitting documentation
51
+ changes:
52
+
53
+ ```sh
54
+ rm -rf tmp/rdoc && bundle exec rdoc --markup markdown --op tmp/rdoc README.md docs/rspec.md docs/minitest.md docs/test-unit.md lib/**/*.rb
55
+ ```
56
+
40
57
  `bin/` holds generated binstubs and is not checked in. Whether bundling writes
41
58
  them for you depends on your Bundler version, so create them once if you would
42
59
  rather type `bin/rake`:
data/Gemfile CHANGED
@@ -24,3 +24,4 @@ gem "rspec"
24
24
  gem "ruby-lsp"
25
25
  gem "sqlite3"
26
26
  gem "standard"
27
+ gem "test-unit"
data/README.md CHANGED
@@ -14,32 +14,63 @@
14
14
 
15
15
  Install as usual: `gem install with_model` or add `gem 'with_model'` to your Gemfile. See [`.github/workflows/ci.yml`](./.github/workflows/ci.yml) for supported (tested) Ruby versions.
16
16
 
17
+ ## Test runner setup
18
+
19
+ Choose the entrypoint for the test runner that owns the declarations. Each
20
+ performs complete global setup for one runner; see its guide for lifecycle and
21
+ manual-setup details.
22
+
17
23
  ### RSpec
18
24
 
19
- Extend `WithModel` into RSpec:
25
+ ```ruby
26
+ require "with_model/rspec"
27
+ ```
28
+
29
+ See the [RSpec guide](docs/rspec.md).
30
+
31
+ ### Minitest
20
32
 
21
33
  ```ruby
22
- require "with_model"
34
+ require "with_model/minitest"
35
+ ```
23
36
 
24
- RSpec.configure do |config|
25
- config.extend WithModel
26
- end
37
+ See the [Minitest guide](docs/minitest.md).
38
+
39
+ ### Test::Unit
40
+
41
+ ```ruby
42
+ require "with_model/test_unit"
27
43
  ```
28
44
 
29
- ### minitest/spec
45
+ See the [Test::Unit guide](docs/test-unit.md).
30
46
 
31
- Extend `WithModel` into minitest/spec and set the test runner explicitly:
47
+ ### Advanced and mixed-runner setup
48
+
49
+ Plain `require "with_model"` remains framework-neutral. For manual setup,
50
+ select a runner and extend its test context directly:
32
51
 
33
52
  ```ruby
53
+ require "minitest"
34
54
  require "with_model"
35
55
 
36
56
  WithModel.runner = :minitest
57
+ Minitest::Test.extend WithModel
58
+ ```
37
59
 
38
- class Minitest::Spec
39
- extend WithModel
60
+ RSpec uses `RSpec.configure { |config| config.extend WithModel }`; Test::Unit
61
+ uses `Test::Unit::TestCase.extend WithModel`. A declaration can override the
62
+ global runner when needed:
63
+
64
+ ```ruby
65
+ with_model :Post, runner: :test_unit do
66
+ table
40
67
  end
41
68
  ```
42
69
 
70
+ Loading multiple convenience entrypoints in one process is unsupported because
71
+ the last one changes the global runner. For mixed runners, use plain
72
+ `with_model`, direct extension, and explicit `runner:` options.
73
+
43
74
  ## Usage
44
75
 
45
76
  After setting up as above, call `with_model` and inside its block pass it a `table` block and a `model` block.
@@ -252,7 +283,13 @@ the order they are declared, and destroyed in the reverse order.
252
283
 
253
284
  ## Requirements
254
285
 
255
- See the [gemspec metadata](https://rubygems.org/gems/with_model) for dependency requirements. RSpec and minitest are indirect dependencies, and `with_model` should support any maintained version of both.
286
+ See the [gemspec metadata](https://rubygems.org/gems/with_model) for Ruby and
287
+ Active Record requirements. RSpec, Minitest, and Test::Unit are optional
288
+ consumer framework dependencies; install the framework used by its selected
289
+ entrypoint. Requiring a selected entrypoint without its framework installed
290
+ raises the framework's ordinary `LoadError`.
291
+
292
+ `scope:` is RSpec-only and is silently ignored by Minitest and Test::Unit.
256
293
 
257
294
  ## Thread-safety
258
295
 
data/Rakefile CHANGED
@@ -8,7 +8,13 @@ RSpec::Core::RakeTask.new
8
8
  require "minitest/test_task"
9
9
  Minitest::TestTask.create
10
10
 
11
+ require "rake/testtask"
12
+ Rake::TestTask.new(:test_unit) do |task|
13
+ task.libs << "lib" << "test_unit" << "test"
14
+ task.pattern = "test_unit/**/*_test.rb"
15
+ end
16
+
11
17
  # standard rake task
12
18
  require "standard/rake"
13
19
 
14
- task default: %i[spec test standard]
20
+ task default: %i[spec test test_unit standard]
data/docs/minitest.md ADDED
@@ -0,0 +1,45 @@
1
+ # Minitest integration
2
+
3
+ Load the Minitest entrypoint before defining tests:
4
+
5
+ ```ruby
6
+ require "with_model/minitest"
7
+ ```
8
+
9
+ This requires `minitest`, then `with_model`, selects the
10
+ `:minitest` runner, and extends `Minitest::Test`. Its class-level DSL is
11
+ inherited by `Minitest::Test` subclasses, including `Minitest::Spec` and
12
+ `ActiveSupport::TestCase`. The entrypoint does not require or start Minitest's
13
+ autorun — your process or test framework owns Minitest startup.
14
+
15
+ ## Lifecycle
16
+
17
+ The Minitest runner creates declared models and tables in `before_setup` and
18
+ destroys them in `after_teardown`. This makes them available to ordinary setup
19
+ methods and keeps them available through teardown. `scope:` is silently ignored
20
+ by Minitest because it is an RSpec-only option.
21
+
22
+ ## Manual setup
23
+
24
+ If another process or tool owns Minitest configuration, configure manually:
25
+
26
+ ```ruby
27
+ require "minitest"
28
+ require "with_model"
29
+
30
+ WithModel.runner = :minitest
31
+ Minitest::Test.extend WithModel
32
+ ```
33
+
34
+ Minitest is an optional consumer dependency. Requiring `with_model/minitest`
35
+ without Minitest installed raises Minitest's ordinary `LoadError`.
36
+
37
+ ## Mixed runners
38
+
39
+ Do not load multiple convenience entrypoints in one process: each changes the
40
+ global runner, and the last one wins. For mixed runners, load plain
41
+ `with_model`, extend each framework boundary directly, and select a runner per
42
+ declaration with `runner:`.
43
+
44
+ See the [general DSL reference](../README.md#usage) for declaring models and
45
+ tables.
data/docs/rspec.md ADDED
@@ -0,0 +1,43 @@
1
+ # RSpec integration
2
+
3
+ Load the RSpec entrypoint before defining example groups:
4
+
5
+ ```ruby
6
+ require "with_model/rspec"
7
+ ```
8
+
9
+ The entrypoint requires `rspec/core` and `with_model`, selects the `:rspec`
10
+ runner, and extends RSpec example groups through `RSpec.configure`. It does not
11
+ start RSpec; the RSpec command-line runner remains in control.
12
+
13
+ ## Lifecycle
14
+
15
+ `with_model` and `with_table` register RSpec `before` and `after` hooks. Models
16
+ and tables are available to examples after setup and are removed after cleanup.
17
+ Use `scope:` when RSpec hook scope is needed, including `scope: :all` for a
18
+ model needed by `before(:all)`. `scope:` is an RSpec-only option.
19
+
20
+ ## Manual setup
21
+
22
+ For runner-owned setup, require the framework and `with_model` separately:
23
+
24
+ ```ruby
25
+ require "rspec/core"
26
+ require "with_model"
27
+
28
+ WithModel.runner = :rspec
29
+ RSpec.configure { |config| config.extend WithModel }
30
+ ```
31
+
32
+ RSpec is an optional consumer dependency. Requiring `with_model/rspec` without
33
+ RSpec installed raises RSpec's ordinary `LoadError`.
34
+
35
+ ## Mixed runners
36
+
37
+ Do not load multiple convenience entrypoints in one process: each changes the
38
+ global runner, and the last one wins. For mixed runners, load plain
39
+ `with_model`, extend each framework boundary directly, and select a runner per
40
+ declaration with `runner:`.
41
+
42
+ See the [general DSL reference](../README.md#usage) for declaring models and
43
+ tables.
data/docs/test-unit.md ADDED
@@ -0,0 +1,73 @@
1
+ # Test::Unit integration
2
+
3
+ Load the Test::Unit entrypoint before defining tests:
4
+
5
+ ```ruby
6
+ require "with_model/test_unit"
7
+ ```
8
+
9
+ The entrypoint requires `test/unit`, then `with_model`, selects the
10
+ `:test_unit` runner, and extends `Test::Unit::TestCase`. Test::Unit retains
11
+ ownership of autorun and fixture execution.
12
+
13
+ ## Lifecycle
14
+
15
+ `with_model` and `with_table` register Test::Unit fixture callbacks with exact
16
+ placement: setup uses `before: :append`, while teardown uses `after: :prepend`.
17
+ Declared models and tables therefore create in declaration order before user or
18
+ inherited setup methods and callbacks. They destroy in reverse order after user
19
+ or inherited teardown methods and callbacks.
20
+
21
+ Test::Unit runs teardown when setup raises. `WithModel::Model#destroy`
22
+ defensively handles creation that never assigned a model. `Table#destroy` checks
23
+ whether its table exists before dropping it, so teardown does not raise solely
24
+ because a reconnect replaced an in-memory database or the table otherwise
25
+ already disappeared. It does not rescue other database, connection, or drop
26
+ errors; those still propagate. This conditional drop check does not promise
27
+ generic partial-creation recovery for bare `with_table`. `scope:` is silently
28
+ ignored by Test::Unit because it is an RSpec-only option.
29
+
30
+ ## Errata and concerns
31
+
32
+ Establish the Active Record connection before `with_model`'s setup callback
33
+ creates tables and models. An ordinary `def setup` runs too late. Connect in
34
+ your test helper before test classes load, or register a connection callback
35
+ with `setup(before: :prepend)`.
36
+
37
+ ```ruby
38
+ class PostTest < Test::Unit::TestCase
39
+ setup(before: :prepend) do
40
+ ActiveRecord::Base.establish_connection(adapter: "sqlite3", database: ":memory:")
41
+ end
42
+
43
+ with_model :Post do
44
+ table
45
+ end
46
+ end
47
+ ```
48
+
49
+ ## Manual setup
50
+
51
+ For runner-owned setup, require Test::Unit and `with_model` separately:
52
+
53
+ ```ruby
54
+ require "test/unit"
55
+ require "with_model"
56
+
57
+ WithModel.runner = :test_unit
58
+ Test::Unit::TestCase.extend WithModel
59
+ ```
60
+
61
+ Test::Unit is an optional consumer dependency. Requiring
62
+ `with_model/test_unit` without Test::Unit installed raises Test::Unit's ordinary
63
+ `LoadError`.
64
+
65
+ ## Mixed runners
66
+
67
+ Do not load multiple convenience entrypoints in one process: each changes the
68
+ global runner, and the last one wins. For mixed runners, load plain
69
+ `with_model`, extend each framework boundary directly, and select a runner per
70
+ declaration with `runner:`.
71
+
72
+ See the [general DSL reference](../README.md#usage) for declaring models and
73
+ tables.
@@ -74,14 +74,6 @@ module WithModel
74
74
  end
75
75
  end
76
76
 
77
- class ActiveRecord::Base
78
- extend WithModel::DescendantsTracker::DestroyedClassesFiltering
79
- end
80
-
81
- module ActiveSupport
82
- module DescendantsTracker
83
- class << self
84
- attr_reader :clear_disabled
85
- end
86
- end
87
- end
77
+ ActiveRecord::Base.singleton_class.prepend(
78
+ WithModel::DescendantsTracker::DestroyedClassesFiltering
79
+ )
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "minitest"
4
+ require "with_model"
5
+
6
+ WithModel.runner = :minitest
7
+ Minitest::Test.extend WithModel
@@ -74,8 +74,6 @@ module WithModel
74
74
  # can only do so while the class still has its name.
75
75
  table.teardown(@model)
76
76
  stubber.unstub_const
77
- cleanup_descendants_tracking
78
- reset_dependencies_cache
79
77
  WithModel::DescendantsTracker.clear([@model])
80
78
  @model = nil
81
79
  end
@@ -132,17 +130,6 @@ module WithModel
132
130
  @model.reset_column_information
133
131
  end
134
132
 
135
- def cleanup_descendants_tracking
136
- ActiveSupport::DescendantsTracker.clear([@model]) \
137
- unless ActiveSupport::DescendantsTracker.clear_disabled
138
- end
139
-
140
- def reset_dependencies_cache
141
- return unless defined?(ActiveSupport::Dependencies::Reference)
142
-
143
- ActiveSupport::Dependencies::Reference.clear!
144
- end
145
-
146
133
  def stubber
147
134
  @stubber ||= ConstantStubber.new const_name
148
135
  end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rspec/core"
4
+ require "with_model"
5
+
6
+ WithModel.runner = :rspec
7
+
8
+ RSpec.configure do |config|
9
+ config.extend WithModel
10
+ end
@@ -38,19 +38,13 @@ module WithModel
38
38
  end
39
39
 
40
40
  def destroy
41
- connection.drop_table(@name)
41
+ connection.drop_table(@name) if exists?
42
42
  end
43
43
 
44
44
  private
45
45
 
46
46
  attr_reader :connection
47
47
 
48
- def exists?
49
- if connection.respond_to?(:data_source_exists?)
50
- connection.data_source_exists?(@name)
51
- else
52
- connection.table_exists?(@name)
53
- end
54
- end
48
+ def exists? = connection.data_source_exists?(@name)
55
49
  end
56
50
  end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "test/unit"
4
+ require "with_model"
5
+
6
+ WithModel.runner = :test_unit
7
+ Test::Unit::TestCase.extend WithModel
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module WithModel
4
- VERSION = "2.3.1"
4
+ VERSION = "2.4.0"
5
5
  end
data/lib/with_model.rb CHANGED
@@ -113,8 +113,13 @@ module WithModel
113
113
  class_eval do
114
114
  include MiniTestLifeCycle.call(object)
115
115
  end
116
+ when :test_unit
117
+ # These options place creation before and destruction after user callbacks,
118
+ # preserving declaration and reverse order across inheritance.
119
+ setup(before: :append) { object.create }
120
+ teardown(after: :prepend) { object.destroy }
116
121
  else
117
- raise ArgumentError, "Unsupported test runner set, expected :rspec or :minitest"
122
+ raise ArgumentError, "Unsupported test runner set, expected :rspec, :minitest, or :test_unit"
118
123
  end
119
124
  end
120
125
  end
@@ -30,7 +30,6 @@ describe "Descendants tracking" do
30
30
 
31
31
  context "with ActiveSupport::DescendantsTracker (cache_classes: true)" do
32
32
  before do
33
- expect(ActiveSupport::DescendantsTracker.clear_disabled).to be_falsey
34
33
  expect { ActiveSupport::DescendantsTracker.clear([]) }.not_to raise_exception
35
34
  end
36
35
 
@@ -40,10 +39,29 @@ describe "Descendants tracking" do
40
39
  context "without ActiveSupport::DescendantsTracker (cache_classes: false)" do
41
40
  before do
42
41
  ActiveSupport::DescendantsTracker.disable_clear!
43
- expect(ActiveSupport::DescendantsTracker.clear_disabled).to be_truthy
44
42
  expect { ActiveSupport::DescendantsTracker.clear([]) }.to raise_exception(RuntimeError)
45
43
  end
46
44
 
47
45
  include_examples "clearing descendants between test runs"
48
46
  end
47
+
48
+ it "keeps filtering ahead of later class method extensions" do
49
+ raw_subclasses = Class.instance_method(:subclasses).super_method
50
+ raw_descendants = Class.instance_method(:descendants).super_method
51
+ override = Module.new do
52
+ define_method(:subclasses) { raw_subclasses.bind_call(self) }
53
+ define_method(:descendants) { raw_descendants.bind_call(self) }
54
+ end
55
+ destroyed_model = stub_const("DestroyedModel", Class.new(ActiveRecord::Base))
56
+ WithModel::DescendantsTracker.clear([destroyed_model])
57
+
58
+ ActiveRecord::Base.extend override
59
+
60
+ expect(ActiveRecord::Base.descendants).not_to include(destroyed_model)
61
+ ensure
62
+ override&.module_eval do
63
+ remove_method :subclasses
64
+ remove_method :descendants
65
+ end
66
+ end
49
67
  end
@@ -0,0 +1,153 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "open3"
4
+ require "rbconfig"
5
+ require "tmpdir"
6
+
7
+ RSpec.describe "runner integration entrypoints" do
8
+ def run_ruby(source, load_paths: ["lib"])
9
+ Open3.capture3(
10
+ RbConfig.ruby,
11
+ *load_paths.map { |path| "-I#{path}" },
12
+ "-e",
13
+ source
14
+ )
15
+ end
16
+
17
+ def expect_native_runner_to_pass(source, load_paths: ["lib"])
18
+ stdout, stderr, status = run_ruby(source, load_paths:)
19
+
20
+ expect(status).to be_success, <<~MESSAGE
21
+ subprocess failed with status #{status.exitstatus}
22
+ stdout:
23
+ #{stdout}
24
+ stderr:
25
+ #{stderr}
26
+ MESSAGE
27
+ end
28
+
29
+ it "configures RSpec and extends new example groups" do
30
+ expect_native_runner_to_pass(<<~'RUBY')
31
+ require "with_model/rspec"
32
+ abort "runner: #{WithModel.runner.inspect}" unless WithModel.runner == :rspec
33
+
34
+ RSpec.describe "entrypoint" do
35
+ it "installs the DSL" do
36
+ expect(self.class).to respond_to(:with_model)
37
+ end
38
+ end
39
+
40
+ exit RSpec::Core::Runner.run([])
41
+ RUBY
42
+ end
43
+
44
+ it "configures Minitest and extends new test subclasses" do
45
+ expect_native_runner_to_pass(<<~'RUBY')
46
+ require "with_model/minitest"
47
+ abort "runner: #{WithModel.runner.inspect}" unless WithModel.runner == :minitest
48
+
49
+ # Assert that minitest/autorun is not loaded
50
+ autorun_loaded = $LOADED_FEATURES.any? { |f| f.end_with?("/minitest/autorun.rb") }
51
+ abort "autorun should not be loaded; loaded features: #{$LOADED_FEATURES.inspect}" if autorun_loaded
52
+
53
+ class MinitestEntrypointTest < Minitest::Test
54
+ def test_installs_the_dsl
55
+ assert_respond_to self.class, :with_model
56
+ end
57
+ end
58
+
59
+ exit(Minitest.run ? 0 : 1)
60
+ RUBY
61
+ end
62
+
63
+ it "configures Test::Unit and extends new test subclasses" do
64
+ expect_native_runner_to_pass(<<~'RUBY')
65
+ require "with_model/test_unit"
66
+ abort "runner: #{WithModel.runner.inspect}" unless WithModel.runner == :test_unit
67
+
68
+ class TestUnitEntrypointTest < Test::Unit::TestCase
69
+ def test_installs_the_dsl
70
+ assert_respond_to self.class, :with_model
71
+ end
72
+ end
73
+ RUBY
74
+ end
75
+
76
+ it "propagates Test::Unit's LoadError when the framework is unavailable" do
77
+ Dir.mktmpdir do |directory|
78
+ Dir.mkdir(File.join(directory, "test"))
79
+ File.write(
80
+ File.join(directory, "test/unit.rb"),
81
+ 'raise LoadError, "distinctive test/unit LoadError"'
82
+ )
83
+
84
+ expect_native_runner_to_pass(<<~'RUBY', load_paths: [directory, "lib"])
85
+ begin
86
+ require "with_model/test_unit"
87
+ rescue LoadError => error
88
+ abort "unexpected LoadError: #{error.message.inspect}" unless
89
+ error.message == "distinctive test/unit LoadError"
90
+ else
91
+ abort "expected a LoadError"
92
+ end
93
+ RUBY
94
+ end
95
+ end
96
+
97
+ it "retains manual Test::Unit setup with plain with_model" do
98
+ expect_native_runner_to_pass(<<~'RUBY')
99
+ require "test/unit"
100
+ require "with_model"
101
+
102
+ WithModel.runner = :test_unit
103
+ Test::Unit::TestCase.extend WithModel
104
+ abort "runner: #{WithModel.runner.inspect}" unless WithModel.runner == :test_unit
105
+
106
+ class PlainWithModelTestUnitTest < Test::Unit::TestCase
107
+ def test_installs_the_dsl
108
+ assert_respond_to self.class, :with_model
109
+ end
110
+ end
111
+ RUBY
112
+ end
113
+
114
+ it "retains manual RSpec setup with plain with_model" do
115
+ expect_native_runner_to_pass(<<~'RUBY')
116
+ require "rspec"
117
+ require "with_model"
118
+
119
+ WithModel.runner = :rspec
120
+ RSpec.configure do |config|
121
+ config.extend WithModel
122
+ end
123
+ abort "runner: #{WithModel.runner.inspect}" unless WithModel.runner == :rspec
124
+
125
+ RSpec.describe "entrypoint" do
126
+ it "installs the DSL" do
127
+ expect(self.class).to respond_to(:with_model)
128
+ end
129
+ end
130
+
131
+ exit RSpec::Core::Runner.run([])
132
+ RUBY
133
+ end
134
+
135
+ it "retains manual Minitest setup with plain with_model" do
136
+ expect_native_runner_to_pass(<<~'RUBY')
137
+ require "minitest"
138
+ require "with_model"
139
+
140
+ WithModel.runner = :minitest
141
+ Minitest::Test.extend WithModel
142
+ abort "runner: #{WithModel.runner.inspect}" unless WithModel.runner == :minitest
143
+
144
+ class PlainWithModelMinitestTest < Minitest::Test
145
+ def test_installs_the_dsl
146
+ assert_respond_to self.class, :with_model
147
+ end
148
+ end
149
+
150
+ exit(Minitest.run ? 0 : 1)
151
+ RUBY
152
+ end
153
+ end
@@ -21,10 +21,6 @@ RSpec.describe "table(false)" do
21
21
  classes = @stubbed_active_record_classes
22
22
  next unless classes
23
23
 
24
- ActiveSupport::DescendantsTracker.clear(classes) \
25
- unless ActiveSupport::DescendantsTracker.clear_disabled
26
- ActiveSupport::Dependencies::Reference.clear! \
27
- if defined?(ActiveSupport::Dependencies::Reference)
28
24
  WithModel::DescendantsTracker.clear(classes)
29
25
  end
30
26
 
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+
5
+ RSpec.describe WithModel::Table do
6
+ let(:connection_owner) do
7
+ klass = Class.new(ActiveRecord::Base)
8
+ stub_const("TableSpecConnection", klass)
9
+ klass.abstract_class = true
10
+ klass.establish_connection(adapter: "sqlite3", database: ":memory:")
11
+ klass
12
+ end
13
+ let(:connection) { connection_owner.connection }
14
+ let(:table) { described_class.new(:temporary_records, connection:) }
15
+
16
+ after { connection_owner.connection_pool.disconnect! }
17
+
18
+ describe "#destroy" do
19
+ it "does not raise after its connection reconnects before teardown" do
20
+ table.create
21
+
22
+ connection.disconnect!
23
+ connection.reconnect!
24
+
25
+ expect(connection.data_source_exists?(:temporary_records)).to be false
26
+ expect { table.destroy }.not_to raise_error
27
+ end
28
+
29
+ it "drops an existing table" do
30
+ table.create
31
+
32
+ table.destroy
33
+
34
+ expect(connection.data_source_exists?(:temporary_records)).to be false
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,186 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "test_unit_helper"
4
+
5
+ class TestUnitLifecycleBaseTest < Test::Unit::TestCase
6
+ FIXTURE_EVENTS = []
7
+
8
+ with_model :TestUnitLifecycleParent do
9
+ table { |t| t.string "name" }
10
+ end
11
+
12
+ def setup
13
+ FIXTURE_EVENTS.clear
14
+ assert_kind_of Class, TestUnitLifecycleParent
15
+ FIXTURE_EVENTS << :inherited_setup
16
+ end
17
+
18
+ def teardown
19
+ assert_kind_of Class, TestUnitLifecycleParent
20
+ FIXTURE_EVENTS << :inherited_teardown
21
+ end
22
+ end
23
+
24
+ class TestUnitLifecycleTest < TestUnitLifecycleBaseTest
25
+ with_model :TestUnitLifecycleChild do
26
+ table do |t|
27
+ t.references "parent", foreign_key: {to_table: TestUnitLifecycleParent.table_name}
28
+ end
29
+
30
+ model { belongs_to :parent, class_name: TestUnitLifecycleParent.name }
31
+ end
32
+
33
+ def setup
34
+ super
35
+ assert_kind_of Class, TestUnitLifecycleChild
36
+ FIXTURE_EVENTS << :local_setup
37
+ end
38
+
39
+ def teardown
40
+ assert_kind_of Class, TestUnitLifecycleChild
41
+ FIXTURE_EVENTS << :local_teardown
42
+ super
43
+ return unless name == "test_models_are_available_through_user_fixtures"
44
+
45
+ assert_equal [
46
+ :inherited_setup,
47
+ :local_setup,
48
+ :test_body,
49
+ :local_teardown,
50
+ :inherited_teardown
51
+ ], FIXTURE_EVENTS
52
+ end
53
+
54
+ def test_models_are_available_through_user_fixtures
55
+ parent = TestUnitLifecycleParent.create!(name: "parent")
56
+ child = TestUnitLifecycleChild.create!(parent: parent)
57
+
58
+ assert_equal parent, child.reload.parent
59
+ FIXTURE_EVENTS << :test_body
60
+ end
61
+
62
+ def test_model_destruction_unwinds_inheritance_order
63
+ parent_test_case = Class.new(Test::Unit::TestCase) do
64
+ with_model :TestUnitLifecycleOrderParent do
65
+ table
66
+ end
67
+ end
68
+ test_case = Class.new(parent_test_case) do
69
+ teardown(after: :prepend) do
70
+ assert_false Object.const_defined?(:TestUnitLifecycleOrderChild)
71
+ assert_false ActiveRecord::Base.connection.data_source_exists?(child_table_name)
72
+ assert_kind_of Class, TestUnitLifecycleOrderParent
73
+ assert ActiveRecord::Base.connection.data_source_exists?(parent_table_name)
74
+ end
75
+
76
+ with_model :TestUnitLifecycleOrderChild do
77
+ table
78
+ end
79
+
80
+ define_method(:parent_table_name) { TestUnitLifecycleOrderParent.table_name }
81
+ define_method(:child_table_name) { @child_table_name }
82
+ define_method(:test_lifecycle) { @child_table_name = TestUnitLifecycleOrderChild.table_name }
83
+ end
84
+
85
+ result = Test::Unit::TestResult.new
86
+ test_case.new("test_lifecycle").run(result) {}
87
+
88
+ assert_equal 1, result.run_count
89
+ assert_equal 0, result.error_count
90
+ end
91
+ end
92
+
93
+ class TestUnitUserHookFailureTest < Test::Unit::TestCase
94
+ def test_setup_error_does_not_prevent_model_cleanup
95
+ test_case = Class.new(Test::Unit::TestCase) do
96
+ class << self
97
+ attr_accessor :created_table_name
98
+ end
99
+
100
+ with_model :TestUnitSetupFailureModel do
101
+ table
102
+ end
103
+
104
+ def setup
105
+ self.class.created_table_name = TestUnitSetupFailureModel.table_name
106
+ raise "setup failed"
107
+ end
108
+
109
+ def test_setup_failure
110
+ end
111
+ end
112
+
113
+ result = Test::Unit::TestResult.new
114
+ test_case.new("test_setup_failure").run(result) {}
115
+
116
+ assert_hook_failure_cleans_up_model(
117
+ result,
118
+ RuntimeError,
119
+ "setup failed",
120
+ :TestUnitSetupFailureModel,
121
+ test_case.created_table_name
122
+ )
123
+ end
124
+
125
+ def test_teardown_error_does_not_prevent_model_cleanup
126
+ test_case = Class.new(Test::Unit::TestCase) do
127
+ class << self
128
+ attr_accessor :created_table_name
129
+ end
130
+
131
+ with_model :TestUnitTeardownFailureModel do
132
+ table
133
+ end
134
+
135
+ def teardown
136
+ self.class.created_table_name = TestUnitTeardownFailureModel.table_name
137
+ raise "teardown failed"
138
+ end
139
+
140
+ def test_teardown_failure
141
+ end
142
+ end
143
+
144
+ result = Test::Unit::TestResult.new
145
+ test_case.new("test_teardown_failure").run(result) {}
146
+
147
+ assert_hook_failure_cleans_up_model(
148
+ result,
149
+ RuntimeError,
150
+ "teardown failed",
151
+ :TestUnitTeardownFailureModel,
152
+ test_case.created_table_name
153
+ )
154
+ end
155
+
156
+ private
157
+
158
+ def assert_hook_failure_cleans_up_model(result, error_class, message, constant, table_name)
159
+ assert_equal 1, result.run_count
160
+ assert_equal 1, result.error_count
161
+ assert_equal error_class, result.errors.first.exception.class
162
+ assert_equal message, result.errors.first.exception.message
163
+ assert_false Object.const_defined?(constant)
164
+ assert_false ActiveRecord::Base.connection.data_source_exists?(table_name)
165
+ end
166
+ end
167
+
168
+ class TestUnitPartialModelCreationFailureTest < Test::Unit::TestCase
169
+ def test_preserves_the_setup_error_when_model_creation_does_not_assign_a_model
170
+ test_case = Class.new(Test::Unit::TestCase) do
171
+ with_model :TestUnitPartiallyCreatedModel, superclass: Object do
172
+ table(false)
173
+ end
174
+
175
+ def test_setup_failure
176
+ end
177
+ end
178
+
179
+ result = Test::Unit::TestResult.new
180
+ test_case.new("test_setup_failure").run(result) {}
181
+
182
+ assert_equal 1, result.run_count
183
+ assert_equal 1, result.error_count
184
+ assert_kind_of WithModel::InvalidSuperclass, result.errors.first.exception
185
+ end
186
+ end
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "test_unit_helper"
4
+
5
+ # Stands in for an application's own model, the usual reason to want single table
6
+ # inheritance: its table is created once and outlives every test here, so a
7
+ # with_model child's rows cannot be disposed of by dropping the table.
8
+ module TestUnitAppModels
9
+ class Cupboard < ActiveRecord::Base
10
+ end
11
+ end
12
+
13
+ ActiveRecord::Base.connection.create_table TestUnitAppModels::Cupboard.table_name,
14
+ force: true do |t|
15
+ t.string "type"
16
+ t.string "name"
17
+ end
18
+
19
+ # A with_model parent and an STI child in the same test case. The child's
20
+ # superclass is resolved per-example, so this only works if the parent is
21
+ # created before the child and destroyed after it.
22
+ class TestUnitStiTest < Test::Unit::TestCase
23
+ with_model :TestUnitVehicle do
24
+ table do |t|
25
+ t.string "type"
26
+ t.string "name"
27
+ end
28
+ end
29
+
30
+ with_model :TestUnitTruck, superclass: -> { TestUnitVehicle } do
31
+ table(false)
32
+ end
33
+
34
+ with_model :TestUnitChest, superclass: "TestUnitAppModels::Cupboard" do
35
+ table(false)
36
+ end
37
+
38
+ def test_the_child_shares_the_parents_table
39
+ assert_equal TestUnitVehicle.table_name, TestUnitTruck.table_name
40
+ end
41
+
42
+ def test_the_child_stores_its_own_type
43
+ truck = TestUnitTruck.create!(name: "Ford F-150")
44
+
45
+ assert_equal "TestUnitTruck", truck.reload.type
46
+ assert_instance_of TestUnitTruck, TestUnitVehicle.first
47
+ end
48
+
49
+ # Rows in a table the child does not own have to be deleted when it goes away,
50
+ # since nothing else will: Cupboard's table is still standing afterwards, and a
51
+ # row naming a class that no longer exists makes it unloadable. Both tests write
52
+ # one and first insist the table is empty, so whichever test happens to run
53
+ # second fails if the rows survived teardown.
54
+ def test_the_childs_rows_do_not_outlive_it
55
+ assert_empty TestUnitAppModels::Cupboard.all,
56
+ "a previous test's rows outlived it"
57
+
58
+ TestUnitChest.create!(name: "sideboard")
59
+
60
+ assert_equal 1, TestUnitAppModels::Cupboard.count
61
+ end
62
+
63
+ def test_the_childs_rows_leave_the_superclass_loadable
64
+ assert_empty TestUnitAppModels::Cupboard.all,
65
+ "a previous test's rows outlived it"
66
+
67
+ TestUnitChest.create!(name: "wardrobe")
68
+
69
+ assert_instance_of TestUnitChest, TestUnitAppModels::Cupboard.first
70
+ end
71
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ $LOAD_PATH.unshift File.expand_path("../lib", __dir__)
4
+
5
+ require "active_record"
6
+ require "test/unit"
7
+ require "with_model"
8
+
9
+ WithModel.runner = :test_unit
10
+
11
+ Test::Unit::TestCase.extend WithModel
12
+
13
+ # WithModel requires ActiveRecord::Base.connection to be established.
14
+ # If ActiveRecord already has a connection, as in a Rails app, this is unnecessary.
15
+ ActiveRecord::Base.establish_connection(adapter: "sqlite3", database: ":memory:")
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "test_unit_helper"
4
+
5
+ class TestUnitWithModelTest < Test::Unit::TestCase
6
+ with_model :TestUnitBlogPost do
7
+ table { |t| t.string "title" }
8
+ model { define_method(:fancy_title) { "Title: #{title}" } }
9
+ end
10
+
11
+ with_table :test_unit_widgets do |t|
12
+ t.string "name"
13
+ end
14
+
15
+ def test_creates_a_temporary_active_record_model
16
+ record = TestUnitBlogPost.create!(title: "New blog post")
17
+
18
+ assert_equal "New blog post", record.reload.title
19
+ end
20
+
21
+ def test_defines_model_methods
22
+ assert_equal "Title: New blog post", TestUnitBlogPost.new(title: "New blog post").fancy_title
23
+ end
24
+
25
+ def test_defines_the_model_constant
26
+ assert_kind_of Class, TestUnitBlogPost
27
+ end
28
+
29
+ def test_creates_a_temporary_table
30
+ assert_true ActiveRecord::Base.connection.data_source_exists?("test_unit_widgets")
31
+ end
32
+ end
33
+
34
+ class TestUnitPerDeclarationRunnerTest < Test::Unit::TestCase
35
+ with_model :TestUnitPerDeclarationModel, runner: :test_unit do
36
+ table
37
+ end
38
+
39
+ def test_supports_a_test_unit_runner_override
40
+ assert_kind_of Class, TestUnitPerDeclarationModel
41
+ end
42
+ end
43
+
44
+ class TestUnitRunnerValidationTest < Test::Unit::TestCase
45
+ def test_names_all_supported_runners_for_an_unsupported_runner
46
+ error = assert_raise(ArgumentError) do
47
+ Class.new(Test::Unit::TestCase) do
48
+ extend WithModel
49
+
50
+ with_model :UnsupportedTestUnitModel, runner: :unsupported do
51
+ table
52
+ end
53
+ end
54
+ end
55
+
56
+ assert_equal "Unsupported test runner set, expected :rspec, :minitest, or :test_unit", error.message
57
+ end
58
+ end
data/with_model.gemspec CHANGED
@@ -9,7 +9,8 @@ Gem::Specification.new do |spec|
9
9
  spec.authors = ["Case Commons, LLC", "Grant Hutchins", "Andrew Marshall"]
10
10
  spec.email = %w[casecommons-dev@googlegroups.com gems@nertzy.com andrew@johnandrewmarshall.com]
11
11
  spec.homepage = "https://github.com/Casecommons/with_model"
12
- spec.summary = "Dynamically build a model within an RSpec context"
12
+ spec.summary = "Dynamically build an Active Record model for " \
13
+ "RSpec, Minitest, and Test::Unit tests"
13
14
  spec.description = spec.summary
14
15
  spec.license = "MIT"
15
16
  spec.metadata["rubygems_mfa_required"] = "true"
@@ -20,5 +21,5 @@ Gem::Specification.new do |spec|
20
21
 
21
22
  spec.required_ruby_version = ">= 3.3"
22
23
 
23
- spec.add_dependency "activerecord", ">= 7.2"
24
+ spec.add_dependency "activerecord", ">= 8.0"
24
25
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: with_model
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.3.1
4
+ version: 2.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Case Commons, LLC
@@ -17,15 +17,16 @@ dependencies:
17
17
  requirements:
18
18
  - - ">="
19
19
  - !ruby/object:Gem::Version
20
- version: '7.2'
20
+ version: '8.0'
21
21
  type: :runtime
22
22
  prerelease: false
23
23
  version_requirements: !ruby/object:Gem::Requirement
24
24
  requirements:
25
25
  - - ">="
26
26
  - !ruby/object:Gem::Version
27
- version: '7.2'
28
- description: Dynamically build a model within an RSpec context
27
+ version: '8.0'
28
+ description: Dynamically build an Active Record model for RSpec, Minitest, and Test::Unit
29
+ tests
29
30
  email:
30
31
  - casecommons-dev@googlegroups.com
31
32
  - gems@nertzy.com
@@ -47,29 +48,41 @@ files:
47
48
  - LICENSE
48
49
  - README.md
49
50
  - Rakefile
51
+ - docs/minitest.md
52
+ - docs/rspec.md
53
+ - docs/test-unit.md
50
54
  - lib/with_model.rb
51
55
  - lib/with_model/constant_stubber.rb
52
56
  - lib/with_model/descendants_tracker.rb
53
57
  - lib/with_model/invalid_superclass.rb
54
58
  - lib/with_model/methods.rb
59
+ - lib/with_model/minitest.rb
55
60
  - lib/with_model/missing_superclass.rb
56
61
  - lib/with_model/model.rb
57
62
  - lib/with_model/model/dsl.rb
58
63
  - lib/with_model/null_table.rb
64
+ - lib/with_model/rspec.rb
59
65
  - lib/with_model/table.rb
66
+ - lib/with_model/test_unit.rb
60
67
  - lib/with_model/version.rb
61
68
  - spec/active_record_behaviors_spec.rb
62
69
  - spec/constant_stubber_spec.rb
63
70
  - spec/descendants_tracking_spec.rb
64
71
  - spec/readme_spec.rb
72
+ - spec/runner_entrypoints_spec.rb
65
73
  - spec/spec_helper.rb
66
74
  - spec/table_false_spec.rb
75
+ - spec/table_spec.rb
67
76
  - spec/with_model_spec.rb
68
77
  - test/declaration_order_test.rb
69
78
  - test/spec_dsl_test.rb
70
79
  - test/sti_test.rb
71
80
  - test/test_helper.rb
72
81
  - test/with_model_test.rb
82
+ - test_unit/lifecycle_test.rb
83
+ - test_unit/sti_test.rb
84
+ - test_unit/test_unit_helper.rb
85
+ - test_unit/with_model_test.rb
73
86
  - with_model.gemspec
74
87
  homepage: https://github.com/Casecommons/with_model
75
88
  licenses:
@@ -90,7 +103,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
90
103
  - !ruby/object:Gem::Version
91
104
  version: '0'
92
105
  requirements: []
93
- rubygems_version: 4.0.18
106
+ rubygems_version: 4.0.20
94
107
  specification_version: 4
95
- summary: Dynamically build a model within an RSpec context
108
+ summary: Dynamically build an Active Record model for RSpec, Minitest, and Test::Unit
109
+ tests
96
110
  test_files: []