tapsoob 0.8.5 → 0.8.7
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.
- checksums.yaml +4 -4
- data/lib/tapsoob/cli/schema.rb +1 -1
- data/lib/tapsoob/schema.rb +33 -10
- data/lib/tapsoob/version.rb +1 -1
- data/spec/integration/postgres_spec.rb +12 -0
- data/spec/spec_helper.rb +3 -2
- data/spec/support/operation_helpers.rb +43 -0
- data/spec/unit/tapsoob/base_spec.rb +222 -0
- data/spec/unit/tapsoob/cli_pipeline_spec.rb +380 -0
- data/spec/unit/tapsoob/config_spec.rb +54 -0
- data/spec/unit/tapsoob/data_stream_spec.rb +48 -0
- data/spec/unit/tapsoob/file_partition_spec.rb +117 -0
- data/spec/unit/tapsoob/keyed_spec.rb +121 -0
- data/spec/unit/tapsoob/progress_event_spec.rb +136 -0
- data/spec/unit/tapsoob/progress_spec.rb +335 -0
- data/spec/unit/tapsoob/pull_spec.rb +335 -0
- data/spec/unit/tapsoob/push_spec.rb +264 -0
- data/spec/unit/tapsoob/schema_spec.rb +154 -0
- data/spec/unit/tapsoob/utils_spec.rb +64 -0
- metadata +11 -1
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
2
|
+
require 'tapsoob/data_stream/keyed'
|
|
3
|
+
|
|
4
|
+
Sequel.extension :core_extensions
|
|
5
|
+
|
|
6
|
+
RSpec.describe Tapsoob::DataStream::Keyed do
|
|
7
|
+
let(:db) do
|
|
8
|
+
d = connect_sqlite
|
|
9
|
+
d.extension :schema_dumper
|
|
10
|
+
d.create_table(:keyed_items) { primary_key :id; String :label }
|
|
11
|
+
10.times { |i| d[:keyed_items].insert(label: "item_#{i}") }
|
|
12
|
+
d
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
after { db.disconnect }
|
|
16
|
+
|
|
17
|
+
subject(:stream) { described_class.new(db, { table_name: :keyed_items, chunksize: 4 }) }
|
|
18
|
+
|
|
19
|
+
# ── primary_key / buffer_limit ───────────────────────────────────────────────
|
|
20
|
+
|
|
21
|
+
describe '#primary_key' do
|
|
22
|
+
it 'returns :id for this table' do
|
|
23
|
+
expect(stream.primary_key).to eq(:id)
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
describe '#buffer_limit' do
|
|
28
|
+
it 'returns filter when buffer is non-empty' do
|
|
29
|
+
stream.state[:filter] = 5
|
|
30
|
+
stream.buffer << { id: 1 }
|
|
31
|
+
expect(stream.buffer_limit).to eq(5)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
it 'returns last_fetched when it is less than filter and buffer is empty' do
|
|
35
|
+
stream.state[:filter] = 10
|
|
36
|
+
stream.state[:last_fetched] = 3
|
|
37
|
+
stream.buffer.clear
|
|
38
|
+
expect(stream.buffer_limit).to eq(3)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
it 'returns filter when last_fetched >= filter' do
|
|
42
|
+
stream.state[:filter] = 5
|
|
43
|
+
stream.state[:last_fetched] = 5
|
|
44
|
+
stream.buffer.clear
|
|
45
|
+
expect(stream.buffer_limit).to eq(5)
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# ── calc_limit ───────────────────────────────────────────────────────────────
|
|
50
|
+
|
|
51
|
+
describe '#calc_limit' do
|
|
52
|
+
it 'returns chunksize * 3 outside Sinatra' do
|
|
53
|
+
expect(stream.calc_limit(100)).to eq(300)
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# ── load_buffer ──────────────────────────────────────────────────────────────
|
|
58
|
+
|
|
59
|
+
describe '#load_buffer' do
|
|
60
|
+
it 'loads rows into the buffer' do
|
|
61
|
+
stream.load_buffer(4)
|
|
62
|
+
expect(stream.buffer.size).to be >= 4
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
it 'updates state[:filter] to the last PK in the buffer' do
|
|
66
|
+
stream.load_buffer(4)
|
|
67
|
+
expect(stream.state[:filter]).to eq(stream.buffer.last[:id])
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
it 'stops early when the table is exhausted' do
|
|
71
|
+
stream.load_buffer(1000)
|
|
72
|
+
expect(stream.buffer.size).to eq(10)
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# ── fetch_buffered ───────────────────────────────────────────────────────────
|
|
77
|
+
|
|
78
|
+
describe '#fetch_buffered' do
|
|
79
|
+
it 'returns up to chunksize rows' do
|
|
80
|
+
rows = stream.fetch_buffered(4)
|
|
81
|
+
expect(rows.size).to eq(4)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
it 'records last_fetched as the PK of the last returned row' do
|
|
85
|
+
rows = stream.fetch_buffered(4)
|
|
86
|
+
expect(stream.state[:last_fetched]).to eq(rows.last[:id])
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
it 'returns nil last_fetched when no rows available' do
|
|
90
|
+
# drain the table
|
|
91
|
+
stream.load_buffer(100)
|
|
92
|
+
stream.buffer.clear
|
|
93
|
+
rows = stream.fetch_buffered(4)
|
|
94
|
+
expect(rows).to be_empty
|
|
95
|
+
expect(stream.state[:last_fetched]).to be_nil
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# ── increment ────────────────────────────────────────────────────────────────
|
|
100
|
+
|
|
101
|
+
describe '#increment' do
|
|
102
|
+
it 'removes n rows from the front of the buffer' do
|
|
103
|
+
stream.load_buffer(6)
|
|
104
|
+
original_fourth = stream.buffer[3]
|
|
105
|
+
stream.increment(3)
|
|
106
|
+
expect(stream.buffer.first).to eq(original_fourth)
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# ── verify_stream ────────────────────────────────────────────────────────────
|
|
111
|
+
|
|
112
|
+
describe '#verify_stream' do
|
|
113
|
+
it 'sets filter to the max PK and clears last_fetched' do
|
|
114
|
+
stream.state[:last_fetched] = 3
|
|
115
|
+
stream.verify_stream
|
|
116
|
+
max_id = db[:keyed_items].max(:id)
|
|
117
|
+
expect(stream.state[:filter]).to eq(max_id)
|
|
118
|
+
expect(stream.state[:last_fetched]).to be_nil
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
2
|
+
require 'tapsoob/progress_event'
|
|
3
|
+
|
|
4
|
+
RSpec.describe Tapsoob::ProgressEvent do
|
|
5
|
+
before do
|
|
6
|
+
described_class.enabled = true
|
|
7
|
+
# Reset throttle state between examples
|
|
8
|
+
described_class.instance_variable_set(:@last_progress_time, {})
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
after { described_class.enabled = false }
|
|
12
|
+
|
|
13
|
+
# ── enabled? ─────────────────────────────────────────────────────────────────
|
|
14
|
+
|
|
15
|
+
describe '.enabled?' do
|
|
16
|
+
it 'reflects the value set by .enabled=' do
|
|
17
|
+
described_class.enabled = false
|
|
18
|
+
expect(described_class.enabled?).to be false
|
|
19
|
+
described_class.enabled = true
|
|
20
|
+
expect(described_class.enabled?).to be true
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# ── emit ─────────────────────────────────────────────────────────────────────
|
|
25
|
+
|
|
26
|
+
describe '.emit' do
|
|
27
|
+
it 'writes a PROGRESS: JSON line to STDERR when enabled' do
|
|
28
|
+
err = StringIO.new
|
|
29
|
+
stub_const('STDERR', err)
|
|
30
|
+
described_class.emit('test_event', foo: 'bar')
|
|
31
|
+
output = err.string
|
|
32
|
+
expect(output).to include('PROGRESS:')
|
|
33
|
+
data = JSON.parse(output.sub('PROGRESS: ', ''))
|
|
34
|
+
expect(data['event']).to eq('test_event')
|
|
35
|
+
expect(data['foo']).to eq('bar')
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
it 'does nothing when disabled' do
|
|
39
|
+
described_class.enabled = false
|
|
40
|
+
err = StringIO.new
|
|
41
|
+
stub_const('STDERR', err)
|
|
42
|
+
described_class.emit('ignored')
|
|
43
|
+
expect(err.string).to be_empty
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# ── throttle helpers ─────────────────────────────────────────────────────────
|
|
48
|
+
|
|
49
|
+
describe '.should_emit_progress?' do
|
|
50
|
+
it 'returns true on the first call for a table' do
|
|
51
|
+
expect(described_class.should_emit_progress?(:my_table)).to be true
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
it 'returns false when called again immediately' do
|
|
55
|
+
described_class.should_emit_progress?(:my_table)
|
|
56
|
+
expect(described_class.should_emit_progress?(:my_table)).to be false
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
describe '.clear_throttle' do
|
|
61
|
+
it 'resets state so the next call returns true' do
|
|
62
|
+
described_class.should_emit_progress?(:my_table)
|
|
63
|
+
described_class.clear_throttle(:my_table)
|
|
64
|
+
expect(described_class.should_emit_progress?(:my_table)).to be true
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# ── high-level event helpers ──────────────────────────────────────────────────
|
|
69
|
+
|
|
70
|
+
{
|
|
71
|
+
schema_start: [3],
|
|
72
|
+
schema_complete: [3],
|
|
73
|
+
data_start: [3, 100],
|
|
74
|
+
data_complete: [3, 100],
|
|
75
|
+
indexes_start: [3],
|
|
76
|
+
indexes_complete: [3],
|
|
77
|
+
sequences_start: [],
|
|
78
|
+
sequences_complete: [],
|
|
79
|
+
}.each do |method, args|
|
|
80
|
+
describe ".#{method}" do
|
|
81
|
+
it 'does not raise' do
|
|
82
|
+
expect { described_class.send(method, *args) }.not_to raise_error
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
describe '.table_start' do
|
|
88
|
+
it 'resets throttle and emits without error' do
|
|
89
|
+
expect { described_class.table_start(:users, 500, workers: 2) }.not_to raise_error
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
describe '.table_progress' do
|
|
94
|
+
it 'emits when throttle allows' do
|
|
95
|
+
described_class.clear_throttle(:users)
|
|
96
|
+
err = StringIO.new
|
|
97
|
+
stub_const('STDERR', err)
|
|
98
|
+
described_class.table_progress(:users, 50, 200)
|
|
99
|
+
expect(err.string).to include('table_progress')
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
it 'skips emission when throttled' do
|
|
103
|
+
described_class.should_emit_progress?(:users) # consume the token
|
|
104
|
+
err = StringIO.new
|
|
105
|
+
stub_const('STDERR', err)
|
|
106
|
+
described_class.table_progress(:users, 50, 200)
|
|
107
|
+
expect(err.string).to be_empty
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
it 'emits 0% when total is 0' do
|
|
111
|
+
described_class.clear_throttle(:empty_table)
|
|
112
|
+
err = StringIO.new
|
|
113
|
+
stub_const('STDERR', err)
|
|
114
|
+
described_class.table_progress(:empty_table, 0, 0)
|
|
115
|
+
data = JSON.parse(err.string.sub('PROGRESS: ', '').strip)
|
|
116
|
+
expect(data['percentage']).to eq(0)
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
describe '.table_complete' do
|
|
121
|
+
it 'clears throttle and emits without error' do
|
|
122
|
+
expect { described_class.table_complete(:users, 500) }.not_to raise_error
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
describe '.error' do
|
|
127
|
+
it 'emits an error event with the message' do
|
|
128
|
+
err = StringIO.new
|
|
129
|
+
stub_const('STDERR', err)
|
|
130
|
+
described_class.error('something broke', table: 'users')
|
|
131
|
+
data = JSON.parse(err.string.sub('PROGRESS: ', '').strip)
|
|
132
|
+
expect(data['event']).to eq('error')
|
|
133
|
+
expect(data['message']).to eq('something broke')
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
end
|
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
2
|
+
require 'tapsoob/progress'
|
|
3
|
+
|
|
4
|
+
RSpec.describe Tapsoob::Progress do
|
|
5
|
+
# All output is redirected to StringIO so nothing hits stdout/terminal.
|
|
6
|
+
|
|
7
|
+
let(:out) { StringIO.new }
|
|
8
|
+
|
|
9
|
+
# ── Bar ──────────────────────────────────────────────────────────────────────
|
|
10
|
+
|
|
11
|
+
describe Tapsoob::Progress::Bar do
|
|
12
|
+
subject(:bar) { described_class.new("Test", 100, out) }
|
|
13
|
+
|
|
14
|
+
describe '#initialize' do
|
|
15
|
+
it 'starts at 0' do
|
|
16
|
+
expect(bar.current).to eq(0)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
it 'stores total' do
|
|
20
|
+
expect(bar.total).to eq(100)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
it 'stores title' do
|
|
24
|
+
expect(bar.title).to eq("Test")
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
describe '#inc' do
|
|
29
|
+
it 'advances current by 1 by default' do
|
|
30
|
+
bar.inc
|
|
31
|
+
expect(bar.current).to eq(1)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
it 'advances current by a custom step' do
|
|
35
|
+
bar.inc(10)
|
|
36
|
+
expect(bar.current).to eq(10)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
it 'clamps current to total' do
|
|
40
|
+
bar.inc(200)
|
|
41
|
+
expect(bar.current).to eq(100)
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
describe '#set' do
|
|
46
|
+
it 'sets current to the given value' do
|
|
47
|
+
bar.set(42)
|
|
48
|
+
expect(bar.current).to eq(42)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
it 'raises on negative value' do
|
|
52
|
+
expect { bar.set(-1) }.to raise_error(RuntimeError)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
it 'raises when value exceeds total' do
|
|
56
|
+
expect { bar.set(101) }.to raise_error(RuntimeError)
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
describe '#finish' do
|
|
61
|
+
it 'sets current to total and marks finished' do
|
|
62
|
+
bar.finish
|
|
63
|
+
expect(bar.current).to eq(100)
|
|
64
|
+
expect(bar.finished?).to be true
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
describe '#halt' do
|
|
69
|
+
it 'marks finished without changing current' do
|
|
70
|
+
bar.inc(30)
|
|
71
|
+
bar.halt
|
|
72
|
+
expect(bar.finished?).to be true
|
|
73
|
+
expect(bar.current).to eq(30)
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
describe '#inspect' do
|
|
78
|
+
it 'includes current/total' do
|
|
79
|
+
expect(bar.inspect).to include('0/100')
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
describe '#file_transfer_mode' do
|
|
84
|
+
it 'switches format_arguments to stat_for_file_transfer' do
|
|
85
|
+
bar.file_transfer_mode
|
|
86
|
+
expect(bar.instance_variable_get(:@format_arguments)).to include(:stat_for_file_transfer)
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
describe '#format=' do
|
|
91
|
+
it 'updates the format string' do
|
|
92
|
+
bar.format = "custom %s"
|
|
93
|
+
expect(bar.instance_variable_get(:@format)).to eq("custom %s")
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
describe '#format_arguments=' do
|
|
98
|
+
it 'updates format arguments' do
|
|
99
|
+
bar.format_arguments = [:title, :percentage]
|
|
100
|
+
expect(bar.instance_variable_get(:@format_arguments)).to eq([:title, :percentage])
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
describe 'convert_bytes (via fmt_stat_for_file_transfer)' do
|
|
105
|
+
it 'renders correctly in file_transfer_mode after finishing' do
|
|
106
|
+
bar.file_transfer_mode
|
|
107
|
+
bar.inc(50)
|
|
108
|
+
bar.finish
|
|
109
|
+
expect(out.string).not_to be_empty
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
it 'renders KB range correctly' do
|
|
113
|
+
b = described_class.new("KB", 2048, out)
|
|
114
|
+
b.file_transfer_mode
|
|
115
|
+
b.inc(2048)
|
|
116
|
+
b.finish
|
|
117
|
+
expect(out.string).to match(/KB/)
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
it 'renders MB range correctly' do
|
|
121
|
+
b = described_class.new("MB", 2 * 1024 * 1024, out)
|
|
122
|
+
b.file_transfer_mode
|
|
123
|
+
b.inc(2 * 1024 * 1024)
|
|
124
|
+
b.finish
|
|
125
|
+
expect(out.string).to match(/MB/)
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
describe 'zero total' do
|
|
130
|
+
subject(:bar) { described_class.new("Empty", 0, out) }
|
|
131
|
+
|
|
132
|
+
it 'does not raise on init' do
|
|
133
|
+
expect { bar }.not_to raise_error
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
it 'finish does not raise' do
|
|
137
|
+
expect { bar.finish }.not_to raise_error
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# ── ReversedBar ──────────────────────────────────────────────────────────────
|
|
143
|
+
|
|
144
|
+
describe Tapsoob::Progress::ReversedBar do
|
|
145
|
+
subject(:bar) { described_class.new("Rev", 100, out) }
|
|
146
|
+
|
|
147
|
+
it 'inherits from Bar' do
|
|
148
|
+
expect(bar).to be_a(Tapsoob::Progress::Bar)
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
it 'reports inverted percentage internally (100 at start)' do
|
|
152
|
+
# do_percentage is private; test via inspect indirectly - just ensure no raise
|
|
153
|
+
expect { bar.inc(10) }.not_to raise_error
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
# ── ThreadSafeBar ────────────────────────────────────────────────────────────
|
|
158
|
+
|
|
159
|
+
describe Tapsoob::Progress::ThreadSafeBar do
|
|
160
|
+
let(:multi) do
|
|
161
|
+
mb = instance_double(Tapsoob::Progress::MultiBar)
|
|
162
|
+
allow(mb).to receive(:update)
|
|
163
|
+
allow(mb).to receive(:finish_bar)
|
|
164
|
+
allow(mb).to receive(:max_title_width).and_return(14)
|
|
165
|
+
mb
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
subject(:bar) { described_class.new("Worker", 50, multi) }
|
|
169
|
+
|
|
170
|
+
describe '#initialize' do
|
|
171
|
+
it 'stores title and total' do
|
|
172
|
+
expect(bar.title).to eq("Worker")
|
|
173
|
+
expect(bar.total).to eq(50)
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
describe '#inc' do
|
|
178
|
+
it 'increments current and notifies multi' do
|
|
179
|
+
expect(multi).to receive(:update)
|
|
180
|
+
bar.inc(5)
|
|
181
|
+
expect(bar.current).to eq(5)
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
describe '#finish' do
|
|
186
|
+
it 'sets current to total and calls finish_bar on multi' do
|
|
187
|
+
expect(multi).to receive(:finish_bar).with(bar)
|
|
188
|
+
bar.finish
|
|
189
|
+
expect(bar.current).to eq(50)
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
describe '#mark_finished / #finished?' do
|
|
194
|
+
it 'returns true after mark_finished' do
|
|
195
|
+
bar.mark_finished
|
|
196
|
+
expect(bar.finished?).to be true
|
|
197
|
+
end
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
describe '#render_to' do
|
|
201
|
+
it 'writes something to the given output' do
|
|
202
|
+
io = StringIO.new
|
|
203
|
+
bar.render_to(io)
|
|
204
|
+
expect(io.string).not_to be_empty
|
|
205
|
+
end
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
describe '#clear' do
|
|
209
|
+
it 'is a no-op (does not raise)' do
|
|
210
|
+
expect { bar.send(:clear) }.not_to raise_error
|
|
211
|
+
end
|
|
212
|
+
end
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
# ── MultiBar ─────────────────────────────────────────────────────────────────
|
|
216
|
+
|
|
217
|
+
describe Tapsoob::Progress::MultiBar do
|
|
218
|
+
subject(:multi) { described_class.new(2) }
|
|
219
|
+
|
|
220
|
+
describe '#initialize' do
|
|
221
|
+
it 'starts with no bars' do
|
|
222
|
+
expect(multi.instance_variable_get(:@bars)).to be_empty
|
|
223
|
+
end
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
describe '#create_bar' do
|
|
227
|
+
it 'returns a ThreadSafeBar' do
|
|
228
|
+
bar = multi.create_bar("Table1", 100)
|
|
229
|
+
expect(bar).to be_a(Tapsoob::Progress::ThreadSafeBar)
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
it 'does not create duplicate bars for the same title' do
|
|
233
|
+
multi.create_bar("Table1", 100)
|
|
234
|
+
multi.create_bar("Table1", 200)
|
|
235
|
+
bars = multi.instance_variable_get(:@bars)
|
|
236
|
+
expect(bars.count { |b| b.title == "Table1" }).to eq(1)
|
|
237
|
+
end
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
describe '#set_info' do
|
|
241
|
+
it 'does not raise' do
|
|
242
|
+
expect { multi.set_info("Processing...") }.not_to raise_error
|
|
243
|
+
end
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
describe '#stop' do
|
|
247
|
+
it 'deactivates the multi bar' do
|
|
248
|
+
multi.stop
|
|
249
|
+
expect(multi.instance_variable_get(:@active)).to be false
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
it 'is idempotent' do
|
|
253
|
+
multi.stop
|
|
254
|
+
expect { multi.stop }.not_to raise_error
|
|
255
|
+
end
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
describe '#max_title_width' do
|
|
259
|
+
it 'grows to accommodate longer titles' do
|
|
260
|
+
multi.create_bar("ShortTitle", 10)
|
|
261
|
+
multi.create_bar("A Much Longer Title Here", 10)
|
|
262
|
+
expect(multi.max_title_width).to be >= "A Much Longer Title Here".length
|
|
263
|
+
end
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
# ── rendering paths (drive render_active_display / redraw_all) ──────────────
|
|
267
|
+
|
|
268
|
+
describe '#update' do
|
|
269
|
+
it 'does not raise when active and initialized with bars' do
|
|
270
|
+
out = StringIO.new
|
|
271
|
+
multi.instance_variable_set(:@out, out)
|
|
272
|
+
multi.instance_variable_set(:@initialized, true)
|
|
273
|
+
multi.instance_variable_set(:@total_lines, 4)
|
|
274
|
+
multi.instance_variable_set(:@last_update, Time.now - 1)
|
|
275
|
+
bar = multi.create_bar("UpdateTable", 10)
|
|
276
|
+
expect { multi.update }.not_to raise_error
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
it 'is a no-op when @active is false' do
|
|
280
|
+
multi.stop
|
|
281
|
+
expect { multi.update }.not_to raise_error
|
|
282
|
+
end
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
describe '#finish_bar' do
|
|
286
|
+
it 'marks the bar as finished without raising' do
|
|
287
|
+
out = StringIO.new
|
|
288
|
+
multi.instance_variable_set(:@out, out)
|
|
289
|
+
multi.instance_variable_set(:@initialized, true)
|
|
290
|
+
multi.instance_variable_set(:@total_lines, 4)
|
|
291
|
+
multi.instance_variable_set(:@last_update, Time.now - 1)
|
|
292
|
+
bar = multi.create_bar("FinishTable", 5)
|
|
293
|
+
expect { multi.finish_bar(bar) }.not_to raise_error
|
|
294
|
+
expect(bar.instance_variable_get(:@finished_p)).to be true
|
|
295
|
+
end
|
|
296
|
+
end
|
|
297
|
+
|
|
298
|
+
describe '#set_info (with rendering)' do
|
|
299
|
+
it 'triggers redraw when initialized and a bar exists' do
|
|
300
|
+
out = StringIO.new
|
|
301
|
+
multi.instance_variable_set(:@out, out)
|
|
302
|
+
multi.instance_variable_set(:@initialized, true)
|
|
303
|
+
multi.instance_variable_set(:@total_lines, 4)
|
|
304
|
+
multi.instance_variable_set(:@last_update, Time.now - 1)
|
|
305
|
+
multi.create_bar("InfoTable", 10)
|
|
306
|
+
expect { multi.set_info("doing things") }.not_to raise_error
|
|
307
|
+
end
|
|
308
|
+
end
|
|
309
|
+
|
|
310
|
+
describe '#stop (initialized)' do
|
|
311
|
+
it 'clears terminal lines when previously initialized' do
|
|
312
|
+
out = StringIO.new
|
|
313
|
+
multi.instance_variable_set(:@out, out)
|
|
314
|
+
multi.instance_variable_set(:@initialized, true)
|
|
315
|
+
multi.instance_variable_set(:@total_lines, 4)
|
|
316
|
+
multi.stop
|
|
317
|
+
expect(out.string).to include("\e[")
|
|
318
|
+
end
|
|
319
|
+
end
|
|
320
|
+
|
|
321
|
+
describe 'render_active_display (via update)' do
|
|
322
|
+
it 'writes escape sequences to output when bars exist' do
|
|
323
|
+
out = StringIO.new
|
|
324
|
+
multi.instance_variable_set(:@out, out)
|
|
325
|
+
multi.instance_variable_set(:@initialized, true)
|
|
326
|
+
multi.instance_variable_set(:@total_lines, 4)
|
|
327
|
+
multi.instance_variable_set(:@last_update, Time.now - 1)
|
|
328
|
+
multi.instance_variable_set(:@info_message, "some info")
|
|
329
|
+
multi.create_bar("RenderTable", 10)
|
|
330
|
+
multi.update
|
|
331
|
+
expect(out.string).not_to be_empty
|
|
332
|
+
end
|
|
333
|
+
end
|
|
334
|
+
end
|
|
335
|
+
end
|