pulse_meter_core 0.4.13

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.
Files changed (81) hide show
  1. data/.gitignore +19 -0
  2. data/.rbenv-version +1 -0
  3. data/.rspec +1 -0
  4. data/.rvmrc +1 -0
  5. data/.travis.yml +8 -0
  6. data/Gemfile +2 -0
  7. data/LICENSE +22 -0
  8. data/README.md +40 -0
  9. data/Rakefile +20 -0
  10. data/lib/pulse_meter/command_aggregator/async.rb +83 -0
  11. data/lib/pulse_meter/command_aggregator/sync.rb +18 -0
  12. data/lib/pulse_meter/command_aggregator/udp.rb +48 -0
  13. data/lib/pulse_meter/mixins/dumper.rb +87 -0
  14. data/lib/pulse_meter/mixins/utils.rb +155 -0
  15. data/lib/pulse_meter/observer.rb +118 -0
  16. data/lib/pulse_meter/observer/extended.rb +32 -0
  17. data/lib/pulse_meter/sensor.rb +61 -0
  18. data/lib/pulse_meter/sensor/base.rb +88 -0
  19. data/lib/pulse_meter/sensor/configuration.rb +106 -0
  20. data/lib/pulse_meter/sensor/counter.rb +39 -0
  21. data/lib/pulse_meter/sensor/hashed_counter.rb +36 -0
  22. data/lib/pulse_meter/sensor/hashed_indicator.rb +24 -0
  23. data/lib/pulse_meter/sensor/indicator.rb +35 -0
  24. data/lib/pulse_meter/sensor/multi.rb +97 -0
  25. data/lib/pulse_meter/sensor/timeline.rb +236 -0
  26. data/lib/pulse_meter/sensor/timeline_reduce.rb +68 -0
  27. data/lib/pulse_meter/sensor/timelined/average.rb +32 -0
  28. data/lib/pulse_meter/sensor/timelined/counter.rb +23 -0
  29. data/lib/pulse_meter/sensor/timelined/hashed_counter.rb +31 -0
  30. data/lib/pulse_meter/sensor/timelined/hashed_indicator.rb +30 -0
  31. data/lib/pulse_meter/sensor/timelined/indicator.rb +23 -0
  32. data/lib/pulse_meter/sensor/timelined/max.rb +19 -0
  33. data/lib/pulse_meter/sensor/timelined/median.rb +14 -0
  34. data/lib/pulse_meter/sensor/timelined/min.rb +19 -0
  35. data/lib/pulse_meter/sensor/timelined/multi_percentile.rb +34 -0
  36. data/lib/pulse_meter/sensor/timelined/percentile.rb +22 -0
  37. data/lib/pulse_meter/sensor/timelined/uniq_counter.rb +22 -0
  38. data/lib/pulse_meter/sensor/timelined/zset_based.rb +37 -0
  39. data/lib/pulse_meter/sensor/uniq_counter.rb +24 -0
  40. data/lib/pulse_meter/server.rb +0 -0
  41. data/lib/pulse_meter/server/command_line_options.rb +0 -0
  42. data/lib/pulse_meter/server/config_options.rb +0 -0
  43. data/lib/pulse_meter/server/sensors.rb +0 -0
  44. data/lib/pulse_meter/udp_server.rb +45 -0
  45. data/lib/pulse_meter_core.rb +66 -0
  46. data/pulse_meter_core.gemspec +33 -0
  47. data/spec/pulse_meter/command_aggregator/async_spec.rb +53 -0
  48. data/spec/pulse_meter/command_aggregator/sync_spec.rb +25 -0
  49. data/spec/pulse_meter/command_aggregator/udp_spec.rb +45 -0
  50. data/spec/pulse_meter/mixins/dumper_spec.rb +162 -0
  51. data/spec/pulse_meter/mixins/utils_spec.rb +212 -0
  52. data/spec/pulse_meter/observer/extended_spec.rb +92 -0
  53. data/spec/pulse_meter/observer_spec.rb +207 -0
  54. data/spec/pulse_meter/sensor/base_spec.rb +106 -0
  55. data/spec/pulse_meter/sensor/configuration_spec.rb +103 -0
  56. data/spec/pulse_meter/sensor/counter_spec.rb +54 -0
  57. data/spec/pulse_meter/sensor/hashed_counter_spec.rb +43 -0
  58. data/spec/pulse_meter/sensor/hashed_indicator_spec.rb +39 -0
  59. data/spec/pulse_meter/sensor/indicator_spec.rb +43 -0
  60. data/spec/pulse_meter/sensor/multi_spec.rb +137 -0
  61. data/spec/pulse_meter/sensor/timeline_spec.rb +88 -0
  62. data/spec/pulse_meter/sensor/timelined/average_spec.rb +6 -0
  63. data/spec/pulse_meter/sensor/timelined/counter_spec.rb +6 -0
  64. data/spec/pulse_meter/sensor/timelined/hashed_counter_spec.rb +8 -0
  65. data/spec/pulse_meter/sensor/timelined/hashed_indicator_spec.rb +8 -0
  66. data/spec/pulse_meter/sensor/timelined/indicator_spec.rb +6 -0
  67. data/spec/pulse_meter/sensor/timelined/max_spec.rb +7 -0
  68. data/spec/pulse_meter/sensor/timelined/median_spec.rb +7 -0
  69. data/spec/pulse_meter/sensor/timelined/min_spec.rb +7 -0
  70. data/spec/pulse_meter/sensor/timelined/multi_percentile_spec.rb +21 -0
  71. data/spec/pulse_meter/sensor/timelined/percentile_spec.rb +17 -0
  72. data/spec/pulse_meter/sensor/timelined/uniq_counter_spec.rb +9 -0
  73. data/spec/pulse_meter/sensor/uniq_counter_spec.rb +28 -0
  74. data/spec/pulse_meter/udp_server_spec.rb +36 -0
  75. data/spec/pulse_meter_spec.rb +73 -0
  76. data/spec/shared_examples/timeline_sensor.rb +439 -0
  77. data/spec/shared_examples/timelined_subclass.rb +23 -0
  78. data/spec/spec_helper.rb +37 -0
  79. data/spec/support/matchers.rb +34 -0
  80. data/spec/support/observered.rb +40 -0
  81. metadata +342 -0
@@ -0,0 +1,33 @@
1
+ # -*- encoding: utf-8 -*-
2
+ Gem::Specification.new do |gem|
3
+ gem.authors = ["Ilya Averyanov", "Sergey Averyanov"]
4
+ gem.email = ["av@fun-box.ru", "averyanov@gmail.com"]
5
+ gem.description = %q{Lightweight metrics processor}
6
+ gem.summary = %q{
7
+ Lightweight Redis-based metrics aggregator and processor
8
+ with simple CLI interface
9
+ }
10
+ gem.homepage = "https://github.com/savonarola/pulse_meter_core"
11
+
12
+ gem.required_ruby_version = '>= 1.9.2'
13
+ gem.files = `git ls-files`.split($\)
14
+ gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
15
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
16
+ gem.name = "pulse_meter_core"
17
+ gem.require_paths = ["lib"]
18
+ gem.version = "0.4.13"
19
+
20
+ gem.add_runtime_dependency('redis')
21
+ gem.add_runtime_dependency('json')
22
+
23
+ gem.add_development_dependency('aquarium')
24
+ gem.add_development_dependency('hashie')
25
+ gem.add_development_dependency('mock_redis')
26
+ gem.add_development_dependency('rake')
27
+ gem.add_development_dependency('redcarpet')
28
+ gem.add_development_dependency('rspec')
29
+ gem.add_development_dependency('simplecov')
30
+ gem.add_development_dependency('timecop')
31
+ gem.add_development_dependency('yard')
32
+
33
+ end
@@ -0,0 +1,53 @@
1
+ require 'spec_helper'
2
+
3
+ describe PulseMeter::CommandAggregator::Async do
4
+ let(:ca){PulseMeter.command_aggregator}
5
+ let(:redis){PulseMeter.redis}
6
+
7
+ describe "#multi" do
8
+ it "should accumulate redis command and execute in a bulk" do
9
+ ca.multi do
10
+ ca.set("xxxx", "zzzz")
11
+ ca.set("yyyy", "zzzz")
12
+ sleep 0.1
13
+ redis.get("xxxx").should be_nil
14
+ redis.get("yyyy").should be_nil
15
+ end
16
+ ca.wait_for_pending_events
17
+ redis.get("xxxx").should == "zzzz"
18
+ redis.get("yyyy").should == "zzzz"
19
+ end
20
+ end
21
+
22
+ describe "any other redis instance method" do
23
+ it "should be delegated to redis" do
24
+ ca.set("xxxx", "zzzz")
25
+ ca.wait_for_pending_events
26
+ redis.get("xxxx").should == "zzzz"
27
+ end
28
+
29
+ it "should be aggregated if queue is not overflooded" do
30
+ redis.set("x", 0)
31
+ ca.max_queue_length.times{ ca.incr("x") }
32
+ ca.wait_for_pending_events
33
+ redis.get("x").to_i.should == ca.max_queue_length
34
+ end
35
+
36
+ it "should not be aggregated if queue is overflooded" do
37
+ redis.set("x", 0)
38
+ (ca.max_queue_length * 2).times{ ca.incr("x") }
39
+ ca.wait_for_pending_events
40
+ redis.get("x").to_i.should < 2 * ca.max_queue_length
41
+ end
42
+ end
43
+
44
+ describe "#wait_for_pending_events" do
45
+ it "should pause execution until aggregator thread sends all commands ro redis" do
46
+ ca.set("xxxx", "zzzz")
47
+ redis.get("xxxx").should be_nil
48
+ ca.wait_for_pending_events
49
+ redis.get("xxxx").should == "zzzz"
50
+ end
51
+ end
52
+
53
+ end
@@ -0,0 +1,25 @@
1
+ require 'spec_helper'
2
+
3
+ describe PulseMeter::CommandAggregator::Sync do
4
+ let(:ca){described_class.instance}
5
+ let(:redis){PulseMeter.redis}
6
+
7
+ describe "#multi" do
8
+ it "should accumulate redis command and execute in a bulk" do
9
+ ca.multi do
10
+ ca.set("xxxx", "zzzz").should == "QUEUED"
11
+ ca.set("yyyy", "zzzz").should == "QUEUED"
12
+ end
13
+ redis.get("xxxx").should == "zzzz"
14
+ redis.get("yyyy").should == "zzzz"
15
+ end
16
+ end
17
+
18
+ describe "any other redis instance method" do
19
+ it "should be delegated to redis" do
20
+ ca.set("xxxx", "zzzz")
21
+ redis.get("xxxx").should == "zzzz"
22
+ end
23
+ end
24
+ end
25
+
@@ -0,0 +1,45 @@
1
+ require 'spec_helper'
2
+
3
+ describe PulseMeter::CommandAggregator::UDP do
4
+ let(:host){'127.0.0.1'}
5
+ let(:port){33333}
6
+ let(:udp_sock){mock(:socket)}
7
+ before do
8
+ UDPSocket.stub!(:new).and_return(udp_sock)
9
+ udp_sock.stub!(:fcntl).and_return(nil)
10
+ @ca = described_class.new([[host, port]])
11
+ end
12
+
13
+ describe "#multi" do
14
+ it "should accumulate redis commands and send them in a bulk" do
15
+ data = [
16
+ ["set", "xxxx", "zzzz"],
17
+ ["set", "yyyy", "zzzz"]
18
+ ].to_json
19
+ udp_sock.should_receive(:send).with(data, 0, host, port).and_return(0)
20
+ @ca.multi do
21
+ @ca.set("xxxx", "zzzz")
22
+ @ca.set("yyyy", "zzzz")
23
+ end
24
+ end
25
+
26
+ it "should ignore standard exceptions" do
27
+ udp_sock.should_receive(:send).and_raise(StandardError)
28
+ @ca.multi do
29
+ @ca.set("xxxx", "zzzz")
30
+ end
31
+ end
32
+ end
33
+
34
+ describe "any other redis instance method" do
35
+ it "should send data imediately" do
36
+ data = [
37
+ ["set", "xxxx", "zzzz"]
38
+ ].to_json
39
+ udp_sock.should_receive(:send).with(data, 0, host, port).and_return(0)
40
+ @ca.set("xxxx", "zzzz")
41
+ end
42
+ end
43
+
44
+ end
45
+
@@ -0,0 +1,162 @@
1
+ require 'spec_helper'
2
+
3
+ describe PulseMeter::Mixins::Dumper do
4
+ class Base
5
+ include PulseMeter::Mixins::Dumper
6
+ end
7
+
8
+ class Bad < Base; end
9
+
10
+ class Good < Base
11
+ attr_accessor :some_value
12
+ attr_accessor :name
13
+
14
+ def redis; PulseMeter.redis; end
15
+
16
+ def initialize(name)
17
+ @name = name.to_s
18
+ @some_value = name
19
+ end
20
+ end
21
+
22
+ class GoodButAnother < Good; end
23
+
24
+ let(:bad_obj){ Bad.new }
25
+ let(:good_obj){ Good.new(:foo) }
26
+ let(:another_good_obj){ Good.new(:bar) }
27
+ let(:good_obj_of_another_type){ GoodButAnother.new(:quux) }
28
+ let(:redis){ PulseMeter.redis }
29
+
30
+ describe '#dump' do
31
+ context "when class violates dump contract" do
32
+ context "when it has no name attribute" do
33
+ it "should raise exception" do
34
+ def bad_obj.redis; PulseMeter.redis; end
35
+ expect{ bad_obj.dump! }.to raise_exception(PulseMeter::DumpError)
36
+ end
37
+ end
38
+
39
+ context "when it has no redis attribute" do
40
+ it "should raise exception" do
41
+ def bad_obj.name; :foo; end
42
+ expect{ bad_obj.dump! }.to raise_exception(PulseMeter::DumpError)
43
+ end
44
+ end
45
+
46
+ context "when redis is not avalable" do
47
+ it "should raise exception" do
48
+ def bad_obj.name; :foo; end
49
+ def bad_obj.redis; nil; end
50
+ expect{ bad_obj.dump! }.to raise_exception(PulseMeter::DumpError)
51
+ end
52
+ end
53
+ end
54
+
55
+ context "when class follows dump contract" do
56
+ it "should not raise dump exception" do
57
+ expect {good_obj.dump!}.not_to raise_exception(PulseMeter::DumpError)
58
+ end
59
+
60
+ it "should save dump to redis" do
61
+ expect {good_obj.dump!}.to change {redis.hlen(Good::DUMP_REDIS_KEY)}.by(1)
62
+ end
63
+ end
64
+
65
+ context "when dump is safe" do
66
+ it "should not overwrite stored objects of the same type" do
67
+ good_obj.some_value = 123
68
+ good_obj.dump!
69
+ good_obj.some_value = 321
70
+ good_obj.dump!
71
+ Base.restore(good_obj.name).some_value.should == 123
72
+ end
73
+
74
+ it "should raise DumpConflictError exception if sensor with the same name but different type already exists" do
75
+ good_obj.name = "duplicate_name"
76
+ good_obj_of_another_type.name = "duplicate_name"
77
+ good_obj.dump!
78
+ expect{good_obj_of_another_type.dump!}.to raise_exception(PulseMeter::DumpConflictError)
79
+ end
80
+ end
81
+ end
82
+
83
+ describe ".restore" do
84
+ context "when object has never been dumped" do
85
+ it "should raise exception" do
86
+ expect{ Base.restore(:nonexistant) }.to raise_exception(PulseMeter::RestoreError)
87
+ end
88
+ end
89
+
90
+ context "when object was dumped" do
91
+ before do
92
+ good_obj.dump!
93
+ end
94
+
95
+ it "should keep object class" do
96
+ Base.restore(good_obj.name).should be_instance_of(good_obj.class)
97
+ end
98
+
99
+ it "should restore object data" do
100
+ restored = Base.restore(good_obj.name)
101
+ restored.some_value.should == good_obj.some_value
102
+ end
103
+
104
+ it "should restore last dumped object" do
105
+ good_obj.some_value = :bar
106
+ good_obj.dump!(false)
107
+ restored = Base.restore(good_obj.name)
108
+ restored.some_value.should == :bar
109
+ end
110
+ end
111
+ end
112
+
113
+ describe ".list_names" do
114
+ context "when redis is not available" do
115
+ before do
116
+ PulseMeter.stub(:redis).and_return(nil)
117
+ end
118
+
119
+ it "should raise exception" do
120
+ expect {Base.list_names}.to raise_exception(PulseMeter::RestoreError)
121
+ end
122
+ end
123
+
124
+ context "when redis if fine" do
125
+ it "should return empty list if nothing is registered" do
126
+ Base.list_names.should == []
127
+ end
128
+
129
+ it "should return list of registered objects" do
130
+ good_obj.dump!(false)
131
+ another_good_obj.dump!(false)
132
+ Base.list_names.should =~ [good_obj.name, another_good_obj.name]
133
+ end
134
+ end
135
+ end
136
+
137
+ describe ".list_objects" do
138
+ before do
139
+ good_obj.dump!
140
+ another_good_obj.dump!
141
+ end
142
+
143
+ it "should return restored objects" do
144
+ objects = Base.list_objects
145
+ objects.map(&:name).should =~ [good_obj.name, another_good_obj.name]
146
+ end
147
+
148
+ it "should skip unrestorable objects" do
149
+ Base.stub(:list_names).and_return([good_obj.name, "scoundrel", another_good_obj.name])
150
+ objects = Base.list_objects
151
+ objects.map(&:name).should =~ [good_obj.name, another_good_obj.name]
152
+ end
153
+ end
154
+
155
+ describe "#cleanup_dump" do
156
+ it "should remove data from redis" do
157
+ good_obj.dump!
158
+ another_good_obj.dump!
159
+ expect {good_obj.cleanup_dump}.to change{good_obj.class.list_names.count}.by(-1)
160
+ end
161
+ end
162
+ end
@@ -0,0 +1,212 @@
1
+ require 'spec_helper'
2
+
3
+ describe PulseMeter::Mixins::Utils do
4
+ class Dummy
5
+ include PulseMeter::Mixins::Utils
6
+ end
7
+
8
+ let(:dummy){ Dummy.new }
9
+
10
+ describe '#constantize' do
11
+ context "when argument is a string with a valid class name" do
12
+ it "should return class" do
13
+ dummy.constantize("PulseMeter::Mixins::Utils").should == PulseMeter::Mixins::Utils
14
+ end
15
+ end
16
+ context "when argument is a string with invalid class name" do
17
+ it "should return nil" do
18
+ dummy.constantize("Pumpkin::Eater").should be_nil
19
+ end
20
+ end
21
+ context "when argument is not a string" do
22
+ it "should return nil" do
23
+ dummy.constantize({}).should be_nil
24
+ end
25
+ end
26
+ end
27
+
28
+ describe "#assert_positive_integer!" do
29
+ it "should extract integer value from hash by passed key" do
30
+ dummy.assert_positive_integer!({:val => 4}, :val).should == 4
31
+ end
32
+
33
+ context "when no default value given" do
34
+ context "when the value by the passed key is not integer" do
35
+ it "should convert non-integers to integers" do
36
+ dummy.assert_positive_integer!({:val => 4.4}, :val).should == 4
37
+ end
38
+
39
+ it "should change the original value to the obtained integer" do
40
+ h = {:val => 4.4}
41
+ dummy.assert_positive_integer!(h, :val).should == 4
42
+ h[:val].should == 4
43
+ end
44
+
45
+ it "should raise exception if the original value cannot be converted to integer"do
46
+ expect{ dummy.assert_positive_integer!({:val => :bad_int}, :val) }.to raise_exception(ArgumentError)
47
+ end
48
+ end
49
+
50
+ it "should raise exception if the value is not positive" do
51
+ expect{ dummy.assert_positive_integer!({:val => -1}, :val) }.to raise_exception(ArgumentError)
52
+ end
53
+
54
+ it "should raise exception if the value is not defined" do
55
+ expect{ dummy.assert_positive_integer!({}, :val) }.to raise_exception(ArgumentError)
56
+ end
57
+ end
58
+
59
+ context "when default value given" do
60
+ it "should prefer value from options to default" do
61
+ dummy.assert_positive_integer!({:val => 4}, :val, 22).should == 4
62
+ end
63
+
64
+ it "should use default value when there is no one in options" do
65
+ dummy.assert_positive_integer!({}, :val, 22).should == 22
66
+ end
67
+
68
+ it "should check default value if it is to be used" do
69
+ expect{dummy.assert_positive_integer!({}, :val, :bad)}.to raise_exception(ArgumentError)
70
+ expect{dummy.assert_positive_integer!({}, :val, -1)}.to raise_exception(ArgumentError)
71
+ end
72
+ end
73
+ end
74
+
75
+ describe "#assert_array!" do
76
+ it "should extract value from hash by passed key" do
77
+ dummy.assert_array!({:val => [:foo]}, :val).should == [:foo]
78
+ end
79
+
80
+ context "when no default value given" do
81
+ it "should raise exception if th value is not an Array" do
82
+ expect{ dummy.assert_array!({:val => :bad}, :val) }.to raise_exception(ArgumentError)
83
+ end
84
+
85
+ it "should raise exception if the value is not defined" do
86
+ expect{ dummy.assert_array!({}, :val) }.to raise_exception(ArgumentError)
87
+ end
88
+ end
89
+
90
+ context "when default value given" do
91
+ it "should prefer value from options to default" do
92
+ dummy.assert_array!({:val => [:foo]}, :val, []).should == [:foo]
93
+ end
94
+
95
+ it "should use default value when there is no one in options" do
96
+ dummy.assert_array!({}, :val, []).should == []
97
+ end
98
+
99
+ it "should check default value if it is to be used" do
100
+ expect{dummy.assert_array!({}, :val, :bad)}.to raise_exception(ArgumentError)
101
+ end
102
+ end
103
+ end
104
+
105
+ describe "#assert_ranged_float!" do
106
+
107
+ it "should extract float value from hash by passed key" do
108
+ dummy.assert_ranged_float!({:val => 4}, :val, 0, 100).should be_generally_equal(4)
109
+ end
110
+
111
+ context "when the value by the passed key is not float" do
112
+ it "should convert non-floats to floats" do
113
+ dummy.assert_ranged_float!({:val => "4.0000"}, :val, 0, 100).should be_generally_equal(4)
114
+ end
115
+
116
+ it "should change the original value to the obtained float" do
117
+ h = {:val => "4.000"}
118
+ dummy.assert_ranged_float!(h, :val, 0, 100).should be_generally_equal(4)
119
+ h[:val].should be_generally_equal(4)
120
+ end
121
+
122
+ it "should raise exception if the original value cannot be converted to float" do
123
+ expect{ dummy.assert_ranged_float!({:val => :bad_float}, :val, 0, 100) }.to raise_exception(ArgumentError)
124
+ end
125
+ end
126
+
127
+ it "should raise exception if the value is not within range" do
128
+ expect{ dummy.assert_ranged_float!({:val => -0.1}, :val, 0, 100) }.to raise_exception(ArgumentError)
129
+ expect{ dummy.assert_ranged_float!({:val => 100.1}, :val, 0, 100) }.to raise_exception(ArgumentError)
130
+ end
131
+
132
+ it "should raise exception if the value is not defined" do
133
+ expect{ dummy.assert_ranged_float!({}, :val) }.to raise_exception(ArgumentError)
134
+ end
135
+ end
136
+
137
+ describe "#uniqid" do
138
+ it "should return uniq strings" do
139
+ uniq_values = (1..1000).map{|_| dummy.uniqid}
140
+ uniq_values.uniq.count.should == uniq_values.count
141
+ end
142
+ end
143
+
144
+ describe "#titleize" do
145
+ it "should convert identificator to title" do
146
+ dummy.titleize("aaa_bbb").should == 'Aaa Bbb'
147
+ dummy.titleize(:aaa_bbb).should == 'Aaa Bbb'
148
+ dummy.titleize("aaa bbb").should == 'Aaa Bbb'
149
+ end
150
+ end
151
+
152
+ describe "#camelize" do
153
+ it "should camelize string" do
154
+ dummy.camelize("aa_bb_cc").should == "aaBbCc"
155
+ dummy.camelize("aa_bb_cc", true).should == "AaBbCc"
156
+ end
157
+ end
158
+
159
+ describe "#underscore" do
160
+ it "should underscore string" do
161
+ dummy.underscore("aaBbCc").should == "aa_bb_cc"
162
+ dummy.underscore("AaBbCc").should == "aa_bb_cc"
163
+ dummy.underscore("aaBb::Cc").should == "aa_bb/cc"
164
+ end
165
+ end
166
+
167
+ describe "#camelize_keys" do
168
+ it "should deeply camelize keys in hashes" do
169
+ dummy.camelize_keys({ :aa_bb_cc => [ { :dd_ee => 123 }, 456 ] }).should =={ 'aaBbCc' => [ { 'ddEe' => 123 }, 456 ] }
170
+ end
171
+ end
172
+
173
+ describe "#symbolize_keys" do
174
+ it "should convert symbolizable keys to symbols" do
175
+ dummy.symbolize_keys({"a" => 5, 6 => 7}).should == {a: 5, 6 => 7}
176
+ end
177
+ end
178
+
179
+ describe "#subsets_of" do
180
+ it "returns all subsets of given array" do
181
+ dummy.subsets_of([1, 2]).sort.should == [[], [1], [2], [1, 2]].sort
182
+ end
183
+ end
184
+
185
+ describe "#each_subset" do
186
+ it "iterates over each subset" do
187
+ subsets = []
188
+ dummy.each_subset([1, 2]) {|s| subsets << s}
189
+ subsets.sort.should == [[], [1], [2], [1, 2]].sort
190
+ end
191
+ end
192
+
193
+ describe '#parse_time' do
194
+ context "when argument is a valid YYYYmmddHHMMSS string" do
195
+ it "should correct Time object" do
196
+ t = dummy.parse_time("19700101000000")
197
+ t.should be_kind_of(Time)
198
+ t.to_i.should == 0
199
+ end
200
+ end
201
+ context "when argument is an invalid YYYYmmddHHMMSS string" do
202
+ it "should raise ArgumentError" do
203
+ expect{ dummy.parse_time("19709901000000") }.to raise_exception(ArgumentError)
204
+ end
205
+ end
206
+ context "when argument is not a YYYYmmddHHMMSS string" do
207
+ it "should raise ArgumentError" do
208
+ expect{ dummy.parse_time("197099010000000") }.to raise_exception(ArgumentError)
209
+ end
210
+ end
211
+ end
212
+ end