Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion lib/tapioca/dsl/compilers/active_record_fixtures.rb
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,21 @@ module Compilers
# # test_case.rbi
# # typed: true
# class ActiveSupport::TestCase
# include ActiveRecord::TestFixtures
#
# sig { returns(T::Array[Post]) } # No names: returns an Array of all fixtures
# sig { params(fixture_name: T.any(String, Symbol)).returns(Post) } # One name: returns the requested fixture
# sig { params(fixture_name: T.any(String, Symbol), other_fixtures: T.any(String, Symbol)) # Many names: returns an Array of the requested fixtures
# .returns(T::Array[Post]) }
# def posts(fixture_name = nil, *other_fixtures); end
# end
# ~~~
#
# The `include` is generated because Rails mixes `ActiveRecord::TestFixtures` into
# `ActiveSupport::TestCase` through the `:active_support_test_case` load hook in
# `rails/test_help.rb`. Since RBI generation does not load an app's test helper, this runtime
# include is not captured. Without it, Sorbet does not see the class methods the module
# contributes via `mixes_in_class_methods`, such as `fixtures`.
#: [ConstantType = singleton(ActiveSupport::TestCase)]
class ActiveRecordFixtures < Compiler
MISSING = Object.new
Expand All @@ -47,9 +55,10 @@ def decorate
end

method_names.select! { |name| fixture_class_mapping_from_fixture_files[name] != MISSING }
return if method_names.empty?

root.create_path(constant) do |mod|
mod.create_include("ActiveRecord::TestFixtures")
Comment thread
KaanOzkan marked this conversation as resolved.

method_names.each do |name|
create_fixture_method(mod, name.to_s)
end
Expand Down
8 changes: 8 additions & 0 deletions manual/compiler_activerecordfixtures.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,18 @@ The generated RBI by this compiler will produce the following
# test_case.rbi
# typed: true
class ActiveSupport::TestCase
include ActiveRecord::TestFixtures

sig { returns(T::Array[Post]) } # No names: returns an Array of all fixtures
sig { params(fixture_name: T.any(String, Symbol)).returns(Post) } # One name: returns the requested fixture
sig { params(fixture_name: T.any(String, Symbol), other_fixtures: T.any(String, Symbol)) # Many names: returns an Array of the requested fixtures
.returns(T::Array[Post]) }
def posts(fixture_name = nil, *other_fixtures); end
end
~~~

The `include` is generated because Rails mixes `ActiveRecord::TestFixtures` into
`ActiveSupport::TestCase` through the `:active_support_test_case` load hook in
`rails/test_help.rb`. Since RBI generation does not load an app's test helper, this runtime
include is not captured. Without it, Sorbet does not see the class methods the module contributes
via `mixes_in_class_methods`, such as `fixtures`.
68 changes: 65 additions & 3 deletions spec/tapioca/dsl/compilers/active_record_fixtures_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -47,12 +47,42 @@ class User
assert_equal(["ActiveSupport::TestCase"], gathered_constants)
end

it "does nothing if there are no fixtures" do
it "makes fixture class methods available when there are no fixtures" do
expected = <<~RBI
# typed: strong

class ActiveSupport::TestCase
include ActiveRecord::TestFixtures
end
RBI

assert_equal(expected, rbi_for("ActiveSupport::TestCase"))
generated_rbi = rbi_for("ActiveSupport::TestCase")
dependencies_rbi = add_content_file("dependencies.rbi", <<~RBI)
# typed: true

module ActiveRecord::TestFixtures
mixes_in_class_methods ::ActiveRecord::TestFixtures::ClassMethods
end

module ActiveRecord::TestFixtures::ClassMethods
def fixtures(*fixture_set_names); end
end

class ActiveSupport::TestCase; end
RBI
generated_rbi_file = add_content_file("generated.rbi", generated_rbi)
test_file = add_content_file("test_case.rb", <<~RUBY)
# typed: true

class PostTest < ActiveSupport::TestCase
fixtures :all
end
RUBY

result = context.sorbet("--no-config", dependencies_rbi, generated_rbi_file, test_file)

assert(result.status, result.err)
assert_equal(expected, generated_rbi)
end

it "ignores fixtures that do not have an associated model" do
Expand All @@ -79,6 +109,8 @@ class Post < ActiveRecord::Base
# typed: strong

class ActiveSupport::TestCase
include ActiveRecord::TestFixtures

sig { returns(T::Array[Post]) }
sig { params(fixture_name: T.any(String, Symbol)).returns(Post) }
sig { params(fixture_name: T.any(String, Symbol), other_fixtures: T.any(String, Symbol)).returns(T::Array[Post]) }
Expand All @@ -89,6 +121,24 @@ def posts(fixture_name = nil, *other_fixtures); end
assert_equal(expected, rbi_for("ActiveSupport::TestCase"))
end

it "generates only the include if no fixture has an associated model" do
add_content_file("test/fixtures/serialized_data.yml", <<~YAML)
---
field1: 123
name: Hello
YAML

expected = <<~RBI
# typed: strong

class ActiveSupport::TestCase
include ActiveRecord::TestFixtures
end
RBI

assert_equal(expected, rbi_for("ActiveSupport::TestCase"))
end

it "generates methods for fixtures" do
add_content_file("test/fixtures/posts.yml", <<~YAML)
super_post:
Expand All @@ -107,6 +157,8 @@ class Post < ActiveRecord::Base
# typed: strong

class ActiveSupport::TestCase
include ActiveRecord::TestFixtures

sig { returns(T::Array[Post]) }
sig { params(fixture_name: T.any(String, Symbol)).returns(Post) }
sig { params(fixture_name: T.any(String, Symbol), other_fixtures: T.any(String, Symbol)).returns(T::Array[Post]) }
Expand Down Expand Up @@ -146,6 +198,8 @@ class User < ActiveRecord::Base
# typed: strong

class ActiveSupport::TestCase
include ActiveRecord::TestFixtures

sig { returns(T::Array[Blog::Post]) }
sig { params(fixture_name: T.any(String, Symbol)).returns(Blog::Post) }
sig { params(fixture_name: T.any(String, Symbol), other_fixtures: T.any(String, Symbol)).returns(T::Array[Blog::Post]) }
Expand Down Expand Up @@ -181,6 +235,8 @@ class Post < ActiveRecord::Base
# typed: strong

class ActiveSupport::TestCase
include ActiveRecord::TestFixtures

sig { returns(T::Array[Post]) }
sig { params(fixture_name: T.any(String, Symbol)).returns(Post) }
sig { params(fixture_name: T.any(String, Symbol), other_fixtures: T.any(String, Symbol)).returns(T::Array[Post]) }
Expand All @@ -204,6 +260,8 @@ def posts_with_other_names(fixture_name = nil, *other_fixtures); end
# typed: strong

class ActiveSupport::TestCase
include ActiveRecord::TestFixtures

sig { returns(T::Array[T.untyped]) }
sig { params(fixture_name: T.any(String, Symbol)).returns(T.untyped) }
sig { params(fixture_name: T.any(String, Symbol), other_fixtures: T.any(String, Symbol)).returns(T::Array[T.untyped]) }
Expand All @@ -214,7 +272,7 @@ def posts(fixture_name = nil, *other_fixtures); end
assert_equal(expected, rbi_for("ActiveSupport::TestCase"))
end

it "generates no methods for file fixtures" do
it "generates only the include for file fixtures" do
add_content_file("test/fixtures/files/posts.yml", <<~YAML)
super_post:
title: An incredible Ruby post
Expand All @@ -225,6 +283,10 @@ def posts(fixture_name = nil, *other_fixtures); end

expected = <<~RBI
# typed: strong

class ActiveSupport::TestCase
include ActiveRecord::TestFixtures
end
RBI

assert_equal(expected, rbi_for("ActiveSupport::TestCase"))
Expand Down
Loading