progress-reporters 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 66a446c2c9ee4116e599389eef90ab5f608c4cf1
4
+ data.tar.gz: 9a7249c0fff340985ad787cbbcec102235b3035b
5
+ SHA512:
6
+ metadata.gz: 5a83ed6314162a9005a517ad286d2d5101ddddaa5da23010beabdebef1567eb55b902d8edc4c65c7961395c41fe6ae249f0ec082a7cffd7862d7773fca383741
7
+ data.tar.gz: 1d8ac329f8646b2e94f08000d7bb831088f8acf93b0f789ba5fa14d25542c216d352fe3eb30bd9bbca893fc50810b102399a2e8e5ee7ff59596f4916b10d2da8
data/Gemfile ADDED
@@ -0,0 +1,14 @@
1
+ source "https://rubygems.org"
2
+
3
+ gemspec
4
+
5
+ group :development, :test do
6
+ gem 'pry-nav'
7
+ gem 'rake'
8
+ gem 'timecop', '~> 0.7.0'
9
+ end
10
+
11
+ group :test do
12
+ gem 'rspec'
13
+ gem 'rr'
14
+ end
@@ -0,0 +1,3 @@
1
+ == 1.0.0
2
+
3
+ * Birthday!
@@ -0,0 +1,18 @@
1
+ # encoding: UTF-8
2
+
3
+ require 'rubygems' unless ENV['NO_RUBYGEMS']
4
+
5
+ require 'bundler'
6
+ require 'rspec/core/rake_task'
7
+ require 'rubygems/package_task'
8
+
9
+ require './lib/progress-reporters'
10
+
11
+ Bundler::GemHelper.install_tasks
12
+
13
+ task :default => :spec
14
+
15
+ desc 'Run specs'
16
+ RSpec::Core::RakeTask.new do |t|
17
+ t.pattern = './spec/**/*_spec.rb'
18
+ end
@@ -0,0 +1,9 @@
1
+ # encoding: UTF-8
2
+
3
+ module ProgressReporters
4
+ autoload :ProgressReporter, 'progress-reporters/progress_reporter'
5
+ autoload :NilProgressReporter, 'progress-reporters/nil_progress_reporter'
6
+ autoload :StagedProgressReporter, 'progress-reporters/staged_progress_reporter'
7
+ autoload :NilStagedProgressReporter, 'progress-reporters/nil_staged_progress_reporter'
8
+ autoload :MeteredProgressReporter, 'progress-reporters/metered_progress_reporter'
9
+ end
@@ -0,0 +1,117 @@
1
+ # encoding: UTF-8
2
+
3
+ module ProgressReporters
4
+
5
+ class MeteredProgressReporter < ProgressReporter
6
+ CALC_TYPES = [:avg, :moving_avg]
7
+ DEFAULT_MOVING_AVG_WINDOW_SIZE = 5
8
+ DEFAULT_CALC_TYPE = :moving_avg
9
+ DEFAULT_PRECISION = 2
10
+
11
+ attr_reader :calc_type, :window_size, :precision
12
+
13
+ def set_calc_type(calc_type)
14
+ if CALC_TYPES.include?(calc_type)
15
+ @calc_type = calc_type
16
+ self
17
+ else
18
+ raise ArgumentError, "#{calc_type} is not a supported calculation type."
19
+ end
20
+ end
21
+
22
+ def set_window_size(window_size)
23
+ @window_size = window_size
24
+ self
25
+ end
26
+
27
+ def set_precision(precision)
28
+ @precision = precision
29
+ self
30
+ end
31
+
32
+ def reset
33
+ super
34
+ @avg_sum = 0
35
+ @avg_count = 0
36
+ @avg_time_sum = 0
37
+ @avg_window = []
38
+ @avg_time_window = []
39
+ @last_timestamp = nil
40
+ @last_count = nil
41
+ end
42
+
43
+ def window_size
44
+ (@window_size || DEFAULT_MOVING_AVG_WINDOW_SIZE).to_f
45
+ end
46
+
47
+ def calc_type
48
+ @calc_type || DEFAULT_CALC_TYPE
49
+ end
50
+
51
+ def precision
52
+ @precision || DEFAULT_PRECISION
53
+ end
54
+
55
+ protected
56
+
57
+ # this won't get called if on_progress_proc is nil
58
+ def notify_of_progress(count, total)
59
+ on_progress_proc.call(
60
+ count, total, percentage(count, total), rate(count, total)
61
+ )
62
+ end
63
+
64
+ def rate(count, total)
65
+ cur_time = Time.now
66
+
67
+ rate = if @last_timestamp
68
+ time_delta = cur_time - @last_timestamp
69
+ count_delta = count - last_count
70
+ apply_calc_type(time_delta, count_delta)
71
+ else
72
+ 0
73
+ end
74
+
75
+ @last_timestamp = cur_time
76
+ @last_count = count
77
+ rate
78
+ end
79
+
80
+ def apply_calc_type(time_delta, count_delta)
81
+ case calc_type
82
+ when :avg
83
+ apply_avg(time_delta, count_delta)
84
+ when :moving_avg
85
+ apply_moving_avg(time_delta, count_delta)
86
+ end
87
+ end
88
+
89
+ def apply_avg(time_delta, count_delta)
90
+ @avg_sum += count_delta
91
+ @avg_time_sum += time_delta
92
+ @avg_count += 1
93
+
94
+ # (avg items) / (avg time) = avg items per second
95
+ answer = (@avg_sum / @avg_count) / (@avg_time_sum / @avg_count)
96
+ answer.round(precision)
97
+ end
98
+
99
+ def apply_moving_avg(time_delta, count_delta)
100
+ add_window_element(time_delta, count_delta)
101
+ avg_count = @avg_window.inject(&:+) / window_size
102
+ avg_time = @avg_time_window.inject(&:+) / window_size
103
+ (avg_count / avg_time).round(precision)
104
+ end
105
+
106
+ def add_window_element(time_delta, count_delta)
107
+ if @avg_window.size >= window_size
108
+ @avg_window.shift
109
+ @avg_time_window.shift
110
+ end
111
+
112
+ @avg_window << count_delta
113
+ @avg_time_window << time_delta
114
+ end
115
+ end
116
+
117
+ end
@@ -0,0 +1,27 @@
1
+ # encoding: UTF-8
2
+
3
+ module ProgressReporters
4
+
5
+ class NilProgressReporter
6
+ def self.instance
7
+ @instance ||= new
8
+ end
9
+
10
+ def on_progress(&block)
11
+ self
12
+ end
13
+
14
+ def on_complete(&block)
15
+ self
16
+ end
17
+
18
+ def set_step(step)
19
+ self
20
+ end
21
+
22
+ def report_progress(count, total); end
23
+ def report_complete; end
24
+ def reset; end
25
+ end
26
+
27
+ end
@@ -0,0 +1,21 @@
1
+ # encoding: UTF-8
2
+
3
+ module ProgressReporters
4
+
5
+ class NilStagedProgressReporter < NilProgressReporter
6
+ def on_stage_changed(&block)
7
+ self
8
+ end
9
+
10
+ def on_stage_finished(&block)
11
+ end
12
+
13
+ def set_stage(stage)
14
+ self
15
+ end
16
+
17
+ def change_stage(new_stage); end
18
+ def report_stage_finished(count, total); end
19
+ end
20
+
21
+ end
@@ -0,0 +1,76 @@
1
+ # encoding: UTF-8
2
+
3
+ module ProgressReporters
4
+
5
+ class ProgressReporter
6
+ DEFAULT_STEP = 1
7
+
8
+ attr_reader :step, :last_count, :on_progress_proc, :on_complete_proc
9
+
10
+ protected :on_progress_proc
11
+ protected :on_complete_proc
12
+
13
+ def initialize
14
+ @step = DEFAULT_STEP
15
+ reset
16
+ end
17
+
18
+ def on_progress(&block)
19
+ @on_progress_proc = block
20
+ self
21
+ end
22
+
23
+ def on_complete(&block)
24
+ @on_complete_proc = block
25
+ self
26
+ end
27
+
28
+ def set_step(step)
29
+ @step = step
30
+ self
31
+ end
32
+
33
+ def report_progress(count, total)
34
+ if on_progress_proc
35
+ notify_of_progress(count, total) if count % step == 0
36
+ end
37
+
38
+ @last_count = count
39
+ end
40
+
41
+ def report_complete
42
+ if on_complete_proc
43
+ notify_of_completion
44
+ end
45
+ end
46
+
47
+ def reset
48
+ @last_count = 0
49
+ end
50
+
51
+ protected
52
+
53
+ # this won't get called if on_progress_proc is nil
54
+ def notify_of_progress(count, total)
55
+ on_progress_proc.call(
56
+ count, total, percentage(count, total)
57
+ )
58
+ end
59
+
60
+ # this won't get called if on_complete_proc is nil
61
+ def notify_of_completion
62
+ on_complete_proc.call
63
+ end
64
+
65
+ private
66
+
67
+ def percentage(count, total, precision = 0)
68
+ if total > 0
69
+ ((count.to_f / total.to_f) * 100).round(precision)
70
+ else
71
+ 0
72
+ end
73
+ end
74
+ end
75
+
76
+ end
@@ -0,0 +1,70 @@
1
+ # encoding: UTF-8
2
+
3
+ module ProgressReporters
4
+
5
+ class StagedProgressReporter < ProgressReporter
6
+ DEFAULT_STAGE = :start
7
+
8
+ attr_reader :stage, :on_stage_changed_proc, :on_stage_finished_proc
9
+ protected :on_stage_changed_proc
10
+ protected :on_stage_finished_proc
11
+
12
+ def initialize
13
+ super
14
+ @stage = DEFAULT_STAGE
15
+ end
16
+
17
+ def set_stage(stage)
18
+ @stage = stage
19
+ self
20
+ end
21
+
22
+ def change_stage(new_stage)
23
+ if on_stage_changed_proc
24
+ notify_of_stage_change(new_stage, stage)
25
+ end
26
+
27
+ @stage = new_stage
28
+ end
29
+
30
+ def report_stage_finished(count, total)
31
+ if on_stage_finished_proc
32
+ notify_of_stage_finish(count, total)
33
+ end
34
+
35
+ @last_count = count
36
+ end
37
+
38
+ def on_stage_changed(&block)
39
+ @on_stage_changed_proc = block
40
+ self
41
+ end
42
+
43
+ def on_stage_finished(&block)
44
+ @on_stage_finished_proc = block
45
+ self
46
+ end
47
+
48
+ protected
49
+
50
+ # this won't get called if on_progress_proc is nil
51
+ def notify_of_progress(count, total)
52
+ on_progress_proc.call(
53
+ count, total, percentage(count, total), stage
54
+ )
55
+ end
56
+
57
+ # this won't get called if on_finished_proc is nil
58
+ def notify_of_stage_finish(count, total)
59
+ on_stage_finished_proc.call(
60
+ count, total, percentage(count, total), stage
61
+ )
62
+ end
63
+
64
+ # this won't get called if on_stage_changed_proc is nil
65
+ def notify_of_stage_change(new_stage, old_stage)
66
+ on_stage_changed_proc.call(new_stage, old_stage)
67
+ end
68
+ end
69
+
70
+ end
@@ -0,0 +1,5 @@
1
+ # encoding: UTF-8
2
+
3
+ module ProgressReporters
4
+ VERSION = '1.0.0'
5
+ end
@@ -0,0 +1,53 @@
1
+ # encoding: UTF-8
2
+
3
+ require 'spec_helper'
4
+
5
+ include ProgressReporters
6
+
7
+ describe ProgressReporters::MeteredProgressReporter do
8
+ before(:each) do
9
+ collector # call to instantiate collector
10
+ end
11
+
12
+ context 'with a metered reporter' do
13
+ let(:reporter) { MeteredProgressReporter.new }
14
+ let(:collector) { MeteredCollector.new(reporter) }
15
+
16
+ context 'with a staged task' do
17
+ let(:task) { MeteredTask.new(reporter) }
18
+
19
+ it 'reports progress and calculates the average rate' do
20
+ reporter.set_calc_type(:avg)
21
+ task.execute(10, 0.5, 0)
22
+
23
+ collector.progress_notifications.each_with_index do |notification, idx|
24
+ expect(notification.quantity).to eq(idx)
25
+ expect(notification.total).to eq(10)
26
+ expect(notification.percentage).to eq(idx * 10)
27
+ expect(notification.rate).to eq(idx == 0 ? 0 : 2.0)
28
+ end
29
+ end
30
+
31
+ it 'reports progress and calculates the moving average rate' do
32
+ reporter.set_calc_type(:moving_avg)
33
+ reporter.set_window_size(2)
34
+ task.execute(10, 1, 1)
35
+
36
+ collector.progress_notifications.each_with_index do |notification, idx|
37
+ expect(notification.quantity).to eq(idx)
38
+ expect(notification.total).to eq(10)
39
+ expect(notification.percentage).to eq(idx * 10)
40
+
41
+ rate = case idx
42
+ when 0 then 0.0
43
+ when 1 then 1.0
44
+ else
45
+ (1 / (0.5 + (idx - 1))).round(2)
46
+ end
47
+
48
+ expect(notification.rate).to eq(rate)
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,67 @@
1
+ # encoding: UTF-8
2
+
3
+ require 'spec_helper'
4
+
5
+ include ProgressReporters
6
+
7
+ describe ProgressReporters::ProgressReporter do
8
+ before(:each) do
9
+ collector # call to instantiate collector
10
+ end
11
+
12
+ context 'with a linear reporter' do
13
+ let(:reporter) { ProgressReporter.new }
14
+ let(:collector) { LinearCollector.new(reporter) }
15
+
16
+ context 'with a linear task' do
17
+ let(:task) { LinearTask.new(reporter) }
18
+
19
+ it 'reports progress' do
20
+ task.execute(10)
21
+
22
+ collector.progress_notifications.each_with_index do |notification, idx|
23
+ expect(notification.quantity).to eq(idx)
24
+ expect(notification.total).to eq(10)
25
+ expect(notification.percentage).to eq(10 * idx)
26
+ end
27
+
28
+ expect(collector.complete_notification).to_not be_nil
29
+ end
30
+
31
+ it 'should only report progress every other time when step is set to 2' do
32
+ reporter.set_step(2)
33
+ task.execute(10)
34
+
35
+ expect(collector.progress_notifications.size).to eq(5)
36
+
37
+ collector.progress_notifications.each_with_index do |notification, idx|
38
+ expect(notification.quantity).to eq(idx * 2)
39
+ expect(notification.total).to eq(10)
40
+ expect(notification.percentage).to eq(10 * (idx * 2))
41
+ end
42
+ end
43
+
44
+ it 'should be able to reset itself' do
45
+ task.execute(10)
46
+ expect(reporter.last_count).to eq(9)
47
+ reporter.reset
48
+ expect(reporter.last_count).to eq(0)
49
+ end
50
+ end
51
+
52
+ context 'with a nil reporter' do
53
+ let(:reporter) { NilProgressReporter.new }
54
+ let(:collector) { LinearCollector.new(reporter) }
55
+
56
+ context 'with a linear task' do
57
+ let(:task) { LinearTask.new(reporter) }
58
+
59
+ it 'should not actually report any progress' do
60
+ task.execute(10)
61
+ expect(collector.progress_notifications).to be_empty
62
+ expect(collector.complete_notification).to be_nil
63
+ end
64
+ end
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,127 @@
1
+ # encoding: UTF-8
2
+
3
+ require 'rspec'
4
+ require 'progress-reporters'
5
+ require 'timecop'
6
+ require 'pry-nav'
7
+
8
+ RSpec.configure do |config|
9
+ config.mock_with :rr
10
+ end
11
+
12
+ LinearNotification = Struct.new(:quantity, :total, :percentage)
13
+ StagedNotification = Struct.new(:quantity, :total, :percentage, :stage)
14
+ StageChangedNotification = Struct.new(:new_stage, :old_stage)
15
+ MeteredNotification = Struct.new(:quantity, :total, :percentage, :rate)
16
+
17
+ class LinearCollector
18
+ attr_reader :progress_reporter
19
+ attr_reader :progress_notifications, :complete_notification
20
+
21
+ def initialize(progress_reporter)
22
+ @progress_notifications = []
23
+
24
+ @progress_reporter = progress_reporter
25
+ .on_progress { |*args| on_progress(*args) }
26
+ .on_complete { |*args| on_complete(*args) }
27
+
28
+ after_initialize
29
+ end
30
+
31
+ protected
32
+
33
+ def after_initialize
34
+ end
35
+
36
+ def on_progress(*args)
37
+ @progress_notifications << make_notification(*args)
38
+ end
39
+
40
+ def on_complete(*args)
41
+ if @complete_notification
42
+ raise StandardError, 'task already complete'
43
+ else
44
+ @complete_notification = make_notification(*args)
45
+ end
46
+ end
47
+
48
+ def make_notification(*args)
49
+ LinearNotification.new(*args)
50
+ end
51
+ end
52
+
53
+ class StagedCollector < LinearCollector
54
+ attr_reader :stage_changed_notifications
55
+
56
+ protected
57
+
58
+ def after_initialize
59
+ @stage_changed_notifications = []
60
+ progress_reporter.on_stage_changed { |*args| on_stage_changed(*args) }
61
+ end
62
+
63
+ def on_stage_changed(*args)
64
+ @stage_changed_notifications << StageChangedNotification.new(*args)
65
+ end
66
+
67
+ def make_notification(*args)
68
+ StagedNotification.new(*args)
69
+ end
70
+ end
71
+
72
+ class MeteredCollector < LinearCollector
73
+ def make_notification(*args)
74
+ MeteredNotification.new(*args)
75
+ end
76
+ end
77
+
78
+ class Task
79
+ attr_reader :progress_reporter
80
+
81
+ def initialize(progress_reporter)
82
+ @progress_reporter = progress_reporter
83
+ end
84
+ end
85
+
86
+ class LinearTask < Task
87
+ def execute(quantity = 10)
88
+ quantity.times do |i|
89
+ progress_reporter.report_progress(i, quantity)
90
+ end
91
+
92
+ progress_reporter.report_complete
93
+ end
94
+ end
95
+
96
+ class StagedTask < Task
97
+ def execute(stages = 2, quantity_per_stage = 10)
98
+ progress_reporter.set_stage(:stage_1)
99
+
100
+ stages.times do |stage|
101
+ quantity_per_stage.times do |i|
102
+ progress_reporter.report_progress(i, quantity_per_stage)
103
+ end
104
+
105
+ progress_reporter.report_stage_finished(
106
+ quantity_per_stage, quantity_per_stage
107
+ )
108
+
109
+ progress_reporter.change_stage(:"stage_#{stage + 2}")
110
+ end
111
+
112
+ progress_reporter.report_complete
113
+ end
114
+ end
115
+
116
+ class MeteredTask < Task
117
+ def execute(quantity = 10, delay_seconds = 1, rate_of_delay = 1)
118
+ Timecop.freeze do
119
+ quantity.times do |i|
120
+ progress_reporter.report_progress(i, quantity)
121
+ Timecop.travel(Time.now + delay_seconds + (rate_of_delay * i))
122
+ end
123
+
124
+ progress_reporter.report_complete
125
+ end
126
+ end
127
+ end
@@ -0,0 +1,63 @@
1
+ # encoding: UTF-8
2
+
3
+ require 'spec_helper'
4
+
5
+ include ProgressReporters
6
+
7
+ describe ProgressReporters::StagedProgressReporter do
8
+ before(:each) do
9
+ collector # call to instantiate collector
10
+ end
11
+
12
+ context 'with a staged reporter' do
13
+ let(:reporter) { StagedProgressReporter.new }
14
+ let(:collector) { StagedCollector.new(reporter) }
15
+
16
+ context 'with a staged task' do
17
+ let(:task) { StagedTask.new(reporter) }
18
+
19
+ it 'reports progress in stages' do
20
+ task.execute(2, 10)
21
+
22
+ expect(collector.progress_notifications.size).to eq(20)
23
+
24
+ collector.progress_notifications.each_with_index do |notification, idx|
25
+ expect(notification.quantity).to eq(idx % 10)
26
+ expect(notification.total).to eq(10)
27
+ expect(notification.percentage).to eq((idx % 10) * 10)
28
+ expect(notification.stage).to eq(:"stage_#{(idx / 10) + 1}")
29
+ end
30
+
31
+ expect(collector.stage_changed_notifications.size).to eq(2)
32
+
33
+ collector.stage_changed_notifications.tap do |notifications|
34
+ notifications[0].first do |notification|
35
+ expect(notification.old_stage).to_eq(:stage_1)
36
+ expect(notification.new_stage).to eq(:stage_2)
37
+ end
38
+
39
+ notifications[1].first do |notification|
40
+ expect(notification.old_stage).to_eq(:stage_2)
41
+ expect(notification.new_stage).to eq(:stage_3)
42
+ end
43
+ end
44
+ end
45
+ end
46
+
47
+ context 'with a nil reporter' do
48
+ let(:reporter) { NilStagedProgressReporter.new }
49
+ let(:collector) { StagedCollector.new(reporter) }
50
+
51
+ context 'with a linear task' do
52
+ let(:task) { StagedTask.new(reporter) }
53
+
54
+ it 'should not actually report any progress' do
55
+ task.execute(10)
56
+ expect(collector.progress_notifications).to be_empty
57
+ expect(collector.stage_changed_notifications).to be_empty
58
+ expect(collector.complete_notification).to be_nil
59
+ end
60
+ end
61
+ end
62
+ end
63
+ end
metadata ADDED
@@ -0,0 +1,57 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: progress-reporters
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Cameron Dutro
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2014-08-23 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description: Callback-oriented way to report the progress of a task.
14
+ email:
15
+ - camertron@gmail.com
16
+ executables: []
17
+ extensions: []
18
+ extra_rdoc_files: []
19
+ files:
20
+ - Gemfile
21
+ - History.txt
22
+ - Rakefile
23
+ - lib/progress-reporters.rb
24
+ - lib/progress-reporters/metered_progress_reporter.rb
25
+ - lib/progress-reporters/nil_progress_reporter.rb
26
+ - lib/progress-reporters/nil_staged_progress_reporter.rb
27
+ - lib/progress-reporters/progress_reporter.rb
28
+ - lib/progress-reporters/staged_progress_reporter.rb
29
+ - lib/progress-reporters/version.rb
30
+ - spec/metered_progress_reporter_spec.rb
31
+ - spec/progress_reporter_spec.rb
32
+ - spec/spec_helper.rb
33
+ - spec/staged_progress_reporter_spec.rb
34
+ homepage: http://github.com/camertron
35
+ licenses: []
36
+ metadata: {}
37
+ post_install_message:
38
+ rdoc_options: []
39
+ require_paths:
40
+ - lib
41
+ required_ruby_version: !ruby/object:Gem::Requirement
42
+ requirements:
43
+ - - '>='
44
+ - !ruby/object:Gem::Version
45
+ version: '0'
46
+ required_rubygems_version: !ruby/object:Gem::Requirement
47
+ requirements:
48
+ - - '>='
49
+ - !ruby/object:Gem::Version
50
+ version: '0'
51
+ requirements: []
52
+ rubyforge_project:
53
+ rubygems_version: 2.2.2
54
+ signing_key:
55
+ specification_version: 4
56
+ summary: Callback-oriented way to report the progress of a task.
57
+ test_files: []