seed_builder 1.2.1 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,20 +1,18 @@
1
1
  module SeedBuilder
2
2
  class Loader
3
3
  def load_seed
4
- initial_logger = Rails.logger
5
-
6
- Rails.logger = Logger.new($stdout)
7
- Rails.logger.level = Logger::INFO
8
-
9
- ActiveRecord::Base.connection.schema_cache.clear!
10
- ActiveRecord::Base.descendants.each(&:reset_column_information)
4
+ prepare_active_record
11
5
 
12
6
  default_seeds_rb = SeedBuilder.config.default_seeds_full_path
13
7
  if File.exist?(default_seeds_rb) && SeedBuilder.config.load_default_seeds?
14
8
  started_at = Time.current
15
- puts "== #{SeedBuilder.config.default_seeds_path}: seeding"
16
- require default_seeds_rb
17
- puts "== #{SeedBuilder.config.default_seeds_path}: seeded (#{(Time.current - started_at).round(4)}s)"
9
+ logger.tagged("seed") do
10
+ logger.info "== #{SeedBuilder.config.default_seeds_path}: seeding"
11
+ end
12
+ load default_seeds_rb
13
+ logger.tagged("seed") do
14
+ logger.info "== #{SeedBuilder.config.default_seeds_path}: seeded (#{(Time.current - started_at).round(4)}s)"
15
+ end
18
16
  end
19
17
 
20
18
  base_path = SeedBuilder.config.seeds_full_path
@@ -22,29 +20,139 @@ module SeedBuilder
22
20
  Dir[SeedBuilder.config.seeds_path_glob]
23
21
  .map { |f| File.basename(f, ".*") }
24
22
  .each do |seed_path|
25
- require "#{base_path}/#{seed_path}.rb"
23
+ load "#{base_path}/#{seed_path}.rb"
26
24
  timestamp, klass_name = seed_path.scan(/^([0-9]+)_(.+)$/).first
27
25
  next if klass_name.blank?
28
26
 
29
- klass = klass_name.camelize.constantize
30
- puts "== #{timestamp} #{klass.name}: seeding"
31
- started_at = Time.current
32
- seed_instance = klass.new
33
- if seed_instance.respond_to?(:change)
34
- seed_instance.change
35
- else
36
- raise "Seed #{klass.name} does not respond to :change"
37
- end
38
- puts "== #{timestamp} #{klass.name}: seeded (#{(Time.current - started_at).round(4)}s)"
39
- rescue ActiveRecord::RecordInvalid => e
40
- puts "Seeding #{klass.name} failed: #{e.record.errors.full_messages}"
41
- raise e
27
+ execute_seed_class(klass_name, timestamp)
42
28
  end
43
29
  else
44
- puts "Seed directory #{base_path} does not exist"
30
+ logger.tagged("seed") do
31
+ logger.warn "Seed directory #{base_path} does not exist"
32
+ end
45
33
  end
34
+ end
35
+
36
+ def load_seed_file(seed_name)
37
+ prepare_active_record
46
38
 
47
- Rails.logger = initial_logger
39
+ base_path = SeedBuilder.config.seeds_full_path
40
+ unless File.exist?(base_path)
41
+ logger.tagged("seed") do
42
+ logger.warn "Seed directory #{base_path} does not exist"
43
+ end
44
+ return
45
+ end
46
+
47
+ # Find the seed file by name
48
+ # Supports: "create_users", "20241206200111_create_users", "20241206200111"
49
+ result = find_seed_file(base_path, seed_name)
50
+
51
+ if result.nil?
52
+ logger.tagged("seed") do
53
+ logger.warn "Seed file '#{seed_name}' not found in #{base_path}"
54
+ end
55
+ return
56
+ elsif result == :ambiguous
57
+ matches = find_seed_file_matches(base_path, seed_name)
58
+ logger.tagged("seed") do
59
+ logger.warn "Multiple seed files match '#{seed_name}':"
60
+ matches.each do |file|
61
+ logger.warn " - #{File.basename(file, ".*")}"
62
+ end
63
+ logger.warn "Please be more specific using the full name with timestamp (e.g., 20241206200111_create_users)"
64
+ end
65
+ return
66
+ end
67
+
68
+ seed_file = result
69
+
70
+ seed_path = File.basename(seed_file, ".*")
71
+ timestamp, klass_name = seed_path.scan(/^([0-9]+)_(.+)$/).first
72
+
73
+ if klass_name.blank?
74
+ logger.tagged("seed") do
75
+ logger.warn "Invalid seed file format: #{seed_path}. Expected format: TIMESTAMP_CLASS_NAME.rb"
76
+ end
77
+ return
78
+ end
79
+
80
+ load seed_file
81
+ execute_seed_class(klass_name, timestamp, seed_name)
82
+ end
83
+
84
+ private
85
+
86
+ def logger
87
+ SeedBuilder.logger
88
+ end
89
+
90
+ def prepare_active_record
91
+ ActiveRecord::Base.connection.schema_cache.clear!
92
+ ActiveRecord::Base.descendants.each(&:reset_column_information)
93
+ end
94
+
95
+ def execute_seed_class(klass_name, timestamp, seed_name = nil)
96
+ klass = klass_name.camelize.constantize
97
+ klass_name_display = klass.name
98
+ logger.tagged("seed") do
99
+ logger.info "== #{timestamp} #{klass_name_display}: seeding"
100
+ end
101
+ started_at = Time.current
102
+ seed_instance = klass.new
103
+ if seed_instance.respond_to?(:change)
104
+ seed_instance.change
105
+ else
106
+ raise "Seed #{klass_name_display} does not respond to :change"
107
+ end
108
+ logger.tagged("seed") do
109
+ logger.info "== #{timestamp} #{klass_name_display}: seeded (#{(Time.current - started_at).round(4)}s)"
110
+ end
111
+ rescue ActiveRecord::RecordInvalid => e
112
+ klass_name_display = defined?(klass) ? klass.name : (klass_name&.camelize || seed_name || klass_name)
113
+ logger.tagged("seed") do
114
+ logger.error "Seeding #{klass_name_display} failed: #{e.record.errors.full_messages}"
115
+ end
116
+ raise e
117
+ rescue => e
118
+ logger.tagged("seed") do
119
+ logger.error "Error loading seed: #{e.message}"
120
+ end
121
+ raise e
122
+ end
123
+
124
+ def find_seed_file(base_path, seed_name)
125
+ # Try exact match first
126
+ exact_match = File.join(base_path, "#{seed_name}.rb")
127
+ return exact_match if File.exist?(exact_match)
128
+
129
+ # Collect all matching files
130
+ matches = find_seed_file_matches(base_path, seed_name)
131
+
132
+ # Return appropriate result based on number of matches
133
+ case matches.length
134
+ when 0
135
+ nil
136
+ when 1
137
+ matches.first
138
+ else
139
+ :ambiguous
140
+ end
141
+ end
142
+
143
+ def find_seed_file_matches(base_path, seed_name)
144
+ matches = []
145
+ Dir[SeedBuilder.config.seeds_path_glob].each do |file|
146
+ basename = File.basename(file, ".*")
147
+ # Match by class name (e.g., "create_users")
148
+ if basename.end_with?("_#{seed_name}") || basename == seed_name
149
+ matches << file
150
+ # Match by timestamp (e.g., "20241206200111")
151
+ elsif basename.start_with?("#{seed_name}_")
152
+ matches << file
153
+ end
154
+ end
155
+ matches
48
156
  end
49
157
  end
50
158
  end
@@ -0,0 +1,15 @@
1
+ module SeedBuilder
2
+ class Railtie < ::Rails::Railtie
3
+ config.to_prepare do
4
+ if SeedBuilder.config.use_seed_loader?
5
+ # Patch Rails.application.load_seed to use our custom loader
6
+ Rails.application.class.class_eval do
7
+ define_method(:load_seed) do
8
+ loader = SeedBuilder::Loader.new
9
+ loader.load_seed
10
+ end
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
data/lib/seed_builder.rb CHANGED
@@ -1,8 +1,9 @@
1
+ require "active_support/tagged_logging"
1
2
  require "seed_builder/config"
2
3
  require "seed_builder/loader"
3
4
 
4
5
  module SeedBuilder
5
- VERSION = "1.2.1".freeze
6
+ VERSION = "1.3.0".freeze
6
7
 
7
8
  module_function
8
9
 
@@ -13,6 +14,26 @@ module SeedBuilder
13
14
  def configure
14
15
  yield config
15
16
  end
17
+
18
+ def logger
19
+ # Use configured logger if set
20
+ base_logger = if config.logger
21
+ config.logger
22
+ elsif defined?(Rails) && Rails.respond_to?(:logger) && Rails.logger
23
+ Rails.logger
24
+ else
25
+ Logger.new($stdout)
26
+ end
27
+
28
+ # If the logger already supports tagging (e.g., Rails.logger is already TaggedLogging),
29
+ # use it directly to avoid double-wrapping and ensure tags propagate properly
30
+ if base_logger.respond_to?(:tagged)
31
+ base_logger
32
+ else
33
+ ActiveSupport::TaggedLogging.new(base_logger)
34
+ end
35
+ end
16
36
  end
17
37
 
18
38
  require "rails_config"
39
+ require "seed_builder/railtie" if defined?(Rails::Railtie)
@@ -0,0 +1,22 @@
1
+ namespace :seed do
2
+ desc "Run a specific seed file by name"
3
+ task :run, [:seed_name] => :environment do |_task, args|
4
+ if args[:seed_name].nil? || args[:seed_name].empty?
5
+ puts "Usage: bin/rails seed:run[SEED_NAME]"
6
+ puts ""
7
+ puts "Run a specific seed file by name."
8
+ puts ""
9
+ puts "Examples:"
10
+ puts " bin/rails seed:run[create_users]"
11
+ puts " bin/rails seed:run[20241206200111_create_users]"
12
+ puts " bin/rails seed:run[20241206200111]"
13
+ puts ""
14
+ puts "Note: If multiple seed files match the name, use the full name with"
15
+ puts " timestamp to avoid ambiguity (e.g., 20241206200111_create_users)."
16
+ exit 1
17
+ end
18
+
19
+ loader = SeedBuilder::Loader.new
20
+ loader.load_seed_file(args[:seed_name])
21
+ end
22
+ end
data/seed_builder.gemspec CHANGED
@@ -11,7 +11,7 @@ Gem::Specification.new do |gem|
11
11
  gem.platform = Gem::Platform::RUBY
12
12
 
13
13
  gem.authors = ["Andrei Makarov"]
14
- gem.email = ["andrei@kiskolabs.com"]
14
+ gem.email = ["contact@kiskolabs.com"]
15
15
  gem.homepage = repository_url
16
16
  gem.summary = "Seed builder with loader and generator"
17
17
  gem.description = "Extension for ActiveRecord to organize seeds in a directory and generate them as migrations"
@@ -23,15 +23,15 @@ Gem::Specification.new do |gem|
23
23
  "rubygems_mfa_required" => "true"
24
24
  }
25
25
 
26
- gem.files = `git ls-files`.split("\n")
26
+ gem.files = `git ls-files`.split("\n").reject { |f| f.match?(%r{^(test|spec|features)/}) }
27
27
  gem.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
28
28
  gem.executables = `git ls-files -- bin/*`.split("\n").map { |f| File.basename(f) }
29
29
 
30
30
  gem.required_ruby_version = ">= 3"
31
31
  gem.require_paths = ["lib"]
32
32
 
33
- gem.add_dependency "rails", ">= 6", "< 8.1"
34
- gem.add_dependency "activerecord", ">= 6", "< 8.1"
33
+ gem.add_dependency "rails", ">= 6.1", "< 8.2"
34
+ gem.add_dependency "activerecord", ">= 6.1", "< 8.2"
35
35
 
36
36
  gem.add_development_dependency "bundler", "~> 2"
37
37
  gem.add_development_dependency "rspec", "~> 3"
@@ -39,4 +39,7 @@ Gem::Specification.new do |gem|
39
39
  gem.add_development_dependency "simplecov", "~> 0.21"
40
40
  gem.add_development_dependency "simplecov-cobertura", "~> 2"
41
41
  gem.add_development_dependency "sqlite3", "~> 2.4"
42
+ gem.add_development_dependency "standard", "~> 1.0"
43
+ gem.add_development_dependency "rbs", "~> 3.0"
44
+ gem.add_development_dependency "appraisal", "~> 2.4"
42
45
  end
@@ -0,0 +1,22 @@
1
+ module SeedBuilder
2
+ class Config
3
+ def default_seeds_path: () -> String
4
+ def default_seeds_path=: (String) -> String
5
+ def default_seeds_full_path: () -> Pathname
6
+ def default_seeds_full_path=: (Pathname) -> Pathname
7
+ def seeds_path: () -> String
8
+ def seeds_path=: (String) -> String
9
+ def seeds_full_path: () -> Pathname
10
+ def seeds_full_path=: (Pathname) -> Pathname
11
+ def seeds_path_glob: () -> String
12
+ def load_default_seeds?: () -> bool
13
+ def load_default_seeds=: (bool) -> bool
14
+ def load_seeds?: () -> bool
15
+ def load_seeds=: (bool) -> bool
16
+ def generate_spec?: () -> bool
17
+ def generate_spec=: (bool) -> bool
18
+ def use_seed_loader?: () -> bool
19
+ def use_seed_loader=: (bool) -> bool
20
+ end
21
+ end
22
+
@@ -0,0 +1,8 @@
1
+ module SeedBuilder
2
+ class Loader
3
+ def initialize: () -> void
4
+ def load_seed: () -> void
5
+ def load_seed_file: (String seed_name) -> void
6
+ end
7
+ end
8
+
@@ -0,0 +1,5 @@
1
+ module SeedBuilder
2
+ class Railtie < ::Rails::Railtie
3
+ end
4
+ end
5
+
@@ -0,0 +1,7 @@
1
+ module SeedBuilder
2
+ VERSION: String
3
+
4
+ def self.config: () -> Config
5
+ def self.configure: () { (Config) -> void } -> void
6
+ end
7
+
@@ -1,5 +1,14 @@
1
1
  require "spec_helper"
2
2
 
3
+ # Define Rails module if not already defined
4
+ unless defined?(Rails)
5
+ module Rails
6
+ class << self
7
+ attr_accessor :root, :env, :logger
8
+ end
9
+ end
10
+ end
11
+
3
12
  describe SeedBuilder::Config do
4
13
  subject(:config) { described_class.new }
5
14
 
@@ -30,4 +39,10 @@ describe SeedBuilder::Config do
30
39
  it "has default seeds_path_glob" do
31
40
  expect(config.seeds_path_glob).to eq Rails.root.join("db/seeds/*.rb").to_s
32
41
  end
42
+
43
+ it "allows setting a custom logger" do
44
+ custom_logger = Logger.new($stdout)
45
+ config.logger = custom_logger
46
+ expect(config.logger).to eq custom_logger
47
+ end
33
48
  end
@@ -1,5 +1,14 @@
1
1
  require "spec_helper"
2
2
 
3
+ # Define Rails module if not already defined
4
+ unless defined?(Rails)
5
+ module Rails
6
+ class << self
7
+ attr_accessor :root, :env, :logger
8
+ end
9
+ end
10
+ end
11
+
3
12
  class SeedBuilderUser < ActiveRecord::Base; end
4
13
 
5
14
  class SeedUser < SeedBuilderUser; end
@@ -7,29 +16,284 @@ class SeedUser < SeedBuilderUser; end
7
16
  describe SeedBuilder::Loader do
8
17
  subject(:loader) { described_class.new }
9
18
 
10
- describe "#load_seed" do
11
- let(:load_default_seeds) { true }
12
- let(:load_seeds) { true }
13
- let(:logger) { Logger.new($stdout) }
19
+ let(:logger) { Logger.new($stdout) }
20
+ let(:seeds_path) { File.expand_path("../../fixtures/seeds", __FILE__) }
21
+ let(:default_seeds_path) { File.expand_path("../../fixtures/seeds.rb", __FILE__) }
22
+
23
+ before do
24
+ allow(Rails).to receive(:root).and_return(rails_root)
25
+ allow(Rails).to receive(:env).and_return(rails_env)
26
+ allow(Rails).to receive(:logger).and_return(logger)
27
+ allow(Rails).to receive(:logger=)
28
+ # Reset SeedBuilder logger to pick up the stubbed Rails.logger
29
+ SeedBuilder.instance_variable_set(:@logger, nil) if SeedBuilder.instance_variable_defined?(:@logger)
14
30
 
31
+ SeedBuilder.configure do |config|
32
+ config.seeds_full_path = seeds_path
33
+ config.default_seeds_full_path = default_seeds_path
34
+ config.load_default_seeds = true
35
+ config.load_seeds = true
36
+ end
37
+ end
38
+
39
+ describe "#load_seed" do
15
40
  before do
16
- allow(Rails).to receive(:root).and_return(rails_root)
17
- allow(Rails).to receive(:env).and_return(rails_env)
18
- allow(Rails).to receive(:logger).and_return(logger)
19
- allow(Rails).to receive(:logger=)
41
+ SeedUser.delete_all
42
+ SeedBuilderUser.delete_all
43
+ end
44
+
45
+ context "when loading default seeds" do
46
+ let(:load_default_seeds) { true }
47
+ let(:load_seeds) { true }
20
48
 
21
- SeedBuilder.configure do |config|
22
- config.seeds_full_path = File.expand_path("../../fixtures/seeds", __FILE__)
23
- config.default_seeds_full_path = File.expand_path("../../fixtures/seeds.rb", __FILE__)
24
- config.load_default_seeds = load_default_seeds
25
- config.load_seeds = load_seeds
49
+ before do
50
+ SeedBuilder.configure do |config|
51
+ config.load_default_seeds = load_default_seeds
52
+ config.load_seeds = load_seeds
53
+ end
54
+ loader.load_seed
55
+ end
56
+
57
+ it "loads the seeds" do
58
+ expect(SeedUser.count).to eq 1
59
+ expect(SeedBuilderUser.count).to eq 1
26
60
  end
27
- loader.load_seed
28
61
  end
29
62
 
30
- it "loads the seeds" do
31
- expect(SeedUser.count).to eq 1
32
- expect(SeedBuilderUser.count).to eq 1
63
+ context "when seeds directory does not exist" do
64
+ let(:log_output) { StringIO.new }
65
+ let(:test_logger) { Logger.new(log_output) }
66
+
67
+ before do
68
+ allow(Rails).to receive(:logger).and_return(test_logger)
69
+ # Reset SeedBuilder logger to pick up the stubbed Rails.logger
70
+ SeedBuilder.instance_variable_set(:@logger, nil) if SeedBuilder.instance_variable_defined?(:@logger)
71
+ SeedBuilder.configure do |config|
72
+ config.seeds_full_path = "/non/existent/path"
73
+ config.load_default_seeds = false
74
+ config.load_seeds = true
75
+ end
76
+ end
77
+
78
+ it "outputs a message" do
79
+ loader.load_seed
80
+ expect(log_output.string).to match(/Seed directory.*does not exist/)
81
+ end
82
+ end
83
+ end
84
+
85
+ describe "#load_seed_file" do
86
+ context "when loading by class name" do
87
+ before do
88
+ SeedBuilderUser.delete_all
89
+ loader.load_seed_file("create_users")
90
+ end
91
+
92
+ it "loads the specific seed file" do
93
+ expect(SeedBuilderUser.count).to eq 1
94
+ expect(SeedBuilderUser.first.email).to eq "test@example.com"
95
+ end
96
+ end
97
+
98
+ context "when loading by full name" do
99
+ before do
100
+ SeedBuilderUser.delete_all
101
+ loader.load_seed_file("20241206200111_create_users")
102
+ end
103
+
104
+ it "loads the specific seed file" do
105
+ expect(SeedBuilderUser.count).to eq 1
106
+ end
107
+ end
108
+
109
+ context "when loading by timestamp" do
110
+ before do
111
+ SeedBuilderUser.delete_all
112
+ loader.load_seed_file("20241206200111")
113
+ end
114
+
115
+ it "loads the specific seed file" do
116
+ expect(SeedBuilderUser.count).to eq 1
117
+ end
118
+ end
119
+
120
+ context "when seed file does not exist" do
121
+ let(:log_output) { StringIO.new }
122
+ let(:test_logger) { Logger.new(log_output) }
123
+
124
+ before do
125
+ allow(Rails).to receive(:logger).and_return(test_logger)
126
+ # Reset SeedBuilder logger to pick up the stubbed Rails.logger
127
+ SeedBuilder.instance_variable_set(:@logger, nil) if SeedBuilder.instance_variable_defined?(:@logger)
128
+ end
129
+
130
+ it "outputs an error message" do
131
+ loader.load_seed_file("nonexistent_seed")
132
+ expect(log_output.string).to match(/Seed file 'nonexistent_seed' not found/)
133
+ end
134
+
135
+ it "does not raise an error" do
136
+ expect { loader.load_seed_file("nonexistent_seed") }.not_to raise_error
137
+ end
138
+ end
139
+
140
+ context "when multiple seed files match the name" do
141
+ let(:seed_path_1) { File.expand_path("../../tmp/rspec/db/seeds/20241206200111_create_users.rb", __FILE__) }
142
+ let(:seed_path_2) { File.expand_path("../../tmp/rspec/db/seeds/20241206200112_create_users.rb", __FILE__) }
143
+ let(:log_output) { StringIO.new }
144
+ let(:test_logger) { Logger.new(log_output) }
145
+
146
+ before do
147
+ allow(Rails).to receive(:logger).and_return(test_logger)
148
+ # Reset SeedBuilder logger to pick up the stubbed Rails.logger
149
+ SeedBuilder.instance_variable_set(:@logger, nil) if SeedBuilder.instance_variable_defined?(:@logger)
150
+ FileUtils.mkdir_p(File.dirname(seed_path_1))
151
+ File.write(seed_path_1, "class CreateUsers; def change; end; end")
152
+ File.write(seed_path_2, "class CreateUsers; def change; end; end")
153
+ SeedBuilder.configure do |config|
154
+ config.seeds_full_path = File.dirname(seed_path_1)
155
+ end
156
+ end
157
+
158
+ after do
159
+ File.delete(seed_path_1) if File.exist?(seed_path_1)
160
+ File.delete(seed_path_2) if File.exist?(seed_path_2)
161
+ end
162
+
163
+ it "outputs an error message listing all matching files" do
164
+ loader.load_seed_file("create_users")
165
+ expect(log_output.string).to match(
166
+ /Multiple seed files match 'create_users':.*20241206200111_create_users.*20241206200112_create_users.*Please be more specific/m
167
+ )
168
+ end
169
+
170
+ it "does not raise an error" do
171
+ expect { loader.load_seed_file("create_users") }.not_to raise_error
172
+ end
173
+
174
+ it "works when using the full name with timestamp" do
175
+ SeedBuilderUser.delete_all
176
+ loader.load_seed_file("20241206200111_create_users")
177
+ expect(log_output.string).not_to match(/Multiple seed files/)
178
+ expect(SeedBuilderUser.count).to eq 0 # The seed doesn't actually create anything, just checks it runs
179
+ end
180
+ end
181
+
182
+ context "when seeds directory does not exist" do
183
+ let(:log_output) { StringIO.new }
184
+ let(:test_logger) { Logger.new(log_output) }
185
+
186
+ before do
187
+ allow(Rails).to receive(:logger).and_return(test_logger)
188
+ # Reset SeedBuilder logger to pick up the stubbed Rails.logger
189
+ SeedBuilder.instance_variable_set(:@logger, nil) if SeedBuilder.instance_variable_defined?(:@logger)
190
+ SeedBuilder.configure do |config|
191
+ config.seeds_full_path = "/non/existent/path"
192
+ end
193
+ end
194
+
195
+ it "outputs an error message" do
196
+ loader.load_seed_file("create_users")
197
+ expect(log_output.string).to match(/Seed directory.*does not exist/)
198
+ end
199
+
200
+ it "does not raise an error" do
201
+ expect { loader.load_seed_file("create_users") }.not_to raise_error
202
+ end
203
+ end
204
+
205
+ context "when seed file has invalid format" do
206
+ let(:invalid_seed_path) { File.expand_path("../../tmp/rspec/invalid_seed.rb", __FILE__) }
207
+ let(:log_output) { StringIO.new }
208
+ let(:test_logger) { Logger.new(log_output) }
209
+
210
+ before do
211
+ allow(Rails).to receive(:logger).and_return(test_logger)
212
+ # Reset SeedBuilder logger to pick up the stubbed Rails.logger
213
+ SeedBuilder.instance_variable_set(:@logger, nil) if SeedBuilder.instance_variable_defined?(:@logger)
214
+ FileUtils.mkdir_p(File.dirname(invalid_seed_path))
215
+ File.write(invalid_seed_path, "class InvalidSeed; end")
216
+ SeedBuilder.configure do |config|
217
+ config.seeds_full_path = File.dirname(invalid_seed_path)
218
+ end
219
+ end
220
+
221
+ after do
222
+ File.delete(invalid_seed_path) if File.exist?(invalid_seed_path)
223
+ end
224
+
225
+ it "outputs an error message" do
226
+ loader.load_seed_file("invalid_seed")
227
+ expect(log_output.string).to match(/Invalid seed file format/)
228
+ end
229
+
230
+ it "does not raise an error" do
231
+ expect { loader.load_seed_file("invalid_seed") }.not_to raise_error
232
+ end
233
+ end
234
+
235
+ context "when seed class does not respond to change" do
236
+ let(:invalid_seed_path) { File.expand_path("../../tmp/rspec/db/seeds/20241206200112_no_change.rb", __FILE__) }
237
+
238
+ before do
239
+ FileUtils.mkdir_p(File.dirname(invalid_seed_path))
240
+ File.write(invalid_seed_path, <<~RUBY)
241
+ class NoChange
242
+ # Does not have change method
243
+ end
244
+ RUBY
245
+ SeedBuilder.configure do |config|
246
+ config.seeds_full_path = File.dirname(invalid_seed_path)
247
+ end
248
+ end
249
+
250
+ after do
251
+ File.delete(invalid_seed_path) if File.exist?(invalid_seed_path)
252
+ end
253
+
254
+ it "raises an error" do
255
+ expect { loader.load_seed_file("no_change") }.to raise_error(/does not respond to :change/)
256
+ end
257
+ end
258
+
259
+ context "when seed raises RecordInvalid" do
260
+ let(:invalid_seed_path) { File.expand_path("../../tmp/rspec/db/seeds/20241206200113_invalid_record.rb", __FILE__) }
261
+ let(:log_output) { StringIO.new }
262
+ let(:test_logger) { Logger.new(log_output) }
263
+
264
+ before do
265
+ allow(Rails).to receive(:logger).and_return(test_logger)
266
+ # Reset SeedBuilder logger to pick up the stubbed Rails.logger
267
+ SeedBuilder.instance_variable_set(:@logger, nil) if SeedBuilder.instance_variable_defined?(:@logger)
268
+ # Add a validation to SeedBuilderUser to make save! fail
269
+ SeedBuilderUser.class_eval do
270
+ validates :email, presence: true
271
+ end
272
+
273
+ FileUtils.mkdir_p(File.dirname(invalid_seed_path))
274
+ File.write(invalid_seed_path, <<~RUBY)
275
+ class InvalidRecord
276
+ def change
277
+ user = SeedBuilderUser.new # Missing required email
278
+ user.save! # This will fail validation
279
+ end
280
+ end
281
+ RUBY
282
+ SeedBuilder.configure do |config|
283
+ config.seeds_full_path = File.dirname(invalid_seed_path)
284
+ end
285
+ end
286
+
287
+ after do
288
+ File.delete(invalid_seed_path) if File.exist?(invalid_seed_path)
289
+ # Remove the validation
290
+ SeedBuilderUser.reset_column_information
291
+ end
292
+
293
+ it "outputs error message and re-raises" do
294
+ expect { loader.load_seed_file("invalid_record") }.to raise_error(ActiveRecord::RecordInvalid)
295
+ expect(log_output.string).to match(/Seeding.*failed/)
296
+ end
33
297
  end
34
298
  end
35
299
  end