progressrus 0.0.1

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.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 4cbd4fa76b6a27688c4f385cb897bbd743a5e970
4
+ data.tar.gz: 0d51ce787ed6e0f5dcf5ab9b01992b84882bd490
5
+ SHA512:
6
+ metadata.gz: d6d9455550209e99c6d714184566f99c651a223b79d30aeeb11b3b80d017f0ded87507735c38215f974380419d82b6883050b06a6fae4b4b8a5e1769c718d675
7
+ data.tar.gz: c2dbc7c4f6f292f08dffe50cd5e159d57bf81926ada214a3f49d568b811f47a3f2cd2b8be8235fa939ac9d1851b56c69687da12a829cf2b7b02c15f210fe8bf6
@@ -0,0 +1,18 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
18
+ vendor/
@@ -0,0 +1,6 @@
1
+ language: ruby
2
+ services:
3
+ - redis-server
4
+ rvm:
5
+ - 2.0.0
6
+ - 2.1.1
data/Gemfile ADDED
@@ -0,0 +1,6 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in resque-pace.gemspec
4
+ gemspec
5
+
6
+ gem 'coveralls', require: false
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2013 Simon Eskildsen
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,113 @@
1
+ # Progressrus [![Build Status](https://travis-ci.org/Sirupsen/progressrus.png?branch=master)](https://travis-ci.org/Sirupsen/progressrus) [![Coverage Status](https://coveralls.io/repos/Sirupsen/progressrus/badge.png?branch=master)](https://coveralls.io/r/Sirupsen/progressrus?branch=master)
2
+
3
+ `Progressrus` provides progress status of long-running jobs. The progress is
4
+ stored in persistence layer. Progressrus currently ships with a Redis adapter,
5
+ but is written in an agnostic way, where multiple layers can be used at the same
6
+ time. For example, one a message queue adapter too for real-time updates.
7
+
8
+ Think of Progressrus as a progress bar where instead of flushing the progress to
9
+ `stdout`, it's pushed to one or more data stores. It can be used with a
10
+ background job engine or just about anything where you need to show the progress
11
+ in a different location than the long-running operation.
12
+
13
+ It works by instructing `Progressrus` about the finishing point (total). When
14
+ the job makes progress towards the total, the job calls `tick`. With ticks
15
+ second(s) apart (configurable) the progress is updated in the data store(s). This
16
+ prevents a job processing e.g. 100 records to hit the data store 100 times.
17
+ Combination of adapters is planned, so you can publish to some kind of real-time
18
+ data source, Redis and even stdout at the same time.
19
+
20
+ `Progressrus` keeps track of the jobs in some scope. This could be a `user_id`.
21
+ This makes it easy to find the jobs and their progress for a specific user,
22
+ without worrying about keeping e.g. the Resque job ids around.
23
+
24
+ `Progressrus` will update the data store with the progress of the job. The key
25
+ for a user with `user_id` `3421` would be: `progressrus:user:3421`. For the
26
+ Redis data store, the key is a Redis hash where the Redis `job_id` is the key
27
+ and the value is a `json` object with information about the progress, i.e.:
28
+
29
+ ```redis
30
+ redis> HGETALL progressrus:user:3421
31
+ 1) "4bacc11a-dda3-405e-b0aa-be8678d16037"
32
+ 2) "{"count\":94,\"total\":100,\"started_at\":\"2013-12-08 10:53:41 -0500\"}"
33
+ ```
34
+
35
+ ## Usage
36
+
37
+ Instrument by creating a `Progressrus` object with the `scope` and `total` amount of
38
+ records to be processed:
39
+
40
+ ```ruby
41
+ class MaintenanceProcessRecords
42
+ def self.perform(record_ids, user_id)
43
+ Record.where(id: record_ids)
44
+ .enum_for(:find_each)
45
+ .with_progress(scope: [:user, user_id], total: # get this somehow, unless you're on rails 4.1) do |record|
46
+ record.do_expensive_things
47
+ end
48
+ end
49
+ end
50
+ ```
51
+
52
+ You can also use the slightly more flexible lower-level API, which is useful in
53
+ some cases:
54
+
55
+ ```ruby
56
+ class MaintenanceProcessRecords
57
+ def self.perform(record_ids, user_id)
58
+ # Construct the pace object.
59
+ progress = Progressrus.new(scope: [:user, user_id], total: record_ids.count)
60
+
61
+ # Start processing the records!
62
+ Record.where(id: record_ids).find_each do |record|
63
+ begin
64
+ record.do_expensive_things
65
+
66
+ # Does a single tick, updates the data store every x seconds this is called.
67
+ progress.tick
68
+ rescue
69
+ # Increments the error count if the above failed
70
+ progress.error
71
+ end
72
+ end
73
+
74
+ # Force an update to the data store and set :completed_at to Time.now
75
+ progress.complete
76
+ end
77
+ end
78
+ ```
79
+
80
+ ## Querying Ticks by scope
81
+
82
+ To query for the progress of jobs for a specific scope:
83
+
84
+ ```ruby
85
+ > Progressrus.all(["walrus", '1234'])
86
+ #=> [
87
+ #<Progressrus::Progress:0x007f0fdc8ab888 @scope=["walrus", "1234"], @total=50, @id="narwhal", @interval=2, @params={:count=>0, :started_at=>"2013-12-12 18:09:44 +0000", :completed_at=>nil, :name=>"oemg-test-2"}, @count=0, @error_count=0, @started_at=2013-12-12 18:09:44 +0000, @persisted_at=2013-12-12 18:09:41 +0000, @store=#<Progressrus::Store::Redis:0x007f0fdc894c28 @redis=#<Redis client v3.0.6 for redis://127.0.0.1:6379/0>, @options={:expire=>1800, :prefix=>"progressrus"}>, @completed_at=nil>,
88
+ #<Progressrus::Progress:0x007f0fdc8ab4a0 @scope=["walrus", "1234"], @total=100, @id="oemg", @interval=2, @params={:count=>0, :started_at=>"2013-12-12 18:09:44 +0000", :completed_at=>nil, :name=>"oemg-test"}, @count=0, @error_count=0, @started_at=2013-12-12 18:09:44 +0000, @persisted_at=2013-12-12 18:09:41 +0000, @store=#<Progressrus::Store::Redis:0x007f0fdc894c28 @redis=#<Redis client v3.0.6 for redis://127.0.0.1:6379/0>, @options={:expire=>1800, :prefix=>"progressrus"}>, @completed_at=nil>
89
+ ]
90
+ ```
91
+
92
+ The `Progressrus` objects contain useful methods such as `#percentage` to return how
93
+ many percent done the job is and `#eta` to return a `Time` object estimation of
94
+ when the job will be complete. The scope is completely independent from the job
95
+ itself, which means you can have jobs from multiple sources in the same scope.
96
+
97
+ ## Querying Progress by scope and id
98
+
99
+ To query for the progress of a specific job:
100
+
101
+ ```ruby
102
+ > Progressrus.find(["walrus", '1234'], 'narwhal')
103
+ #=> #<Progressrus::Progress:0x007f0fdc8ab888 @scope=["walrus", "1234"], @total=50, @id="narwhal", @interval=2, @params={:count=>0, :started_at=>"2013-12-12 18:09:44 +0000", :completed_at=>nil, :name=>"oemg-test-2"}, @count=0, @error_count=0, @started_at=2013-12-12 18:09:44 +0000, @persisted_at=2013-12-12 18:09:41 +0000, @store=#<Progressrus::Store::Redis:0x007f0fdc894c28 @redis=#<Redis client v3.0.6 for redis://127.0.0.1:6379/0>, @options={:expire=>1800, :prefix=>"progressrus"}>, @completed_at=nil>
104
+ ```
105
+
106
+ ## Todo
107
+
108
+ * Tighter Resque/Sidekiq/DJ integration
109
+ * Rack interface
110
+ * SQL adapter
111
+ * Document adapter-specific options
112
+ * Enumerable integration for higher-level API
113
+ * Documentation on how to do sharding
@@ -0,0 +1,15 @@
1
+ require "bundler/gem_tasks"
2
+ require 'rake/testtask'
3
+
4
+ Dir.glob('tasks/*.rake').each { |r| load r}
5
+
6
+ task :default => :test
7
+
8
+ desc 'Run the test suite.'
9
+
10
+ Rake::TestTask.new(:test) do |t|
11
+ t.libs << 'lib'
12
+ t.libs << 'test'
13
+ t.pattern = 'test/**/*_test.rb'
14
+ t.verbose = true
15
+ end
@@ -0,0 +1,156 @@
1
+ require 'json'
2
+ require 'securerandom'
3
+ require 'redis'
4
+ require 'time'
5
+ require_relative "progressrus/store"
6
+ require_relative "progressrus/store/base"
7
+ require_relative "progressrus/store/redis"
8
+ require_relative "progressrus/core_ext/enumerable"
9
+
10
+ class Progressrus
11
+ class << self
12
+ def stores
13
+ @@stores ||= Store.new(Store::Redis.new(::Redis.new(host: ENV["PROGRESSRUS_REDIS_HOST"] || "localhost")))
14
+ end
15
+
16
+ def scope(scope, store: :first)
17
+ stores.find_by_name(store).scope(scope)
18
+ end
19
+ alias_method :all, :scope
20
+
21
+ def find(scope, id, store: :first)
22
+ stores.find_by_name(store).find(scope, id)
23
+ end
24
+
25
+ def flush(scope, id = nil, store: :first)
26
+ stores.find_by_name(store).flush(scope, id)
27
+ end
28
+ end
29
+
30
+ attr_reader :name, :scope, :total, :id, :params, :store, :count,
31
+ :started_at, :completed_at, :failed_at, :stores, :error_count
32
+
33
+ alias_method :completed?, :completed_at
34
+ alias_method :started?, :started_at
35
+ alias_method :failed?, :failed_at
36
+
37
+ attr_writer :params
38
+
39
+ def initialize(scope: "progressrus", total: nil, name: nil,
40
+ id: SecureRandom.uuid, params: {}, stores: Progressrus.stores,
41
+ completed_at: nil, started_at: nil, count: 0, failed_at: nil,
42
+ error_count: 0, persist: false)
43
+
44
+ raise ArgumentError, "Total cannot be zero or negative." if total && total <= 0
45
+
46
+ @name = name || id
47
+ @scope = Array(scope).map(&:to_s)
48
+ @total = total
49
+ @id = id
50
+ @params = params
51
+ @stores = stores
52
+ @count = count
53
+ @error_count = error_count
54
+
55
+ @started_at = parse_time(started_at)
56
+ @completed_at = parse_time(completed_at)
57
+ @failed_at = parse_time(failed_at)
58
+
59
+ persist(force: true) if persist
60
+ end
61
+
62
+ def tick(ticks = 1, now: Time.now)
63
+ @started_at ||= now if ticks >= 1
64
+ @count += ticks
65
+ persist
66
+ end
67
+
68
+ def error(ticks = 1, now: Time.now)
69
+ @error_count ||= 0
70
+ @error_count += ticks
71
+ end
72
+
73
+ def count=(new_count, **args)
74
+ tick(new_count - @count, *args)
75
+ end
76
+
77
+ def complete(now: Time.now)
78
+ @started_at ||= now
79
+ @completed_at = now
80
+ persist(force: true)
81
+ end
82
+
83
+ def flush
84
+ stores.each { |store| store.flush(scope, id) }
85
+ end
86
+
87
+ def status
88
+ return :completed if completed?
89
+ return :failed if failed?
90
+ return :running if running?
91
+ :started
92
+ end
93
+
94
+ def running?
95
+ count > 0
96
+ end
97
+
98
+ def fail(now: Time.now)
99
+ @started_at ||= now
100
+ @failed_at = now
101
+ persist(force: true)
102
+ end
103
+
104
+ def to_serializeable
105
+ {
106
+ name: name,
107
+ id: id,
108
+ scope: scope,
109
+ started_at: started_at,
110
+ completed_at: completed_at,
111
+ failed_at: failed_at,
112
+ count: count,
113
+ total: total,
114
+ params: params,
115
+ error_count: error_count
116
+ }
117
+ end
118
+
119
+ def total=(new_total)
120
+ raise ArgumentError, "Total cannot be zero or negative." if new_total <= 0
121
+ @total = new_total
122
+ end
123
+
124
+ def total
125
+ @total ||= 1
126
+ end
127
+
128
+ def elapsed(now: Time.now)
129
+ now - started_at
130
+ end
131
+
132
+ def percentage
133
+ count.to_f / total
134
+ end
135
+
136
+ def eta(now: Time.now)
137
+ return nil if count.to_i == 0
138
+
139
+ processed_per_second = (count.to_f / elapsed(now: now))
140
+ left = (total - count)
141
+ seconds_to_finished = left / processed_per_second
142
+ now + seconds_to_finished
143
+ end
144
+
145
+ private
146
+ def persist(force: false)
147
+ stores.each { |store| store.persist(self, force: force) }
148
+ end
149
+
150
+ def parse_time(time)
151
+ return Time.parse(time) if time.is_a?(String)
152
+ time
153
+ end
154
+ end
155
+
156
+ require 'progressrus/railtie' if defined?(Rails)
@@ -0,0 +1,31 @@
1
+ module Enumerable
2
+ def with_progress(**args, &block)
3
+ if block_given?
4
+ progresser = progress(args)
5
+ begin
6
+ ret = each { |*o|
7
+ res = yield(*o)
8
+ progresser.tick
9
+ res
10
+ }
11
+ rescue
12
+ progresser.fail
13
+ raise
14
+ end
15
+ progresser.complete
16
+ ret
17
+ else
18
+ enum_for(:with_progress, args)
19
+ end
20
+ end
21
+
22
+ private
23
+ def progress(args)
24
+ @progress ||= begin
25
+ # Lazily read the size, for some enumerable this may be quite expensive and
26
+ # using this method should come with a warning in the documentation.
27
+ total = self.size unless args[:total]
28
+ @progress = Progressrus.new({total: total}.merge(args))
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,7 @@
1
+ class Progressrus
2
+ class Railtie < Rails::Railtie
3
+ rake_tasks do
4
+ load File.expand_path('../../../tasks/redis_store.rake', __FILE__)
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,11 @@
1
+ require 'sinatra/base'
2
+
3
+ class Progressrus
4
+ class Server < Sinatra::Base
5
+ get '/' do
6
+ "Hello World"
7
+ end
8
+
9
+ run! if app_file == $0
10
+ end
11
+ end
@@ -0,0 +1,24 @@
1
+ class Progressrus
2
+ class Store < Array
3
+ def initialize(default)
4
+ @default = default
5
+ self << default
6
+ end
7
+
8
+ def default
9
+ @default
10
+ end
11
+
12
+ def default!
13
+ clear
14
+ self << default
15
+ end
16
+
17
+ def find_by_name(name)
18
+ return first if name == :first
19
+ return last if name == :last
20
+
21
+ find { |store| store.name == name }
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,23 @@
1
+ class Progressrus
2
+ class Store
3
+ class NotImplementedError < StandardError; end
4
+
5
+ class Base
6
+ def persist(progress)
7
+ raise NotImplementedError
8
+ end
9
+
10
+ def scope(scope)
11
+ raise NotImplementedError
12
+ end
13
+
14
+ def find(scope, id)
15
+ raise NotImplementedError
16
+ end
17
+
18
+ def flush(scope, id = nil)
19
+ raise NotImplementedError
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,61 @@
1
+ class Progressrus
2
+ class Store
3
+ class Redis < Base
4
+ attr_reader :redis, :interval, :persisted_at, :prefix, :name
5
+
6
+ def initialize(instance, prefix: "progressrus", interval: 1, now: Time.now)
7
+ @name = :redis
8
+ @redis = instance
9
+ @persisted_ats = Hash.new({})
10
+ @interval = interval
11
+ @prefix = prefix
12
+ end
13
+
14
+ def persist(progress, now: Time.now, force: false)
15
+ if outdated?(progress) || force
16
+ redis.hset(key(progress.scope), progress.id, progress.to_serializeable.to_json)
17
+ @persisted_ats[progress.scope][progress.id] = now
18
+ end
19
+ end
20
+
21
+ def scope(scope)
22
+ scope = redis.hgetall(key(scope))
23
+ scope.each_pair { |id, value|
24
+ scope[id] = Progressrus.new(deserialize(value))
25
+ }
26
+ end
27
+
28
+ def find(scope, id)
29
+ value = redis.hget(key(scope), id)
30
+ return unless value
31
+
32
+ Progressrus.new(deserialize(value))
33
+ end
34
+
35
+ def flush(scope, id = nil)
36
+ if id
37
+ redis.hdel(key(scope), id)
38
+ else
39
+ redis.del(key(scope))
40
+ end
41
+ end
42
+
43
+ private
44
+ def key(scope)
45
+ "#{prefix}:#{scope.join(":")}"
46
+ end
47
+
48
+ def deserialize(value)
49
+ JSON.parse(value, symbolize_names: true)
50
+ end
51
+
52
+ def outdated?(progress, now: Time.now)
53
+ (now - interval).to_i >= persisted_at(progress).to_i
54
+ end
55
+
56
+ def persisted_at(progress)
57
+ @persisted_ats[progress.scope][progress.id]
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,3 @@
1
+ class Progressrus
2
+ VERSION = "0.0.1"
3
+ end
@@ -0,0 +1,28 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'progressrus/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "progressrus"
8
+ spec.version = Progressrus::VERSION
9
+ spec.authors = ["Simon Eskildsen"]
10
+ spec.email = ["sirup@sirupsen.com"]
11
+ spec.description = %q{Monitor the progress of remote, long-running jobs.}
12
+ spec.summary = %q{Monitor the progress of remote, long-running jobs.}
13
+ spec.homepage = "https://github.com/Sirupsen/progressrus"
14
+ spec.license = "MIT"
15
+
16
+ spec.files = `git ls-files`.split($/)
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = ["lib"]
20
+
21
+ spec.add_dependency "redis", "~> 3.0"
22
+ spec.add_development_dependency "bundler", "~> 1.3"
23
+ spec.add_development_dependency "rake"
24
+ spec.add_development_dependency "mocha", "~> 0.14"
25
+ spec.add_development_dependency "pry"
26
+ spec.add_development_dependency "byebug"
27
+ spec.add_development_dependency "pry-byebug"
28
+ end
@@ -0,0 +1,13 @@
1
+ require_relative '../lib/progressrus'
2
+ require 'rake'
3
+
4
+ namespace :progressrus do
5
+ namespace :store do
6
+ desc "Flushes the current Progressrus.store."
7
+ task :flush, :environment do |t, args|
8
+ scope = *args
9
+ raise ArgumentError.new("Must pass [scope] to progressrus:store:flush[scope(,parts)] task.") unless scope.length > 0
10
+ Progressrus.stores.first.flush(scope) if Progressrus.stores.first
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,65 @@
1
+ require_relative "test_helper"
2
+
3
+ class IntegrationTest < Minitest::Unit::TestCase
4
+ def setup
5
+ @progress = Progressrus.new(scope: "walrus", total: 20)
6
+ end
7
+
8
+ def teardown
9
+ Progressrus.stores.first.flush(@progress.scope)
10
+ end
11
+
12
+ def test_create_tick_and_see_status_in_redis
13
+ @progress.tick
14
+
15
+ ticks = Progressrus.scope(["walrus"]).values
16
+
17
+ assert_equal 1, ticks.length
18
+
19
+ tick = ticks.first
20
+ assert_equal 20, tick.total
21
+ assert_equal 1, tick.count
22
+ end
23
+
24
+ def test_create_multiple_ticks_and_see_them_in_redis
25
+ @progress.tick
26
+
27
+ progress2 = Progressrus.new(scope: ["walrus"], total: 50)
28
+ progress2.tick
29
+
30
+ ticks = Progressrus.scope(["walrus"]).values
31
+
32
+ assert_equal 2, ticks.length
33
+
34
+ assert_equal [20, 50], ticks.map(&:total).sort
35
+ assert_equal [1,1], ticks.map(&:count)
36
+ end
37
+
38
+ def test_tick_on_enumerable
39
+ a = (0..10)
40
+ b = a.with_progress(scope: "walrus").map(&:to_i)
41
+
42
+ ticks = Progressrus.scope(["walrus"]).values
43
+
44
+ assert_equal a.to_a, b
45
+ assert_equal 1, ticks.length
46
+ assert_equal a.size, ticks.first.total
47
+ assert_equal a.size, ticks.first.count
48
+ assert_instance_of Time, ticks.first.completed_at
49
+ end
50
+
51
+ def test_tick_on_enumerable_calls_fail_on_exception
52
+ a = (0..10)
53
+
54
+ assert_raises ArgumentError do
55
+ a.with_progress(scope: "walrus").each do
56
+ raise ArgumentError
57
+ end
58
+ end
59
+
60
+ ticks = Progressrus.scope(["walrus"]).values
61
+
62
+ assert_equal 0, ticks.first.count
63
+ assert_instance_of Time, ticks.first.failed_at
64
+ end
65
+ end
@@ -0,0 +1,377 @@
1
+ require_relative "test_helper"
2
+
3
+ class ProgressrusTest < Minitest::Unit::TestCase
4
+ def setup
5
+ @progress = Progressrus.new(scope: :progressrus, total: 100)
6
+ end
7
+
8
+ def teardown
9
+ Progressrus.stores.default!
10
+ end
11
+
12
+ def test_defaults_to_redis_store
13
+ assert_instance_of Progressrus::Store::Redis, Progressrus.stores.first
14
+ end
15
+
16
+ def test_add_to_store
17
+ Progressrus.stores << Progressrus::Store::Base.new
18
+ assert_instance_of Progressrus::Store::Base, Progressrus.stores[1]
19
+ end
20
+
21
+ def test_scope_should_initialize_with_symbol_or_string
22
+ progressrus = Progressrus.new(scope: :walrus)
23
+ assert_equal ['walrus'], progressrus.scope
24
+ end
25
+
26
+ def test_scope_should_initialize_with_array
27
+ progressrus = Progressrus.new(scope: ['walruses', 1])
28
+ assert_equal ['walruses', '1'], progressrus.scope
29
+ end
30
+
31
+ def test_initialize_with_name_should_use_name
32
+ progressrus = Progressrus.new(name: 'Wally')
33
+ assert_equal 'Wally', progressrus.name
34
+ end
35
+
36
+ def test_initialize_without_name_should_use_id
37
+ progressrus = Progressrus.new(id: 'oemg')
38
+ assert_equal 'oemg', progressrus.name
39
+ end
40
+
41
+ def test_initialize_with_persist
42
+ Progressrus.any_instance.expects(:persist).with(force: true).once
43
+ progressrus = Progressrus.new(persist: true)
44
+ end
45
+
46
+ def test_tick_should_set_started_at_if_not_already_set_and_tick_greater_than_zero
47
+ @progress.tick
48
+ assert @progress.started_at
49
+ end
50
+
51
+ def test_tick_should_not_set_started_at_if_zero_but_persist
52
+ @progress.expects(:persist).once
53
+ @progress.tick(0)
54
+ refute @progress.started_at
55
+ end
56
+
57
+ def test_tick_should_increment_count_by_one_if_not_specified
58
+ @progress.tick
59
+ assert_equal 1, @progress.count
60
+ end
61
+
62
+ def test_tick_should_increment_count
63
+ @progress.tick(50)
64
+ assert_equal 50, @progress.count
65
+ end
66
+
67
+ def test_error_should_not_call_tick
68
+ @progress.expects(:tick).never
69
+ @progress.error
70
+ end
71
+
72
+ def test_error_should_increment_error_count_by_one_if_amount_not_specified
73
+ @progress.error
74
+ assert_equal 1, @progress.error_count
75
+ end
76
+
77
+ def test_error_should_increment_error_count
78
+ @progress.error(25)
79
+ assert_equal 25, @progress.error_count
80
+ end
81
+
82
+ def test_eta_should_return_nil_if_no_count
83
+ progress = Progressrus.new
84
+ assert_equal nil, progress.eta
85
+ end
86
+
87
+ def test_eta_should_return_time_in_future_based_on_time_elapsed
88
+ time = Time.now
89
+ @progress.tick(10, now: time - 10)
90
+
91
+ eta = @progress.eta(now: time)
92
+
93
+ assert_equal time + 90, eta
94
+ assert_instance_of Time, eta
95
+ end
96
+
97
+ def test_percentage_should_return_the_percentage_as_a_fraction
98
+ @progress.tick(50)
99
+
100
+ assert_equal 0.5, @progress.percentage
101
+ end
102
+
103
+ def test_percentage_with_no_count_should_be_zero
104
+ assert_equal 0, @progress.percentage
105
+ end
106
+
107
+ def test_percentage_should_be_able_to_return_more_than_1
108
+ @progress.tick(120)
109
+
110
+ assert_equal 1.2, @progress.percentage
111
+ end
112
+
113
+ def test_percentage_should_be_0_if_total_0
114
+ assert_equal 0, @progress.percentage
115
+ end
116
+
117
+ def test_elapsed_should_return_the_delta_between_now_and_started_at
118
+ time = Time.now
119
+ @progress.tick(10, now: time - 10)
120
+
121
+ elapsed = @progress.elapsed(now: time)
122
+
123
+ assert_equal 10, elapsed
124
+ end
125
+
126
+ def test_to_serializeable_set_total_to_1_if_no_total
127
+ @progress.instance_variable_set(:@total, nil)
128
+ assert_equal 1, @progress.to_serializeable[:total]
129
+ end
130
+
131
+ def test_total_when_total_is_nil_is_1
132
+ @progress.instance_variable_set(:@total, nil)
133
+ assert_equal 1, @progress.total
134
+ end
135
+
136
+ def test_to_serializeable_should_return_a_hash_of_options
137
+ progress = Progressrus.new(
138
+ name: 'Wally',
139
+ id: 'oemg',
140
+ scope: ['walruses', 'forall'],
141
+ total: 100,
142
+ params: { job_id: 'oemg' }
143
+ )
144
+
145
+ serialization = {
146
+ name: 'Wally',
147
+ id: 'oemg',
148
+ scope: ['walruses', 'forall'],
149
+ total: 100,
150
+ params: { job_id: 'oemg' },
151
+ started_at: nil,
152
+ completed_at: nil,
153
+ failed_at: nil,
154
+ count: 0,
155
+ error_count: 0
156
+ }
157
+
158
+ assert_equal serialization, progress.to_serializeable
159
+ end
160
+
161
+ def test_complete_should_set_completed_at_and_persist
162
+ now = Time.now
163
+ @progress.expects(:persist)
164
+
165
+ @progress.complete(now: Time.now)
166
+
167
+ assert_equal now.to_i, @progress.completed_at.to_i
168
+ end
169
+
170
+ def test_should_not_be_able_to_set_total_to_0
171
+ assert_raises ArgumentError do
172
+ @progress.total = 0
173
+ end
174
+ end
175
+
176
+ def test_should_not_be_able_to_set_total_to_a_negative_number
177
+ assert_raises ArgumentError do
178
+ @progress.total = -1
179
+ end
180
+ end
181
+
182
+ def test_persist_yields_persist_to_each_store
183
+ mysql = mock()
184
+ mysql.expects(:persist).once
185
+
186
+ redis = Progressrus.stores.first
187
+ redis.expects(:persist).once
188
+
189
+ Progressrus.stores << mysql
190
+
191
+ @progress.tick
192
+ end
193
+
194
+ def test_should_not_be_able_to_initialize_with_total_0
195
+ assert_raises ArgumentError do
196
+ Progressrus.new(total: 0)
197
+ end
198
+ end
199
+
200
+ def test_should_not_be_able_to_initialize_with_total_as_a_negative_number
201
+ assert_raises ArgumentError do
202
+ Progressrus.new(total: -1)
203
+ end
204
+ end
205
+
206
+ def test_date_fields_should_deserialize_properly
207
+ @progress.tick(1)
208
+ @progress.complete
209
+ progress = Progressrus.find(@progress.scope, @progress.id)
210
+ assert_instance_of Time, progress.started_at
211
+ assert_instance_of Time, progress.completed_at
212
+ end
213
+
214
+
215
+ def test_default_scope_on_first
216
+ Progressrus.stores.clear
217
+
218
+ mysql = mock()
219
+ redis = mock()
220
+
221
+ Progressrus.stores << mysql
222
+ Progressrus.stores << redis
223
+
224
+ mysql.expects(:scope).once
225
+ redis.expects(:scope).never
226
+
227
+ Progressrus.scope(["oemg"])
228
+ end
229
+
230
+ def test_support_scope_last
231
+ Progressrus.stores.clear
232
+
233
+ mysql = mock()
234
+ redis = mock()
235
+
236
+ Progressrus.stores << mysql
237
+ Progressrus.stores << redis
238
+
239
+ mysql.expects(:scope).never
240
+ redis.expects(:scope).once
241
+
242
+ Progressrus.scope(["oemg"], store: :last)
243
+ end
244
+
245
+ def test_support_scope_by_name
246
+ Progressrus.stores.clear
247
+
248
+ mysql = mock()
249
+ redis = mock()
250
+
251
+ mysql.stubs(:name).returns(:mysql)
252
+ redis.stubs(:name).returns(:redis)
253
+
254
+ Progressrus.stores << mysql
255
+ Progressrus.stores << redis
256
+
257
+ mysql.expects(:scope).never
258
+ redis.expects(:scope).once
259
+
260
+ Progressrus.all(["oemg"], store: :redis)
261
+ end
262
+
263
+ def test_find_should_find_a_progressrus_by_scope_and_id
264
+ @progress.tick
265
+ progress = Progressrus.find(@progress.scope, @progress.id)
266
+
267
+ assert_instance_of Progressrus, progress
268
+ end
269
+
270
+ def test_completed_should_set_started_at_if_never_ticked
271
+ refute @progress.started_at
272
+ @progress.complete
273
+
274
+ assert_instance_of Time, @progress.started_at
275
+ assert_instance_of Time, @progress.completed_at
276
+ end
277
+
278
+ def test_able_to_set_count
279
+ @progress.count = 100
280
+ assert_equal 100, @progress.count
281
+ end
282
+
283
+ def test_call_persist_after_setting_count
284
+ @progress.expects(:persist).once
285
+
286
+ @progress.count = 100
287
+ end
288
+
289
+ def test_set_started_at_if_not_set
290
+ @progress.instance_variable_set(:@started_at, nil)
291
+ @progress.count = 100
292
+
293
+ assert_instance_of Time, @progress.started_at
294
+ end
295
+
296
+ def test_flush_should_flush_a_progressrus_by_scope_and_id
297
+ @progress.tick
298
+
299
+ Progressrus.flush(@progress.scope, @progress.id)
300
+
301
+ assert_nil Progressrus.find(@progress.scope, @progress.id)
302
+ end
303
+
304
+ def test_flush_should_flush_a_progressrus_scope_without_an_id
305
+ @progress.tick
306
+
307
+ Progressrus.flush(@progress.scope)
308
+
309
+ assert_equal({}, Progressrus.scope(@progress.scope))
310
+ end
311
+
312
+ def test_flush_instance_of_progressrus
313
+ @progress.tick
314
+
315
+ @progress.flush
316
+
317
+ assert_nil Progressrus.find(@progress.scope, @progress.id)
318
+ end
319
+
320
+ def test_call_with_progress_on_enumerable_as_final_in_chain
321
+ a = [1,2,3]
322
+ Progressrus.any_instance.expects(:tick).times(a.count)
323
+
324
+ b = []
325
+ a.each.with_progress do |number|
326
+ b << number
327
+ end
328
+
329
+ assert_equal a, b
330
+ end
331
+
332
+ def test_call_with_progress_on_enumerable_in_middle_of_chain
333
+ a = [1,2,3]
334
+ Progressrus.any_instance.expects(:tick).times(a.count)
335
+
336
+ b = a.each.with_progress.map { |number| number }
337
+
338
+ assert_equal a, b
339
+ end
340
+
341
+ def test_fail_should_set_failed_at_and_persist
342
+ now = Time.now
343
+ @progress.expects(:persist)
344
+
345
+ @progress.fail(now: now)
346
+
347
+ assert_equal now.to_i, @progress.failed_at.to_i
348
+ end
349
+
350
+ def test_status_returns_completed_when_job_is_completed
351
+ @progress.complete
352
+
353
+ assert_equal :completed, @progress.status
354
+ end
355
+
356
+ def test_status_returns_failed_when_job_has_failed
357
+ @progress.fail
358
+
359
+ assert_equal :failed, @progress.status
360
+ end
361
+
362
+ def test_status_returns_running_when_job_hasnt_failed_or_completed
363
+ @progress.tick
364
+
365
+ assert_equal :running, @progress.status
366
+ end
367
+
368
+ def test_status_returns_started_when_job_hasnt_ticked
369
+ assert_equal :started, @progress.status
370
+ end
371
+
372
+ def test_running_returns_true_when_job_has_ticked
373
+ @progress.tick
374
+
375
+ assert @progress.running?
376
+ end
377
+ end
@@ -0,0 +1,31 @@
1
+ require_relative "../test_helper"
2
+
3
+ class BaseStoreTest < Minitest::Unit::TestCase
4
+ def setup
5
+ @base = Progressrus::Store::Base.new
6
+ end
7
+
8
+ def test_persist_raises_not_implemented
9
+ assert_raises Progressrus::Store::NotImplementedError do
10
+ @base.persist(nil)
11
+ end
12
+ end
13
+
14
+ def test_scope_raises_not_implemented
15
+ assert_raises Progressrus::Store::NotImplementedError do
16
+ @base.scope(nil)
17
+ end
18
+ end
19
+
20
+ def test_find_raises_not_implemented
21
+ assert_raises Progressrus::Store::NotImplementedError do
22
+ @base.find(nil, nil)
23
+ end
24
+ end
25
+
26
+ def test_flush_raises_not_implemented
27
+ assert_raises Progressrus::Store::NotImplementedError do
28
+ @base.flush(nil)
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,100 @@
1
+ require_relative "../test_helper"
2
+
3
+ class RedisStoreTest < Minitest::Unit::TestCase
4
+ def setup
5
+ @scope = ["walrus", "1234"]
6
+ @progress = Progressrus.new(
7
+ scope: @scope,
8
+ id: "oemg",
9
+ total: 100,
10
+ name: "oemg-name"
11
+ )
12
+
13
+ @another_progress = Progressrus.new(
14
+ scope: @scope,
15
+ id: "oemg-two",
16
+ total: 100,
17
+ name: "oemg-name-two"
18
+ )
19
+
20
+ @store = Progressrus::Store::Redis.new(::Redis.new(host: ENV["PROGRESSRUS_REDIS_HOST"] || "localhost"))
21
+ end
22
+
23
+ def teardown
24
+ @store.flush(@scope)
25
+ end
26
+
27
+ def test_persist_should_set_key_value_if_outdated
28
+ @store.persist(@progress)
29
+
30
+ assert_equal 'oemg', @store.find(['walrus', '1234'], 'oemg').id
31
+ end
32
+
33
+ def test_persist_should_not_set_key_value_if_not_outdated
34
+ @store.redis.expects(:hset).once
35
+
36
+ @store.persist(@progress)
37
+ @store.persist(@progress)
38
+ end
39
+
40
+ def test_scope_should_return_progressruses_indexed_by_id
41
+ @store.persist(@progress)
42
+ @store.persist(@another_progress)
43
+ actual = @store.scope(@scope)
44
+
45
+ assert_equal @progress.id, actual['oemg'].id
46
+ assert_equal @another_progress.id, actual['oemg-two'].id
47
+ end
48
+
49
+ def test_scope_should_return_an_empty_hash_if_nothing_is_found
50
+ assert_equal({}, @store.scope(@scope))
51
+ end
52
+
53
+ def test_find_should_return_a_single_progressrus_for_scope_and_id
54
+ @store.persist(@progress)
55
+
56
+ assert_equal @progress.id, @store.find(@scope, 'oemg').id
57
+ end
58
+
59
+ def test_find_should_return_nil_if_nothing_is_found
60
+ assert_equal nil, @store.find(@scope, 'oemg')
61
+ end
62
+
63
+ def test_flush_should_delete_by_scope
64
+ @store.persist(@progress)
65
+ @store.persist(@another_progress)
66
+
67
+ @store.flush(@scope)
68
+
69
+ assert_equal({}, @store.scope(@scope))
70
+ end
71
+
72
+ def test_flush_should_delete_by_scope_and_id
73
+ @store.persist(@progress)
74
+ @store.persist(@another_progress)
75
+
76
+ @store.flush(@scope, 'oemg')
77
+
78
+ assert_equal nil, @store.find(@scope, 'oemg')
79
+ assert @store.find(@scope, 'oemg-two')
80
+ end
81
+
82
+ def test_initializes_name_to_redis
83
+ assert_equal :redis, @store.name
84
+ end
85
+
86
+ def test_persist_should_not_write_by_default
87
+ @store.redis.expects(:hset).once
88
+
89
+ @store.persist(@progress)
90
+ @store.persist(@progress)
91
+ end
92
+
93
+ def test_persist_should_write_if_forced
94
+ @store.redis.expects(:hset).twice
95
+
96
+ @store.persist(@progress)
97
+ @store.persist(@progress, force: true)
98
+ end
99
+
100
+ end
@@ -0,0 +1,13 @@
1
+ require_relative '../test_helper'
2
+
3
+ class StoreRakeTest < Minitest::Unit::TestCase
4
+ def setup
5
+ load File.expand_path("../../../tasks/redis_store.rake", __FILE__)
6
+ Rake::Task.define_task(:environment)
7
+ end
8
+
9
+ def test_store_flush_should_flush_the_store_with_mutli_key_scopes
10
+ Progressrus::Store::Redis.any_instance.expects(:flush).with([1, 'test'])
11
+ Rake::Task['progressrus:store:flush'].invoke(1,'test')
12
+ end
13
+ end
@@ -0,0 +1,11 @@
1
+ require 'coveralls'
2
+ Coveralls.wear!
3
+
4
+ require 'minitest/unit'
5
+ require 'minitest/autorun'
6
+ require "mocha/setup"
7
+ require 'pry'
8
+ require 'byebug'
9
+ require 'pry-byebug'
10
+
11
+ require_relative "../lib/progressrus.rb"
metadata ADDED
@@ -0,0 +1,170 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: progressrus
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Simon Eskildsen
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2014-12-09 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: redis
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '3.0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '3.0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: bundler
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '1.3'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '1.3'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rake
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: mocha
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '0.14'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '0.14'
69
+ - !ruby/object:Gem::Dependency
70
+ name: pry
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: byebug
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ - !ruby/object:Gem::Dependency
98
+ name: pry-byebug
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ version: '0'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ">="
109
+ - !ruby/object:Gem::Version
110
+ version: '0'
111
+ description: Monitor the progress of remote, long-running jobs.
112
+ email:
113
+ - sirup@sirupsen.com
114
+ executables: []
115
+ extensions: []
116
+ extra_rdoc_files: []
117
+ files:
118
+ - ".gitignore"
119
+ - ".travis.yml"
120
+ - Gemfile
121
+ - LICENSE.txt
122
+ - README.md
123
+ - Rakefile
124
+ - lib/progressrus.rb
125
+ - lib/progressrus/core_ext/enumerable.rb
126
+ - lib/progressrus/railtie.rb
127
+ - lib/progressrus/server.rb
128
+ - lib/progressrus/store.rb
129
+ - lib/progressrus/store/base.rb
130
+ - lib/progressrus/store/redis.rb
131
+ - lib/progressrus/version.rb
132
+ - progressrus.gemspec
133
+ - tasks/redis_store.rake
134
+ - test/integration_test.rb
135
+ - test/progressrus_test.rb
136
+ - test/store/base_test.rb
137
+ - test/store/redis_test.rb
138
+ - test/tasks/redis_store_rake_test.rb
139
+ - test/test_helper.rb
140
+ homepage: https://github.com/Sirupsen/progressrus
141
+ licenses:
142
+ - MIT
143
+ metadata: {}
144
+ post_install_message:
145
+ rdoc_options: []
146
+ require_paths:
147
+ - lib
148
+ required_ruby_version: !ruby/object:Gem::Requirement
149
+ requirements:
150
+ - - ">="
151
+ - !ruby/object:Gem::Version
152
+ version: '0'
153
+ required_rubygems_version: !ruby/object:Gem::Requirement
154
+ requirements:
155
+ - - ">="
156
+ - !ruby/object:Gem::Version
157
+ version: '0'
158
+ requirements: []
159
+ rubyforge_project:
160
+ rubygems_version: 2.2.2
161
+ signing_key:
162
+ specification_version: 4
163
+ summary: Monitor the progress of remote, long-running jobs.
164
+ test_files:
165
+ - test/integration_test.rb
166
+ - test/progressrus_test.rb
167
+ - test/store/base_test.rb
168
+ - test/store/redis_test.rb
169
+ - test/tasks/redis_store_rake_test.rb
170
+ - test/test_helper.rb