diff --git a/CHANGELOG.md b/CHANGELOG.md
index 98aba01..e123878 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -49,6 +49,13 @@
### Fixed
+- Discover loaded Active Record models by their configured table names and write
+ Rails-native `_fixture.model_class` metadata for model-backed fixture files,
+ including namespaced models and STI roots. Raw SQL fallback and ineligible-model
+ files omit that metadata; unresolved metadata can fall through to Rails'
+ conventional inference, while unrelated loaded models for one table still raise
+ deterministically
+ ([#109](https://github.com/rdy/fixture_builder/issues/109)).
- Omit database-generated columns from generated fixtures so Rails can load
snapshots from tables that have them
([#100](https://github.com/rdy/fixture_builder/issues/100)).
diff --git a/README.md b/README.md
index 35db6e9..b9179c1 100644
--- a/README.md
+++ b/README.md
@@ -107,17 +107,23 @@ When the block finishes, it dumps the state of the database into fixtures, like
```yaml
# users.yml
+_fixture:
+ model_class: User
david:
created_at: 2010-09-18 17:21:23.926511 Z
unique_name: david
id: 1
# products.yml
+_fixture:
+ model_class: Product
i_pod:
name: iPod
id: 1
# purchases.yml
+_fixture:
+ model_class: Purchase
purchase_001:
product_id: 1
user_id: 1
@@ -155,12 +161,14 @@ There are also additional configuration options that can be changed to override
By default these are set as:
-* files_to_check: %w{ db/schema.rb }
-* fixture_builder_file: Rails.root.join("tmp/fixture_builder.yml")
-* record_name_fields: %w{ unique_name display_name name title username login }
-* skip_tables: %w{ schema_migrations ar_internal_metadata }
-* select_sql: SELECT * FROM %
s
-* delete_sql: DELETE FROM %s
+```ruby
+files_to_check: %w{ db/schema.rb }
+fixture_builder_file: Rails.root.join("tmp/fixture_builder.yml")
+record_name_fields: %w{ unique_name display_name name title username login }
+skip_tables: %w{ schema_migrations ar_internal_metadata }
+select_sql: "SELECT * FROM %s"
+delete_sql: "DELETE FROM %s"
+```
FixtureBuilder omits database-generated columns from snapshots because Rails
fixtures cannot write them.
@@ -262,6 +270,47 @@ manifest, so the next build has no manifest-based migration suppression. Marker
migration does not remove historical unmarked duplicates; inspect or move those
manually.
+### Model-aware fixture files
+
+FixtureBuilder discovers a loaded Active Record model for each exported table,
+including models whose configured table name differs from the conventional name.
+A model is eligible when it is concrete, named, uses the exported table and the
+same connection pool, and has usable primary keys. Independent eligible models
+that share a table raise `AmbiguousModelError`. It writes Rails-native metadata
+for every model-backed file, so Rails can select the model without a separate
+fixture-class mapping. This also supports conventionally named models,
+namespaced models, and the root model for an STI table:
+
+```yaml
+_fixture:
+ model_class: Catalog::Creature
+forest_dweller:
+ name: Forest dweller
+```
+
+`_fixture` and `model_class` are String YAML keys, and the value is the model's
+String name. Rails treats `_fixture` as metadata rather than a record. A model
+name still resolves normally when Rails loads the fixture. FixtureBuilder does
+not eager-load the application, but it primes conventional model autoloading
+before considering already-loaded models.
+
+Rails honors an explicit `set_fixture_class` or `class_names` mapping before
+file metadata. If the metadata's class name cannot be resolved, Rails can fall
+through to its conventional fixture-name inference. Files for raw SQL fallback
+tables and tables with ineligible models omit the metadata; that omission does
+not disable Rails' conventional inference, so a raw file whose basename matches
+an unrelated model can still be loaded as that model.
+
+Model-aware loading uses Rails' native fixture transformations. For example,
+Rails can fill timestamps and primary keys, convert enums, honor STI and
+associations, select the model's connection pool, and interpolate `$LABEL` in
+string values. This is not a byte-for-byte promise for every special fixture
+value.
+
+Tools that consume generated YAML as plain records must exclude the `_fixture`
+metadata row. FixtureBuilder reserves `_fixture` as a record label and rejects
+it during generation.
+
Sequence Collisions
===================
diff --git a/doc/plans/2026-08-29-maintainer-backlog-triage.md b/doc/plans/2026-08-29-maintainer-backlog-triage.md
index ef57c3b..57c5be2 100644
--- a/doc/plans/2026-08-29-maintainer-backlog-triage.md
+++ b/doc/plans/2026-08-29-maintainer-backlog-triage.md
@@ -2,7 +2,7 @@
plan: Maintainer backlog triage
status: active
created: "2026-08-29"
-last_updated: "2026-09-03"
+last_updated: "2026-09-07"
owner: Grant Hutchins
scope: Open issues and pull requests in rdy/fixture_builder
---
@@ -189,6 +189,28 @@ the direction of #94.
The model-independent extraction idea may inform #49, but it does not justify
reviving the proposed public hook surface.
+## Current replacement stack
+
+### Model-aware fixture generation (PR 1)
+
+In progress from current `master`: use loaded Active Record models by their
+configured table names, emit Rails-native `_fixture.model_class` metadata for
+model-backed YAML, and preserve raw SQL fallback for tables without an eligible
+model. The implementation keeps Rails fixture loading authoritative: explicit
+fixture-class mappings take precedence and unresolved metadata may use
+conventional inference. Its regression coverage is organized by final ownership in
+`test/fixture_builder_test.rb`, `test/configuration_test.rb`,
+`test/configuration/manifest_test.rb`, `test/fixtures_path_test.rb`,
+`test/builder/`, and `test/model_resolver/`.
+
+### Ephemeral test-model migration (PR 2)
+
+Planned as the dependent follow-up: migrate remaining ordinary fixture-builder
+tests to models whose schemas are declared by their owning test cases. Retain
+manual setup only for raw SQL, constant/autoload timing, alternate pools, schema
+errors, namespaced table-name boundaries, and STI. Keep the metadata-aware and
+metadata-free legacy fixture inputs as separate compatibility paths.
+
## Execution order
### Phase 1: Backlog cleanup
@@ -215,7 +237,7 @@ reviving the proposed public hook surface.
- **Implementation:** Remove the `Date::DATE_FORMATS` mutation from
`lib/fixture_builder/builder.rb`.
- **Tests:** Add focused ISO-date and global-state coverage to
- `test/fixture_builder_test.rb`.
+ `test/builder/serialization_test.rb`.
- **Completion gate:** The focused test file and `bin/rake` pass without a date
deprecation warning, then #69 closes through the implementation pull request.
@@ -304,8 +326,9 @@ PostgreSQL reproduction.
- **Implementation:** Replace the unconditional `order(:id)` behavior in
`lib/fixture_builder/builder.rb` with declared-primary-key ordering and a
deterministic fallback for keyless tables.
-- **Tests:** Extend `test/fixture_builder_test.rb` and its schema/models with
- custom-primary-key and keyless-table cases. Prove two generations are stable.
+- **Tests:** Extend `test/builder/raw_sql_test.rb` and its owning ephemeral
+ schemas with custom-primary-key and keyless-table cases. Prove two generations
+ are stable.
- **Completion gate:** The regression fails before the implementation, then the
focused test file and `bin/rake` pass without weakening deterministic output.
@@ -315,9 +338,10 @@ PostgreSQL reproduction.
`lib/fixture_builder/`, expose it through
`lib/fixture_builder/configuration.rb`, and update builder path handling and
manifest traversal.
-- **Tests:** Extend `test/fixture_builder_test.rb` with a namespaced model, JSON
- data, nested fixture output, recursive cleanup, and manifest invalidation.
- Add isolated value-object tests if its validation has meaningful branches.
+- **Tests:** Extend `test/builder/metadata_test.rb` and
+ `test/model_resolver/table_name_test.rb` with a namespaced model, JSON data,
+ nested fixture output, recursive cleanup, and manifest invalidation. Add
+ isolated value-object tests if its validation has meaningful branches.
- **Completion gate:** The end-to-end regression fails first; then focused tests
and `bin/rake` pass. Update `README.md` and `CHANGELOG.md` in the same pull
request.
@@ -332,8 +356,8 @@ primitive makes a small dependency stack clearer.
- [ ] If no required use case remains, replace their implementation with Arel
in `lib/fixture_builder/builder.rb` and remove the setters from
`lib/fixture_builder/configuration.rb`.
-- [ ] Update `test/fixture_builder_test.rb`, `README.md`, and `CHANGELOG.md` for
- every removed public API.
+- [ ] Update `test/configuration_test.rb`, `test/builder/`, `README.md`, and
+ `CHANGELOG.md` for every removed public API.
- [ ] Run `bin/rake`, the stable Ruby/Rails matrix, and the required GitHub
Actions workflow before releasing 0.7.
diff --git a/fixture_builder.gemspec b/fixture_builder.gemspec
index 964a46d..af47a71 100644
--- a/fixture_builder.gemspec
+++ b/fixture_builder.gemspec
@@ -34,4 +34,5 @@ Gem::Specification.new do |s|
s.add_development_dependency "rake"
s.add_development_dependency "sqlite3"
s.add_development_dependency "test-unit"
+ s.add_development_dependency "with_model"
end
diff --git a/lib/fixture_builder.rb b/lib/fixture_builder.rb
index 152306b..87ce29b 100644
--- a/lib/fixture_builder.rb
+++ b/lib/fixture_builder.rb
@@ -4,6 +4,7 @@
require "fixture_builder/configuration"
require "fixture_builder/namer"
require "fixture_builder/fixture_file"
+require "fixture_builder/ambiguous_model_error"
require "fixture_builder/builder"
require "fixture_builder/fixtures_path"
diff --git a/lib/fixture_builder/ambiguous_model_error.rb b/lib/fixture_builder/ambiguous_model_error.rb
new file mode 100644
index 0000000..0dd55dc
--- /dev/null
+++ b/lib/fixture_builder/ambiguous_model_error.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+module FixtureBuilder
+ class AmbiguousModelError < StandardError
+ attr_reader :table_name, :models
+
+ def initialize(table_name, models)
+ @table_name = table_name
+ @models = models.sort_by(&:name)
+ super("Multiple models match table #{table_name}: #{@models.map(&:name).join(", ")}")
+ end
+ end
+end
diff --git a/lib/fixture_builder/builder.rb b/lib/fixture_builder/builder.rb
index 6d3c315..539955d 100644
--- a/lib/fixture_builder/builder.rb
+++ b/lib/fixture_builder/builder.rb
@@ -1,5 +1,7 @@
# frozen_string_literal: true
+require "fixture_builder/model_resolver"
+
module FixtureBuilder
class Builder
include Delegations::Namer
@@ -17,6 +19,7 @@ def generate!
clean_out_old_data
create_fixture_objects
names_from_ivars!
+ @models_by_table = resolve_models_by_table
write_data_to_files
after_build&.call
end
@@ -58,9 +61,7 @@ def names_from_ivars!
end
def write_data_to_files
- emitted_files = write_empty_files ? dump_empty_fixtures_for_all_tables : []
- emitted_files |= dump_tables
- remove_stale_fixture_files(emitted_files)
+ remove_stale_fixture_files(dump_tables)
end
def clean_out_old_data
@@ -82,24 +83,13 @@ def say(*messages)
end
# standard:enable Rails/Output
- def dump_empty_fixtures_for_all_tables
- tables.map do |table_name|
- write_fixture_file({}, table_name)
- File.basename(fixture_file(table_name))
- end
- end
-
def dump_tables
fixtures = tables.inject([]) do |files, table_name|
- table_klass = begin
- table_name.classify.constantize
- rescue
- nil
- end
- rows = if table_klass && table_klass < ActiveRecord::Base
- generated_names = generated_column_names(table_klass.table_name)
+ table_klass = @models_by_table.fetch(table_name)
+ generated_names = generated_column_names(table_name)
+ rows = if table_klass
table_klass.unscoped do
- table_klass.order(:id).all.collect do |obj|
+ table_klass.order(Array(table_klass.primary_key)).all.collect do |obj|
attrs = obj.attributes_before_type_cast.slice(*table_klass.column_names)
attrs.each do |attr_name, value|
column_type = table_klass.columns_hash.fetch(attr_name).type
@@ -111,16 +101,21 @@ def dump_tables
end
end
else
- generated_names = generated_column_names(table_name)
ActiveRecord::Base.connection.select_all(format(select_sql,
table: ActiveRecord::Base.connection.quote_table_name(table_name)))
.map { |row| row.except(*generated_names) }
end
- next files if rows.empty?
+ next files if rows.empty? && !write_empty_files
fixture_data = rows.inject({}) do |hash, record|
- hash.merge(record_name(record, table_name) => record)
+ label = record_name(record, table_name)
+ if label == "_fixture"
+ raise ArgumentError, "Fixture table #{table_name} contains reserved record label _fixture"
+ end
+
+ hash.merge(label => record)
end
+ fixture_data = {"_fixture" => {"model_class" => table_klass.name}}.merge(fixture_data) if table_klass
write_fixture_file fixture_data, table_name
@@ -130,12 +125,21 @@ def dump_tables
fixtures
end
+ private
+
+ def resolve_models_by_table
+ resolver = ModelResolver.new(connection_pool: ActiveRecord::Base.connection_pool)
+ tables.each_with_object({}) do |table_name, models_by_table|
+ models_by_table[table_name] = resolver.resolve(table_name)
+ end
+ end
+
# A database-generated (virtual/stored generated) column cannot be
# inserted, so Rails rejects a fixture file containing it. Only those
# column names are removed from the extracted rows; everything else a row
# carries - including an expression a custom `select_sql` selects - is
# left as it was produced.
- private def generated_column_names(table_name)
+ def generated_column_names(table_name)
connection = ActiveRecord::Base.connection
return [] unless connection.supports_virtual_columns?
diff --git a/lib/fixture_builder/model_resolver.rb b/lib/fixture_builder/model_resolver.rb
new file mode 100644
index 0000000..e66a814
--- /dev/null
+++ b/lib/fixture_builder/model_resolver.rb
@@ -0,0 +1,39 @@
+# frozen_string_literal: true
+
+module FixtureBuilder
+ class ModelResolver
+ def initialize(connection_pool:)
+ @connection_pool = connection_pool
+ end
+
+ def resolve(table_name)
+ # Prime conventional autoloading before considering already-loaded models.
+ table_name.classify.safe_constantize
+ candidates = ActiveRecord::Base.descendants.select do |model|
+ eligible_model?(model, table_name)
+ end
+ root_models = candidates.reject do |model|
+ candidates.any? { |candidate| candidate != model && model < candidate }
+ end
+
+ return if root_models.empty?
+ return root_models.first if root_models.one?
+
+ raise AmbiguousModelError.new(table_name, root_models)
+ end
+
+ private
+
+ def eligible_model?(model, table_name)
+ return false if model.abstract_class?
+
+ model_name = model.name
+ return false unless model_name && model_name.safe_constantize.equal?(model)
+ return false unless model.table_name == table_name
+ return false unless model.connection_pool.equal?(@connection_pool)
+
+ primary_keys = Array(model.primary_key).compact
+ primary_keys.any? && primary_keys.all? { |key| model.columns_hash.key?(key) }
+ end
+ end
+end
diff --git a/test/builder/generated_columns_test.rb b/test/builder/generated_columns_test.rb
new file mode 100644
index 0000000..4d038d4
--- /dev/null
+++ b/test/builder/generated_columns_test.rb
@@ -0,0 +1,35 @@
+# frozen_string_literal: false
+
+require_relative "../test_helper"
+
+module BuilderTests
+ class GeneratedColumnsTest < Test::Unit::TestCase
+ prepend IsolatedFixtureFilesystem
+
+ with_model :GeneratedCreature do
+ table do |table|
+ table.string :name, null: false
+ table.virtual :name_length, type: :integer, as: "length(name)", stored: true
+ end
+ end
+
+ def test_generated_columns_are_excluded_for_model_backed_tables
+ force_fixture_generation
+
+ table_name = GeneratedCreature.table_name
+ FixtureBuilder.configure do |fbuilder|
+ fbuilder.files_to_check = []
+ fbuilder.skip_tables = ActiveRecord::Base.connection.tables - [table_name]
+ fbuilder.factory { GeneratedCreature.create!(name: "Myrddin") }
+ end
+
+ generated_fixture = YAML.safe_load_file(fixture_path("#{table_name}.yml"))
+ assert_equal "Myrddin", generated_fixture.dig("myrddin", "name")
+ assert_not_include generated_fixture.fetch("myrddin"), "name_length"
+
+ GeneratedCreature.delete_all
+ create_fixtures(table_name)
+ assert_equal 7, GeneratedCreature.find_by!(name: "Myrddin").name_length
+ end
+ end
+end
diff --git a/test/builder/generated_fixture_ownership_test.rb b/test/builder/generated_fixture_ownership_test.rb
index c5f1ab2..5f6b262 100644
--- a/test/builder/generated_fixture_ownership_test.rb
+++ b/test/builder/generated_fixture_ownership_test.rb
@@ -14,8 +14,11 @@ class GeneratedFixtureOwnershipTest < Test::Unit::TestCase
def setup
@directory = Dir.mktmpdir("fixture-builder-ownership")
- ActiveRecord::Base.establish_connection(adapter: "sqlite3", database: ":memory:")
- ActiveRecord::Base.connection.create_table(:ownership_records) { |table| table.string :name }
+ # with_table captures this adapter when the suite loads, so reset the
+ # in-memory database without replacing its connection pool.
+ connection = ActiveRecord::Base.connection
+ connection.tables.each { |table| connection.drop_table(table) }
+ connection.create_table(:ownership_records) { |table| table.string :name }
OwnershipRecord.reset_column_information
end
diff --git a/test/builder/metadata_test.rb b/test/builder/metadata_test.rb
new file mode 100644
index 0000000..1ca2c53
--- /dev/null
+++ b/test/builder/metadata_test.rb
@@ -0,0 +1,151 @@
+# frozen_string_literal: false
+
+require_relative "../test_helper"
+
+module ModelMetadataNamespace
+end
+
+module BuilderTests
+ class MetadataTest < Test::Unit::TestCase
+ prepend IsolatedFixtureFilesystem
+
+ with_model :Creature do
+ table do |table|
+ table.string :name, null: false
+ end
+ end
+
+ with_model :MythicalCreature do
+ table do |table|
+ table.string :name, null: false
+ table.string :type
+ end
+ end
+
+ with_model :Dragon, superclass: :MythicalCreature do
+ table(false)
+ end
+
+ with_table :model_metadata_namespaced_creatures do |table|
+ table.string :name, null: false
+ end
+
+ with_model :"ModelMetadataNamespace::Chimera" do
+ table
+
+ model do
+ self.table_name = "model_metadata_namespaced_creatures"
+ end
+ end
+
+ with_table :model_metadata_raw_tables do |table|
+ table.string :name
+ end
+
+ def test_model_backed_files_describe_their_model_and_load_without_a_class_map
+ table_name = Creature.table_name
+ assert_equal "Creature", Creature.name
+ assert_not_equal Creature.name, table_name.classify
+
+ generate_for(table_name) { Creature.create!(name: "$LABEL") }
+
+ fixture = YAML.safe_load_file(fixture_path("#{table_name}.yml"))
+ assert_equal({"model_class" => Creature.name}, fixture.fetch("_fixture"))
+ records = fixture.except("_fixture")
+ assert_equal "$LABEL", records.values.first.fetch("name")
+
+ Creature.delete_all
+ create_fixtures(table_name)
+ assert_equal records.keys.first, Creature.find_by!(name: records.keys.first).name
+ assert_nil Creature.find_by(name: "_fixture")
+ end
+
+ def test_namespaced_model_round_trips_through_generated_native_metadata
+ table_name = "model_metadata_namespaced_creatures"
+ model = ModelMetadataNamespace::Chimera
+
+ generate_for(table_name) { model.create!(name: "Namespaced creature") }
+
+ generated_fixture_path = fixture_path("#{table_name}.yml")
+ fixture = YAML.safe_load_file(generated_fixture_path)
+ assert_equal "ModelMetadataNamespace::Chimera", model.name
+ assert_equal table_name, model.table_name
+ assert_not_include "/", table_name
+ assert File.exist?(generated_fixture_path)
+ assert_equal({"model_class" => model.name}, fixture.fetch("_fixture"))
+
+ model.delete_all
+ create_fixtures(table_name)
+ assert_equal "Namespaced creature", model.find_by!(name: "Namespaced creature").name
+ assert_nil model.find_by(name: "_fixture")
+ end
+
+ def test_empty_model_and_raw_tables_follow_write_empty_files
+ model_table = Creature.table_name
+ raw_table = "model_metadata_raw_tables"
+
+ [true, false].each do |write_empty_files|
+ generate_for(model_table, raw_table) do |fbuilder|
+ fbuilder.write_empty_files = write_empty_files
+ end
+
+ if write_empty_files
+ assert_equal({"_fixture" => {"model_class" => Creature.name}},
+ YAML.safe_load_file(fixture_path("#{model_table}.yml")))
+ assert_equal({}, YAML.safe_load_file(fixture_path("#{raw_table}.yml")))
+ else
+ assert_false File.exist?(fixture_path("#{model_table}.yml"))
+ assert_false File.exist?(fixture_path("#{raw_table}.yml"))
+ end
+ end
+ end
+
+ def test_reserved_fixture_label_raises_for_model_and_raw_tables
+ model_table = Creature.table_name
+ error = assert_raise(ArgumentError) do
+ generate_for(model_table) { Creature.create!(name: "_fixture") }
+ end
+ assert_match(/#{model_table}.*_fixture/, error.message)
+
+ raw_table = "model_metadata_raw_tables"
+ error = assert_raise(ArgumentError) do
+ generate_for(raw_table) do
+ ActiveRecord::Base.connection.execute("INSERT INTO #{raw_table} (name) VALUES ('_fixture')")
+ end
+ end
+ assert_match(/#{raw_table}.*_fixture/, error.message)
+ end
+
+ def test_sti_files_describe_the_root_model_and_load_sibling_records
+ table_name = MythicalCreature.table_name
+ generate_for(table_name) do
+ MythicalCreature.create!(name: "Base")
+ Dragon.create!(name: "Sibling")
+ end
+
+ fixture = YAML.safe_load_file(fixture_path("#{table_name}.yml"))
+ assert_equal MythicalCreature.name, fixture.dig("_fixture", "model_class")
+
+ MythicalCreature.delete_all
+ create_fixtures(table_name)
+ assert_equal [MythicalCreature.name, Dragon.name],
+ MythicalCreature.order(:name).map { |record| record.class.name }
+ end
+
+ private
+
+ def generate_for(*table_names, &factory)
+ force_fixture_generation
+ FixtureBuilder.configure do |fbuilder|
+ fbuilder.files_to_check = []
+ fbuilder.skip_tables = ActiveRecord::Base.connection.tables - table_names
+ if factory.arity == 1
+ factory.call(fbuilder)
+ fbuilder.factory {}
+ else
+ fbuilder.factory(&factory)
+ end
+ end
+ end
+ end
+end
diff --git a/test/builder/raw_sql_test.rb b/test/builder/raw_sql_test.rb
new file mode 100644
index 0000000..d77da04
--- /dev/null
+++ b/test/builder/raw_sql_test.rb
@@ -0,0 +1,62 @@
+# frozen_string_literal: false
+
+require_relative "../test_helper"
+
+module BuilderTests
+ class RawSqlTest < Test::Unit::TestCase
+ prepend IsolatedFixtureFilesystem
+
+ with_table :generated_column_records do |table|
+ table.string :name, null: false
+ table.virtual :name_length, type: :integer, as: "length(name)", stored: true
+ end
+
+ def test_generated_columns_are_excluded_for_raw_query_tables
+ table_name = "generated_column_records"
+ force_fixture_generation
+ quoted_table_name = ActiveRecord::Base.connection.quote_table_name(table_name)
+ FixtureBuilder.configure do |fbuilder|
+ fbuilder.files_to_check = []
+ fbuilder.skip_tables = ActiveRecord::Base.connection.tables - [table_name]
+ fbuilder.factory do
+ ActiveRecord::Base.connection.execute(
+ "INSERT INTO #{quoted_table_name} (name) VALUES ('Merlin')"
+ )
+ end
+ end
+
+ generated_fixture = YAML.safe_load_file(fixture_path("#{table_name}.yml"))
+ assert_equal "Merlin", generated_fixture.dig("merlin", "name")
+ assert_not_include generated_fixture.fetch("merlin"), "name_length"
+
+ ActiveRecord::Base.connection.delete("DELETE FROM #{quoted_table_name}")
+ create_fixtures(table_name)
+ assert_equal 6,
+ ActiveRecord::Base.connection.select_value("SELECT name_length FROM #{quoted_table_name}")
+ end
+
+ def test_raw_query_select_aliases_are_preserved
+ table_name = "generated_column_records"
+ force_fixture_generation
+ quoted_table_name = ActiveRecord::Base.connection.quote_table_name(table_name)
+ capture_output do
+ FixtureBuilder.configure do |fbuilder|
+ fbuilder.files_to_check = []
+ fbuilder.skip_tables = ActiveRecord::Base.connection.tables - [table_name]
+ fbuilder.select_sql = "SELECT *, upper(name) AS shouted_name FROM %s"
+ fbuilder.factory do
+ ActiveRecord::Base.connection.execute(
+ "INSERT INTO #{quoted_table_name} (name) VALUES ('Merlin')"
+ )
+ end
+ end
+ end
+
+ generated_fixture = YAML.safe_load_file(fixture_path("#{table_name}.yml"))
+ record = generated_fixture.fetch("merlin")
+ assert_equal "Merlin", record["name"]
+ assert_equal "MERLIN", record["shouted_name"]
+ assert_not_include record, "name_length"
+ end
+ end
+end
diff --git a/test/builder/serialization_test.rb b/test/builder/serialization_test.rb
new file mode 100644
index 0000000..deaed3a
--- /dev/null
+++ b/test/builder/serialization_test.rb
@@ -0,0 +1,84 @@
+# frozen_string_literal: false
+
+require_relative "../test_helper"
+
+# standard:disable Rails/ApplicationRecord
+module BuilderTests
+ class SerializationTest < Test::Unit::TestCase
+ include TestDatabase
+ prepend IsolatedFixtureFilesystem
+
+ def setup
+ super
+ create_and_blow_away_old_db
+ end
+
+ def test_serialization
+ force_fixture_generation
+
+ FixtureBuilder.configure do |fbuilder|
+ fbuilder.files_to_check += Dir[test_path("*.rb")]
+ fbuilder.factory do
+ @enty = MagicalCreature.create(name: "Enty", species: "ent",
+ powers: %w[shading rooting seeding])
+ end
+ end
+ generated_fixture = YAML.load(File.open(fixture_path("#{MagicalCreature.table_name}.yml")))
+ assert_equal "---\n- shading\n- rooting\n- seeding\n", generated_fixture["enty"]["powers"]
+ end
+
+ def test_dates_are_iso_formatted_without_mutating_global_date_formats
+ force_fixture_generation
+
+ default_date_format_exists = Date::DATE_FORMATS.key?(:default)
+ default_date_format = Date::DATE_FORMATS[:default]
+ custom_date_format = "%m/%d/%Y"
+ Date::DATE_FORMATS[:default] = custom_date_format
+
+ begin
+ date_format_during_generation = nil
+ FixtureBuilder.configure do |fbuilder|
+ fbuilder.files_to_check += Dir[test_path("*.rb")]
+ fbuilder.name_model_with MagicalCreature do |_record, index|
+ date_format_during_generation = Date::DATE_FORMATS[:default]
+ "creature_#{index}"
+ end
+ fbuilder.factory do
+ MagicalCreature.create!(name: "Ariel", species: "mermaid", born_on: Date.new(1990, 1, 2))
+ end
+ end
+
+ fixture_contents = File.read(fixture_path("#{MagicalCreature.table_name}.yml"))
+ assert_includes fixture_contents, "born_on: '1990-01-02'\n"
+ assert_equal custom_date_format, date_format_during_generation
+ assert_equal custom_date_format, Date::DATE_FORMATS[:default]
+ ensure
+ if default_date_format_exists
+ Date::DATE_FORMATS[:default] = default_date_format
+ else
+ Date::DATE_FORMATS.delete(:default)
+ end
+ end
+
+ if default_date_format_exists
+ assert_equal default_date_format, Date::DATE_FORMATS[:default]
+ else
+ assert_not_include Date::DATE_FORMATS, :default
+ end
+ end
+
+ def test_do_not_include_virtual_attributes
+ force_fixture_generation
+
+ FixtureBuilder.configure do |fbuilder|
+ fbuilder.files_to_check += Dir[test_path("*.rb")]
+ fbuilder.factory do
+ MagicalCreature.create(name: "Uni", species: "unicorn", powers: %w[rainbows flying])
+ end
+ end
+ generated_fixture = YAML.load(File.open(fixture_path("#{MagicalCreature.table_name}.yml")))
+ assert !generated_fixture["uni"].key?("virtual")
+ end
+ end
+end
+# standard:enable Rails/ApplicationRecord
diff --git a/test/builder/table_name_test.rb b/test/builder/table_name_test.rb
new file mode 100644
index 0000000..1cc3445
--- /dev/null
+++ b/test/builder/table_name_test.rb
@@ -0,0 +1,54 @@
+# frozen_string_literal: false
+
+require_relative "../test_helper"
+
+module BuilderTests
+ class TableNameTest < Test::Unit::TestCase
+ prepend IsolatedFixtureFilesystem
+
+ with_model :MappedCreature do
+ table do |table|
+ table.string :name, null: false
+ table.virtual :name_length, type: :integer, as: "length(name)", stored: true
+ end
+ end
+
+ with_table :mapped_creatures do |table|
+ table.string :unrelated
+ table.virtual :name, type: :string, as: "upper(unrelated)", stored: true
+ end
+
+ def test_model_and_conventional_decoy_tables_exclude_their_own_generated_columns
+ model_table_name = MappedCreature.table_name
+ decoy_table_name = MappedCreature.name.tableize
+
+ assert_equal "mapped_creatures", decoy_table_name
+ assert_not_equal model_table_name, decoy_table_name
+
+ connection = ActiveRecord::Base.connection
+ assert_equal %w[id name name_length], connection.columns(model_table_name).map(&:name).sort
+ assert_equal %w[id name unrelated], connection.columns(decoy_table_name).map(&:name).sort
+
+ FixtureBuilder.configure do |fbuilder|
+ fbuilder.files_to_check = []
+ fbuilder.skip_tables = connection.tables - [model_table_name, decoy_table_name]
+ fbuilder.factory do
+ MappedCreature.create!(name: "Nimue")
+ connection.execute("INSERT INTO #{connection.quote_table_name(decoy_table_name)} (unrelated) VALUES ('Morgana')")
+ end
+ end
+
+ model_fixture = YAML.safe_load_file(fixture_path("#{model_table_name}.yml"))
+ assert_equal "MappedCreature", model_fixture.dig("_fixture", "model_class")
+ model_record = model_fixture.fetch("nimue")
+ assert_equal "Nimue", model_record["name"]
+ assert_not_include model_record, "name_length"
+
+ decoy_fixture = YAML.safe_load_file(fixture_path("#{decoy_table_name}.yml"))
+ assert_not_include decoy_fixture, "_fixture"
+ decoy_record = decoy_fixture.fetch("#{decoy_table_name}_001")
+ assert_equal "Morgana", decoy_record["unrelated"]
+ assert_not_include decoy_record, "name"
+ end
+ end
+end
diff --git a/test/configuration/generated_file_marker_test.rb b/test/configuration/generated_file_marker_test.rb
index c852e69..a57cc20 100644
--- a/test/configuration/generated_file_marker_test.rb
+++ b/test/configuration/generated_file_marker_test.rb
@@ -19,8 +19,9 @@ def setup
@source = File.join(@directory, "source.rb")
FileUtils.mkdir_p(@fixtures)
File.write(@source, "source\n")
- ActiveRecord::Base.establish_connection(adapter: "sqlite3", database: ":memory:")
- ActiveRecord::Base.connection.create_table(:marker_migration_records) { |table| table.string :name }
+ connection = ActiveRecord::Base.connection
+ connection.tables.each { |table| connection.drop_table(table) }
+ connection.create_table(:marker_migration_records) { |table| table.string :name }
MarkerMigrationRecord.reset_column_information
end
diff --git a/test/configuration/manifest_test.rb b/test/configuration/manifest_test.rb
new file mode 100644
index 0000000..842cf50
--- /dev/null
+++ b/test/configuration/manifest_test.rb
@@ -0,0 +1,239 @@
+# frozen_string_literal: false
+
+require_relative "../test_helper"
+
+# standard:disable Rails/ApplicationRecord
+module ConfigurationTests
+ class ManifestTest < Test::Unit::TestCase
+ include TestDatabase
+ prepend IsolatedFixtureFilesystem
+
+ def setup
+ super
+ create_and_blow_away_old_db
+ end
+
+ def test_malformed_manifest_raises_without_running_factory
+ manifest_path = fixture_builder_file
+ File.write(manifest_path, "---\ninvalid: [\n")
+ factory_called = false
+
+ assert_raise(Psych::SyntaxError) do
+ FixtureBuilder.configure do |fbuilder|
+ fbuilder.files_to_check += Dir[test_path("*.rb")]
+ fbuilder.factory { factory_called = true }
+ end
+ end
+
+ assert_false factory_called
+ end
+
+ def test_skips_rebuild_for_valid_empty_fixture_snapshot
+ force_fixture_generation
+ FileUtils.rm_f(Dir[fixture_path("*.yml")])
+ builds = 0
+
+ FixtureBuilder.configure do |fbuilder|
+ fbuilder.files_to_check += Dir[test_path("*.rb")]
+ fbuilder.write_empty_files = false
+ fbuilder.factory { builds += 1 }
+ end
+
+ manifest_path = fixture_builder_file
+ assert_empty YAML.safe_load_file(manifest_path).fetch("fixtures")
+ reset_fixture_builder_configuration
+
+ FixtureBuilder.configure do |fbuilder|
+ fbuilder.files_to_check += Dir[test_path("*.rb")]
+ fbuilder.write_empty_files = false
+ fbuilder.factory { builds += 1 }
+ end
+
+ assert_equal 1, builds
+ end
+
+ def test_rebuilding_due_to_differing_file_hashes
+ force_fixture_generation_due_to_differing_file_hashes
+
+ FixtureBuilder.configure do |fbuilder|
+ fbuilder.files_to_check += Dir[test_path("*.rb")]
+ fbuilder.factory do
+ @enty = MagicalCreature.create(name: "Enty", species: "ent",
+ powers: %w[shading rooting seeding])
+ end
+ end
+ generated_fixture = YAML.load(File.open(fixture_path("#{MagicalCreature.table_name}.yml")))
+ assert_equal "---\n- shading\n- rooting\n- seeding\n", generated_fixture["enty"]["powers"]
+ end
+
+ def test_rebuilds_when_generated_fixture_hashes_differ
+ force_fixture_generation
+
+ FixtureBuilder.configure do |fbuilder|
+ fbuilder.files_to_check += Dir[test_path("*.rb")]
+ fbuilder.factory do
+ @enty = MagicalCreature.create(name: "Enty", species: "ent",
+ powers: %w[shading rooting seeding])
+ end
+ end
+
+ reset_fixture_builder_configuration
+ fixture_path = fixture_path("#{MagicalCreature.table_name}.yml")
+ generated_fixture = YAML.load_file(fixture_path)
+ generated_fixture["enty"]["retired_column"] = "bogus"
+ File.write(fixture_path, generated_fixture.to_yaml)
+
+ FixtureBuilder.configure do |fbuilder|
+ fbuilder.files_to_check += Dir[test_path("*.rb")]
+ fbuilder.factory do
+ @enty = MagicalCreature.create(name: "Enty", species: "ent",
+ powers: %w[shading rooting seeding])
+ end
+ end
+
+ regenerated_fixture = YAML.load_file(fixture_path)
+ assert_false regenerated_fixture["enty"].key?("retired_column")
+ assert_equal "Enty", regenerated_fixture["enty"]["name"]
+ assert_equal "ent", regenerated_fixture["enty"]["species"]
+ end
+
+ def test_fresh_manifest_returns_without_acquiring_lock
+ force_fixture_generation
+ builds = 0
+ lock_path = nil
+
+ FixtureBuilder.configure do |fbuilder|
+ fbuilder.files_to_check += Dir[test_path("*.rb")]
+ lock_path = fbuilder.lock_path
+ fbuilder.factory do
+ builds += 1
+ @enty = MagicalCreature.create(name: "Enty", species: "ent")
+ end
+ end
+
+ assert_path_exist lock_path
+ FileUtils.rm(lock_path)
+ reset_fixture_builder_configuration
+
+ FixtureBuilder.configure do |fbuilder|
+ fbuilder.files_to_check += Dir[test_path("*.rb")]
+ fbuilder.factory { builds += 1 }
+ end
+
+ assert_equal 1, builds
+ assert_path_not_exist lock_path
+ end
+
+ def test_raising_after_build_invalidates_manifest_and_retries
+ force_fixture_generation
+ builds = 0
+ factory = proc do
+ builds += 1
+ @enty = MagicalCreature.create(name: "Enty", species: "ent")
+ end
+
+ FixtureBuilder.configure do |fbuilder|
+ fbuilder.files_to_check += Dir[test_path("*.rb")]
+ fbuilder.factory(&factory)
+ end
+
+ manifest_path = fixture_builder_file
+ fixture_path = fixture_path("#{MagicalCreature.table_name}.yml")
+ generated_fixture = YAML.safe_load_file(fixture_path)
+ generated_fixture["enty"]["retired_column"] = "bogus"
+ File.write(fixture_path, generated_fixture.to_yaml)
+ reset_fixture_builder_configuration
+
+ assert_raise(RuntimeError) do
+ FixtureBuilder.configure do |fbuilder|
+ fbuilder.files_to_check += Dir[test_path("*.rb")]
+ fbuilder.after_build = proc { raise "after build failure" }
+ fbuilder.factory(&factory)
+ end
+ end
+
+ assert_false File.exist?(manifest_path)
+
+ reset_fixture_builder_configuration
+ FixtureBuilder.configure do |fbuilder|
+ fbuilder.files_to_check += Dir[test_path("*.rb")]
+ fbuilder.factory(&factory)
+ end
+
+ assert_equal 3, builds
+ assert_equal 1, YAML.safe_load_file(manifest_path)["version"]
+ end
+
+ def test_sha256_manifest_digests_when_deprecated_use_sha1_digests_is_enabled
+ force_fixture_generation_due_to_differing_file_hashes
+
+ source_path = Pathname.new(__FILE__)
+ FixtureBuilder.configure do |fbuilder|
+ _output, warning = capture_output do
+ fbuilder.use_sha1_digests = true
+ end
+
+ assert_true fbuilder.use_sha1_digests
+ assert_match(
+ /use_sha1_digests is deprecated and will be removed in FixtureBuilder 0.7; it is ignored because SHA-256 is always used/,
+ warning
+ )
+
+ fbuilder.files_to_check = [source_path]
+ fbuilder.factory do
+ @enty = MagicalCreature.create(name: "Enty", species: "ent",
+ powers: %w[shading rooting seeding])
+ end
+
+ manifest = YAML.safe_load_file(fixture_builder_file)
+ fixture_path = fixture_path("#{MagicalCreature.table_name}.yml")
+ assert_equal 1, manifest["version"]
+ assert_equal Digest::SHA256.file(source_path).hexdigest,
+ manifest.fetch("sources").fetch(source_path.to_s)
+ assert_equal Digest::SHA256.file(fixture_path).hexdigest,
+ manifest.fetch("fixtures").fetch(File.basename(fixture_path))
+
+ first_modified_time = File.mtime(fixture_path)
+ fbuilder.factory do
+ end
+ second_modified_time = File.mtime(fixture_path)
+ assert_equal first_modified_time, second_modified_time
+ end
+ end
+
+ data(
+ "empty document" => "",
+ "false" => "false\\n",
+ "scalar" => "scalar\\n",
+ "flat manifest" => {"source.rb" => "old digest"}.to_yaml,
+ "unsupported future version" => {"version" => 2, "sources" => {}, "fixtures" => {}}.to_yaml,
+ "invalid current shape" => {"version" => 1, "sources" => {}, "fixtures" => {}, 1 => "invalid"}.to_yaml
+ )
+ def test_rebuilds_parsed_invalid_manifest(payload)
+ assert_manifest_rebuilds(payload)
+ end
+
+ private
+
+ def assert_manifest_rebuilds(payload)
+ force_fixture_generation
+ builds = 0
+ factory = proc do
+ builds += 1
+ @enty = MagicalCreature.create(name: "Enty", species: "ent")
+ end
+ FixtureBuilder.configure do |fbuilder|
+ fbuilder.files_to_check += Dir[test_path("*.rb")]
+ fbuilder.factory(&factory)
+ end
+ File.write(fixture_builder_file, payload)
+ reset_fixture_builder_configuration
+ FixtureBuilder.configure do |fbuilder|
+ fbuilder.files_to_check += Dir[test_path("*.rb")]
+ fbuilder.factory(&factory)
+ end
+ assert_equal 2, builds
+ end
+ end
+end
+# standard:enable Rails/ApplicationRecord
diff --git a/test/configuration_test.rb b/test/configuration_test.rb
new file mode 100644
index 0000000..a7a8b9b
--- /dev/null
+++ b/test/configuration_test.rb
@@ -0,0 +1,97 @@
+# frozen_string_literal: false
+
+require_relative "test_helper"
+
+class ConfigurationTest < Test::Unit::TestCase
+ prepend IsolatedFixtureFilesystem
+
+ class Model
+ def self.table_name
+ "models"
+ end
+ end
+
+ def test_name_with
+ hash = {"email" => "bob@example.com"}
+ FixtureBuilder.configure do |config|
+ config.name_model_with ConfigurationTest::Model do |record_hash, index|
+ [record_hash["email"].split("@").first, index].join("_")
+ end
+ end
+ assert_equal "bob_001",
+ FixtureBuilder.configuration.send(:record_name, hash, ConfigurationTest::Model.table_name)
+ end
+
+ def test_sql_setters_reject_positional_table_format_without_warning
+ {select_sql: "SELECT * FROM %s", delete_sql: "DELETE FROM %s"}.each do |attribute, sql|
+ configuration = FixtureBuilder::Configuration.new
+
+ _output, warning = capture_output do
+ error = assert_raise(ArgumentError) do
+ configuration.public_send("#{attribute}=", sql)
+ end
+
+ assert_equal(
+ "Positional %s table placeholders are no longer supported; use %s or %{table}. " \
+ "See https://docs.ruby-lang.org/en/3.3/format_specifications_rdoc.html" \
+ "#label-Reference+by+Name.",
+ error.message
+ )
+ end
+
+ assert_empty warning
+ end
+ end
+
+ def test_sql_setters_warn_on_every_assignment_and_retain_named_table_formats
+ ["%s", "%{table}"].each do |table_format|
+ {
+ select_sql: "SELECT * FROM #{table_format}",
+ delete_sql: "DELETE FROM #{table_format}"
+ }.each do |attribute, sql|
+ configuration = FixtureBuilder::Configuration.new
+
+ _output, warning = capture_output do
+ configuration.public_send("#{attribute}=", sql)
+ end
+
+ assert_include(
+ warning,
+ "#{attribute}= is deprecated and planned for removal in FixtureBuilder 0.7. " \
+ "If you are actively using this feature, please share your use case at " \
+ "https://github.com/rdy/fixture_builder/issues/94 so we can consider the best way " \
+ "to continue to support it."
+ )
+ assert_equal sql, configuration.public_send(attribute)
+ end
+ end
+ end
+
+ def test_configuration_constructor_accepts_deprecated_use_sha1_digests_option
+ _output, warning = capture_output do
+ configuration = FixtureBuilder::Configuration.new(use_sha1_digests: true)
+
+ assert_true configuration.use_sha1_digests
+ end
+
+ assert_match(
+ /use_sha1_digests is deprecated and will be removed in FixtureBuilder 0.7; it is ignored because SHA-256 is always used/,
+ warning
+ )
+ end
+
+ def test_fixtures_dir
+ assert_equal fixture_directory, FixtureBuilder.configuration.send(:fixtures_dir).to_s
+ end
+
+ def test_lock_path_tracks_fixture_builder_file
+ configuration = FixtureBuilder::Configuration.new
+ configuration.fixture_builder_file = "tmp/first-fixture-builder.yml"
+ assert_equal "#{File.expand_path(configuration.fixture_builder_file)}.lock",
+ configuration.lock_path
+
+ configuration.fixture_builder_file = "tmp/second-fixture-builder.yml"
+ assert_equal "#{File.expand_path(configuration.fixture_builder_file)}.lock",
+ configuration.lock_path
+ end
+end
diff --git a/test/fixture_builder_test.rb b/test/fixture_builder_test.rb
index 957f02e..ab974c7 100644
--- a/test/fixture_builder_test.rb
+++ b/test/fixture_builder_test.rb
@@ -1,243 +1,15 @@
# frozen_string_literal: false
-require File.expand_path(File.join(File.dirname(__FILE__), "test_helper"))
-
-class FixtureBuilderTestModel
- def self.table_name
- "models"
- end
-end
+require_relative "test_helper"
+# standard:disable Rails/ApplicationRecord
class FixtureBuilderTest < Test::Unit::TestCase
include TestDatabase
+ prepend IsolatedFixtureFilesystem
- def teardown
- FixtureBuilder.instance_variable_set(:@configuration, nil)
- end
-
- def test_name_with
- hash = {"email" => "bob@example.com"}
- FixtureBuilder.configure do |config|
- config.name_model_with FixtureBuilderTestModel do |record_hash, index|
- [record_hash["email"].split("@").first, index].join("_")
- end
- end
- assert_equal "bob_001",
- FixtureBuilder.configuration.send(:record_name, hash, FixtureBuilderTestModel.table_name)
- end
-
- def test_ivar_naming
- create_and_blow_away_old_db
- force_fixture_generation
-
- FixtureBuilder.configure do |fbuilder|
- fbuilder.files_to_check += Dir[test_path("*.rb")]
- fbuilder.factory do
- @king_of_gnomes = MagicalCreature.create(name: "robert", species: "gnome")
- end
- end
- generated_fixture = YAML.load(File.open(test_path("fixtures/magical_creatures.yml")))
- assert_equal "king_of_gnomes", generated_fixture.keys.first
- end
-
- def test_serialization
- create_and_blow_away_old_db
- force_fixture_generation
-
- FixtureBuilder.configure do |fbuilder|
- fbuilder.files_to_check += Dir[test_path("*.rb")]
- fbuilder.factory do
- @enty = MagicalCreature.create(name: "Enty", species: "ent",
- powers: %w[shading rooting seeding])
- end
- end
- generated_fixture = YAML.load(File.open(test_path("fixtures/magical_creatures.yml")))
- assert_equal "---\n- shading\n- rooting\n- seeding\n", generated_fixture["enty"]["powers"]
- end
-
- def test_dates_are_iso_formatted_without_mutating_global_date_formats
- create_and_blow_away_old_db
- force_fixture_generation
-
- default_date_format_exists = Date::DATE_FORMATS.key?(:default)
- default_date_format = Date::DATE_FORMATS[:default]
- custom_date_format = "%m/%d/%Y"
- Date::DATE_FORMATS[:default] = custom_date_format
-
- begin
- date_format_during_generation = nil
- FixtureBuilder.configure do |fbuilder|
- fbuilder.files_to_check += Dir[test_path("*.rb")]
- fbuilder.name_model_with MagicalCreature do |_record, index|
- date_format_during_generation = Date::DATE_FORMATS[:default]
- "creature_#{index}"
- end
- fbuilder.factory do
- MagicalCreature.create!(name: "Ariel", species: "mermaid", born_on: Date.new(1990, 1, 2))
- end
- end
-
- fixture_contents = File.read(test_path("fixtures/magical_creatures.yml"))
- assert_includes fixture_contents, "born_on: '1990-01-02'\n"
- assert_equal custom_date_format, date_format_during_generation
- assert_equal custom_date_format, Date::DATE_FORMATS[:default]
- ensure
- if default_date_format_exists
- Date::DATE_FORMATS[:default] = default_date_format
- else
- Date::DATE_FORMATS.delete(:default)
- end
- end
-
- if default_date_format_exists
- assert_equal default_date_format, Date::DATE_FORMATS[:default]
- else
- assert_not_include Date::DATE_FORMATS, :default
- end
- end
-
- def test_do_not_include_virtual_attributes
- create_and_blow_away_old_db
- force_fixture_generation
-
- FixtureBuilder.configure do |fbuilder|
- fbuilder.files_to_check += Dir[test_path("*.rb")]
- fbuilder.factory do
- MagicalCreature.create(name: "Uni", species: "unicorn", powers: %w[rainbows flying])
- end
- end
- generated_fixture = YAML.load(File.open(test_path("fixtures/magical_creatures.yml")))
- assert !generated_fixture["uni"].key?("virtual")
- end
-
- def test_generated_columns_are_excluded_for_model_backed_tables
- create_and_blow_away_old_db
- force_fixture_generation
-
- table_name = GeneratedCreature.table_name
- FixtureBuilder.configure do |fbuilder|
- fbuilder.files_to_check = []
- fbuilder.skip_tables = ActiveRecord::Base.connection.tables - [table_name]
- fbuilder.factory { GeneratedCreature.create!(name: "Myrddin") }
- end
-
- generated_fixture = YAML.safe_load_file(test_path("fixtures/#{table_name}.yml"))
- assert_equal "Myrddin", generated_fixture.dig("myrddin", "name")
- assert_not_include generated_fixture.fetch("myrddin"), "name_length"
-
- GeneratedCreature.delete_all
- create_fixtures(table_name)
- assert_equal 7, GeneratedCreature.find_by!(name: "Myrddin").name_length
- end
-
- def test_generated_columns_are_excluded_for_raw_query_tables
- create_and_blow_away_old_db
- force_fixture_generation
-
- table_name = GENERATED_COLUMN_RECORDS_TABLE
- FixtureBuilder.configure do |fbuilder|
- fbuilder.files_to_check = []
- fbuilder.skip_tables = ActiveRecord::Base.connection.tables - [table_name]
- fbuilder.factory do
- ActiveRecord::Base.connection.execute(
- "INSERT INTO #{table_name} (name) VALUES ('Merlin')"
- )
- end
- end
-
- generated_fixture = YAML.safe_load_file(test_path("fixtures/#{table_name}.yml"))
- assert_equal "Merlin", generated_fixture.dig("merlin", "name")
- assert_not_include generated_fixture.fetch("merlin"), "name_length"
-
- ActiveRecord::Base.connection.delete("DELETE FROM #{table_name}")
- create_fixtures(table_name)
- assert_equal 6,
- ActiveRecord::Base.connection.select_value("SELECT name_length FROM #{table_name}")
- end
-
- def test_raw_query_select_aliases_are_preserved
+ def setup
+ super
create_and_blow_away_old_db
- force_fixture_generation
-
- table_name = GENERATED_COLUMN_RECORDS_TABLE
- capture_output do
- FixtureBuilder.configure do |fbuilder|
- fbuilder.files_to_check = []
- fbuilder.skip_tables = ActiveRecord::Base.connection.tables - [table_name]
- fbuilder.select_sql = "SELECT *, upper(name) AS shouted_name FROM %s"
- fbuilder.factory do
- ActiveRecord::Base.connection.execute(
- "INSERT INTO #{table_name} (name) VALUES ('Merlin')"
- )
- end
- end
- end
-
- generated_fixture = YAML.safe_load_file(test_path("fixtures/#{table_name}.yml"))
- record = generated_fixture.fetch("merlin")
- assert_equal "Merlin", record["name"]
- # A custom `select_sql` is executed as written, so its alias reaches the
- # snapshot. Only the database-generated column, which cannot be inserted,
- # is removed. The alias is deliberately not a column of the table, so this
- # fixture is not expected to load.
- assert_equal "MERLIN", record["shouted_name"]
- assert_not_include record, "name_length"
- end
-
- def test_generated_columns_come_from_the_model_table_name
- create_and_blow_away_old_db
- force_fixture_generation
-
- table_name = RELOCATED_CREATURES_TABLE
- FixtureBuilder.configure do |fbuilder|
- fbuilder.files_to_check = []
- fbuilder.skip_tables = ActiveRecord::Base.connection.tables - [table_name]
- fbuilder.factory { RelocatedCreature.create!(name: "Nimue") }
- end
-
- generated_fixture = YAML.safe_load_file(test_path("fixtures/#{table_name}.yml"))
- # `name` is a plain column on the model's own table, so it must survive even
- # though the iterated table of the same inferred name generates it.
- assert_include generated_fixture, "nimue"
- record = generated_fixture.fetch("nimue")
- assert_include record, "name"
- assert_equal "Nimue", record["name"]
- assert_not_include record, "unrelated"
- end
-
- def test_custom_json_attribute_type_round_trips_through_fixtures
- create_and_blow_away_old_db
- force_fixture_generation
- wizard_data = WizardData.new(
- level: 99,
- title: "The Grey",
- allies: %w[Frodo Aragorn]
- )
-
- FixtureBuilder.configure do |fbuilder|
- fbuilder.files_to_check += Dir[test_path("*.rb")]
- fbuilder.factory do
- MagicalCreature.create!(
- name: "Gandalf",
- species: "wizard",
- wizard_data: wizard_data
- )
- end
- end
-
- generated_fixture = YAML.safe_load_file(test_path("fixtures/magical_creatures.yml"))
- assert_equal(
- {"level" => 99, "title" => "The Grey", "allies" => %w[Frodo Aragorn]},
- generated_fixture.dig("gandalf", "wizard_data")
- )
-
- MagicalCreature.delete_all
- ActiveRecord::FixtureSet.create_fixtures(
- test_path("fixtures"),
- MagicalCreature.table_name
- )
-
- assert_equal wizard_data, MagicalCreature.find_by!(name: "Gandalf").wizard_data
end
def test_configure
@@ -253,64 +25,6 @@ def test_deprecator_has_fixture_builder_metadata
assert_equal "FixtureBuilder", FixtureBuilder.deprecator.gem_name
end
- def test_sql_setters_reject_positional_table_format_without_warning
- {select_sql: "SELECT * FROM %s", delete_sql: "DELETE FROM %s"}.each do |attribute, sql|
- configuration = FixtureBuilder::Configuration.new
-
- _output, warning = capture_output do
- error = assert_raise(ArgumentError) do
- configuration.public_send("#{attribute}=", sql)
- end
-
- assert_equal(
- "Positional %s table placeholders are no longer supported; use %s or %{table}. " \
- "See https://docs.ruby-lang.org/en/3.3/format_specifications_rdoc.html" \
- "#label-Reference+by+Name.",
- error.message
- )
- end
-
- assert_empty warning
- end
- end
-
- def test_sql_setters_warn_on_every_assignment_and_retain_named_table_formats
- ["%s", "%{table}"].each do |table_format|
- {
- select_sql: "SELECT * FROM #{table_format}",
- delete_sql: "DELETE FROM #{table_format}"
- }.each do |attribute, sql|
- configuration = FixtureBuilder::Configuration.new
-
- _output, warning = capture_output do
- configuration.public_send("#{attribute}=", sql)
- end
-
- assert_include(
- warning,
- "#{attribute}= is deprecated and planned for removal in FixtureBuilder 0.7. " \
- "If you are actively using this feature, please share your use case at " \
- "https://github.com/rdy/fixture_builder/issues/94 so we can consider the best way " \
- "to continue to support it."
- )
- assert_equal sql, configuration.public_send(attribute)
- end
- end
- end
-
- def test_configuration_constructor_accepts_deprecated_use_sha1_digests_option
- _output, warning = capture_output do
- configuration = FixtureBuilder::Configuration.new(use_sha1_digests: true)
-
- assert_true configuration.use_sha1_digests
- end
-
- assert_match(
- /use_sha1_digests is deprecated and will be removed in FixtureBuilder 0.7; it is ignored because SHA-256 is always used/,
- warning
- )
- end
-
def test_configuration_accepts_deprecated_use_sha1_digests_option_when_memoized
configuration = FixtureBuilder.configuration
@@ -350,290 +64,51 @@ def test_configure_rejects_unknown_options
end
end
- def test_absolute_rails_fixtures_path_uses_database_tasks_fixtures_path
- original_fixtures_path = ActiveRecord::Tasks::DatabaseTasks.fixtures_path
- authoritative_path = test_path("authoritative_fixtures")
- ActiveRecord::Tasks::DatabaseTasks.fixtures_path = authoritative_path
-
- assert_same authoritative_path,
- FixtureBuilder::FixturesPath.absolute_rails_fixtures_path
- ensure
- ActiveRecord::Tasks::DatabaseTasks.fixtures_path = original_fixtures_path
- end
-
- def test_absolute_rails_fixtures_path_propagates_database_tasks_errors
- database_tasks = ActiveRecord::Tasks::DatabaseTasks
- original_method = database_tasks.method(:fixtures_path)
- error_class = Class.new(StandardError)
- database_tasks.define_singleton_method(:fixtures_path) { raise error_class }
-
- assert_raise(error_class) do
- FixtureBuilder::FixturesPath.absolute_rails_fixtures_path
- end
- ensure
- database_tasks.singleton_class.remove_method(:fixtures_path)
- database_tasks.define_singleton_method(:fixtures_path, original_method)
- end
-
- def test_fixtures_dir
- assert_match(%r{test/fixtures$}, FixtureBuilder.configuration.send(:fixtures_dir).to_s)
- end
-
- def test_rebuilding_due_to_differing_file_hashes
- create_and_blow_away_old_db
- force_fixture_generation_due_to_differing_file_hashes
-
- FixtureBuilder.configure do |fbuilder|
- fbuilder.files_to_check += Dir[test_path("*.rb")]
- fbuilder.factory do
- @enty = MagicalCreature.create(name: "Enty", species: "ent",
- powers: %w[shading rooting seeding])
- end
- end
- generated_fixture = YAML.load(File.open(test_path("fixtures/magical_creatures.yml")))
- assert_equal "---\n- shading\n- rooting\n- seeding\n", generated_fixture["enty"]["powers"]
- end
-
- def test_rebuilds_when_generated_fixture_hashes_differ
- create_and_blow_away_old_db
- force_fixture_generation
-
- FixtureBuilder.configure do |fbuilder|
- fbuilder.files_to_check += Dir[test_path("*.rb")]
- fbuilder.factory do
- @enty = MagicalCreature.create(name: "Enty", species: "ent",
- powers: %w[shading rooting seeding])
- end
- end
-
- FixtureBuilder.instance_variable_set(:@configuration, nil)
- fixture_path = test_path("fixtures/magical_creatures.yml")
- generated_fixture = YAML.load_file(fixture_path)
- generated_fixture["enty"]["retired_column"] = "bogus"
- File.write(fixture_path, generated_fixture.to_yaml)
-
- FixtureBuilder.configure do |fbuilder|
- fbuilder.files_to_check += Dir[test_path("*.rb")]
- fbuilder.factory do
- @enty = MagicalCreature.create(name: "Enty", species: "ent",
- powers: %w[shading rooting seeding])
- end
- end
-
- regenerated_fixture = YAML.load_file(fixture_path)
- assert_false regenerated_fixture["enty"].key?("retired_column")
- assert_equal "Enty", regenerated_fixture["enty"]["name"]
- assert_equal "ent", regenerated_fixture["enty"]["species"]
- end
-
- data(
- "empty document" => "",
- "false" => "false\n",
- "scalar" => "scalar\n",
- "flat manifest" => {"source.rb" => "old digest"}.to_yaml,
- "unsupported future version" => {"version" => 2, "sources" => {}, "fixtures" => {}}.to_yaml,
- "invalid current shape" => {
- "version" => 1,
- "sources" => {},
- "fixtures" => {},
- 1 => "invalid"
- }.to_yaml
- )
- def test_rebuilds_parsed_invalid_manifest(payload)
- assert_manifest_rebuilds(payload)
- end
-
- def test_malformed_manifest_raises_without_running_factory
- create_and_blow_away_old_db
- manifest_path = File.expand_path("../tmp/fixture_builder.yml", __dir__)
- File.write(manifest_path, "---\ninvalid: [\n")
- factory_called = false
-
- assert_raise(Psych::SyntaxError) do
- FixtureBuilder.configure do |fbuilder|
- fbuilder.files_to_check += Dir[test_path("*.rb")]
- fbuilder.factory { factory_called = true }
- end
- end
-
- assert_false factory_called
- end
-
- def test_lock_path_tracks_fixture_builder_file
- configuration = FixtureBuilder::Configuration.new
- configuration.fixture_builder_file = "tmp/first-fixture-builder.yml"
- assert_equal "#{File.expand_path(configuration.fixture_builder_file)}.lock",
- configuration.lock_path
-
- configuration.fixture_builder_file = "tmp/second-fixture-builder.yml"
- assert_equal "#{File.expand_path(configuration.fixture_builder_file)}.lock",
- configuration.lock_path
- end
-
- def test_fresh_manifest_returns_without_acquiring_lock
- create_and_blow_away_old_db
+ def test_ivar_naming
force_fixture_generation
- builds = 0
- lock_path = nil
FixtureBuilder.configure do |fbuilder|
fbuilder.files_to_check += Dir[test_path("*.rb")]
- lock_path = fbuilder.lock_path
fbuilder.factory do
- builds += 1
- @enty = MagicalCreature.create(name: "Enty", species: "ent")
+ @king_of_gnomes = MagicalCreature.create(name: "robert", species: "gnome")
end
end
-
- assert_path_exist lock_path
- FileUtils.rm(lock_path)
- FixtureBuilder.instance_variable_set(:@configuration, nil)
-
- FixtureBuilder.configure do |fbuilder|
- fbuilder.files_to_check += Dir[test_path("*.rb")]
- fbuilder.factory { builds += 1 }
- end
-
- assert_equal 1, builds
- assert_path_not_exist lock_path
- end
-
- def test_skips_rebuild_for_valid_empty_fixture_snapshot
- create_and_blow_away_old_db
- force_fixture_generation
- fixture_snapshot = Dir[test_path("fixtures/*.yml")].to_h do |filename|
- [filename, File.binread(filename)]
- end
- FileUtils.rm_f(fixture_snapshot.keys)
- builds = 0
-
- FixtureBuilder.configure do |fbuilder|
- fbuilder.files_to_check += Dir[test_path("*.rb")]
- fbuilder.write_empty_files = false
- fbuilder.factory { builds += 1 }
- end
-
- manifest_path = File.expand_path("../tmp/fixture_builder.yml", __dir__)
- assert_empty YAML.safe_load_file(manifest_path).fetch("fixtures")
- FixtureBuilder.instance_variable_set(:@configuration, nil)
-
- FixtureBuilder.configure do |fbuilder|
- fbuilder.files_to_check += Dir[test_path("*.rb")]
- fbuilder.write_empty_files = false
- fbuilder.factory { builds += 1 }
- end
-
- assert_equal 1, builds
- ensure
- current_fixtures = Dir[test_path("fixtures/*.yml")]
- FileUtils.rm_f(current_fixtures - fixture_snapshot.keys) if fixture_snapshot
- fixture_snapshot&.each { |filename, contents| File.binwrite(filename, contents) }
+ generated_fixture = YAML.load(File.open(fixture_path("#{MagicalCreature.table_name}.yml")))
+ assert_equal "king_of_gnomes", generated_fixture.except("_fixture").keys.first
end
- def test_raising_after_build_invalidates_manifest_and_retries
- create_and_blow_away_old_db
+ def test_custom_json_attribute_type_round_trips_through_fixtures
force_fixture_generation
- builds = 0
- factory = proc do
- builds += 1
- @enty = MagicalCreature.create(name: "Enty", species: "ent")
- end
-
- FixtureBuilder.configure do |fbuilder|
- fbuilder.files_to_check += Dir[test_path("*.rb")]
- fbuilder.factory(&factory)
- end
-
- manifest_path = Rails.root.join("tmp/fixture_builder.yml")
- fixture_path = test_path("fixtures/magical_creatures.yml")
- generated_fixture = YAML.safe_load_file(fixture_path)
- generated_fixture["enty"]["retired_column"] = "bogus"
- File.write(fixture_path, generated_fixture.to_yaml)
- create_and_blow_away_old_db
- FixtureBuilder.instance_variable_set(:@configuration, nil)
-
- assert_raise(RuntimeError) do
- FixtureBuilder.configure do |fbuilder|
- fbuilder.files_to_check += Dir[test_path("*.rb")]
- fbuilder.after_build = proc { raise "after build failure" }
- fbuilder.factory(&factory)
- end
- end
-
- assert_false File.exist?(manifest_path)
+ wizard_data = WizardData.new(
+ level: 99,
+ title: "The Grey",
+ allies: %w[Frodo Aragorn]
+ )
- FixtureBuilder.instance_variable_set(:@configuration, nil)
FixtureBuilder.configure do |fbuilder|
fbuilder.files_to_check += Dir[test_path("*.rb")]
- fbuilder.factory(&factory)
- end
-
- assert_equal 3, builds
- assert_equal 1, YAML.safe_load_file(manifest_path)["version"]
- end
-
- def test_sha256_manifest_digests_when_deprecated_use_sha1_digests_is_enabled
- create_and_blow_away_old_db
- force_fixture_generation_due_to_differing_file_hashes
-
- source_path = Pathname.new(test_path("fixture_builder_test.rb"))
- FixtureBuilder.configure do |fbuilder|
- _output, warning = capture_output do
- fbuilder.use_sha1_digests = true
- end
-
- assert_true fbuilder.use_sha1_digests
- assert_match(
- /use_sha1_digests is deprecated and will be removed in FixtureBuilder 0.7; it is ignored because SHA-256 is always used/,
- warning
- )
-
- fbuilder.files_to_check = [source_path]
- fbuilder.factory do
- @enty = MagicalCreature.create(name: "Enty", species: "ent",
- powers: %w[shading rooting seeding])
- end
-
- manifest = YAML.safe_load_file(File.expand_path("../tmp/fixture_builder.yml", __dir__))
- fixture_path = test_path("fixtures/magical_creatures.yml")
- assert_equal 1, manifest["version"]
- assert_equal Digest::SHA256.file(source_path).hexdigest,
- manifest.fetch("sources").fetch(source_path.to_s)
- assert_equal Digest::SHA256.file(fixture_path).hexdigest,
- manifest.fetch("fixtures").fetch(File.basename(fixture_path))
-
- first_modified_time = File.mtime(fixture_path)
fbuilder.factory do
+ MagicalCreature.create!(
+ name: "Gandalf",
+ species: "wizard",
+ wizard_data: wizard_data
+ )
end
- second_modified_time = File.mtime(fixture_path)
- assert_equal first_modified_time, second_modified_time
- end
- end
-
- private
-
- def assert_manifest_rebuilds(payload)
- create_and_blow_away_old_db
- force_fixture_generation
- builds = 0
- factory = proc do
- builds += 1
- @enty = MagicalCreature.create(name: "Enty", species: "ent")
end
- FixtureBuilder.configure do |fbuilder|
- fbuilder.files_to_check += Dir[test_path("*.rb")]
- fbuilder.factory(&factory)
- end
+ generated_fixture = YAML.safe_load_file(fixture_path("#{MagicalCreature.table_name}.yml"))
+ assert_equal(
+ {"level" => 99, "title" => "The Grey", "allies" => %w[Frodo Aragorn]},
+ generated_fixture.dig("gandalf", "wizard_data")
+ )
- manifest_path = File.expand_path("../tmp/fixture_builder.yml", __dir__)
- File.write(manifest_path, payload)
- FixtureBuilder.instance_variable_set(:@configuration, nil)
- FixtureBuilder.configure do |fbuilder|
- fbuilder.files_to_check += Dir[test_path("*.rb")]
- fbuilder.factory(&factory)
- end
+ MagicalCreature.delete_all
+ ActiveRecord::FixtureSet.create_fixtures(
+ fixture_directory,
+ MagicalCreature.table_name
+ )
- assert_equal 2, builds
+ assert_equal wizard_data, MagicalCreature.find_by!(name: "Gandalf").wizard_data
end
end
+# standard:enable Rails/ApplicationRecord
diff --git a/test/fixtures_path_test.rb b/test/fixtures_path_test.rb
new file mode 100644
index 0000000..8784ec8
--- /dev/null
+++ b/test/fixtures_path_test.rb
@@ -0,0 +1,32 @@
+# frozen_string_literal: false
+
+require_relative "test_helper"
+
+class FixturesPathTest < Test::Unit::TestCase
+ prepend IsolatedFixtureFilesystem
+
+ def test_absolute_rails_fixtures_path_uses_database_tasks_fixtures_path
+ original_fixtures_path = ActiveRecord::Tasks::DatabaseTasks.fixtures_path
+ authoritative_path = test_path("authoritative_fixtures")
+ ActiveRecord::Tasks::DatabaseTasks.fixtures_path = authoritative_path
+
+ assert_same authoritative_path,
+ FixtureBuilder::FixturesPath.absolute_rails_fixtures_path
+ ensure
+ ActiveRecord::Tasks::DatabaseTasks.fixtures_path = original_fixtures_path
+ end
+
+ def test_absolute_rails_fixtures_path_propagates_database_tasks_errors
+ database_tasks = ActiveRecord::Tasks::DatabaseTasks
+ original_method = database_tasks.method(:fixtures_path)
+ error_class = Class.new(StandardError)
+ database_tasks.define_singleton_method(:fixtures_path) { raise error_class }
+
+ assert_raise(error_class) do
+ FixtureBuilder::FixturesPath.absolute_rails_fixtures_path
+ end
+ ensure
+ database_tasks.singleton_class.remove_method(:fixtures_path)
+ database_tasks.define_singleton_method(:fixtures_path, original_method)
+ end
+end
diff --git a/test/model_resolver/ambiguity/alphabetical_definition_order_test.rb b/test/model_resolver/ambiguity/alphabetical_definition_order_test.rb
new file mode 100644
index 0000000..e807827
--- /dev/null
+++ b/test/model_resolver/ambiguity/alphabetical_definition_order_test.rb
@@ -0,0 +1,26 @@
+# frozen_string_literal: false
+
+require_relative "../../test_helper"
+require_relative "../../support/model_resolver_ambiguity_behavior"
+
+# standard:disable Rails/ApplicationRecord
+module ModelResolverTests
+ module Ambiguity
+ class AlphabeticalDefinitionOrderTest < Test::Unit::TestCase
+ prepend IsolatedFixtureFilesystem
+
+ with_model :Alpha do
+ table { |table| table.string :name }
+ end
+
+ with_model :Beta do
+ table { |table| table.string :name }
+ end
+
+ # Both definition orders must reject ambiguous models, report their names
+ # alphabetically, and leave the existing fixture untouched.
+ include ModelResolverTests::Ambiguity::Behavior
+ end
+ end
+end
+# standard:enable Rails/ApplicationRecord
diff --git a/test/model_resolver/ambiguity/reverse_alphabetical_definition_order_test.rb b/test/model_resolver/ambiguity/reverse_alphabetical_definition_order_test.rb
new file mode 100644
index 0000000..a67d1cf
--- /dev/null
+++ b/test/model_resolver/ambiguity/reverse_alphabetical_definition_order_test.rb
@@ -0,0 +1,26 @@
+# frozen_string_literal: false
+
+require_relative "../../test_helper"
+require_relative "../../support/model_resolver_ambiguity_behavior"
+
+# standard:disable Rails/ApplicationRecord
+module ModelResolverTests
+ module Ambiguity
+ class ReverseAlphabeticalDefinitionOrderTest < Test::Unit::TestCase
+ prepend IsolatedFixtureFilesystem
+
+ with_model :Beta do
+ table { |table| table.string :name }
+ end
+
+ with_model :Alpha do
+ table { |table| table.string :name }
+ end
+
+ # Both definition orders must reject ambiguous models, report their names
+ # alphabetically, and leave the existing fixture untouched.
+ include ModelResolverTests::Ambiguity::Behavior
+ end
+ end
+end
+# standard:enable Rails/ApplicationRecord
diff --git a/test/model_resolver/ambiguous_model_error_test.rb b/test/model_resolver/ambiguous_model_error_test.rb
new file mode 100644
index 0000000..565962c
--- /dev/null
+++ b/test/model_resolver/ambiguous_model_error_test.rb
@@ -0,0 +1,28 @@
+# frozen_string_literal: true
+
+require_relative "../test_helper"
+
+module ModelResolverTests
+ class ErrorTest < Test::Unit::TestCase
+ prepend IsolatedFixtureFilesystem
+
+ def test_ambiguous_model_error_exposes_its_table_name_and_models
+ magical_creature = named_model("MagicalCreature")
+ generated_creature = named_model("GeneratedCreature")
+ error = FixtureBuilder::AmbiguousModelError.new("creatures", [magical_creature, generated_creature])
+
+ assert_equal "creatures", error.table_name
+ assert_equal [generated_creature, magical_creature], error.models
+ assert_equal(
+ "Multiple models match table creatures: GeneratedCreature, MagicalCreature",
+ error.message
+ )
+ end
+
+ private
+
+ def named_model(name)
+ Class.new.tap { |model| model.define_singleton_method(:name) { name } }
+ end
+ end
+end
diff --git a/test/model_resolver/selection_test.rb b/test/model_resolver/selection_test.rb
new file mode 100644
index 0000000..09524ec
--- /dev/null
+++ b/test/model_resolver/selection_test.rb
@@ -0,0 +1,196 @@
+# frozen_string_literal: false
+
+require_relative "../test_helper"
+
+# Regression tests for model resolution by configured table name (#109).
+# standard:disable Rails/ApplicationRecord
+module ModelResolverTests
+ class SelectionTest < Test::Unit::TestCase
+ prepend IsolatedFixtureFilesystem
+
+ with_table :fixture_builder_autoloaded_models do |table|
+ table.json :wizard_data
+ end
+
+ with_table :legendary_creatures do |table|
+ table.string :name
+ end
+
+ with_model :LegendaryCreature do
+ table
+
+ model do
+ self.abstract_class = true
+ end
+ end
+
+ with_model :Phoenix,
+ superclass: :LegendaryCreature do
+ table
+
+ model do
+ self.table_name = "legendary_creatures"
+ end
+ end
+
+ with_model :Unicorn,
+ superclass: :LegendaryCreature do
+ table
+
+ model do
+ self.table_name = "legendary_creatures"
+ end
+ end
+
+ with_table :separate_pools do |table|
+ table.string :name
+ end
+
+ with_model :SeparatePool do
+ table
+
+ model do
+ establish_connection(adapter: "sqlite3", database: ":memory:")
+ self.table_name = "separate_pools"
+ end
+ end
+
+ with_table :schema_errors do |table|
+ table.string :name
+ end
+
+ with_model :SchemaError do
+ table
+
+ model do
+ self.table_name = "schema_errors"
+ end
+ end
+
+ with_table :fixture_builder_id_less_models, id: false do |table|
+ table.string :name
+ end
+
+ def teardown
+ SeparatePool.connection_pool.disconnect!
+ super
+ end
+
+ def test_conventionally_named_autoloaded_model_uses_model_backed_serialization
+ table_name = "fixture_builder_autoloaded_models"
+
+ with_autoloaded_model("FixtureBuilderAutoloadedModel") do
+ force_fixture_generation
+ build_fixtures_for(table_name) do
+ value = ActiveRecord::Base.connection.quote({"level" => 99}.to_json)
+ ActiveRecord::Base.connection.execute(
+ "INSERT INTO #{table_name} (wizard_data) VALUES (#{value})"
+ )
+ end
+
+ model = Object.const_get(:FixtureBuilderAutoloadedModel)
+ assert_equal table_name, model.table_name
+ assert_equal :json, model.columns_hash.fetch("wizard_data").type
+
+ fixture = YAML.safe_load_file(fixture_path("#{table_name}.yml"))
+ assert_equal({"model_class" => model.name}, fixture.fetch("_fixture"))
+ assert_equal({"level" => 99}, fixture.except("_fixture").values.first["wizard_data"])
+
+ model.delete_all
+ create_fixtures(table_name)
+ assert_equal WizardData.new(level: 99, title: nil, allies: nil), model.first!.wizard_data
+ end
+ end
+
+ def test_concrete_siblings_under_an_abstract_ancestor_remain_ambiguous
+ table_name = "legendary_creatures"
+ force_fixture_generation
+
+ error = assert_raise(FixtureBuilder::AmbiguousModelError) do
+ build_fixtures_for(table_name) do
+ ActiveRecord::Base.connection.execute(
+ "INSERT INTO #{table_name} (name) VALUES ('Merlin')"
+ )
+ end
+ end
+
+ assert_equal %w[Phoenix Unicorn],
+ error.models.map(&:name)
+ end
+
+ def test_separate_pool_model_with_the_same_table_name_is_ignored
+ table_name = "separate_pools"
+ force_fixture_generation
+ build_fixtures_for(table_name) do
+ ActiveRecord::Base.connection.execute(
+ "INSERT INTO #{table_name} (name) VALUES ('Base pool row')"
+ )
+ end
+
+ fixture = YAML.safe_load_file(fixture_path("#{table_name}.yml"))
+ assert_equal "Base pool row", fixture.except("_fixture").values.first["name"]
+ end
+
+ def test_candidate_schema_errors_propagate
+ table_name = "schema_errors"
+ error_class = Class.new(StandardError)
+ SchemaError.define_singleton_method(:columns_hash) { raise error_class }
+ force_fixture_generation
+
+ assert_raise(error_class) do
+ build_fixtures_for(table_name) do
+ ActiveRecord::Base.connection.execute(
+ "INSERT INTO #{table_name} (name) VALUES ('Merlin')"
+ )
+ end
+ end
+ end
+
+ def test_id_less_model_table_uses_raw_sql_and_preserves_select_aliases
+ table_name = "fixture_builder_id_less_models"
+ force_fixture_generation
+ FixtureBuilder.configure do |fbuilder|
+ fbuilder.files_to_check = []
+ fbuilder.skip_tables = ActiveRecord::Base.connection.tables - [table_name]
+ fbuilder.select_sql = "SELECT *, upper(name) AS shouted_name FROM %s"
+ fbuilder.factory do
+ ActiveRecord::Base.connection.execute(
+ "INSERT INTO #{table_name} (name) VALUES ('Merlin')"
+ )
+ end
+ end
+
+ fixture = YAML.safe_load_file(fixture_path("#{table_name}.yml"))
+ assert_not_include fixture, "_fixture"
+ assert_equal "Merlin", fixture.except("_fixture").values.first["name"]
+ assert_equal "MERLIN", fixture.except("_fixture").values.first["shouted_name"]
+ end
+
+ private
+
+ def build_fixtures_for(*table_names, &factory)
+ FixtureBuilder.configure do |fbuilder|
+ fbuilder.files_to_check = []
+ fbuilder.skip_tables = ActiveRecord::Base.connection.tables - table_names
+ fbuilder.factory(&factory)
+ end
+ end
+
+ # Autoload behavior inherently requires a file-backed constant rather than
+ # with_model's eagerly installed constant; with_table still owns its schema.
+ def with_autoloaded_model(class_name)
+ path = test_path("#{class_name.underscore}.rb")
+ File.write(path, <<~RUBY)
+ Object.const_set(:#{class_name}, Class.new(ActiveRecord::Base) do
+ attribute :wizard_data, WizardDataType.new
+ end)
+ RUBY
+ Object.autoload(class_name.to_sym, path)
+ yield
+ ensure
+ Object.send(:remove_const, class_name) if Object.const_defined?(class_name, false)
+ FileUtils.rm_f(path) if path
+ end
+ end
+end
+# standard:enable Rails/ApplicationRecord
diff --git a/test/model_resolver/sti_test.rb b/test/model_resolver/sti_test.rb
new file mode 100644
index 0000000..1eebc29
--- /dev/null
+++ b/test/model_resolver/sti_test.rb
@@ -0,0 +1,52 @@
+# frozen_string_literal: false
+
+require_relative "../test_helper"
+
+# standard:disable Rails/ApplicationRecord
+module ModelResolverTests
+ class StiTest < Test::Unit::TestCase
+ prepend IsolatedFixtureFilesystem
+
+ with_model :Creature do
+ table do |table|
+ table.string :name
+ table.string :type
+ end
+ end
+
+ with_model :Dragon, superclass: :Creature do
+ table(false)
+ end
+
+ def test_sti_models_dump_all_subtype_rows_through_the_base_model
+ table_name = Creature.table_name
+ assert_equal Creature, resolve_model(table_name)
+ force_fixture_generation
+
+ build_fixtures_for(table_name) do
+ Creature.create!(name: "Base creature")
+ Dragon.create!(name: "Subclass creature")
+ end
+
+ fixture = YAML.safe_load_file(fixture_path("#{table_name}.yml"))
+ assert_equal %w[Base\ creature Subclass\ creature], fixture.except("_fixture").values.pluck("name")
+ assert_nil fixture.except("_fixture").values.first["type"]
+ assert_equal Dragon.name, fixture.except("_fixture").values.last["type"]
+ end
+
+ private
+
+ def build_fixtures_for(*table_names, &factory)
+ FixtureBuilder.configure do |fbuilder|
+ fbuilder.files_to_check = []
+ fbuilder.skip_tables = ActiveRecord::Base.connection.tables - table_names
+ fbuilder.factory(&factory)
+ end
+ end
+
+ def resolve_model(table_name)
+ FixtureBuilder::ModelResolver.new(connection_pool: ActiveRecord::Base.connection_pool).resolve(table_name)
+ end
+ end
+end
+# standard:enable Rails/ApplicationRecord
diff --git a/test/model_resolver/table_name_test.rb b/test/model_resolver/table_name_test.rb
new file mode 100644
index 0000000..e6c225d
--- /dev/null
+++ b/test/model_resolver/table_name_test.rb
@@ -0,0 +1,73 @@
+# frozen_string_literal: false
+
+require_relative "../test_helper"
+
+# standard:disable Rails/ApplicationRecord
+module ModelResolverTests
+ class TableNameTest < Test::Unit::TestCase
+ prepend IsolatedFixtureFilesystem
+
+ with_model :ArchivedCreature do
+ table do |table|
+ table.string :name, null: false
+ table.json :wizard_data
+ end
+
+ model do
+ attribute :wizard_data, WizardDataType.new
+ end
+ end
+
+ with_model :RawCreature do
+ table(id: false) do |table|
+ table.string :unrelated
+ table.virtual :name, type: :string, as: "upper(unrelated)", stored: true
+ end
+ end
+
+ def test_configured_table_name_uses_model_backed_custom_serialization
+ archive_table = ArchivedCreature.table_name
+ raw_table = RawCreature.table_name
+ assert_equal ArchivedCreature, resolve_model(archive_table)
+ assert_nil resolve_model(raw_table)
+
+ force_fixture_generation
+ build_fixtures_for(archive_table, raw_table) do
+ ArchivedCreature.create!(
+ name: "Nimue",
+ wizard_data: WizardData.new(level: 99, title: "Lady of the Lake", allies: ["Arthur"])
+ )
+ ActiveRecord::Base.connection.execute(
+ "INSERT INTO #{raw_table} (unrelated) VALUES ('Morgana')"
+ )
+ end
+
+ archive_fixture = YAML.safe_load_file(fixture_path("#{archive_table}.yml"))
+ assert_equal(
+ {"level" => 99, "title" => "Lady of the Lake", "allies" => ["Arthur"]},
+ archive_fixture.dig("nimue", "wizard_data")
+ )
+
+ relocated_fixture = YAML.safe_load_file(fixture_path("#{raw_table}.yml"))
+ assert_not_include relocated_fixture, "_fixture"
+ record = relocated_fixture.fetch("#{raw_table}_001")
+ assert_equal "Morgana", record["unrelated"]
+ assert_not_include record, "name"
+ end
+
+ private
+
+ def build_fixtures_for(*table_names, &factory)
+ FixtureBuilder.configure do |fbuilder|
+ fbuilder.files_to_check = []
+ fbuilder.skip_tables = ActiveRecord::Base.connection.tables - table_names
+ fbuilder.factory(&factory)
+ end
+ end
+
+ def resolve_model(table_name)
+ FixtureBuilder::ModelResolver.new(connection_pool: ActiveRecord::Base.connection_pool).resolve(table_name)
+ end
+ end
+end
+# standard:enable Rails/ApplicationRecord
diff --git a/test/support/model_resolver_ambiguity_behavior.rb b/test/support/model_resolver_ambiguity_behavior.rb
new file mode 100644
index 0000000..a56faf7
--- /dev/null
+++ b/test/support/model_resolver_ambiguity_behavior.rb
@@ -0,0 +1,41 @@
+# frozen_string_literal: false
+
+module ModelResolverTests
+ module Ambiguity
+ module Behavior
+ def test_unrelated_models_raise_before_replacing_the_fixture
+ table_name = Alpha.table_name
+ Beta.table_name = table_name
+ Beta.reset_column_information
+ fixture_path = fixture_path("#{table_name}.yml")
+ original_fixture = "existing fixture bytes\n"
+ File.binwrite(fixture_path, original_fixture)
+ force_fixture_generation
+
+ error = assert_raise(FixtureBuilder::AmbiguousModelError) do
+ build_fixtures_for(table_name) do
+ ActiveRecord::Base.connection.execute("INSERT INTO #{table_name} (name) VALUES ('Merlin')")
+ end
+ end
+
+ assert_equal table_name, error.table_name
+ assert_equal %w[Alpha Beta], error.models.map(&:name)
+ assert_equal(
+ "Multiple models match table #{table_name}: Alpha, Beta",
+ error.message
+ )
+ assert_equal original_fixture, File.binread(fixture_path)
+ end
+
+ private
+
+ def build_fixtures_for(*table_names, &factory)
+ FixtureBuilder.configure do |fbuilder|
+ fbuilder.files_to_check = []
+ fbuilder.skip_tables = ActiveRecord::Base.connection.tables - table_names
+ fbuilder.factory(&factory)
+ end
+ end
+ end
+ end
+end
diff --git a/test/support/test_database.rb b/test/support/test_database.rb
index 70f247e..c5b0eab 100644
--- a/test/support/test_database.rb
+++ b/test/support/test_database.rb
@@ -38,8 +38,10 @@ def self.included(base)
def create_and_blow_away_old_db
ActiveRecord::Base.configurations = {"test" => CONFIGURATION}
- ActiveRecord::Base.establish_connection(:test)
connection = ActiveRecord::Base.connection
+ connection.tables.each { |table| connection.drop_table(table) }
+ connection.schema_cache.clear!
+ ActiveRecord::FixtureSet.reset_cache
connection.create_table(:magical_creatures, force: true) do |t|
t.column :name, :string
t.column :species, :string
@@ -73,7 +75,6 @@ def create_and_blow_away_old_db
t.string :name, null: false
end
- GeneratedCreature.reset_column_information
RelocatedCreature.reset_column_information
end
diff --git a/test/test_helper.rb b/test/test_helper.rb
index c79eabe..ee3f13c 100644
--- a/test/test_helper.rb
+++ b/test/test_helper.rb
@@ -2,6 +2,8 @@
require "rubygems"
require "bundler/setup"
+require "fileutils"
+require "tmpdir"
require "test/unit"
class Rails
@@ -22,6 +24,52 @@ def test_path(glob)
require "active_record"
require "active_record/fixtures"
+module IsolatedFixtureFilesystem
+ def setup
+ @original_fixtures_path = ActiveRecord::Tasks::DatabaseTasks.fixtures_path
+ @temporary_fixture_root = Dir.mktmpdir("fixture_builder_test")
+ @fixture_directory = File.join(@temporary_fixture_root, "fixtures")
+ FileUtils.mkdir_p(@fixture_directory)
+ ActiveRecord::Tasks::DatabaseTasks.fixtures_path = @fixture_directory
+ reset_fixture_builder_configuration
+ super
+ rescue
+ clean_up_isolated_fixture_filesystem
+ raise
+ end
+
+ def teardown
+ super
+ ensure
+ clean_up_isolated_fixture_filesystem
+ end
+
+ private
+
+ attr_reader :fixture_directory
+
+ def fixture_path(path = "")
+ File.join(fixture_directory, path)
+ end
+
+ def fixture_builder_file
+ File.join(@temporary_fixture_root, "fixture_builder.yml")
+ end
+
+ def reset_fixture_builder_configuration
+ FixtureBuilder.instance_variable_set(:@configuration, nil)
+ FixtureBuilder.configuration.fixture_directory = fixture_directory
+ FixtureBuilder.configuration.fixture_builder_file = fixture_builder_file
+ end
+
+ def clean_up_isolated_fixture_filesystem
+ FixtureBuilder.instance_variable_set(:@configuration, nil)
+ ensure
+ ActiveRecord::Tasks::DatabaseTasks.fixtures_path = @original_fixtures_path if defined?(@original_fixtures_path)
+ FileUtils.remove_entry(@temporary_fixture_root) if defined?(@temporary_fixture_root) && File.exist?(@temporary_fixture_root)
+ end
+end
+
def create_fixtures(*table_names, &block)
fixture_set = ActiveRecord::FixtureSet
@@ -34,16 +82,20 @@ def create_fixtures(*table_names, &block)
# rewritten YAML is reparsed; Rails 8.0 and 8.1 use the original load path.
if fixture_set.respond_to?(:without_parsing_cache)
fixture_set.without_parsing_cache do
- fixture_set.create_fixtures(test_path("fixtures"), table_names, {}, &block)
+ fixture_set.create_fixtures(fixture_directory, table_names, {}, &block)
end
else
- fixture_set.create_fixtures(test_path("fixtures"), table_names, {}, &block)
+ fixture_set.create_fixtures(fixture_directory, table_names, {}, &block)
end
end
require "sqlite3"
require "fixture_builder"
require_relative "support/test_database"
+ActiveRecord::Base.configurations = {"test" => {"adapter" => "sqlite3", "database" => ":memory:"}}
+ActiveRecord::Base.establish_connection(:test)
+
+require "with_model/test_unit"
class WizardData
attr_reader :level, :title, :allies
@@ -96,12 +148,6 @@ def wizard_data(attributes)
end
# standard:disable Rails/ApplicationRecord
-class GeneratedCreature < ActiveRecord::Base
-end
-
-# Inferable from the `relocated_creatures` table name, but backed by a
-# differently named table, so writable column names must come from
-# `table_name` rather than the table FixtureBuilder is iterating.
class RelocatedCreature < ActiveRecord::Base
self.table_name = "creature_archive"
end
@@ -109,20 +155,29 @@ class RelocatedCreature < ActiveRecord::Base
class MagicalCreature < ActiveRecord::Base
validates_presence_of :name, :species
serialize :powers, type: Array
-
default_scope -> { where(deleted: false) }
-
attribute :virtual, ActiveRecord::Type::Integer.new
attribute :wizard_data, WizardDataType.new
end
# standard:enable Rails/ApplicationRecord
def force_fixture_generation
- FileUtils.rm_f(File.expand_path("../tmp/fixture_builder.yml", __dir__))
+ FileUtils.rm_f(current_fixture_builder_file)
+ reset_fixture_builder_configuration if isolated_fixture_filesystem?
end
def force_fixture_generation_due_to_differing_file_hashes
- path = File.expand_path("../tmp/fixture_builder.yml", __dir__)
- FileUtils.mkdir_p(File.dirname(path))
- File.write(path, "blah blah blah")
+ FileUtils.mkdir_p(File.dirname(current_fixture_builder_file))
+ File.write(current_fixture_builder_file, "blah blah blah")
+ reset_fixture_builder_configuration if isolated_fixture_filesystem?
+end
+
+def current_fixture_builder_file
+ return fixture_builder_file if isolated_fixture_filesystem?
+
+ File.expand_path("../tmp/fixture_builder.yml", __dir__)
+end
+
+def isolated_fixture_filesystem?
+ respond_to?(:fixture_builder_file, true)
end