kicks 3.0.0.pre

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 (73) hide show
  1. checksums.yaml +7 -0
  2. data/.github/workflows/ci.yml +24 -0
  3. data/.gitignore +12 -0
  4. data/ChangeLog.md +142 -0
  5. data/Dockerfile +24 -0
  6. data/Dockerfile.slim +20 -0
  7. data/Gemfile +8 -0
  8. data/Guardfile +8 -0
  9. data/LICENSE.txt +22 -0
  10. data/README.md +209 -0
  11. data/Rakefile +12 -0
  12. data/bin/sneakers +6 -0
  13. data/docker-compose.yml +24 -0
  14. data/examples/benchmark_worker.rb +22 -0
  15. data/examples/max_retry_handler.rb +68 -0
  16. data/examples/metrics_worker.rb +34 -0
  17. data/examples/middleware_worker.rb +36 -0
  18. data/examples/newrelic_metrics_worker.rb +40 -0
  19. data/examples/profiling_worker.rb +69 -0
  20. data/examples/sneakers.conf.rb.example +11 -0
  21. data/examples/title_scraper.rb +36 -0
  22. data/examples/workflow_worker.rb +23 -0
  23. data/kicks.gemspec +44 -0
  24. data/lib/sneakers/cli.rb +122 -0
  25. data/lib/sneakers/concerns/logging.rb +34 -0
  26. data/lib/sneakers/concerns/metrics.rb +34 -0
  27. data/lib/sneakers/configuration.rb +125 -0
  28. data/lib/sneakers/content_encoding.rb +47 -0
  29. data/lib/sneakers/content_type.rb +47 -0
  30. data/lib/sneakers/error_reporter.rb +33 -0
  31. data/lib/sneakers/errors.rb +2 -0
  32. data/lib/sneakers/handlers/maxretry.rb +219 -0
  33. data/lib/sneakers/handlers/oneshot.rb +26 -0
  34. data/lib/sneakers/metrics/logging_metrics.rb +16 -0
  35. data/lib/sneakers/metrics/newrelic_metrics.rb +32 -0
  36. data/lib/sneakers/metrics/null_metrics.rb +13 -0
  37. data/lib/sneakers/metrics/statsd_metrics.rb +21 -0
  38. data/lib/sneakers/middleware/config.rb +23 -0
  39. data/lib/sneakers/publisher.rb +49 -0
  40. data/lib/sneakers/queue.rb +87 -0
  41. data/lib/sneakers/runner.rb +91 -0
  42. data/lib/sneakers/spawner.rb +30 -0
  43. data/lib/sneakers/support/production_formatter.rb +11 -0
  44. data/lib/sneakers/support/utils.rb +18 -0
  45. data/lib/sneakers/tasks.rb +66 -0
  46. data/lib/sneakers/version.rb +3 -0
  47. data/lib/sneakers/worker.rb +162 -0
  48. data/lib/sneakers/workergroup.rb +60 -0
  49. data/lib/sneakers.rb +125 -0
  50. data/log/.gitkeep +0 -0
  51. data/scripts/local_integration +2 -0
  52. data/scripts/local_worker +3 -0
  53. data/spec/fixtures/integration_worker.rb +18 -0
  54. data/spec/fixtures/require_worker.rb +23 -0
  55. data/spec/gzip_helper.rb +15 -0
  56. data/spec/sneakers/cli_spec.rb +75 -0
  57. data/spec/sneakers/concerns/logging_spec.rb +39 -0
  58. data/spec/sneakers/concerns/metrics_spec.rb +38 -0
  59. data/spec/sneakers/configuration_spec.rb +97 -0
  60. data/spec/sneakers/content_encoding_spec.rb +81 -0
  61. data/spec/sneakers/content_type_spec.rb +81 -0
  62. data/spec/sneakers/integration_spec.rb +158 -0
  63. data/spec/sneakers/publisher_spec.rb +179 -0
  64. data/spec/sneakers/queue_spec.rb +169 -0
  65. data/spec/sneakers/runner_spec.rb +70 -0
  66. data/spec/sneakers/sneakers_spec.rb +77 -0
  67. data/spec/sneakers/support/utils_spec.rb +44 -0
  68. data/spec/sneakers/tasks/sneakers_run_spec.rb +115 -0
  69. data/spec/sneakers/worker_handlers_spec.rb +469 -0
  70. data/spec/sneakers/worker_spec.rb +712 -0
  71. data/spec/sneakers/workergroup_spec.rb +83 -0
  72. data/spec/spec_helper.rb +21 -0
  73. metadata +352 -0
@@ -0,0 +1,66 @@
1
+ require 'sneakers'
2
+ require 'sneakers/runner'
3
+
4
+ task :environment
5
+
6
+ namespace :sneakers do
7
+ desc "Start work (set $WORKERS=Klass1,Klass2)"
8
+ task :run do
9
+ Sneakers.server = true
10
+ Rake::Task['environment'].invoke
11
+
12
+ if defined?(::Rails)
13
+ if Rails.autoloaders.zeitwerk_enabled?
14
+ ::Zeitwerk::Loader.eager_load_all
15
+ else
16
+ ::Rails.application.eager_load!
17
+ end
18
+ end
19
+
20
+ workers, missing_workers = get_worker_classes
21
+
22
+ unless missing_workers.nil? || missing_workers.empty?
23
+ puts "Missing workers: #{missing_workers.join(', ')}" if missing_workers
24
+ puts "Did you `require` properly?"
25
+ exit(1)
26
+ end
27
+
28
+ if workers.empty?
29
+ puts <<EOF
30
+ Error: No workers found.
31
+ Please set the classes of the workers you want to run like so:
32
+
33
+ $ export WORKERS=MyWorker,FooWorker
34
+ $ rake sneakers:run
35
+
36
+ You can also configure them with
37
+ $ Sneakers.rake_worker_classes
38
+
39
+ If you use something that responds to :call it will execute that
40
+
41
+ Eventually, if nothing before applied, every class is used where you directly included the Sneakers::Worker
42
+ EOF
43
+ exit(1)
44
+ end
45
+ opts = (!ENV['WORKER_COUNT'].nil? ? {:workers => ENV['WORKER_COUNT'].to_i} : {})
46
+ r = Sneakers::Runner.new(workers, opts)
47
+
48
+ r.run
49
+ end
50
+
51
+ private
52
+
53
+ def get_worker_classes
54
+ if ENV["WORKERS"]
55
+ Sneakers::Utils.parse_workers(ENV['WORKERS'])
56
+ elsif Sneakers.rake_worker_classes
57
+ if Sneakers.rake_worker_classes.respond_to?(:call)
58
+ [Sneakers.rake_worker_classes.call]
59
+ else
60
+ [Sneakers.rake_worker_classes]
61
+ end
62
+ else
63
+ [Sneakers::Worker::Classes]
64
+ end || [[]]
65
+ end
66
+ end
@@ -0,0 +1,3 @@
1
+ module Sneakers
2
+ VERSION = "3.0.0.pre"
3
+ end
@@ -0,0 +1,162 @@
1
+ require 'sneakers/queue'
2
+ require 'sneakers/support/utils'
3
+
4
+ module Sneakers
5
+ module Worker
6
+ attr_reader :queue, :id, :opts
7
+
8
+ # For now, a worker is hardly dependant on these concerns
9
+ # (because it uses methods from them directly.)
10
+ include Concerns::Logging
11
+ include Concerns::Metrics
12
+ include Sneakers::ErrorReporter
13
+
14
+ def initialize(queue = nil, pool = nil, opts = {})
15
+ opts = opts.merge(self.class.queue_opts || {})
16
+ queue_name = self.class.queue_name
17
+ opts = Sneakers::CONFIG.merge(opts)
18
+
19
+ @should_ack = opts[:ack]
20
+ @pool = pool || Concurrent::FixedThreadPool.new(opts[:threads] || Sneakers::Configuration::DEFAULTS[:threads])
21
+ @call_with_params = respond_to?(:work_with_params)
22
+ @content_type = opts[:content_type]
23
+ @content_encoding = opts[:content_encoding]
24
+
25
+ @queue = queue || Sneakers::Queue.new(
26
+ queue_name,
27
+ opts
28
+ )
29
+
30
+ @opts = opts
31
+ @id = Utils.make_worker_id(queue_name)
32
+ end
33
+
34
+ def ack!; :ack end
35
+ def reject!; :reject; end
36
+ def requeue!; :requeue; end
37
+
38
+ def publish(msg, opts)
39
+ to_queue = opts.delete(:to_queue)
40
+ opts[:routing_key] ||= to_queue
41
+ return unless opts[:routing_key]
42
+ serialized_msg = Sneakers::ContentType.serialize(msg, opts[:content_type])
43
+ encoded_msg = Sneakers::ContentEncoding.encode(serialized_msg, opts[:content_encoding])
44
+ @queue.exchange.publish(encoded_msg, **opts)
45
+ end
46
+
47
+ def do_work(delivery_info, metadata, msg, handler)
48
+ worker_trace "Working off: #{msg.inspect}"
49
+
50
+ @pool.post do
51
+ process_work(delivery_info, metadata, msg, handler)
52
+ end
53
+ end
54
+
55
+ def process_work(delivery_info, metadata, msg, handler)
56
+ res = nil
57
+ error = nil
58
+
59
+ begin
60
+ metrics.increment("work.#{self.class.name}.started")
61
+ metrics.timing("work.#{self.class.name}.time") do
62
+ decoded_msg = ContentEncoding.decode(msg, @content_encoding || metadata && metadata[:content_encoding])
63
+ deserialized_msg = ContentType.deserialize(decoded_msg, @content_type || metadata && metadata[:content_type])
64
+
65
+ app = -> (deserialized_msg, delivery_info, metadata, handler) do
66
+ if @call_with_params
67
+ work_with_params(deserialized_msg, delivery_info, metadata)
68
+ else
69
+ work(deserialized_msg)
70
+ end
71
+ end
72
+
73
+ middlewares = Sneakers.middleware.to_a
74
+ block_to_call = middlewares.reverse.reduce(app) do |mem, h|
75
+ h[:class].new(mem, *h[:args])
76
+ end
77
+ res = block_to_call.call(deserialized_msg, delivery_info, metadata, handler)
78
+ end
79
+ rescue SignalException, SystemExit
80
+ # ServerEngine handles these exceptions, so they are not expected to be raised within the worker.
81
+ # Nevertheless, they are listed here to ensure that they are not caught by the rescue block below.
82
+ raise
83
+ rescue Exception => ex
84
+ res = :error
85
+ error = ex
86
+ worker_error(ex, log_msg: log_msg(msg), class: self.class.name,
87
+ message: msg, delivery_info: delivery_info, metadata: metadata)
88
+ ensure
89
+ if @should_ack
90
+ case res
91
+ # note to future-self. never acknowledge multiple (multiple=true) messages under threads.
92
+ when :ack then handler.acknowledge(delivery_info, metadata, msg)
93
+ when :error then handler.error(delivery_info, metadata, msg, error)
94
+ when :reject then handler.reject(delivery_info, metadata, msg)
95
+ when :requeue then handler.reject(delivery_info, metadata, msg, true)
96
+ else
97
+ handler.noop(delivery_info, metadata, msg)
98
+ end
99
+ metrics.increment("work.#{self.class.name}.handled.#{res || 'noop'}")
100
+ end
101
+
102
+ metrics.increment("work.#{self.class.name}.ended")
103
+ end
104
+ end
105
+
106
+ def stop
107
+ worker_trace "Stopping worker: unsubscribing."
108
+ @queue.unsubscribe
109
+ worker_trace "Stopping worker: shutting down thread pool."
110
+ @pool.shutdown
111
+ @pool.wait_for_termination
112
+ worker_trace "Stopping worker: I'm gone."
113
+ end
114
+
115
+ def run
116
+ worker_trace "New worker: subscribing."
117
+ @queue.subscribe(self)
118
+ worker_trace "New worker: I'm alive."
119
+ end
120
+
121
+ # Construct a log message with some standard prefix for this worker
122
+ def log_msg(msg)
123
+ "[#{@id}][#{Thread.current}][#{@queue.name}][#{@queue.opts}] #{msg}"
124
+ end
125
+
126
+ def worker_trace(msg)
127
+ logger.debug(log_msg(msg))
128
+ end
129
+
130
+ Classes = []
131
+
132
+ def self.included(base)
133
+ base.extend ClassMethods
134
+ Classes << base if base.is_a? Class
135
+ end
136
+
137
+ module ClassMethods
138
+ attr_reader :queue_opts
139
+ attr_reader :queue_name
140
+
141
+ def from_queue(q, opts={})
142
+ @queue_name = q.to_s
143
+ @queue_opts = opts
144
+ end
145
+
146
+ def enqueue(msg, opts={})
147
+ opts[:routing_key] ||= @queue_opts[:routing_key]
148
+ opts[:content_type] ||= @queue_opts[:content_type]
149
+ opts[:content_encoding] ||= @queue_opts[:content_encoding]
150
+ opts[:to_queue] ||= @queue_name
151
+
152
+ publisher.publish(msg, opts)
153
+ end
154
+
155
+ private
156
+
157
+ def publisher
158
+ @publisher ||= Sneakers::Publisher.new(queue_opts)
159
+ end
160
+ end
161
+ end
162
+ end
@@ -0,0 +1,60 @@
1
+ module Sneakers
2
+ module WorkerGroup
3
+ @workers = []
4
+
5
+ def initialize
6
+ @stop_flag = ServerEngine::BlockingFlag.new
7
+ end
8
+
9
+ def before_fork
10
+ fbefore = Sneakers::CONFIG[:hooks][:before_fork]
11
+ fbefore.call if fbefore
12
+ end
13
+
14
+ def after_fork # note! this is not Serverengine#after_start, this is ours!
15
+ fafter = Sneakers::CONFIG[:hooks][:after_fork]
16
+ fafter.call if fafter
17
+ end
18
+
19
+ def run
20
+ after_fork
21
+
22
+ # Allocate single thread pool if share_threads is set. This improves load balancing
23
+ # when used with many workers.
24
+ pool = config[:share_threads] ? Concurrent::FixedThreadPool.new(config[:threads]) : nil
25
+
26
+ worker_classes = config[:worker_classes]
27
+
28
+ if worker_classes.respond_to? :call
29
+ worker_classes = worker_classes.call
30
+ end
31
+
32
+ # If we don't provide a connection to a worker,
33
+ # the queue used in the worker will create a new one
34
+
35
+ @workers = worker_classes.map do |worker_class|
36
+ worker_class.new(nil, pool, { connection: config[:connection] })
37
+ end
38
+
39
+ # if more than one worker this should be per worker
40
+ # accumulate clients and consumers as well
41
+ @workers.each do |worker|
42
+ worker.run
43
+ end
44
+ # end per worker
45
+ #
46
+ until @stop_flag.wait_for_set(Sneakers::CONFIG[:amqp_heartbeat])
47
+ Sneakers.logger.debug("Heartbeat: running threads [#{Thread.list.count}]")
48
+ # report aggregated stats?
49
+ end
50
+ end
51
+
52
+ def stop
53
+ Sneakers.logger.info("Shutting down workers")
54
+ @workers.each do |worker|
55
+ worker.stop
56
+ end
57
+ @stop_flag.set!
58
+ end
59
+ end
60
+ end
data/lib/sneakers.rb ADDED
@@ -0,0 +1,125 @@
1
+ require 'sneakers/version'
2
+ require 'concurrent/executors'
3
+ require 'bunny'
4
+ require 'logger'
5
+ require 'serverengine'
6
+
7
+ module Sneakers
8
+ module Handlers
9
+ end
10
+ module Concerns
11
+ end
12
+ end
13
+
14
+ require 'sneakers/configuration'
15
+ require 'sneakers/errors'
16
+ require 'sneakers/support/production_formatter'
17
+ require 'sneakers/concerns/logging'
18
+ require 'sneakers/concerns/metrics'
19
+ require 'sneakers/handlers/oneshot'
20
+ require 'sneakers/content_type'
21
+ require 'sneakers/content_encoding'
22
+ require 'sneakers/middleware/config'
23
+ require 'sneakers/worker'
24
+ require 'sneakers/publisher'
25
+
26
+ module Sneakers
27
+ extend self
28
+
29
+ CONFIG = Configuration.new
30
+
31
+ def configure(opts={})
32
+ # worker > userland > defaults
33
+ CONFIG.merge!(opts)
34
+ setup_general_logger!
35
+ setup_worker_concerns!
36
+ setup_general_publisher!
37
+ @configured = true
38
+ end
39
+
40
+ def clear!
41
+ CONFIG.clear
42
+ @logger = nil
43
+ @publisher = nil
44
+ @configured = false
45
+ end
46
+
47
+ def daemonize!(loglevel=Logger::INFO)
48
+ CONFIG[:log] = 'sneakers.log'
49
+ CONFIG[:daemonize] = true
50
+ setup_general_logger!
51
+ logger.level = loglevel
52
+ end
53
+
54
+ def rake_worker_classes=(worker_classes)
55
+ @rake_worker_classes = worker_classes
56
+ end
57
+
58
+ def rake_worker_classes
59
+ @rake_worker_classes
60
+ end
61
+
62
+ def logger=(logger)
63
+ @logger = logger
64
+ end
65
+
66
+ def logger
67
+ @logger
68
+ end
69
+
70
+ def publish(msg, routing)
71
+ @publisher.publish(msg, routing)
72
+ end
73
+
74
+ def configured?
75
+ @configured
76
+ end
77
+
78
+ def server=(server)
79
+ @server = server
80
+ end
81
+
82
+ def server?
83
+ @server
84
+ end
85
+
86
+ def configure_server
87
+ yield self if server?
88
+ end
89
+
90
+ # Register a proc to handle any error which occurs within the Sneakers process.
91
+ #
92
+ # Sneakers.error_reporters << proc { |exception, worker, context_hash| MyErrorService.notify(exception, context_hash) }
93
+ #
94
+ # The default error handler logs errors to Sneakers.logger.
95
+ # Ripped off from https://github.com/mperham/sidekiq/blob/6ad6a3aa330deebd76c6cf0d353f66abd3bef93b/lib/sidekiq.rb#L165-L174
96
+ def error_reporters
97
+ CONFIG[:error_reporters]
98
+ end
99
+
100
+ def middleware
101
+ @middleware ||= Sneakers::Middleware::Config
102
+ end
103
+
104
+ private
105
+
106
+ def setup_general_logger!
107
+ if [:info, :debug, :error, :warn].all?{ |meth| CONFIG[:log].respond_to?(meth) }
108
+ @logger = CONFIG[:log]
109
+ else
110
+ @logger = ServerEngine::DaemonLogger.new(CONFIG[:log])
111
+ @logger.formatter = Sneakers::Support::ProductionFormatter
112
+ end
113
+ end
114
+
115
+ def setup_worker_concerns!
116
+ Worker.configure_logger(Sneakers::logger)
117
+ Worker.configure_metrics(CONFIG[:metrics])
118
+ CONFIG[:handler] ||= Sneakers::Handlers::Oneshot
119
+ end
120
+
121
+ def setup_general_publisher!
122
+ @publisher = Sneakers::Publisher.new
123
+ end
124
+ end
125
+
data/log/.gitkeep ADDED
File without changes
@@ -0,0 +1,2 @@
1
+ #!/bin/sh
2
+ docker-compose run -e INTEGRATION=1 -e INTEGRATION_LOG=1 sneakers rake test
@@ -0,0 +1,3 @@
1
+ #!/bin/sh
2
+ docker-compose run sneakers sneakers work TitleScraper --require examples/title_scraper.rb
3
+
@@ -0,0 +1,18 @@
1
+ require 'sneakers'
2
+ require 'thread'
3
+ require 'redis'
4
+
5
+
6
+ redis_addr = compose_or_localhost("redis")
7
+ puts "REDIS is at #{redis_addr}"
8
+ $redis = Redis.new(:host => redis_addr)
9
+
10
+ class IntegrationWorker
11
+ include Sneakers::Worker
12
+
13
+ def work(msg)
14
+ $redis.incr(self.class.queue_name)
15
+ ack!
16
+ end
17
+ end
18
+
@@ -0,0 +1,23 @@
1
+ require 'sneakers'
2
+ require 'open-uri'
3
+
4
+
5
+ class TitleScraper
6
+ include Sneakers::Worker
7
+
8
+ from_queue 'downloads'
9
+
10
+ def work(msg)
11
+ title = extract_title(open(msg))
12
+ logger.info "FOUND <#{title}>"
13
+ ack!
14
+ end
15
+
16
+ private
17
+
18
+ def extract_title(html)
19
+ html =~ /<title>(.*?)<\/title>/
20
+ $1
21
+ end
22
+ end
23
+
@@ -0,0 +1,15 @@
1
+ require 'stringio'
2
+ require 'zlib'
3
+
4
+ # Simple gzip encoder/decoder for testing
5
+ def gzip_compress(s)
6
+ io = StringIO.new('w')
7
+ w = Zlib::GzipWriter.new(io)
8
+ w.write(s)
9
+ w.close
10
+ io.string
11
+ end
12
+
13
+ def gzip_decompress(s)
14
+ Zlib::GzipReader.new(StringIO.new(s, 'rb')).read
15
+ end
@@ -0,0 +1,75 @@
1
+ require 'spec_helper'
2
+ require 'sneakers'
3
+ require 'sneakers/cli'
4
+ require 'sneakers/runner'
5
+
6
+ describe Sneakers::CLI do
7
+ describe "#work" do
8
+ before do
9
+ any_instance_of(Sneakers::Runner) do |runner|
10
+ stub(runner).run{ true }
11
+ end
12
+ end
13
+
14
+ after do
15
+ # require cleanup
16
+ Object.send(:remove_const, :TitleScraper) if Object.constants.include?(:TitleScraper)
17
+ end
18
+
19
+ describe 'with dirty class loading' do
20
+ it "should perform a run" do
21
+ any_instance_of(Sneakers::Runner) do |runner|
22
+ mock(runner).run{ true }
23
+ end
24
+ out = capture_io{ Sneakers::CLI.start [
25
+ 'work',
26
+ "TitleScraper",
27
+ "--require=#{File.expand_path('../fixtures/require_worker.rb', File.dirname(__FILE__))}"
28
+ ]}.join ''
29
+
30
+ _(out).must_match(/Workers.*:.*TitleScraper.*/)
31
+
32
+ end
33
+
34
+ it "should be able to run as front-running process" do
35
+ out = capture_io{ Sneakers::CLI.start [
36
+ 'work',
37
+ "TitleScraper",
38
+ "--require=#{File.expand_path('../fixtures/require_worker.rb', File.dirname(__FILE__))}"
39
+ ]}.join ''
40
+
41
+ _(out).must_match(/Log.*Console/)
42
+ end
43
+
44
+ it "should be able to run as daemonized process" do
45
+ out = capture_io{ Sneakers::CLI.start [
46
+ 'work',
47
+ "TitleScraper",
48
+ "--daemonize",
49
+ "--require=#{File.expand_path('../fixtures/require_worker.rb', File.dirname(__FILE__))}"
50
+ ]}.join ''
51
+
52
+ _(out).must_match(/sneakers.log/)
53
+ end
54
+ end
55
+
56
+ it "should fail when no workers found" do
57
+ out = capture_io{ Sneakers::CLI.start ['work', 'TitleScraper'] }.join ''
58
+ _(out).must_match(/Missing workers: TitleScraper/)
59
+ end
60
+
61
+ it "should run all workers when run without specifying any" do
62
+ out = capture_io{ Sneakers::CLI.start [
63
+ "work",
64
+ "--require=#{File.expand_path("../fixtures/require_worker.rb", File.dirname(__FILE__))}"
65
+ ]}.join ''
66
+
67
+ _(out).must_match(/Workers.*:.*TitleScraper.*/)
68
+ end
69
+
70
+ after do
71
+ # require cleanup
72
+ Object.send(:remove_const, :TitleScraper) if Object.constants.include?(:TitleScraper)
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,39 @@
1
+ require 'spec_helper'
2
+ require 'sneakers'
3
+ require 'logger'
4
+
5
+
6
+ class Foobar
7
+ include Sneakers::Concerns::Logging
8
+ end
9
+
10
+ describe Sneakers::Concerns::Logging do
11
+ describe ".configure" do
12
+ before do
13
+ Foobar.logger = nil
14
+ end
15
+
16
+ it "should configure a default logger when included" do
17
+ _(Foobar.logger).must_be_nil
18
+ Foobar.configure_logger
19
+ _(Foobar.logger).wont_be_nil
20
+ _(Foobar.logger.formatter).must_equal Sneakers::Support::ProductionFormatter
21
+ end
22
+
23
+ it "should supply accessible instance logger" do
24
+ _(Foobar.logger).must_be_nil
25
+ Foobar.configure_logger
26
+ f = Foobar.new
27
+ _(f.logger).must_equal Foobar.logger
28
+ _(f.logger).wont_be_nil
29
+ end
30
+
31
+ it "should configure a given logger when specified" do
32
+ _(Foobar.logger).must_be_nil
33
+ log = Logger.new(STDOUT)
34
+ Foobar.configure_logger(log)
35
+ _(Foobar.logger).must_equal log
36
+ end
37
+ end
38
+ end
39
+
@@ -0,0 +1,38 @@
1
+ require 'spec_helper'
2
+ require 'sneakers'
3
+ require 'logger'
4
+
5
+
6
+ class Foometrics
7
+ include Sneakers::Concerns::Metrics
8
+ end
9
+
10
+ describe Sneakers::Concerns::Metrics do
11
+ describe ".configure" do
12
+ before do
13
+ Foometrics.metrics = nil
14
+ end
15
+
16
+ it "should configure a default logger when included" do
17
+ _(Foometrics.metrics).must_be_nil
18
+ Foometrics.configure_metrics
19
+ _(Foometrics.metrics).wont_be_nil
20
+ end
21
+
22
+ it "should supply accessible instance logger" do
23
+ _(Foometrics.metrics).must_be_nil
24
+ Foometrics.configure_metrics
25
+ f = Foometrics.new
26
+ _(f.metrics).must_equal Foometrics.metrics
27
+ _(f.metrics).wont_be_nil
28
+ end
29
+
30
+ it "should configure a given metrics when specified" do
31
+ _(Foometrics.metrics).must_be_nil
32
+ o = Object.new
33
+ Foometrics.configure_metrics(o)
34
+ _(Foometrics.metrics).must_equal o
35
+ end
36
+ end
37
+ end
38
+