progressrus 0.0.1

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: 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