dilation 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +17 -0
- data/.rspec +2 -0
- data/.yardopts +4 -0
- data/Gemfile +11 -0
- data/LICENSE +22 -0
- data/README.md +122 -0
- data/Rakefile +9 -0
- data/dilation.gemspec +24 -0
- data/lib/dilation.rb +17 -0
- data/lib/dilation/core.rb +135 -0
- data/lib/dilation/test_support/handler.rb +40 -0
- data/lib/dilation/timers/coarse.rb +28 -0
- data/lib/dilation/timers/test.rb +13 -0
- data/lib/dilation/timers/timer.rb +33 -0
- data/lib/dilation/utils/dilator.rb +80 -0
- data/lib/dilation/utils/events.rb +91 -0
- data/lib/dilation/version.rb +4 -0
- data/spec/dilation/core_spec.rb +144 -0
- data/spec/dilation/utils/dilator_spec.rb +85 -0
- data/spec/integration.rb +42 -0
- data/spec/spec_mini.rb +2 -0
- data/spec/support/matchers.rb +19 -0
- data/spec/support/shared_examples.rb +69 -0
- metadata +125 -0
data/.gitignore
ADDED
data/.rspec
ADDED
data/.yardopts
ADDED
data/Gemfile
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2012 Jim Deville
|
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.
|
data/README.md
ADDED
@@ -0,0 +1,122 @@
|
|
1
|
+
# Dilation
|
2
|
+
|
3
|
+
A timer that provides an event based interface to hook into for your app. In addition, the back end timer can be swapped out, so for testing, you can control the passage of time.
|
4
|
+
|
5
|
+
## Synopsis
|
6
|
+
|
7
|
+
``` ruby
|
8
|
+
require 'rubygems'
|
9
|
+
require 'dilation'
|
10
|
+
|
11
|
+
class WakeHandler
|
12
|
+
def call
|
13
|
+
puts 'awake'
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def stop_handler
|
18
|
+
puts 'stop'
|
19
|
+
end
|
20
|
+
|
21
|
+
c = Dilation::Core.new
|
22
|
+
|
23
|
+
trap 'INT' do
|
24
|
+
c.stop
|
25
|
+
abort
|
26
|
+
end
|
27
|
+
|
28
|
+
c.listen_for :start do puts 'start' end
|
29
|
+
c.listen_for :stop, method(:stop_handler)
|
30
|
+
c.listen_for :sleep, lambda { puts 'sleep' }
|
31
|
+
c.listen_for :wake, WakeHandler.new
|
32
|
+
c.listen_for :tick, lambda { puts 'tick' }
|
33
|
+
|
34
|
+
puts 'sleeping'
|
35
|
+
|
36
|
+
c.sleep 5
|
37
|
+
|
38
|
+
puts 'starting'
|
39
|
+
counter = 0
|
40
|
+
|
41
|
+
c.listen_for :tick, lambda { counter += 1 }
|
42
|
+
|
43
|
+
c.start
|
44
|
+
while counter < 5
|
45
|
+
end
|
46
|
+
|
47
|
+
c.stop
|
48
|
+
|
49
|
+
puts 'ok'
|
50
|
+
```
|
51
|
+
|
52
|
+
TODO
|
53
|
+
|
54
|
+
## Installation
|
55
|
+
|
56
|
+
Add this line to your application's Gemfile:
|
57
|
+
|
58
|
+
gem 'dilation'
|
59
|
+
|
60
|
+
And then execute:
|
61
|
+
|
62
|
+
$ bundle
|
63
|
+
|
64
|
+
Or install it yourself as:
|
65
|
+
|
66
|
+
$ gem install dilation
|
67
|
+
|
68
|
+
## Usage
|
69
|
+
|
70
|
+
The [rubydoc.info docs](http://rubydoc.info/gems/dilation/frames) contain
|
71
|
+
API documentation. The API docs contain detailed info about all of Dilation's public API.
|
72
|
+
|
73
|
+
## Release Policy
|
74
|
+
|
75
|
+
Dilation follows the principles of [semantic versioning](http://semver.org/).
|
76
|
+
The [API documentation](http://rubydoc.info/gems/dilation/frames) define
|
77
|
+
Dilation's public API. Patch level releases contain only bug fixes. Minor
|
78
|
+
releases contain backward-compatible new features. Major new releases
|
79
|
+
contain backwards-incompatible changes to the public API.
|
80
|
+
|
81
|
+
## Ruby Interpreter Compatibility
|
82
|
+
|
83
|
+
Dilation has been tested on the following ruby interpreters:
|
84
|
+
|
85
|
+
* MRI 1.9.2
|
86
|
+
|
87
|
+
## Development
|
88
|
+
|
89
|
+
* Source hosted on [GitHub](http://github.com/jredville/dilation).
|
90
|
+
* Report issues on [GitHub Issues](http://github.com/jredville/dilation/issues).
|
91
|
+
* Pull requests are very welcome! Please include spec and/or feature coverage for every patch,
|
92
|
+
and create a topic branch for every separate change you make.
|
93
|
+
* Documentation is generated with [YARD](http://yardoc.org/) ([cheat sheet](http://cheat.errtheblog.com/s/yard/)).
|
94
|
+
To generate while developing:
|
95
|
+
|
96
|
+
```
|
97
|
+
yard server --reload
|
98
|
+
```
|
99
|
+
|
100
|
+
## Todo
|
101
|
+
|
102
|
+
* Better interface to swap backend
|
103
|
+
* dilation/test_support
|
104
|
+
* [Code Climate](http://codeclimate.com)
|
105
|
+
* [Travis](http://travis-ci.org/)
|
106
|
+
** test on interpreters
|
107
|
+
*** 1.8.7
|
108
|
+
*** 1.9.3
|
109
|
+
*** REE
|
110
|
+
*** Rubinius
|
111
|
+
* make Celluloid optional
|
112
|
+
* ability to override Kernel#sleep
|
113
|
+
|
114
|
+
## Thanks
|
115
|
+
|
116
|
+
* [Myron Marston](http://github.com/myronmarston) for [VCR](http://github.com/myronmarston/vcr), which wasn't used here, but influences how I want to run this project and this readme.
|
117
|
+
* [Microsoft's Reactive Extensions](http://msdn.microsoft.com/en-us/data/gg577609.aspx) team for the idea of [controlling the flow of time](http://channel9.msdn.com/Shows/Going+Deep/Wes-Dyer-and-Jeffrey-Van-Gogh-Inside-Rx-Virtual-Time)
|
118
|
+
|
119
|
+
## Copyright
|
120
|
+
|
121
|
+
Copyright (c) 2010-2012 Jim Deville. Released under the terms of the
|
122
|
+
MIT license. See LICENSE for details.
|
data/Rakefile
ADDED
data/dilation.gemspec
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
require File.expand_path('../lib/dilation/version', __FILE__)
|
3
|
+
|
4
|
+
Gem::Specification.new do |gem|
|
5
|
+
gem.name = "dilation"
|
6
|
+
gem.authors = ["Jim Deville"]
|
7
|
+
gem.email = ["james.deville@gmail.com"]
|
8
|
+
gem.description = %q{A controllable timer}
|
9
|
+
gem.summary = %q{A timer that provides an event based interface to hook into for your app. In addition, the back end timer can be swapped out, so for testing, you can control the passage of time.}
|
10
|
+
gem.homepage = "http://github.com/jredville/dilation"
|
11
|
+
|
12
|
+
gem.files = `git ls-files`.split($\)
|
13
|
+
gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
|
14
|
+
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
15
|
+
gem.require_paths = ["lib"]
|
16
|
+
gem.version = Dilation::VERSION
|
17
|
+
gem.platform = Gem::Platform::RUBY
|
18
|
+
gem.required_ruby_version = '>= 1.9.2'
|
19
|
+
gem.required_rubygems_version = '>= 1.3.5'
|
20
|
+
|
21
|
+
gem.add_dependency('celluloid')
|
22
|
+
gem.add_development_dependency('rake', '~> 0.9.2')
|
23
|
+
gem.add_development_dependency('rspec')
|
24
|
+
end
|
data/lib/dilation.rb
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
require "dilation/version"
|
2
|
+
require "dilation/core"
|
3
|
+
|
4
|
+
# Top level namespace for Dilation
|
5
|
+
# @see Dilation::Core
|
6
|
+
#
|
7
|
+
# @author Jim Deville <james.deville@gmail.com>
|
8
|
+
module Dilation
|
9
|
+
# Contains classes to be used for tests
|
10
|
+
module TestSupport end
|
11
|
+
|
12
|
+
# Contains timers for the backend of Core
|
13
|
+
module Timers end
|
14
|
+
|
15
|
+
# Utility classes that support the implementation
|
16
|
+
module Utils end
|
17
|
+
end
|
@@ -0,0 +1,135 @@
|
|
1
|
+
require_relative 'utils/events'
|
2
|
+
require_relative 'utils/dilator'
|
3
|
+
require_relative 'timers/coarse'
|
4
|
+
|
5
|
+
module Dilation
|
6
|
+
# Dilation::Core is the main object that allows you to interact
|
7
|
+
# with and test timers.
|
8
|
+
#
|
9
|
+
# @author Jim Deville <james.deville@gmail.com>
|
10
|
+
# @todo better interface to swap the timer
|
11
|
+
class Core
|
12
|
+
include Utils::Events
|
13
|
+
event :tick, :start, :stop, :sleep, :wake
|
14
|
+
|
15
|
+
# @param [#call] value The factory for the timer
|
16
|
+
attr_writer :timer_source
|
17
|
+
|
18
|
+
def initialize
|
19
|
+
@dilator = Utils::Dilator.new
|
20
|
+
end
|
21
|
+
|
22
|
+
# Slows down (dilates) time for your code
|
23
|
+
#
|
24
|
+
# @example Run at half time
|
25
|
+
# core.dilate(2)
|
26
|
+
# core.tick #=> nothing
|
27
|
+
# core.tick #=> fires tick event
|
28
|
+
#
|
29
|
+
# @overload dilate
|
30
|
+
# Sets a factor of 1 which is the same as no dilation
|
31
|
+
# @overload dilate(factor)
|
32
|
+
# Sets a factor of `factor` which implies one `Core#tick` per
|
33
|
+
# `factor` `Core#timer` ticks
|
34
|
+
# @param [Number] factor the dilation factor
|
35
|
+
# @return [Number] the dilation factor
|
36
|
+
# @see Dilation::Utils::Dilator
|
37
|
+
def dilate(factor = 1)
|
38
|
+
@dilator.factor = factor
|
39
|
+
@dilator.invert
|
40
|
+
factor
|
41
|
+
end
|
42
|
+
|
43
|
+
# Speeds up (contracts) time for your code
|
44
|
+
#
|
45
|
+
# @example Run at 2x time
|
46
|
+
# core.contract(2)
|
47
|
+
# core.tick #=> fires 2 tick events
|
48
|
+
#
|
49
|
+
# @overload contract
|
50
|
+
# Sets a factor of 1 which is the same as no contraction
|
51
|
+
# @overload contract(factor)
|
52
|
+
# Sets a factor of `factor` which implies `factor` `Core#tick`s
|
53
|
+
# per one `Core#timer` tick
|
54
|
+
# @param [Number] factor the contraction factor
|
55
|
+
# @return [Number] the contraction factor
|
56
|
+
# @see Dilation::Utils::Dilator
|
57
|
+
def contract(factor = 1)
|
58
|
+
@dilator.factor = factor
|
59
|
+
@dilator.uninvert
|
60
|
+
factor
|
61
|
+
end
|
62
|
+
|
63
|
+
# @private
|
64
|
+
# Called by the timer on each tick. Fires tick events based on
|
65
|
+
# the dilation/contraction factor
|
66
|
+
#
|
67
|
+
# @see #dilate
|
68
|
+
# @see #contract
|
69
|
+
def tick
|
70
|
+
if started?
|
71
|
+
@dilator.run { __tick }
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
# Starts the timer and fires the start event
|
76
|
+
def start
|
77
|
+
timer.start
|
78
|
+
@started = true
|
79
|
+
__start
|
80
|
+
end
|
81
|
+
|
82
|
+
# Stops the timer and fires the stop event
|
83
|
+
def stop
|
84
|
+
timer.stop
|
85
|
+
@started = false
|
86
|
+
__stop
|
87
|
+
end
|
88
|
+
|
89
|
+
# Sleeps for `time` (based on Core#tick).
|
90
|
+
#
|
91
|
+
# @note This method blocks the caller
|
92
|
+
#
|
93
|
+
# @example Sleep for 5 seconds
|
94
|
+
# core.sleep 5 #=> 5
|
95
|
+
# @example Sleep for 1 second
|
96
|
+
# core.sleep #=> 1
|
97
|
+
# @overload sleep(time)
|
98
|
+
# Sleep for the given number of second
|
99
|
+
# @param [Number] time the time to sleep for in seconds
|
100
|
+
# @overload sleep
|
101
|
+
# Sleep for 1 second
|
102
|
+
# @return [Number] time
|
103
|
+
def sleep(time=1)
|
104
|
+
sleep_handler = lambda { time -= 1}
|
105
|
+
listen_for :tick, sleep_handler
|
106
|
+
__sleep
|
107
|
+
start
|
108
|
+
while time > 0
|
109
|
+
end
|
110
|
+
stop
|
111
|
+
wake
|
112
|
+
time
|
113
|
+
ensure
|
114
|
+
dont_listen_for :tick, sleep_handler
|
115
|
+
end
|
116
|
+
|
117
|
+
# @return [Timer] the timer object for this core
|
118
|
+
#
|
119
|
+
# @see Dilation::Timers::Coarse
|
120
|
+
def timer
|
121
|
+
@timer ||= timer_source.call(self)
|
122
|
+
end
|
123
|
+
|
124
|
+
# @todo does this need to be public?
|
125
|
+
# @return [Boolean] is this core started or not
|
126
|
+
def started?
|
127
|
+
defined?(@started) && @started
|
128
|
+
end
|
129
|
+
|
130
|
+
private
|
131
|
+
def timer_source
|
132
|
+
@timer_source ||= lambda { |obj| Timers::Coarse.new(obj) }
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
module Dilation
|
2
|
+
module TestSupport
|
3
|
+
# A handler that can be used in tests to confirm that events are hooked up
|
4
|
+
#
|
5
|
+
# @example Rspec example
|
6
|
+
# describe "Example" do
|
7
|
+
# let(:handler) { Dilation::TestSupport::Handler.new }
|
8
|
+
# subject { MyCoreBasedClass.new }
|
9
|
+
#
|
10
|
+
# it "triggers handler" do
|
11
|
+
# subject.listen_for :tick, handler
|
12
|
+
# subject.tick
|
13
|
+
# handler.should be_triggered
|
14
|
+
# end
|
15
|
+
# end
|
16
|
+
#
|
17
|
+
# @author Jim Deville <james.deville@gmail.com>
|
18
|
+
class Handler
|
19
|
+
# @return [Number] the number of times triggered
|
20
|
+
attr_reader :count
|
21
|
+
|
22
|
+
def initialize
|
23
|
+
@count = 0
|
24
|
+
end
|
25
|
+
|
26
|
+
# Trigger this handler
|
27
|
+
# @note increments the counter
|
28
|
+
#
|
29
|
+
# @return [Number] the new count
|
30
|
+
def call
|
31
|
+
@count += 1
|
32
|
+
end
|
33
|
+
|
34
|
+
# @return [Boolean] true if this handler has been called
|
35
|
+
def triggered?
|
36
|
+
@count > 0
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
require 'celluloid'
|
2
|
+
require_relative 'timer'
|
3
|
+
|
4
|
+
Celluloid.logger = nil
|
5
|
+
|
6
|
+
module Dilation
|
7
|
+
module Timers
|
8
|
+
# A Celluloid based timer that is the default for Dilation::Core
|
9
|
+
# @see http://celluloid.io/
|
10
|
+
#
|
11
|
+
# @author Jim Deville <james.deville@gmail.com>
|
12
|
+
class Coarse < Timer
|
13
|
+
include Celluloid
|
14
|
+
|
15
|
+
# Stop this timer
|
16
|
+
def stop
|
17
|
+
defined?(@timer) && @timer.cancel
|
18
|
+
super
|
19
|
+
end
|
20
|
+
|
21
|
+
# Start this timer, ticking every second
|
22
|
+
def start
|
23
|
+
@timer = every(1) { tick }
|
24
|
+
super
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
require_relative 'timer'
|
2
|
+
|
3
|
+
module Dilation
|
4
|
+
module Timers
|
5
|
+
# A test timer that does not fire by itself. Primarily meant to
|
6
|
+
# be used inside of tests so you can control the passage of time
|
7
|
+
#
|
8
|
+
# @author Jim Deville <james.deville@gmail.com>
|
9
|
+
# @todo make this default for tests
|
10
|
+
class Test < Timer
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
module Dilation
|
2
|
+
module Timers
|
3
|
+
# @abstract Subclass and override {#start} and {#stop}
|
4
|
+
#
|
5
|
+
# @author Jim Deville <james.deville@gmail.com>
|
6
|
+
class Timer
|
7
|
+
# @param [#tick] target the object to notify on ticks
|
8
|
+
def initialize(target)
|
9
|
+
@target = target
|
10
|
+
end
|
11
|
+
|
12
|
+
# This method should be called by subclasses on each tick
|
13
|
+
def tick
|
14
|
+
@target.tick
|
15
|
+
end
|
16
|
+
|
17
|
+
# @return [Boolean] true if this timer is running
|
18
|
+
def running?
|
19
|
+
defined?(@started) && @started
|
20
|
+
end
|
21
|
+
|
22
|
+
# Stop this timer
|
23
|
+
def stop
|
24
|
+
@started = false
|
25
|
+
end
|
26
|
+
|
27
|
+
# Start this timer
|
28
|
+
def start
|
29
|
+
@started = true
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,80 @@
|
|
1
|
+
module Dilation
|
2
|
+
module Utils
|
3
|
+
# This class is used for controlling the dilation and contraction
|
4
|
+
# factors of Core
|
5
|
+
#
|
6
|
+
# @author Jim Deville <james.deville@gmail.com>
|
7
|
+
# @todo Fix floating point issue (see specs)
|
8
|
+
class Dilator
|
9
|
+
def initialize
|
10
|
+
self.factor = 1
|
11
|
+
end
|
12
|
+
|
13
|
+
# Set the factor and reset the dilator
|
14
|
+
#
|
15
|
+
# @param [Number] val the new factor
|
16
|
+
def factor=(val)
|
17
|
+
@factor = val
|
18
|
+
reset
|
19
|
+
end
|
20
|
+
|
21
|
+
# Invert the dilator. When inverted, `factor` calls to {#run}
|
22
|
+
# yield once. Also resets the dilator
|
23
|
+
#
|
24
|
+
# @see {#run}
|
25
|
+
def invert
|
26
|
+
@invert = true
|
27
|
+
reset
|
28
|
+
end
|
29
|
+
|
30
|
+
# Uninvert the dilator. When uninverted, one call to {#run}
|
31
|
+
# yield `factor` times. Also resets the dilator
|
32
|
+
#
|
33
|
+
# @see {#run}
|
34
|
+
def uninvert
|
35
|
+
@invert = false
|
36
|
+
reset
|
37
|
+
end
|
38
|
+
|
39
|
+
# @return [Boolean] true if this dilator is inverted
|
40
|
+
def inverted?
|
41
|
+
defined?(@invert) && @invert
|
42
|
+
end
|
43
|
+
|
44
|
+
# Reset this dilator
|
45
|
+
# @todo should this be public?
|
46
|
+
def reset
|
47
|
+
@count = 0
|
48
|
+
end
|
49
|
+
|
50
|
+
# Run ths dilator based on the factor
|
51
|
+
#
|
52
|
+
# @yield if the factor is met
|
53
|
+
# @see {#invert}
|
54
|
+
# @see {#uninvert}
|
55
|
+
def run(&blk)
|
56
|
+
begin
|
57
|
+
blk.call
|
58
|
+
end while run_again? if ready?
|
59
|
+
end
|
60
|
+
|
61
|
+
private
|
62
|
+
def ready?
|
63
|
+
@count += factor
|
64
|
+
@count >= 1
|
65
|
+
end
|
66
|
+
|
67
|
+
def factor
|
68
|
+
@invert ? 1/@factor.to_f : @factor
|
69
|
+
end
|
70
|
+
|
71
|
+
def run_again?
|
72
|
+
@count -= 1
|
73
|
+
if @count < 1
|
74
|
+
return false
|
75
|
+
end
|
76
|
+
true
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
@@ -0,0 +1,91 @@
|
|
1
|
+
module Dilation
|
2
|
+
module Utils
|
3
|
+
# Defines an event handler system loosely based off of the DOM
|
4
|
+
# addEventListener pattern
|
5
|
+
#
|
6
|
+
# @example
|
7
|
+
# class Foo
|
8
|
+
# include Dilation::Utils::Events
|
9
|
+
# event :bar, :baz
|
10
|
+
# def baz
|
11
|
+
# bar
|
12
|
+
# __baz
|
13
|
+
# end
|
14
|
+
# end
|
15
|
+
# f = Foo.new
|
16
|
+
# f.listen_for :bar, { puts 'bar' }
|
17
|
+
# f.listen_for :baz, { puts 'baz' }
|
18
|
+
# f.baz #=> prints bar then baz
|
19
|
+
#
|
20
|
+
# @author Jim Deville <james.deville@gmail.com>
|
21
|
+
# @todo fix return types
|
22
|
+
module Events
|
23
|
+
# @private
|
24
|
+
def self.included(base)
|
25
|
+
base.send :extend, ClassMethods
|
26
|
+
end
|
27
|
+
|
28
|
+
module ClassMethods
|
29
|
+
# Defines one or more events with the name and the __name variant
|
30
|
+
#
|
31
|
+
# @param [Array<String>, Array<Symbol>] names the names of the events to create
|
32
|
+
def event(*names)
|
33
|
+
names.each do |name|
|
34
|
+
define_method(name) { fire name }
|
35
|
+
alias_method :"__#{name}", name
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
# Registers a block or callable for the given event
|
41
|
+
#
|
42
|
+
# @overload listen_for(name, handler)
|
43
|
+
# registers the handler for the event
|
44
|
+
# @param [String, Symbol, #to_sym] name the event to listen for
|
45
|
+
# @param [#call] handler the handler to trigger for this event
|
46
|
+
# @overload listen_for(name, &blk)
|
47
|
+
# registers the block for the event
|
48
|
+
# @param [String, Symbol, #to_sym] name the event to listen for
|
49
|
+
# @param blk the block to trigger for this event
|
50
|
+
# @raise ArgumentError if handler or blk is not present
|
51
|
+
def listen_for(name, handler = nil, &blk)
|
52
|
+
to_add = handler || blk
|
53
|
+
raise ArgumentError.new "handler or block required" unless to_add
|
54
|
+
handlers[name.to_sym] << to_add
|
55
|
+
end
|
56
|
+
|
57
|
+
# Removes a callable for the given event
|
58
|
+
#
|
59
|
+
# @param [String, Symbol, #to_sym] name the event to remove from
|
60
|
+
# @param [#call] handler the handler to remove
|
61
|
+
def dont_listen_for(name, handler)
|
62
|
+
handlers[name.to_sym].delete(handler)
|
63
|
+
end
|
64
|
+
|
65
|
+
# Clears all handlers for the event
|
66
|
+
#
|
67
|
+
# @param [String, Symbol, #to_sym] name the event to clear
|
68
|
+
def clear(name)
|
69
|
+
handlers[name.to_sym] = []
|
70
|
+
end
|
71
|
+
|
72
|
+
# Clear all tracked handlers
|
73
|
+
def clear_all
|
74
|
+
self.handlers = Hash.new { |h, k| h[k] = [] }
|
75
|
+
end
|
76
|
+
|
77
|
+
# Triggers the given event
|
78
|
+
#
|
79
|
+
# @param [String, Symbol, #to_sym] name event to trigger
|
80
|
+
def fire(name)
|
81
|
+
handlers[name.to_sym].each(&:call)
|
82
|
+
end
|
83
|
+
|
84
|
+
private
|
85
|
+
attr_writer :handlers
|
86
|
+
def handlers
|
87
|
+
@handlers || clear_all
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
@@ -0,0 +1,144 @@
|
|
1
|
+
require 'spec_mini'
|
2
|
+
require 'dilation/core'
|
3
|
+
require 'dilation/timers/test'
|
4
|
+
|
5
|
+
describe Dilation::Core do
|
6
|
+
let(:core) { Dilation::Core.new }
|
7
|
+
let(:timer) { Dilation::Timers::Test.new(subject) }
|
8
|
+
|
9
|
+
include_context "core event"
|
10
|
+
|
11
|
+
subject do
|
12
|
+
core.tap do |c|
|
13
|
+
c.timer_source = lambda { |o| timer }
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
context "starting and stopping" do
|
18
|
+
before do
|
19
|
+
subject.listen_for :tick, handler
|
20
|
+
end
|
21
|
+
|
22
|
+
it "starts timer when start is called" do
|
23
|
+
timer.should_receive(:start)
|
24
|
+
subject.start
|
25
|
+
end
|
26
|
+
|
27
|
+
it "stops timer when stop is called" do
|
28
|
+
timer.should_receive(:stop)
|
29
|
+
subject.stop
|
30
|
+
end
|
31
|
+
|
32
|
+
it "does not get ticks before timer is started" do
|
33
|
+
expect { timer.tick }.not_to trigger(handler)
|
34
|
+
end
|
35
|
+
|
36
|
+
it "gets ticks after timer is started" do
|
37
|
+
subject.start
|
38
|
+
expect { timer.tick }.to trigger(handler)
|
39
|
+
end
|
40
|
+
|
41
|
+
it "does not get ticks after timer is stopped" do
|
42
|
+
subject.start
|
43
|
+
subject.stop
|
44
|
+
expect { timer.tick }.not_to trigger(handler)
|
45
|
+
end
|
46
|
+
|
47
|
+
it "ignores multiple start calls"
|
48
|
+
it "ignores multiple stop calls"
|
49
|
+
it "stop when not started is ignored"
|
50
|
+
end
|
51
|
+
|
52
|
+
context "timer" do
|
53
|
+
before do
|
54
|
+
subject.start
|
55
|
+
end
|
56
|
+
|
57
|
+
it "gets tick called by the timer" do
|
58
|
+
subject.listen_for :tick, handler
|
59
|
+
expect { timer.tick }.to trigger(handler)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
context "dilation" do
|
64
|
+
before do
|
65
|
+
subject.listen_for :tick, handler
|
66
|
+
subject.start
|
67
|
+
end
|
68
|
+
|
69
|
+
it "returns the new factor" do
|
70
|
+
subject.dilate.should == 1
|
71
|
+
subject.dilate(2).should == 2
|
72
|
+
subject.contract.should == 1
|
73
|
+
subject.contract(2).should == 2
|
74
|
+
end
|
75
|
+
|
76
|
+
it "receives one tick per tick of the timer by default" do
|
77
|
+
10.times { timer.tick }
|
78
|
+
handler.count.should == 10
|
79
|
+
end
|
80
|
+
|
81
|
+
it "dilates time" do
|
82
|
+
subject.dilate(2) #2 real ticks per tick
|
83
|
+
10.times { timer.tick }
|
84
|
+
handler.count.should == 5
|
85
|
+
end
|
86
|
+
|
87
|
+
it "defaults to a dilation factor of 1" do
|
88
|
+
subject.dilate
|
89
|
+
10.times { timer.tick }
|
90
|
+
handler.count.should == 10
|
91
|
+
end
|
92
|
+
|
93
|
+
it "contracts time" do
|
94
|
+
subject.contract(2) #2 ticks for every real tick
|
95
|
+
10.times { timer.tick }
|
96
|
+
handler.count.should == 20
|
97
|
+
end
|
98
|
+
|
99
|
+
it "defaults to a contraction factor of 1" do
|
100
|
+
subject.contract
|
101
|
+
10.times { timer.tick }
|
102
|
+
handler.count.should == 10
|
103
|
+
end
|
104
|
+
|
105
|
+
it "resets dilation with contraction" do
|
106
|
+
subject.dilate(2)
|
107
|
+
subject.contract(2)
|
108
|
+
10.times { timer.tick }
|
109
|
+
handler.count.should == 20
|
110
|
+
end
|
111
|
+
|
112
|
+
it "resets contraction with dilation" do
|
113
|
+
subject.contract(2)
|
114
|
+
subject.dilate(2)
|
115
|
+
10.times { timer.tick }
|
116
|
+
handler.count.should == 5
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
context "events" do
|
121
|
+
context "tick" do
|
122
|
+
before do
|
123
|
+
subject.start
|
124
|
+
end
|
125
|
+
it_behaves_like "a core event", :tick, :tick
|
126
|
+
end
|
127
|
+
|
128
|
+
context "start" do
|
129
|
+
it_behaves_like "a core event", :start, :start
|
130
|
+
end
|
131
|
+
|
132
|
+
context "stop" do
|
133
|
+
it_behaves_like "a core event", :stop, :stop
|
134
|
+
end
|
135
|
+
|
136
|
+
# context "sleep" do
|
137
|
+
# it_behaves_like "a core event", :sleep, :sleep
|
138
|
+
# end
|
139
|
+
|
140
|
+
context "wake" do
|
141
|
+
it_behaves_like "a core event", :wake, :wake
|
142
|
+
end
|
143
|
+
end
|
144
|
+
end
|
@@ -0,0 +1,85 @@
|
|
1
|
+
require 'dilation/utils/dilator'
|
2
|
+
|
3
|
+
describe Dilation::Utils::Dilator do
|
4
|
+
before do
|
5
|
+
@counter = 0
|
6
|
+
@blk = lambda { @counter += 1 }
|
7
|
+
end
|
8
|
+
|
9
|
+
shared_examples "an inverter" do
|
10
|
+
it "inverts" do
|
11
|
+
subject.invert
|
12
|
+
subject.should be_inverted
|
13
|
+
end
|
14
|
+
|
15
|
+
it "uninverts" do
|
16
|
+
subject.uninvert
|
17
|
+
subject.should_not be_inverted
|
18
|
+
end
|
19
|
+
|
20
|
+
it "defaults to 1 run per tick" do
|
21
|
+
expect { |b| subject.run(&b) }.to yield_control
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
context "when uninverted" do
|
26
|
+
it_behaves_like "an inverter"
|
27
|
+
|
28
|
+
it { subject.should_not be_inverted }
|
29
|
+
|
30
|
+
it "runs n times per tick if factor is set to n" do
|
31
|
+
subject.factor = 2
|
32
|
+
subject.run &@blk
|
33
|
+
@counter.should == 2
|
34
|
+
end
|
35
|
+
|
36
|
+
it "runs fractional factors" do
|
37
|
+
subject.factor = 1.5
|
38
|
+
subject.run &@blk
|
39
|
+
@counter.should == 1
|
40
|
+
subject.run &@blk
|
41
|
+
@counter.should == 3
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
context "when inverted" do
|
46
|
+
before do
|
47
|
+
subject.invert
|
48
|
+
end
|
49
|
+
|
50
|
+
it_behaves_like "an inverter"
|
51
|
+
|
52
|
+
it { subject.should be_inverted }
|
53
|
+
|
54
|
+
it "runs once every n times if factor is set to n and object is inverted" do
|
55
|
+
subject.factor = 3
|
56
|
+
3.times { subject.run &@blk }
|
57
|
+
@counter.should == 1
|
58
|
+
end
|
59
|
+
|
60
|
+
it "resets the count when factor is changed" do
|
61
|
+
subject.factor = 2
|
62
|
+
subject.run &@blk
|
63
|
+
subject.factor = 3
|
64
|
+
2.times { subject.run &@blk }
|
65
|
+
@counter.should == 0
|
66
|
+
end
|
67
|
+
|
68
|
+
it "runs fractional factors" do
|
69
|
+
pending "floating point issue"
|
70
|
+
subject.factor = 1.5
|
71
|
+
3.times { subject.run &@blk }
|
72
|
+
@counter.should == 2
|
73
|
+
end
|
74
|
+
|
75
|
+
it "resets the count when the inversion is changed" do
|
76
|
+
pending "floating point"
|
77
|
+
subject.invert
|
78
|
+
subject.factor = 1.5
|
79
|
+
subject.run &@blk
|
80
|
+
subject.invert
|
81
|
+
subject.run &@blk
|
82
|
+
@counter.should == 1
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
data/spec/integration.rb
ADDED
@@ -0,0 +1,42 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'dilation'
|
3
|
+
|
4
|
+
class WakeHandler
|
5
|
+
def call
|
6
|
+
puts 'awake'
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
def stop_handler
|
11
|
+
puts 'stop'
|
12
|
+
end
|
13
|
+
|
14
|
+
c = Dilation::Core.new
|
15
|
+
|
16
|
+
trap 'INT' do
|
17
|
+
c.stop
|
18
|
+
abort
|
19
|
+
end
|
20
|
+
|
21
|
+
c.listen_for :start do puts 'start' end
|
22
|
+
c.listen_for :stop, method(:stop_handler)
|
23
|
+
c.listen_for :sleep, lambda { puts 'sleep' }
|
24
|
+
c.listen_for :wake, WakeHandler.new
|
25
|
+
c.listen_for :tick, lambda { puts 'tick' }
|
26
|
+
|
27
|
+
puts 'sleeping'
|
28
|
+
|
29
|
+
c.sleep 5
|
30
|
+
|
31
|
+
puts 'starting'
|
32
|
+
counter = 0
|
33
|
+
|
34
|
+
c.listen_for :tick, lambda { counter += 1 }
|
35
|
+
|
36
|
+
c.start
|
37
|
+
while counter < 5
|
38
|
+
end
|
39
|
+
|
40
|
+
c.stop
|
41
|
+
|
42
|
+
puts 'ok'
|
data/spec/spec_mini.rb
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
RSpec::Matchers.define(:trigger) do |handler|
|
2
|
+
match do |actual|
|
3
|
+
@not_to_trigger ||= []
|
4
|
+
@to_trigger ||= []
|
5
|
+
@to_trigger << handler
|
6
|
+
actual.call
|
7
|
+
@to_trigger.all?(&:triggered?) && @not_to_trigger.none?(&:triggered?)
|
8
|
+
end
|
9
|
+
|
10
|
+
chain :but_not do |handler|
|
11
|
+
@not_to_trigger ||= []
|
12
|
+
@not_to_trigger << handler
|
13
|
+
end
|
14
|
+
|
15
|
+
chain :and_also do |handler|
|
16
|
+
@to_trigger ||= []
|
17
|
+
@to_trigger << handler
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
require 'dilation/test_support/handler'
|
2
|
+
|
3
|
+
shared_context "core event" do
|
4
|
+
let(:handler) { Dilation::TestSupport::Handler.new }
|
5
|
+
let(:handler2) { Dilation::TestSupport::Handler.new }
|
6
|
+
end
|
7
|
+
|
8
|
+
shared_examples "a core event" do |event_name, trigger|
|
9
|
+
it_behaves_like "a core event details", event_name, trigger
|
10
|
+
it_behaves_like "a core event details", event_name, "__#{trigger}"
|
11
|
+
end
|
12
|
+
|
13
|
+
shared_examples "a core event details" do |event_name, trigger|
|
14
|
+
include_context "core event"
|
15
|
+
|
16
|
+
it "can trigger without handlers" do
|
17
|
+
expect { subject.send trigger }.to_not raise_error
|
18
|
+
end
|
19
|
+
|
20
|
+
it "can be subscribed via #listen_for with :#{event_name}" do
|
21
|
+
subject.listen_for event_name, handler
|
22
|
+
expect { subject.send trigger }.to trigger(handler)
|
23
|
+
end
|
24
|
+
|
25
|
+
it "can be subscribed multiple times via #listen_for with :#{event_name}" do
|
26
|
+
subject.listen_for event_name, handler
|
27
|
+
subject.listen_for event_name.to_s, handler2
|
28
|
+
expect { subject.send trigger }.to trigger(handler).and_also(handler2)
|
29
|
+
end
|
30
|
+
|
31
|
+
it "listen_for requires a block or a handler" do
|
32
|
+
expect { subject.listen_for event_name }.to raise_error ArgumentError
|
33
|
+
end
|
34
|
+
|
35
|
+
it "listen_for ignores the block if a handler is given" do
|
36
|
+
subject.listen_for event_name, handler do
|
37
|
+
handler2.call
|
38
|
+
end
|
39
|
+
expect { subject.send trigger }.to trigger(handler).but_not(handler2)
|
40
|
+
end
|
41
|
+
|
42
|
+
it "can subscribe to a block" do
|
43
|
+
subject.listen_for event_name do
|
44
|
+
handler.call
|
45
|
+
end
|
46
|
+
expect { subject.send trigger }.to trigger(handler)
|
47
|
+
end
|
48
|
+
|
49
|
+
it "can clear a listener" do
|
50
|
+
subject.listen_for event_name, handler
|
51
|
+
subject.listen_for event_name, handler2
|
52
|
+
subject.dont_listen_for event_name, handler2
|
53
|
+
expect { subject.send trigger }.to trigger(handler).but_not(handler2)
|
54
|
+
end
|
55
|
+
|
56
|
+
it "works after clearing listeners" do
|
57
|
+
subject.listen_for event_name, handler
|
58
|
+
subject.clear event_name
|
59
|
+
subject.listen_for event_name, handler2
|
60
|
+
expect { subject.send trigger }.to trigger(handler2).but_not(handler)
|
61
|
+
end
|
62
|
+
|
63
|
+
it "works after clearing all listeners" do
|
64
|
+
subject.listen_for event_name, handler
|
65
|
+
subject.clear_all
|
66
|
+
subject.listen_for event_name, handler2
|
67
|
+
expect { subject.send trigger }.to trigger(handler2).but_not(handler)
|
68
|
+
end
|
69
|
+
end
|
metadata
ADDED
@@ -0,0 +1,125 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: dilation
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.2
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Jim Deville
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2012-06-22 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: celluloid
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ! '>='
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '0'
|
22
|
+
type: :runtime
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ! '>='
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: '0'
|
30
|
+
- !ruby/object:Gem::Dependency
|
31
|
+
name: rake
|
32
|
+
requirement: !ruby/object:Gem::Requirement
|
33
|
+
none: false
|
34
|
+
requirements:
|
35
|
+
- - ~>
|
36
|
+
- !ruby/object:Gem::Version
|
37
|
+
version: 0.9.2
|
38
|
+
type: :development
|
39
|
+
prerelease: false
|
40
|
+
version_requirements: !ruby/object:Gem::Requirement
|
41
|
+
none: false
|
42
|
+
requirements:
|
43
|
+
- - ~>
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
version: 0.9.2
|
46
|
+
- !ruby/object:Gem::Dependency
|
47
|
+
name: rspec
|
48
|
+
requirement: !ruby/object:Gem::Requirement
|
49
|
+
none: false
|
50
|
+
requirements:
|
51
|
+
- - ! '>='
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: '0'
|
54
|
+
type: :development
|
55
|
+
prerelease: false
|
56
|
+
version_requirements: !ruby/object:Gem::Requirement
|
57
|
+
none: false
|
58
|
+
requirements:
|
59
|
+
- - ! '>='
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
description: A controllable timer
|
63
|
+
email:
|
64
|
+
- james.deville@gmail.com
|
65
|
+
executables: []
|
66
|
+
extensions: []
|
67
|
+
extra_rdoc_files: []
|
68
|
+
files:
|
69
|
+
- .gitignore
|
70
|
+
- .rspec
|
71
|
+
- .yardopts
|
72
|
+
- Gemfile
|
73
|
+
- LICENSE
|
74
|
+
- README.md
|
75
|
+
- Rakefile
|
76
|
+
- dilation.gemspec
|
77
|
+
- lib/dilation.rb
|
78
|
+
- lib/dilation/core.rb
|
79
|
+
- lib/dilation/test_support/handler.rb
|
80
|
+
- lib/dilation/timers/coarse.rb
|
81
|
+
- lib/dilation/timers/test.rb
|
82
|
+
- lib/dilation/timers/timer.rb
|
83
|
+
- lib/dilation/utils/dilator.rb
|
84
|
+
- lib/dilation/utils/events.rb
|
85
|
+
- lib/dilation/version.rb
|
86
|
+
- spec/dilation/core_spec.rb
|
87
|
+
- spec/dilation/utils/dilator_spec.rb
|
88
|
+
- spec/integration.rb
|
89
|
+
- spec/spec_mini.rb
|
90
|
+
- spec/support/matchers.rb
|
91
|
+
- spec/support/shared_examples.rb
|
92
|
+
homepage: http://github.com/jredville/dilation
|
93
|
+
licenses: []
|
94
|
+
post_install_message:
|
95
|
+
rdoc_options: []
|
96
|
+
require_paths:
|
97
|
+
- lib
|
98
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
99
|
+
none: false
|
100
|
+
requirements:
|
101
|
+
- - ! '>='
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: 1.9.2
|
104
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
105
|
+
none: false
|
106
|
+
requirements:
|
107
|
+
- - ! '>='
|
108
|
+
- !ruby/object:Gem::Version
|
109
|
+
version: 1.3.5
|
110
|
+
requirements: []
|
111
|
+
rubyforge_project:
|
112
|
+
rubygems_version: 1.8.24
|
113
|
+
signing_key:
|
114
|
+
specification_version: 3
|
115
|
+
summary: A timer that provides an event based interface to hook into for your app.
|
116
|
+
In addition, the back end timer can be swapped out, so for testing, you can control
|
117
|
+
the passage of time.
|
118
|
+
test_files:
|
119
|
+
- spec/dilation/core_spec.rb
|
120
|
+
- spec/dilation/utils/dilator_spec.rb
|
121
|
+
- spec/integration.rb
|
122
|
+
- spec/spec_mini.rb
|
123
|
+
- spec/support/matchers.rb
|
124
|
+
- spec/support/shared_examples.rb
|
125
|
+
has_rdoc:
|