torkify 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,19 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
18
+ *.tags
19
+ log/
data/Gemfile ADDED
@@ -0,0 +1,5 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in torkify.gemspec
4
+ gemspec
5
+ gem 'rb-inotify'
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2013 Jon Cairns
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,230 @@
1
+ # Torkify
2
+
3
+ Torkify integrates with [tork][1], which is a solution for automating tests as you change your source files.
4
+
5
+ Torkify hooks in to tork's remote events, and allows you to write ruby code that's called when particular events fire. This makes it easy for you to write code that triggers cool stuff when your tests pass or fail.
6
+
7
+ ## An example
8
+
9
+ You define callbacks by creating an observer that defines certain methods. Here's an example that creates a system notification:
10
+
11
+ ```ruby
12
+ # my_tork_notifier.rb
13
+ require 'torkify'
14
+
15
+ class SystemNotifier
16
+ def notify(text)
17
+ # Do a system call to fire a popup notification, e.g. `notify-send`
18
+ end
19
+
20
+ def on_pass(event)
21
+ notify "Test passed: #{event.file}"
22
+ end
23
+
24
+ def on_fail(event)
25
+ notify "Test failed: #{event.file}, log file #{event.log_file}"
26
+ end
27
+ end
28
+
29
+ listener = Torkify.listener
30
+ listener.add_observer SystemNotifier.new
31
+ listener.start
32
+ ```
33
+
34
+ This connects to an existing tork process that's running in the current directory.
35
+
36
+ ## Usage
37
+
38
+ Create a ruby script, load torkify and set up a new listener:
39
+
40
+ ```ruby
41
+ # my_tork_notifier.rb
42
+
43
+ require 'torkify'
44
+
45
+ listener = Torkify.listener
46
+ ```
47
+
48
+ This listener allows you to add observer objects which you create yourself:
49
+
50
+ ```ruby
51
+ # my_tork_notifier.rb
52
+
53
+ listener.add_observer MyObserver.new
54
+ listener.start # connect to tork and pass all events to the observer(s)
55
+ ```
56
+
57
+ ### Observer callback methods
58
+
59
+ Your observer classes can define any number of the following methods:
60
+
61
+ * `on_startup`: when torkify starts
62
+ * `on_shutdown`: when torkify shuts down
63
+ * `on_test`: when a test is started
64
+ * `on_pass`: when a test passes
65
+ * `on_fail`: when a test fails
66
+ * `on_pass_now_fail`: when a previously passed test fails
67
+ * `on_fail_now_pass`: when a previously failed test passes
68
+ * `on_absorb`: when tork re-absorbs the environment
69
+
70
+ Each method takes an optional event object as a parameter: this contains all the contextual information about that event. See **callback event objects** below for a complete list of accessible attributes on the events.
71
+
72
+ ### Options for starting torkify
73
+
74
+ Calling `start()` on the listener will try and attach to a running tork process in the same directory as the script is run. This starts the process `tork-remote tork-engine` in the current directory. If you want to change the command that's called (e.g. if bundler is giving you grief), then you can pass that as the first parameter to start:
75
+
76
+ ```ruby
77
+ # my_tork_notifier.rb
78
+
79
+ listener.start 'bundle exec tork-remote tork-engine' # Will use this command instead
80
+ ```
81
+
82
+ If tork is running in a different directory, you can pass in a path as the second parameter:
83
+
84
+ ```ruby
85
+ # my_tork_notifier.rb
86
+
87
+ listener.start 'tork-remote tork-engine', '/home/user/project'
88
+ ```
89
+
90
+ `start()` will assume that tork is running, and will exit if no tork process is found. If you want it to keep looping until tork starts, use `start_loop()`:
91
+
92
+ ```ruby
93
+ # my_tork_notifier.rb
94
+
95
+ listener.start_loop # you can pass the same parameters as with start()
96
+ ```
97
+
98
+ ### Starting with tork
99
+
100
+ You may not want to keep tork and torkify separate - for convenience, torkify allows you to start both at the same time. It forks torkify as a child process and runs tork in the parent, allowing you to interact with tork via STDIN but giving you the callbacks of torkify. Just use `start_with_tork()`:
101
+
102
+ ```ruby
103
+ # my_tork_notifier.rb
104
+
105
+ listener.start_with_tork 'bundle exec tork', 'default:logdir' # Starts both tork and torkify
106
+ ```
107
+
108
+ Both parameters are optional. The first is the command to execute tork, and the second is the `$TORK_CONFIGS` environment variable.
109
+
110
+ ### Multiple observers
111
+
112
+ You can add multiple observers to your listener:
113
+
114
+ ```ruby
115
+ require 'torkify'
116
+
117
+ class FirstObserver
118
+ #...
119
+ end
120
+
121
+ class SecondObserver
122
+ #...
123
+ end
124
+
125
+ listener.Torkify.listener
126
+ listener.add_observer FirstObserver.new
127
+ listener.add_observer SecondObserver.new
128
+ listener.start
129
+ ```
130
+
131
+ ### Callback event objects
132
+
133
+ Here's an example observer, and the accessible data on the events provided in the callbacks:
134
+
135
+ ```ruby
136
+
137
+ class MyObserver
138
+ def on_test(event)
139
+ event.type #=> "test"
140
+ event.file #=> "spec/example_spec.rb"
141
+ event.log_file #=> "spec/example_spec.rb.log"
142
+ event.lines #=> [10, 11, 12]
143
+ event.worker #=> 0
144
+ end
145
+
146
+ def on_pass(event)
147
+ event.type #=> "pass"
148
+ event.file #=> "spec/example_spec.rb"
149
+ event.log_file #=> "spec/example_spec.rb.log"
150
+ event.lines #=> [10, 11, 12]
151
+ event.worker #=> 0
152
+ event.exit_code #=> 0
153
+ event.pid #=> 22813
154
+ end
155
+
156
+ def on_fail(event)
157
+ event.type #=> "fail"
158
+ event.file #=> "spec/example_spec.rb"
159
+ event.log_file #=> "spec/example_spec.rb.log"
160
+ event.lines #=> [10, 11, 12]
161
+ event.worker #=> 0
162
+ event.exit_code #=> 0
163
+ event.pid #=> 22813
164
+ end
165
+
166
+ # The event has an inner event, which is the same type of event
167
+ # that's passed to #on_fail()
168
+ def on_pass_now_fail(event)
169
+ event.type #=> "pass_now_fail"
170
+ event.file #=> "spec/example_spec.rb"
171
+ event.event.type #=> "fail"
172
+ end
173
+
174
+ # The event has an inner event, which is the same type of event
175
+ # that's passed to #on_pass()
176
+ def on_fail_now_pass(event)
177
+ event.type #=> "fail_now_pass"
178
+ event.file #=> "spec/example_spec.rb"
179
+ event.event.type #=> "pass"
180
+ end
181
+
182
+ def on_absorb(event)
183
+ event.type #=> "absorb"
184
+ end
185
+
186
+ def on_startup(event)
187
+ event.type #=> "startup"
188
+ end
189
+
190
+ def on_shutdown(event)
191
+ event.type #=> "shutdown"
192
+ end
193
+ end
194
+ ```
195
+
196
+ ## Installation
197
+
198
+ Add this line to your application's Gemfile:
199
+
200
+ gem 'torkify'
201
+
202
+ And then execute:
203
+
204
+ $ bundle
205
+
206
+ Or install it yourself as:
207
+
208
+ $ gem install torkify
209
+
210
+ ## Usage
211
+
212
+ TODO: Write usage instructions here
213
+
214
+ ## Contributing
215
+
216
+ The guidelines are:
217
+
218
+ * The tests must pass (run python vdebugtests.py in the top directory of the plugin)
219
+ * Your commit messages should follow the rules outlined [here][2]
220
+
221
+ The steps are:
222
+
223
+ 1. Fork it
224
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
225
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
226
+ 4. Push to the branch (`git push origin my-new-feature`)
227
+ 5. Create new Pull Request
228
+
229
+ [1]: https://github.com/sunaku/tork
230
+ [2]: http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html
data/Rakefile ADDED
@@ -0,0 +1,6 @@
1
+ require "bundler/gem_tasks"
2
+ require 'rspec/core/rake_task'
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.0.1
@@ -0,0 +1,33 @@
1
+ require_relative 'events/event'
2
+ require_relative 'event_parser'
3
+
4
+ module Torkify
5
+
6
+ # Connect the socket reader and observers, and dispatch events.
7
+ class Conductor
8
+ attr_accessor :observers
9
+
10
+ # Create with a set of observers.
11
+ def initialize(observers)
12
+ @observers = observers
13
+ end
14
+
15
+ # Start reading from the reader, which is an IO-like object.
16
+ #
17
+ # Parse each line and dispatch it as an event object to all observers.
18
+ def start(reader)
19
+ dispatch Event.new 'startup'
20
+ parser = EventParser.new
21
+ reader.each_line do |line|
22
+ event = parser.parse line
23
+ dispatch event
24
+ end
25
+ dispatch Event.new 'shutdown'
26
+ end
27
+
28
+ protected
29
+ def dispatch(event)
30
+ @observers.dispatch(event)
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,36 @@
1
+ require 'json'
2
+ require_relative 'events/event'
3
+ require_relative 'events/test_event'
4
+ require_relative 'events/pass_or_fail_event'
5
+ require_relative 'events/status_change_event'
6
+
7
+ module Torkify
8
+
9
+ # Parse raw strings passed by tork into event objects.
10
+ class EventParser
11
+
12
+ # Parse a raw string and return an object based on the event type.
13
+ #
14
+ # E.g. a raw string like:
15
+ # > '["test","spec/reader_spec.rb",[],"spec/reader_spec.rb.log",3]'
16
+ def parse(line)
17
+ raw = JSON.load line
18
+ event_from_data raw
19
+ end
20
+
21
+ protected
22
+ # Create an event object from the array of data.
23
+ def event_from_data(data)
24
+ case data.first
25
+ when 'test'
26
+ TestEvent.new(*data)
27
+ when /^(pass|fail)$/
28
+ PassOrFailEvent.new(*data)
29
+ when /^(pass_now_fail|fail_now_pass)$/
30
+ StatusChangeEvent.new(data[0], data[1], event_from_data(data[2]))
31
+ else
32
+ Event.new(*data)
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,20 @@
1
+ require_relative 'event_message'
2
+
3
+ module Torkify
4
+
5
+ # Event used for all events that have no associated data.
6
+ #
7
+ # Types:
8
+ #
9
+ # - absorb
10
+ # - shutdown
11
+ # - startup
12
+ # - anything else...
13
+ class Event < Struct.new(:type)
14
+ include EventMessage
15
+
16
+ def to_s
17
+ type
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,7 @@
1
+ module Torkify
2
+ module EventMessage
3
+ def message
4
+ "on_#{type}".to_sym
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,25 @@
1
+ require_relative 'event_message'
2
+
3
+ module Torkify
4
+
5
+ # Event used for test passes or failures.
6
+ #
7
+ # Types:
8
+ #
9
+ # - pass
10
+ # - fail
11
+ class PassOrFailEvent < Struct.new(:type, :file, :lines, :log_file, :worker, :exit_code, :exit_info)
12
+ include EventMessage
13
+
14
+ # Get the PID from the exit info.
15
+ def pid
16
+ matched = exit_info.scan(/pid ([0-9]+)/).first
17
+ matched.first.to_i if matched
18
+ end
19
+
20
+ def to_s
21
+ s = "#{type.upcase} #{file}"
22
+ s += lines.any? ? " (lines #{lines.join(', ')})" : ''
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,20 @@
1
+ require_relative 'event_message'
2
+
3
+ module Torkify
4
+
5
+ # Event used for changes in test status.
6
+ #
7
+ # Types:
8
+ #
9
+ # - pass_now_fail
10
+ # - fail_now_pass
11
+ #
12
+ # Includes the actual fail/pass event as a separate object.
13
+ class StatusChangeEvent < Struct.new(:type, :file, :event)
14
+ include EventMessage
15
+
16
+ def to_s
17
+ "#{type.upcase.gsub('_',' ')} #{file}"
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,15 @@
1
+ require_relative 'event_message'
2
+
3
+ module Torkify
4
+ # Event used when a test is started.
5
+ #
6
+ # This is currently only one type: 'test'
7
+ class TestEvent < Struct.new(:type, :file, :lines, :log_file, :worker)
8
+ include EventMessage
9
+
10
+ def to_s
11
+ s = "#{type.upcase} #{file}"
12
+ s += lines.any? ? " (lines #{lines.join(', ')})" : ''
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,5 @@
1
+ module Torkify
2
+ # Used when there's a failure connecting to a tork process.
3
+ class TorkError < StandardError
4
+ end
5
+ end
@@ -0,0 +1,91 @@
1
+ require_relative 'conductor'
2
+ require_relative 'observer_set'
3
+ require_relative 'reader'
4
+ require_relative 'exceptions'
5
+
6
+ module Torkify
7
+ class Listener
8
+ # Create a torkify listener with optional command and directory specified.
9
+ #
10
+ # The command is what's used to start the tork remote engine. The
11
+ # directory is where the command will be executed.
12
+ def initialize(command = 'tork-remote tork-engine', dir = Dir.pwd)
13
+ @command = command
14
+ @dir = dir
15
+ @conductor = Conductor.new ObserverSet.new
16
+ end
17
+
18
+ # Add an observer object to be notified of tork events.
19
+ #
20
+ # The object will be notified of events if it contains the following
21
+ # methods:
22
+ #
23
+ # - on_startup(event) - when torkify starts
24
+ # - on_shutdown(event) - when torkify shuts down
25
+ # - on_test(event) - when a test is started
26
+ # - on_pass(event) - when a test passes
27
+ # - on_fail(event) - when a test fails
28
+ # - on_fail_now_pass(event) - when a previously failed test passes
29
+ # - on_pass_now_fail(event) - when a previously passed test fails
30
+ # - on_absorb(event) - when tork reabsorbs overhead
31
+ #
32
+ # It doesn't have to inherit from a particular type, just define the
33
+ # method. The argument is optional, and in each case it gives an event.
34
+ def add_observer(observer)
35
+ @conductor.observers << observer
36
+ self
37
+ end
38
+
39
+ # Start the torkify listener and dispatch events to observers.
40
+ #
41
+ # This runs once, and connects to an existing tork process. If tork
42
+ # stops running then torkify will shut down.
43
+ #
44
+ # For continuous listening use #start_loop() instead.
45
+ #
46
+ # To start tork as well, use #start_with_tork().
47
+ def start
48
+ reader = Reader.new(@command, @dir)
49
+ Torkify.logger.info { 'Started torkify listener' }
50
+ @conductor.start reader
51
+ Torkify.logger.info { 'Stopping torkify' }
52
+ self
53
+ rescue Torkify::TorkError
54
+ Torkify.logger.info { "Tork is not running" }
55
+ self
56
+ end
57
+
58
+ # Start the torkify listener and loop until it connects to a tork process.
59
+ #
60
+ # If the tork process stops, it will keep looping until another starts.
61
+ #
62
+ # Calls #start().
63
+ def start_loop
64
+ loop do
65
+ sleep 2
66
+ start
67
+ end
68
+ rescue => e
69
+ Torkify.logger.error { e }
70
+ end
71
+
72
+ # Start the torkify listener and tork itself.
73
+ #
74
+ # It forks the current process and runs torkify as the child. It then
75
+ # runs tork as the main process, allowing for stdin to be passed to tork.
76
+ #
77
+ # Calls #start_loop().
78
+ #
79
+ # The command to run tork can be passed as an argument, and the
80
+ # TORK_CONFIGS environment variable can be passed as the second argument.
81
+ def start_with_tork(command = 'tork', tork_env = 'default')
82
+ if fork
83
+ start_loop
84
+ else
85
+ # Run tork in main process to keep stdin
86
+ Torkify.logger.info { "Starting tork" }
87
+ exec({'TORK_CONFIGS' => tork_env}, command)
88
+ end
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,62 @@
1
+ require 'set'
2
+
3
+ module Torkify
4
+ # Wrapper around a Set class, that adds a dispatch method.
5
+ #
6
+ # Dispatch sends an event that calls a method on all observers.
7
+ class ObserverSet
8
+ def initialize(set = Set.new)
9
+ @set = set
10
+ end
11
+
12
+ # Call a method on all observers, depending on the event type.
13
+ #
14
+ # The method is the event type prefixed with "on_". E.g. 'test' would be
15
+ # 'on_test'.
16
+ def dispatch(event)
17
+ Torkify.logger.debug event.to_s
18
+ @set.each do |observer|
19
+ dispatch_each observer, event.message, event
20
+ end
21
+ end
22
+
23
+ # Don't return a Set, return an ObserverSet.
24
+ def |(enum)
25
+ self.class.new(@set | enum)
26
+ end
27
+
28
+ def method_missing(method, *args, &blk)
29
+ @set.send method, *args, &blk
30
+ end
31
+
32
+ def respond_to?(name, include_private = false)
33
+ @set.respond_to? name, include_private
34
+ end
35
+
36
+ alias :+ :|
37
+ alias :union :|
38
+
39
+ private
40
+ # Send the messages to a given observer, with the event object.
41
+ def dispatch_each(observer, message, event)
42
+ method = observer.method(message)
43
+ observer.send message, *method_args(method, event)
44
+ rescue NameError
45
+ Torkify.logger.warn { "No method #{message} defined on #{observer.inspect}" }
46
+ rescue => e
47
+ Torkify.logger.error { "Caught exception from #{observer} during ##{message}: #{e}" }
48
+ end
49
+
50
+ # Determine whether to include the event in the arguments.
51
+ #
52
+ # The arity of the obsever's method is checked to see whether an
53
+ # argument is received or not.
54
+ def method_args(method, event)
55
+ dispatch_args = []
56
+ unless method.arity === 0
57
+ dispatch_args << event
58
+ end
59
+ dispatch_args
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,35 @@
1
+ require 'open3'
2
+ require_relative 'exceptions'
3
+
4
+ module Torkify
5
+ class Reader
6
+ # Open the tork command and initialize the streams.
7
+ #
8
+ # STDOUT is kept as the underlying stream, and this class can be used as
9
+ # an IO-like object on STDOUT.
10
+ #
11
+ # A TorkError is raised if the command fails, and its message is whatever
12
+ # has been written to the command's STDERR stream.
13
+ def initialize(command = 'tork-remote tork-engine', run_in_dir = Dir.pwd)
14
+ Dir.chdir(run_in_dir) do
15
+ _, @io, stderr, _ = Open3.popen3 command
16
+
17
+ if @io.eof?
18
+ raise TorkError, stderr.read.strip
19
+ end
20
+ end
21
+ end
22
+
23
+ # Pass all unknown methods straight to the underlying IO object.
24
+ #
25
+ # This allows this class to be used in an IO like way.
26
+ def method_missing(method, *args, &blk)
27
+ @io.send method, *args, &blk
28
+ end
29
+
30
+ # Allow respond_to? to work with method_missing.
31
+ def respond_to?(method, include_private = false)
32
+ @io.respond_to? method, include_private
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,3 @@
1
+ module Torkify
2
+ VERSION = "0.0.1"
3
+ end
data/lib/torkify.rb ADDED
@@ -0,0 +1,40 @@
1
+ require "torkify/version"
2
+
3
+ # Listen to tork events and execute ruby code when they happen.
4
+ #
5
+ # E.g.
6
+ #
7
+ # listener = Torkify.listener
8
+ # class Observer
9
+ # def on_pass(event)
10
+ # puts event.to_s
11
+ # end
12
+ # end
13
+ # listener.add_observer Observer.new
14
+ # listener.start
15
+ # # or listener.start_loop
16
+ # # or listener.start_with_tork
17
+ module Torkify
18
+
19
+ # Create a listener object and load all required files.
20
+ def self.listener(*args)
21
+ require 'torkify/listener'
22
+ Listener.new(*args)
23
+ end
24
+
25
+ # Create a logger object, or retrieve the existing logger.
26
+ #
27
+ # Uses Log4r.
28
+ def self.logger
29
+ require 'log4r'
30
+ include Log4r
31
+
32
+ log = Logger['torkify']
33
+ unless log
34
+ log = Logger.new 'torkify'
35
+ log.outputters = Outputter.stdout
36
+ log.level = INFO
37
+ end
38
+ log
39
+ end
40
+ end