tabstabs 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gitignore +20 -0
- data/.ruby-version +1 -0
- data/.travis.yml +7 -0
- data/Gemfile +3 -0
- data/LICENSE.txt +22 -0
- data/README.md +421 -0
- data/Rakefile +5 -0
- data/lib/tabs_tabs.rb +26 -0
- data/lib/tabs_tabs/config.rb +65 -0
- data/lib/tabs_tabs/helpers.rb +27 -0
- data/lib/tabs_tabs/metrics/counter.rb +69 -0
- data/lib/tabs_tabs/metrics/counter/stats.rb +51 -0
- data/lib/tabs_tabs/metrics/task.rb +72 -0
- data/lib/tabs_tabs/metrics/task/token.rb +89 -0
- data/lib/tabs_tabs/metrics/value.rb +91 -0
- data/lib/tabs_tabs/metrics/value/stats.rb +55 -0
- data/lib/tabs_tabs/resolution.rb +65 -0
- data/lib/tabs_tabs/resolutionable.rb +48 -0
- data/lib/tabs_tabs/resolutions/day.rb +40 -0
- data/lib/tabs_tabs/resolutions/hour.rb +40 -0
- data/lib/tabs_tabs/resolutions/minute.rb +40 -0
- data/lib/tabs_tabs/resolutions/month.rb +40 -0
- data/lib/tabs_tabs/resolutions/week.rb +40 -0
- data/lib/tabs_tabs/resolutions/year.rb +40 -0
- data/lib/tabs_tabs/storage.rb +105 -0
- data/lib/tabs_tabs/tabs_tabs.rb +117 -0
- data/lib/tabs_tabs/version.rb +3 -0
- data/spec/lib/tabs_tabs/config_spec.rb +60 -0
- data/spec/lib/tabs_tabs/metrics/counter/stats_spec.rb +42 -0
- data/spec/lib/tabs_tabs/metrics/counter_spec.rb +196 -0
- data/spec/lib/tabs_tabs/metrics/task/token_spec.rb +18 -0
- data/spec/lib/tabs_tabs/metrics/task_spec.rb +103 -0
- data/spec/lib/tabs_tabs/metrics/value/stats_spec.rb +61 -0
- data/spec/lib/tabs_tabs/metrics/value_spec.rb +160 -0
- data/spec/lib/tabs_tabs/resolution_spec.rb +52 -0
- data/spec/lib/tabs_tabs/resolutionable_spec.rb +53 -0
- data/spec/lib/tabs_tabs/resolutions/day_spec.rb +23 -0
- data/spec/lib/tabs_tabs/resolutions/hour_spec.rb +23 -0
- data/spec/lib/tabs_tabs/resolutions/minute_spec.rb +23 -0
- data/spec/lib/tabs_tabs/resolutions/month_spec.rb +23 -0
- data/spec/lib/tabs_tabs/resolutions/week_spec.rb +24 -0
- data/spec/lib/tabs_tabs/resolutions/year_spec.rb +23 -0
- data/spec/lib/tabs_tabs/storage_spec.rb +138 -0
- data/spec/lib/tabs_tabs_spec.rb +223 -0
- data/spec/spec_helper.rb +17 -0
- data/spec/support/custom_resolutions.rb +40 -0
- data/tabs_tabs.gemspec +31 -0
- metadata +213 -0
@@ -0,0 +1,117 @@
|
|
1
|
+
module TabsTabs
|
2
|
+
extend self
|
3
|
+
extend TabsTabs::Storage
|
4
|
+
|
5
|
+
class UnknownTypeError < StandardError; end
|
6
|
+
class DuplicateMetricError < StandardError; end
|
7
|
+
class UnknownMetricError < StandardError; end
|
8
|
+
class MetricTypeMismatchError < StandardError; end
|
9
|
+
class ResolutionMissingError < StandardError; end
|
10
|
+
|
11
|
+
METRIC_TYPES = ["counter", "value", "task"]
|
12
|
+
|
13
|
+
def configure
|
14
|
+
yield(Config)
|
15
|
+
end
|
16
|
+
|
17
|
+
def redis
|
18
|
+
Config.redis
|
19
|
+
end
|
20
|
+
|
21
|
+
def config
|
22
|
+
Config
|
23
|
+
end
|
24
|
+
|
25
|
+
def increment_counter(key, timestamp=Time.now)
|
26
|
+
create_metric(key, "counter") unless metric_exists?(key)
|
27
|
+
raise MetricTypeMismatchError.new("Only counter metrics can be incremented") unless metric_type(key) == "counter"
|
28
|
+
get_metric(key).increment(timestamp)
|
29
|
+
end
|
30
|
+
|
31
|
+
def record_value(key, value, timestamp=Time.now)
|
32
|
+
create_metric(key, "value") unless metric_exists?(key)
|
33
|
+
raise MetricTypeMismatchError.new("Only value metrics can record a value") unless metric_type(key) == "value"
|
34
|
+
get_metric(key).record(value, timestamp)
|
35
|
+
end
|
36
|
+
|
37
|
+
def start_task(key, token, timestamp=Time.now)
|
38
|
+
create_metric(key, "task")
|
39
|
+
raise MetricTypeMismatchError.new("Only task metrics can start a task") unless metric_type(key) == "task"
|
40
|
+
get_metric(key).start(token, timestamp)
|
41
|
+
end
|
42
|
+
|
43
|
+
def complete_task(key, token, timestamp=Time.now)
|
44
|
+
raise MetricTypeMismatchError.new("Only task metrics can complete a task") unless metric_type(key) == "task"
|
45
|
+
get_metric(key).complete(token, timestamp)
|
46
|
+
end
|
47
|
+
|
48
|
+
def create_metric(key, type)
|
49
|
+
raise UnknownTypeError.new("Unknown metric type: #{type}") unless METRIC_TYPES.include?(type)
|
50
|
+
raise DuplicateMetricError.new("Metric already exists: #{key}") if metric_exists?(key)
|
51
|
+
hset "metrics", key, type
|
52
|
+
metric_klass(type).new(key)
|
53
|
+
end
|
54
|
+
|
55
|
+
def get_metric(key)
|
56
|
+
raise UnknownMetricError.new("Unknown metric: #{key}") unless metric_exists?(key)
|
57
|
+
type = hget("metrics", key)
|
58
|
+
metric_klass(type).new(key)
|
59
|
+
end
|
60
|
+
|
61
|
+
def counter_total(key)
|
62
|
+
unless metric_exists?(key)
|
63
|
+
if block_given?
|
64
|
+
return yield
|
65
|
+
else
|
66
|
+
raise UnknownMetricError.new("Unknown metric: #{key}")
|
67
|
+
end
|
68
|
+
end
|
69
|
+
raise MetricTypeMismatchError.new("Only counter metrics can be incremented") unless metric_type(key) == "counter"
|
70
|
+
get_metric(key).total
|
71
|
+
end
|
72
|
+
|
73
|
+
def get_stats(key, period, resolution)
|
74
|
+
raise UnknownMetricError.new("Unknown metric: #{key}") unless metric_exists?(key)
|
75
|
+
metric = get_metric(key)
|
76
|
+
metric.stats(period, resolution)
|
77
|
+
end
|
78
|
+
|
79
|
+
def metric_type(key)
|
80
|
+
raise UnknownMetricError.new("Unknown metric: #{key}") unless metric_exists?(key)
|
81
|
+
hget "metrics", key
|
82
|
+
end
|
83
|
+
|
84
|
+
def list_metrics
|
85
|
+
hkeys "metrics"
|
86
|
+
end
|
87
|
+
|
88
|
+
def metric_exists?(key)
|
89
|
+
list_metrics.include? key
|
90
|
+
end
|
91
|
+
|
92
|
+
def drop_metric!(key)
|
93
|
+
raise UnknownMetricError.new("Unknown metric: #{key}") unless metric_exists?(key)
|
94
|
+
metric = get_metric(key)
|
95
|
+
metric.drop!
|
96
|
+
hdel "metrics", key
|
97
|
+
end
|
98
|
+
|
99
|
+
def drop_all_metrics!
|
100
|
+
metrics = self.list_metrics
|
101
|
+
metrics.each { |key| self.drop_metric! key }
|
102
|
+
end
|
103
|
+
|
104
|
+
def drop_resolution_for_metric!(key, resolution)
|
105
|
+
raise UnknownMetricError.new("Unknown metric: #{key}") unless metric_exists?(key)
|
106
|
+
raise ResolutionMissingError.new(resolution) unless TabsTabs::Resolution.all.include? resolution
|
107
|
+
metric = get_metric(key)
|
108
|
+
metric.drop_by_resolution!(resolution) unless metric_type(key) == "task"
|
109
|
+
end
|
110
|
+
|
111
|
+
private
|
112
|
+
|
113
|
+
def metric_klass(type)
|
114
|
+
"TabsTabs::Metrics::#{type.classify}".constantize
|
115
|
+
end
|
116
|
+
|
117
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
require File.expand_path("../../../support/custom_resolutions", __FILE__)
|
3
|
+
|
4
|
+
describe TabsTabs::Config do
|
5
|
+
context "#decimal_precision" do
|
6
|
+
|
7
|
+
before do
|
8
|
+
@precision = TabsTabs::Config.decimal_precision
|
9
|
+
end
|
10
|
+
|
11
|
+
after do
|
12
|
+
TabsTabs::Config.decimal_precision = @precision
|
13
|
+
end
|
14
|
+
|
15
|
+
it "should set/get the decimal precision" do
|
16
|
+
TabsTabs::Config.decimal_precision = 4
|
17
|
+
expect(TabsTabs::Config.decimal_precision).to eq(4)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
context "#register_resolution" do
|
22
|
+
it "should register a resolution" do
|
23
|
+
TabsTabs::Resolution.register(WellFormedResolution)
|
24
|
+
expect(TabsTabs::Resolution.all).to include(:seconds)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
context "#unregister_resolution" do
|
29
|
+
it "should unregister a resolution" do
|
30
|
+
TabsTabs::Resolution.unregister(:minute)
|
31
|
+
expect(TabsTabs::Resolution.all).to_not include(:minute)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
context "#set_expirations" do
|
36
|
+
|
37
|
+
after do
|
38
|
+
TabsTabs::Config.reset_expirations
|
39
|
+
end
|
40
|
+
|
41
|
+
it "should allow multiple resolutions to be expired" do
|
42
|
+
TabsTabs::Config.set_expirations({minute: 1.day, hour: 1.week })
|
43
|
+
expect(TabsTabs::Config.expiration_settings[:minute]).to eq(1.day)
|
44
|
+
expect(TabsTabs::Config.expiration_settings[:hour]).to eq(1.week)
|
45
|
+
end
|
46
|
+
|
47
|
+
it "should raise ResolutionMissingError if expiration passed in for invalid resolution" do
|
48
|
+
expect{ TabsTabs::Config.set_expirations({missing_resolution: 1.day }) }
|
49
|
+
.to raise_error(TabsTabs::ResolutionMissingError)
|
50
|
+
end
|
51
|
+
|
52
|
+
end
|
53
|
+
|
54
|
+
context "#prefix" do
|
55
|
+
it "should allow custom prefix for tabstabs keys" do
|
56
|
+
TabsTabs::Config.prefix = "rspec"
|
57
|
+
expect(TabsTabs::Config.prefix).to eq("rspec")
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
describe TabsTabs::Metrics::Counter::Stats do
|
4
|
+
|
5
|
+
let(:period) { (Time.now - 2.days..Time.now) }
|
6
|
+
let(:resolution) { :hour }
|
7
|
+
let(:values) do
|
8
|
+
[
|
9
|
+
{ "timestamp" => Time.now - 30.hours, "count" => 44 },
|
10
|
+
{ "timestamp" => Time.now - 20.hours, "count" => 123 },
|
11
|
+
{ "timestamp" => Time.now - 10.hours, "count" => 92 }
|
12
|
+
]
|
13
|
+
end
|
14
|
+
let(:stats) { TabsTabs::Metrics::Counter::Stats.new(period, resolution, values) }
|
15
|
+
|
16
|
+
it "is enumerable" do
|
17
|
+
expect(stats).to respond_to :each
|
18
|
+
expect(TabsTabs::Metrics::Counter::Stats.ancestors).to include Enumerable
|
19
|
+
end
|
20
|
+
|
21
|
+
it "#total returns the total count for the entire set" do
|
22
|
+
expect(stats.total).to eq 259
|
23
|
+
end
|
24
|
+
|
25
|
+
it "min returns the min for the entire set" do
|
26
|
+
expect(stats.min).to eq 44
|
27
|
+
end
|
28
|
+
|
29
|
+
it "max returns the max for the entire set" do
|
30
|
+
expect(stats.max).to eq 123
|
31
|
+
end
|
32
|
+
|
33
|
+
it "avg returns the average for the entire set" do
|
34
|
+
expect(stats.avg).to eq 86.33333
|
35
|
+
end
|
36
|
+
|
37
|
+
it "avg returns 0 if values empty" do
|
38
|
+
stats = TabsTabs::Metrics::Counter::Stats.new(period, resolution, [])
|
39
|
+
expect(stats.avg).to be_zero
|
40
|
+
end
|
41
|
+
|
42
|
+
end
|
@@ -0,0 +1,196 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
describe TabsTabs::Metrics::Counter do
|
4
|
+
|
5
|
+
include TabsTabs::Storage
|
6
|
+
|
7
|
+
let(:metric) { TabsTabs.create_metric("foo", "counter") }
|
8
|
+
let(:now) { Time.utc(2000, 1, 1, 0, 0) }
|
9
|
+
|
10
|
+
describe "incrementing stats" do
|
11
|
+
|
12
|
+
before { Timecop.freeze(now) }
|
13
|
+
|
14
|
+
it "increments the value for the expected periods" do
|
15
|
+
metric.increment
|
16
|
+
time = Time.utc(now.year, now.month, now.day, now.hour)
|
17
|
+
stats = metric.stats(((now - 2.hours)..(now + 4.hours)), :hour)
|
18
|
+
expect(stats).to include({ "timestamp" => time, "count" => 1 })
|
19
|
+
end
|
20
|
+
|
21
|
+
it "applys the increment to the specified timestamp if one is supplied" do
|
22
|
+
time = Time.utc(now.year, now.month, now.day, now.hour) - 2.hours
|
23
|
+
metric.increment(time)
|
24
|
+
stats = metric.stats(((now - 3.hours)..now), :hour)
|
25
|
+
expect(stats).to include({ "timestamp" => time, "count" => 1 })
|
26
|
+
end
|
27
|
+
|
28
|
+
it "raises ResolutionMissingError if unregistered resolution requested" do
|
29
|
+
time = Time.utc(now.year, now.month, now.day, now.hour) - 2.hours
|
30
|
+
metric.increment(time)
|
31
|
+
TabsTabs::Resolution.unregister(:hour)
|
32
|
+
expect { metric.stats(((now - 3.hours)..now), :hour) }.to raise_error(TabsTabs::ResolutionMissingError)
|
33
|
+
end
|
34
|
+
|
35
|
+
end
|
36
|
+
|
37
|
+
describe "total count" do
|
38
|
+
|
39
|
+
it "is incremented every time regardless of resolution" do
|
40
|
+
30.times { metric.increment }
|
41
|
+
expect(metric.total).to eq(30)
|
42
|
+
end
|
43
|
+
|
44
|
+
end
|
45
|
+
|
46
|
+
describe "retrieving stats" do
|
47
|
+
|
48
|
+
before do
|
49
|
+
Timecop.freeze(now)
|
50
|
+
end
|
51
|
+
|
52
|
+
after do
|
53
|
+
Timecop.return
|
54
|
+
end
|
55
|
+
|
56
|
+
def create_span(time_unit)
|
57
|
+
metric.increment
|
58
|
+
Timecop.freeze(now + 1.send(time_unit))
|
59
|
+
metric.increment
|
60
|
+
Timecop.freeze(now + 3.send(time_unit))
|
61
|
+
metric.increment
|
62
|
+
Timecop.freeze(now + 6.send(time_unit))
|
63
|
+
metric.increment
|
64
|
+
metric.increment
|
65
|
+
Timecop.freeze(now)
|
66
|
+
end
|
67
|
+
|
68
|
+
it "returns the expected results for an minutely metric" do
|
69
|
+
create_span(:minute)
|
70
|
+
stats = metric.stats(now..(now + 7.minutes), :minute)
|
71
|
+
expect(stats).to include({ "timestamp" => (now + 3.minutes), "count" => 1 })
|
72
|
+
expect(stats).to include({ "timestamp" => (now + 6.minutes), "count" => 2 })
|
73
|
+
end
|
74
|
+
|
75
|
+
it "returns the expected results for an hourly metric" do
|
76
|
+
create_span(:hours)
|
77
|
+
stats = metric.stats(now..(now + 7.hours), :hour)
|
78
|
+
expect(stats).to include({ "timestamp" => (now + 3.hours), "count" => 1 })
|
79
|
+
expect(stats).to include({ "timestamp" => (now + 6.hours), "count" => 2 })
|
80
|
+
end
|
81
|
+
|
82
|
+
it "returns the expected results for a daily metric" do
|
83
|
+
create_span(:days)
|
84
|
+
stats = metric.stats(now..(now + 7.days), :day)
|
85
|
+
expect(stats).to include({ "timestamp" => (now + 3.days), "count" => 1 })
|
86
|
+
expect(stats).to include({ "timestamp" => (now + 6.days), "count" => 2 })
|
87
|
+
end
|
88
|
+
|
89
|
+
it "returns the expected results for a monthly metric" do
|
90
|
+
create_span(:months)
|
91
|
+
stats = metric.stats(now..(now + 7.months), :month)
|
92
|
+
expect(stats).to include({ "timestamp" => (now + 3.months), "count" => 1 })
|
93
|
+
expect(stats).to include({ "timestamp" => (now + 6.months), "count" => 2 })
|
94
|
+
end
|
95
|
+
|
96
|
+
it "returns the expected results for a yearly metric" do
|
97
|
+
create_span(:years)
|
98
|
+
stats = metric.stats(now..(now + 7.years), :year)
|
99
|
+
expect(stats).to include({ "timestamp" => (now + 3.years), "count" => 1 })
|
100
|
+
expect(stats).to include({ "timestamp" => (now + 6.years), "count" => 2 })
|
101
|
+
end
|
102
|
+
|
103
|
+
it "returns zeros for time periods which do not have any events" do
|
104
|
+
create_span(:days)
|
105
|
+
stats = metric.stats(now..(now + 7.days), :day)
|
106
|
+
expect(stats.detect{|s| s["timestamp"] == (now + 2.day)}["count"]).to eq(0)
|
107
|
+
end
|
108
|
+
|
109
|
+
context "for weekly metrics" do
|
110
|
+
|
111
|
+
let(:period) do
|
112
|
+
(now - 2.days)..((now + 7.weeks) + 2.days)
|
113
|
+
end
|
114
|
+
|
115
|
+
it "returns the expected results for a weekly metric" do
|
116
|
+
create_span(:weeks)
|
117
|
+
stats = metric.stats(period, :week)
|
118
|
+
expect(stats.detect{|s| s["timestamp"] == (now + 1.week).beginning_of_week}["count"]).to eq(1)
|
119
|
+
expect(stats).to include({ "timestamp" => (now + 3.weeks).beginning_of_week, "count" => 1 })
|
120
|
+
expect(stats).to include({ "timestamp" => (now + 6.weeks).beginning_of_week, "count" => 2 })
|
121
|
+
end
|
122
|
+
|
123
|
+
it "normalizes the period to the first day of the week" do
|
124
|
+
create_span(:weeks)
|
125
|
+
stats = metric.stats(period, :week)
|
126
|
+
expect(stats.first["timestamp"]).to eq(period.first.beginning_of_week)
|
127
|
+
expect(stats.last["timestamp"]).to eq(period.last.beginning_of_week)
|
128
|
+
end
|
129
|
+
|
130
|
+
end
|
131
|
+
|
132
|
+
end
|
133
|
+
|
134
|
+
describe ".drop!" do
|
135
|
+
|
136
|
+
before do
|
137
|
+
3.times { metric.increment }
|
138
|
+
expect(exists("stat:counter:foo:total")).to be_truthy
|
139
|
+
@count_keys = (TabsTabs::Resolution.all.map do |res|
|
140
|
+
smembers("stat:counter:foo:keys:#{res}")
|
141
|
+
end).flatten
|
142
|
+
metric.drop!
|
143
|
+
end
|
144
|
+
|
145
|
+
it "deletes the counter total key" do
|
146
|
+
expect(exists("stat:counter:foo:total")).to be_falsey
|
147
|
+
end
|
148
|
+
|
149
|
+
it "deletes all resolution count keys" do
|
150
|
+
@count_keys.each do |key|
|
151
|
+
expect(exists(key)).to be_falsey
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
155
|
+
it "deletes all resolution key collection keys" do
|
156
|
+
TabsTabs::Resolution.all.each do |res|
|
157
|
+
expect(exists("stat:counter:foo:keys:#{res}")).to be_falsey
|
158
|
+
end
|
159
|
+
end
|
160
|
+
|
161
|
+
end
|
162
|
+
|
163
|
+
describe ".drop_by_resolution!" do
|
164
|
+
before do
|
165
|
+
Timecop.freeze(now)
|
166
|
+
2.times { metric.increment }
|
167
|
+
metric.drop_by_resolution!(:minute)
|
168
|
+
end
|
169
|
+
|
170
|
+
it "deletes all metrics for a resolution" do
|
171
|
+
stats = metric.stats((now - 1.minute)..(now + 1.minute), :minute)
|
172
|
+
expect(stats.total).to eq(0)
|
173
|
+
end
|
174
|
+
end
|
175
|
+
|
176
|
+
describe "expiration of counter metrics" do
|
177
|
+
let(:expires_setting){ 6.hours }
|
178
|
+
let(:now){ Time.utc(2050, 1, 1, 0, 0) }
|
179
|
+
|
180
|
+
before do
|
181
|
+
TabsTabs::Config.set_expirations({minute: expires_setting })
|
182
|
+
end
|
183
|
+
|
184
|
+
after do
|
185
|
+
TabsTabs::Config.reset_expirations
|
186
|
+
end
|
187
|
+
|
188
|
+
it "sets an expiration when recording a value" do
|
189
|
+
metric.increment(now)
|
190
|
+
redis_expire_date = Time.now + TabsTabs::Storage.ttl(metric.storage_key(:minute, now))
|
191
|
+
expire_date = now + expires_setting + TabsTabs::Resolutions::Minute.to_seconds
|
192
|
+
expect(redis_expire_date).to be_within(2.seconds).of(expire_date)
|
193
|
+
end
|
194
|
+
end
|
195
|
+
|
196
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
describe TabsTabs::Metrics::Task::Token do
|
4
|
+
|
5
|
+
describe "#time_elapsed" do
|
6
|
+
|
7
|
+
let(:token) { TabsTabs::Metrics::Task::Token.new("foo", "bar") }
|
8
|
+
let(:time) { Time.now }
|
9
|
+
|
10
|
+
it "should return the time between when the task/token started and completed" do
|
11
|
+
token.start(time - 2.days)
|
12
|
+
token.complete(time - 1.day)
|
13
|
+
expect(token.time_elapsed(:hour)).to eq(24)
|
14
|
+
end
|
15
|
+
|
16
|
+
end
|
17
|
+
|
18
|
+
end
|
@@ -0,0 +1,103 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
describe TabsTabs::Metrics::Task do
|
4
|
+
|
5
|
+
let(:metric) { TabsTabs.create_metric("foo", "task") }
|
6
|
+
let(:now) { Time.utc(2000, 1, 1, 0, 0) }
|
7
|
+
let(:token_1) { "aaaa" }
|
8
|
+
let(:token_2) { "bbbb" }
|
9
|
+
let(:token_3) { "cccc" }
|
10
|
+
|
11
|
+
describe ".start" do
|
12
|
+
|
13
|
+
let(:token) { double(:token) }
|
14
|
+
let(:time) { Time.now }
|
15
|
+
|
16
|
+
it "calls start on the given token" do
|
17
|
+
expect(TabsTabs::Metrics::Task::Token).to receive(:new).with(token_1, "foo").and_return(token)
|
18
|
+
expect(token).to receive(:start)
|
19
|
+
metric.start(token_1)
|
20
|
+
end
|
21
|
+
|
22
|
+
it "passes through the specified timestamp" do
|
23
|
+
allow(TabsTabs::Metrics::Task::Token).to receive(:new).and_return(token)
|
24
|
+
expect(token).to receive(:start).with(time)
|
25
|
+
metric.start(token_1, time)
|
26
|
+
end
|
27
|
+
|
28
|
+
end
|
29
|
+
|
30
|
+
describe ".complete" do
|
31
|
+
|
32
|
+
let(:token) { double(:token) }
|
33
|
+
let(:time) { Time.now }
|
34
|
+
|
35
|
+
it "calls complete on the given token" do
|
36
|
+
expect(TabsTabs::Metrics::Task::Token).to receive(:new).with(token_1, "foo").and_return(token)
|
37
|
+
expect(token).to receive(:complete)
|
38
|
+
metric.complete(token_1)
|
39
|
+
end
|
40
|
+
|
41
|
+
it "passes through the specified timestamp" do
|
42
|
+
allow(TabsTabs::Metrics::Task::Token).to receive(:new).and_return(token)
|
43
|
+
expect(token).to receive(:complete).with(time)
|
44
|
+
metric.complete(token_1, time)
|
45
|
+
end
|
46
|
+
|
47
|
+
it "raises an UnstartedTaskMetricError if the metric has not yet been started" do
|
48
|
+
expect { metric.complete("foobar") }.to raise_error(TabsTabs::Metrics::Task::UnstartedTaskMetricError)
|
49
|
+
end
|
50
|
+
|
51
|
+
end
|
52
|
+
|
53
|
+
describe ".stats" do
|
54
|
+
|
55
|
+
it "returns zeroes across the board for no stats" do
|
56
|
+
stats = metric.stats((now - 5.minutes)..(now + 5.minutes), :minute)
|
57
|
+
|
58
|
+
expect(stats.started_within_period).to eq 0
|
59
|
+
expect(stats.completed_within_period).to eq 0
|
60
|
+
expect(stats.started_and_completed_within_period).to eq 0
|
61
|
+
expect(stats.completion_rate).to eq 0.0
|
62
|
+
expect(stats.average_completion_time).to eq 0.0
|
63
|
+
end
|
64
|
+
|
65
|
+
it "returns the expected value" do
|
66
|
+
Timecop.freeze(now)
|
67
|
+
metric.start(token_1)
|
68
|
+
metric.start(token_2)
|
69
|
+
Timecop.freeze(now + 2.minutes)
|
70
|
+
metric.complete(token_1)
|
71
|
+
metric.start(token_3)
|
72
|
+
Timecop.freeze(now + 3.minutes)
|
73
|
+
metric.complete(token_3)
|
74
|
+
stats = metric.stats((now - 5.minutes)..(now + 5.minutes), :minute)
|
75
|
+
|
76
|
+
expect(stats.started_within_period).to eq 3
|
77
|
+
expect(stats.completed_within_period).to eq 2
|
78
|
+
expect(stats.started_and_completed_within_period).to eq 2
|
79
|
+
expect(stats.completion_rate).to eq 0.18182
|
80
|
+
expect(stats.average_completion_time).to eq 1.5
|
81
|
+
end
|
82
|
+
|
83
|
+
it "returns the expected value for a week" do
|
84
|
+
Timecop.freeze(now)
|
85
|
+
metric.start(token_1)
|
86
|
+
metric.start(token_2)
|
87
|
+
Timecop.freeze(now + 1.week)
|
88
|
+
metric.complete(token_1)
|
89
|
+
metric.start(token_3)
|
90
|
+
Timecop.freeze(now + 3.weeks)
|
91
|
+
metric.complete(token_3)
|
92
|
+
stats = metric.stats((now - 5.weeks)..(now + 5.weeks), :week)
|
93
|
+
|
94
|
+
expect(stats.started_within_period).to eq 3
|
95
|
+
expect(stats.completed_within_period).to eq 2
|
96
|
+
expect(stats.started_and_completed_within_period).to eq 2
|
97
|
+
expect(stats.completion_rate).to eq 0.18182
|
98
|
+
expect(stats.average_completion_time).to eq 1.5
|
99
|
+
end
|
100
|
+
|
101
|
+
end
|
102
|
+
|
103
|
+
end
|