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
30 changes: 18 additions & 12 deletions doc/plans/2026-08-29-maintainer-backlog-triage.md
Original file line number Diff line number Diff line change
Expand Up @@ -191,25 +191,31 @@ reviving the proposed public hook surface.

## Current replacement stack

### Model-aware fixture generation (PR 1)
### Model-aware fixture generation (PR 1, [#116](https://github.com/rdy/fixture_builder/pull/116))

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
Completed from current `master`: loaded Active Record models are resolved by
their configured table names, model-backed YAML emits Rails-native
`_fixture.model_class` metadata, and tables without an eligible model retain the
raw SQL fallback. Rails fixture loading remains 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.
### Ephemeral test-model migration (PR 2, [#117](https://github.com/rdy/fixture_builder/pull/117))

Completed as the dependent follow-up: the remaining ordinary fixture-builder
tests now use models whose schemas are declared by their owning test cases, and
the shared persistent test database helper is removed. The former shared
`MagicalCreature` schema now belongs inline to the owning integration case in
`test/fixture_builder_test.rb`, manifest lifecycle case in
`test/configuration/manifest_test.rb`, and serialization case in
`test/builder/serialization_test.rb`; `test/configuration_test.rb` remains the
model-free configuration contract case. Manual setup remains only at the
intentional raw SQL, constant/autoload timing, alternate pool, schema-error,
namespaced table-name, and STI boundaries. Metadata-aware and metadata-free
legacy fixture inputs remain separate compatibility paths.

## Execution order

Expand Down
72 changes: 36 additions & 36 deletions test/builder/generated_fixture_ownership_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,62 +3,62 @@
require_relative "../test_helper"
require "tmpdir"

# standard:disable Rails/ApplicationRecord
class OwnershipRecord < ActiveRecord::Base
end
# standard:enable Rails/ApplicationRecord

module BuilderTests
class GeneratedFixtureOwnershipTest < Test::Unit::TestCase
MARKER = "# Generated by fixture_builder. Do not edit.\n"

with_model :PopulatedRecord do
table { |table| table.string :name }
end

with_model :EmptyRecord do
table
end

with_model :DroppedRecord do
table { |table| table.string :name }
end

def setup
@directory = Dir.mktmpdir("fixture-builder-ownership")
# 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

def teardown
FileUtils.remove_entry(@directory) if File.exist?(@directory)
end

def test_marks_populated_and_empty_output
generate { OwnershipRecord.create!(name: "Gandalf") }
generate { PopulatedRecord.create!(name: "Gandalf") }

populated = fixture_path("ownership_records")
empty = fixture_path("empty_records")
populated = fixture_path(PopulatedRecord.table_name)
empty = fixture_path(EmptyRecord.table_name)
assert_equal MARKER, File.open(populated, &:readline)
assert_equal MARKER, File.open(empty, &:readline)
assert_equal "Gandalf", YAML.safe_load_file(populated).dig("gandalf", "name")
assert_equal({}, YAML.safe_load_file(empty))
assert_equal({"_fixture" => {"model_class" => EmptyRecord.name}}, YAML.safe_load_file(empty))

OwnershipRecord.delete_all
ActiveRecord::FixtureSet.create_fixtures(@directory, "ownership_records")
assert_equal "Gandalf", OwnershipRecord.find_by!(name: "Gandalf").name
PopulatedRecord.delete_all
ActiveRecord::FixtureSet.create_fixtures(@directory, PopulatedRecord.table_name)
assert_equal "Gandalf", PopulatedRecord.find_by!(name: "Gandalf").name
end

def test_removes_marked_fixture_after_table_is_dropped
ActiveRecord::Base.connection.create_table(:dropped_records) { |table| table.string :name }
generate(tables: %w[ownership_records empty_records dropped_records])
stale = fixture_path("dropped_records")
generate(tables: [PopulatedRecord.table_name, EmptyRecord.table_name, DroppedRecord.table_name])
stale = fixture_path(DroppedRecord.table_name)
assert_path_exist stale
ActiveRecord::Base.connection.drop_table(:dropped_records)
ActiveRecord::Base.connection.drop_table(DroppedRecord.table_name)

generate { OwnershipRecord.create!(name: "Current") }
generate { PopulatedRecord.create!(name: "Current") }

assert_path_not_exist stale
assert_path_exist fixture_path("ownership_records")
assert_path_exist fixture_path(PopulatedRecord.table_name)
end

def test_preserves_unmarked_orphans
orphan = fixture_path("unknown_records")
File.binwrite(orphan, "unknown: true\n")

generate { OwnershipRecord.create!(name: "Current") }
generate { PopulatedRecord.create!(name: "Current") }

assert_equal "unknown: true\n", File.binread(orphan)
end
Expand All @@ -74,7 +74,7 @@ def test_requires_exact_first_line_marker
[path, contents]
end

generate { OwnershipRecord.create!(name: "Current") }
generate { PopulatedRecord.create!(name: "Current") }

paths.each_value { |path, contents| assert_equal contents, File.read(path) }
end
Expand All @@ -83,16 +83,16 @@ def test_removes_edited_marked_orphans
stale = fixture_path("edited_records")
File.write(stale, MARKER + "human_edit: true\n")

generate { OwnershipRecord.create!(name: "Current") }
generate { PopulatedRecord.create!(name: "Current") }

assert_path_not_exist stale
end

def test_warns_before_overwriting_unmarked_current_output
path = fixture_path("ownership_records")
path = fixture_path(PopulatedRecord.table_name)
File.write(path, "old: true\n")

_output, warning = capture_output { generate { OwnershipRecord.create!(name: "Current") } }
_output, warning = capture_output { generate { PopulatedRecord.create!(name: "Current") } }

assert_include warning, path
assert_include warning, "replacing an unmarked fixture"
Expand All @@ -101,18 +101,18 @@ def test_warns_before_overwriting_unmarked_current_output
end

def test_overwrites_marked_output_without_warning
path = fixture_path("ownership_records")
path = fixture_path(PopulatedRecord.table_name)
File.write(path, MARKER + "edited: true\n")

_output, warning = capture_output { generate { OwnershipRecord.create!(name: "Current") } }
_output, warning = capture_output { generate { PopulatedRecord.create!(name: "Current") } }

assert_empty warning
assert_equal "Current", YAML.safe_load_file(path).dig("current", "name")
end

def test_preserves_unmarked_omitted_empty_output
marked = fixture_path("empty_records")
unmarked = fixture_path("ownership_records")
marked = fixture_path(EmptyRecord.table_name)
unmarked = fixture_path(PopulatedRecord.table_name)
File.write(marked, MARKER + "{}\n")
File.write(unmarked, "hand_authored: true\n")

Expand All @@ -129,7 +129,7 @@ def test_removes_marked_output_for_newly_skipped_table
File.write(marked, MARKER + "{}\n")
File.write(unmarked, "hand_authored: true\n")

generate { OwnershipRecord.create!(name: "Current") }
generate { PopulatedRecord.create!(name: "Current") }

assert_path_not_exist marked
assert_path_exist unmarked
Expand All @@ -140,10 +140,10 @@ def test_removes_marked_output_for_newly_skipped_table
def generate(
write_empty_files: true,
legacy_marker_migration: false,
tables: %w[ownership_records empty_records],
tables: nil,
&block
)
ActiveRecord::Base.connection.create_table(:empty_records) unless ActiveRecord::Base.connection.table_exists?(:empty_records)
tables ||= [PopulatedRecord.table_name, EmptyRecord.table_name]
configuration = FixtureBuilder::Configuration.new
configuration.fixture_directory = @directory
configuration.write_empty_files = write_empty_files
Expand Down
21 changes: 17 additions & 4 deletions test/builder/serialization_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,25 @@
# standard:disable Rails/ApplicationRecord
module BuilderTests
class SerializationTest < Test::Unit::TestCase
include TestDatabase
prepend IsolatedFixtureFilesystem

def setup
super
create_and_blow_away_old_db
with_model :MagicalCreature do
table do |table|
table.string :name
table.string :species
table.string :powers
table.json :wizard_data
table.date :born_on
table.boolean :deleted, default: false, null: false
end

model do
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
end

def test_serialization
Expand Down
39 changes: 17 additions & 22 deletions test/configuration/generated_file_marker_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,26 +3,21 @@
require_relative "../test_helper"
require "tmpdir"

# standard:disable Rails/ApplicationRecord
class MarkerMigrationRecord < ActiveRecord::Base
end
# standard:enable Rails/ApplicationRecord

module ConfigurationTests
class GeneratedFileMarkerTest < Test::Unit::TestCase
MARKER = "# Generated by fixture_builder. Do not edit.\n"

with_model :MarkerRecord do
table { |table| table.string :name }
end

def setup
@directory = Dir.mktmpdir("fixture-builder-marker-migration")
@fixtures = File.join(@directory, "fixtures")
@manifest = File.join(@directory, "fixture_builder.yml")
@source = File.join(@directory, "source.rb")
FileUtils.mkdir_p(@fixtures)
File.write(@source, "source\n")
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

def teardown
Expand All @@ -40,7 +35,7 @@ def test_migrates_compatible_v1_without_overwrite_warning
_output, warning = capture_output do
configuration.factory do
builds += 1
MarkerMigrationRecord.create!(name: "Migrated")
MarkerRecord.create!(name: "Migrated")
end
end

Expand All @@ -57,26 +52,26 @@ def test_persists_adoption_after_success
config = configuration
config.factory do
builds += 1
MarkerMigrationRecord.create!(name: "Migrated")
MarkerRecord.create!(name: "Migrated")
end

manifest = YAML.safe_load_file(@manifest)
assert_equal 1, manifest.fetch("version")
assert_equal true, manifest.fetch("generated_file_marker")
assert_equal Digest::SHA256.file(fixture_path).hexdigest,
manifest.fetch("fixtures").fetch("marker_migration_records.yml")
manifest.fetch("fixtures").fetch("#{MarkerRecord.table_name}.yml")

config.factory { builds += 1 }
assert_equal 1, builds
end

def test_warns_on_unmarked_collision_after_adoption
config = configuration
config.factory { MarkerMigrationRecord.create!(name: "First") }
config.factory { MarkerRecord.create!(name: "First") }
File.write(fixture_path, "unmarked: true\n")

_output, warning = capture_output do
config.factory { MarkerMigrationRecord.create!(name: "Second") }
config.factory { MarkerRecord.create!(name: "Second") }
end

assert_include warning, "replacing an unmarked fixture"
Expand All @@ -97,7 +92,7 @@ def test_does_not_suppress_for_unrecognized_manifests(manifest)
manifest ? File.write(@manifest, manifest.to_yaml) : FileUtils.rm_f(@manifest)

_output, warning = capture_output do
configuration.factory { MarkerMigrationRecord.create!(name: "Current") }
configuration.factory { MarkerRecord.create!(name: "Current") }
end

assert_include warning, "replacing an unmarked fixture"
Expand All @@ -111,14 +106,14 @@ def test_does_not_adopt_when_after_build_raises
config.after_build = proc { raise "after build failure" }

assert_raise(RuntimeError) do
config.factory { MarkerMigrationRecord.create!(name: "Failed") }
config.factory { MarkerRecord.create!(name: "Failed") }
end
assert_path_not_exist @manifest

File.write(fixture_path, "still_unmarked: true\n")
retry_config = configuration
_output, warning = capture_output do
retry_config.factory { MarkerMigrationRecord.create!(name: "Retried") }
retry_config.factory { MarkerRecord.create!(name: "Retried") }
end
assert_include warning, "replacing an unmarked fixture"
end
Expand All @@ -134,7 +129,7 @@ def test_does_not_adopt_when_output_write_fails
config.factory do
FileUtils.rm_f(output_path)
File.chmod(0o500, fixture_directory)
MarkerMigrationRecord.create!(name: "Failed")
MarkerRecord.create!(name: "Failed")
end
end

Expand All @@ -150,13 +145,13 @@ def test_rechecks_adoption_under_generation_lock
config.define_singleton_method(:rebuild_fixtures_preflight?) do
adopted = YAML.safe_load_file(fixture_builder_file)
adopted["generated_file_marker"] = true
adopted["fixtures"]["marker_migration_records.yml"] = "stale"
adopted["fixtures"]["#{MarkerRecord.table_name}.yml"] = "stale"
File.write(fixture_builder_file, adopted.to_yaml)
true
end

_output, warning = capture_output do
config.factory { MarkerMigrationRecord.create!(name: "Locked") }
config.factory { MarkerRecord.create!(name: "Locked") }
end

assert_include warning, "replacing an unmarked fixture"
Expand All @@ -169,12 +164,12 @@ def configuration
config.fixture_directory = @fixtures
config.fixture_builder_file = @manifest
config.files_to_check = [@source]
config.skip_tables = ActiveRecord::Base.connection.tables - ["marker_migration_records"]
config.skip_tables = ActiveRecord::Base.connection.tables - [MarkerRecord.table_name]
config.write_empty_files = false
end
end

def fixture_path(table_name = "marker_migration_records")
def fixture_path(table_name = MarkerRecord.table_name)
File.join(@fixtures, "#{table_name}.yml")
end

Expand Down
Loading