Skip to content

Commit 9f9bd53

Browse files
MatheusRichclaude
andcommitted
Add class-level progress macro for Migration
Lets users declare `progress eta: true` once at the class level instead of passing `eta: true` on every `report` call. The `eta` option moves from ProgressReporter#report to the constructor, and Migration forwards class-level options automatically. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent e702505 commit 9f9bd53

4 files changed

Lines changed: 70 additions & 13 deletions

File tree

README.md

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -180,10 +180,26 @@ end
180180
It accepts a percentage (0–100) and throttles output, so it's safe to call on
181181
every iteration. At 100%, it shows the total elapsed time.
182182

183-
Pass `eta: true` to show estimated time remaining:
183+
Use `progress eta: true` at the class level to show estimated time remaining:
184184

185185
```ruby
186-
progress.report(processed.to_f / total * 100, eta: true)
186+
class BackfillUsernames < DataCustoms::Migration
187+
progress eta: true
188+
189+
def up
190+
scope = User.where(username: nil)
191+
total = scope.count
192+
processed = 0
193+
194+
find_each(scope) do |user|
195+
user.update!(username: "guest_#{user.id}")
196+
processed += 1
197+
progress.report(processed.to_f / total * 100)
198+
end
199+
end
200+
201+
# ...
202+
end
187203
```
188204

189205
```

lib/data_customs/migration.rb

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,14 @@ class Migration
55
DEFAULT_BATCH_SIZE = 1000
66
DEFAULT_THROTTLE = 0.01
77

8+
def self.progress(**options)
9+
@progress_options = options
10+
end
11+
12+
def self.progress_options
13+
@progress_options || {}
14+
end
15+
816
def self.run(...)
917
ActiveRecord::Base.transaction do
1018
new(...).run
@@ -29,7 +37,7 @@ def run
2937

3038
def with_progress_reporter(&block)
3139
ProgressOutput.wrap do |output|
32-
@_progress = ProgressReporter.new(output)
40+
@_progress = ProgressReporter.new(output, **self.class.progress_options)
3341
block.call
3442
end
3543
end

lib/data_customs/progress_reporter.rb

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,13 @@ class ProgressReporter
66
PRINT_INTERVAL = 1 # seconds
77
ETA_MIN_ELAPSED = 2 # seconds before showing ETA
88

9-
def initialize(output)
9+
def initialize(output, eta: false)
1010
@output = ThrottledOutput.new(output, interval: PRINT_INTERVAL)
1111
@started_at = Process.clock_gettime(Process::CLOCK_MONOTONIC)
12+
@eta = eta
1213
end
1314

14-
def report(percentage, eta: false)
15+
def report(percentage)
1516
percentage = percentage.floor.clamp(0, 100)
1617
line = bar(percentage)
1718

@@ -22,7 +23,7 @@ def report(percentage, eta: false)
2223
return
2324
end
2425

25-
if eta && percentage > 0
26+
if @eta && percentage > 0
2627
elapsed = Process.clock_gettime(Process::CLOCK_MONOTONIC) - @started_at
2728
if elapsed >= ETA_MIN_ELAPSED
2829
remaining = elapsed / percentage * (100 - percentage)

spec/data_customs/migration_spec.rb

Lines changed: 39 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,11 @@ def verify! = nil
132132
end
133133

134134
it "shows 'estimating...' before enough time has elapsed" do
135-
migration = build_migration { progress.report(50, eta: true) }
135+
migration = Class.new(DataCustoms::Migration) do
136+
progress eta: true
137+
define_method(:up) { progress.report(50) }
138+
def verify! = nil
139+
end
136140

137141
expect { migration.run }.to output(
138142
/Progress: .+ 50% \(estimating\.\.\.\)\n/
@@ -144,9 +148,13 @@ def verify! = nil
144148
# ProgressReporter.new, report(25) eta + throttle, report(50) eta + throttle
145149
allow(Process).to receive(:clock_gettime).and_return(now, now, now, now + 5.0, now + 5.0)
146150

147-
migration = build_migration do
148-
progress.report(25, eta: true)
149-
progress.report(50, eta: true)
151+
migration = Class.new(DataCustoms::Migration) do
152+
progress eta: true
153+
define_method(:up) do
154+
progress.report(25)
155+
progress.report(50)
156+
end
157+
def verify! = nil
150158
end
151159

152160
expect { migration.run }.to output(
@@ -170,9 +178,13 @@ def verify! = nil
170178
end
171179

172180
it "does not show ETA at 0% or 100%" do
173-
migration = build_migration do
174-
progress.report(0, eta: true)
175-
progress.report(100, eta: true)
181+
migration = Class.new(DataCustoms::Migration) do
182+
progress eta: true
183+
define_method(:up) do
184+
progress.report(0)
185+
progress.report(100)
186+
end
187+
def verify! = nil
176188
end
177189

178190
expect { migration.run }.to output(
@@ -183,6 +195,26 @@ def verify! = nil
183195
end
184196
end
185197

198+
describe ".progress" do
199+
it "configures ETA at the class level" do
200+
now = Process.clock_gettime(Process::CLOCK_MONOTONIC)
201+
allow(Process).to receive(:clock_gettime).and_return(now, now, now, now + 5.0, now + 5.0)
202+
203+
migration = Class.new(DataCustoms::Migration) do
204+
progress eta: true
205+
define_method(:up) do
206+
progress.report(25)
207+
progress.report(50)
208+
end
209+
def verify! = nil
210+
end
211+
212+
expect { migration.run }.to output(
213+
/Progress: .+ 50% \(\d+s left\)\n/
214+
).to_stdout
215+
end
216+
end
217+
186218
describe "helpers" do
187219
it "batches records" do
188220
3.times { |i| TestUser.create!(name: "User #{i}") }

0 commit comments

Comments
 (0)