spectator-emacs 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/.document ADDED
@@ -0,0 +1,2 @@
1
+ lib/**/*.rb
2
+ bin/spectator-emacs
data/.gitignore ADDED
@@ -0,0 +1,7 @@
1
+ /html/
2
+ /pkg/
3
+ /doc/
4
+ /.yardoc/
5
+ *~
6
+ \#*#
7
+ \.#*
data/.rspec ADDED
@@ -0,0 +1 @@
1
+ -f RspecOrgFormatter
data/.yardopts ADDED
@@ -0,0 +1 @@
1
+ -m markdown
data/ChangeLog.rdoc ADDED
@@ -0,0 +1,4 @@
1
+ === 0.1.0 / 2013-01-25
2
+
3
+ * Initial release:
4
+
data/Gemfile ADDED
@@ -0,0 +1,6 @@
1
+ source :rubygems
2
+ gemspec
3
+
4
+ group :test do
5
+ gem 'rspec_org_formatter'
6
+ end
data/Gemfile.lock ADDED
@@ -0,0 +1,54 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ spectator-emacs (0.1.0)
5
+ docopt
6
+ open4
7
+ rb-inotify (~> 0.8.8)
8
+ rspec
9
+ spectator (~> 1.2)
10
+
11
+ GEM
12
+ remote: http://rubygems.org/
13
+ specs:
14
+ diff-lcs (1.1.3)
15
+ docopt (0.5.0)
16
+ ffi (1.3.1)
17
+ json (1.7.6)
18
+ listen (0.7.2)
19
+ notify (0.4.0)
20
+ open4 (1.3.0)
21
+ rb-inotify (0.8.8)
22
+ ffi (>= 0.5.0)
23
+ rdoc (3.12)
24
+ json (~> 1.4)
25
+ redcarpet (2.2.2)
26
+ rspec (2.12.0)
27
+ rspec-core (~> 2.12.0)
28
+ rspec-expectations (~> 2.12.0)
29
+ rspec-mocks (~> 2.12.0)
30
+ rspec-core (2.12.2)
31
+ rspec-expectations (2.12.1)
32
+ diff-lcs (~> 1.1.3)
33
+ rspec-mocks (2.12.2)
34
+ rspec_org_formatter (0.2.2)
35
+ rspec (>= 2.6.0)
36
+ rubygems-tasks (0.2.3)
37
+ spectator (1.2.6)
38
+ listen
39
+ notify
40
+ term-ansicolor
41
+ term-ansicolor (1.0.7)
42
+ yard (0.8.3)
43
+
44
+ PLATFORMS
45
+ ruby
46
+
47
+ DEPENDENCIES
48
+ rdoc (~> 3.0)
49
+ redcarpet
50
+ rspec (~> 2.4)
51
+ rspec_org_formatter
52
+ rubygems-tasks (~> 0.2)
53
+ spectator-emacs!
54
+ yard
data/LICENSE.txt ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2013 Alessandro Piras
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,155 @@
1
+ # spectator-emacs
2
+
3
+ * [Homepage](https://github.com/laynor/spectator-emacs#readme)
4
+ * [Issues](https://github.com/laynor/spectator-emacs/issues)
5
+ * [Documentation](http://rubydoc.info/gems/spectator-emacs/frames)
6
+ * Email [mailto:laynor at gmail.com]
7
+
8
+ ## Description
9
+
10
+ `spectator-emacs` is a [Spectator][spectator]
11
+ extension that provides discreet notificatoins in the Emacs modeline,
12
+ via the [Enotify][enotify] Emacs notification
13
+ system.
14
+
15
+ The RSpec output is displayed in an emacs buffer, and using the
16
+ [RSpec Org Formatter][RSpecOrgFormatter],
17
+ they are nicely formatted as an org-mode file. Minimize your switching
18
+ from Emacs to the shell or the browser to just display the test
19
+ results!
20
+
21
+ If you hate growl-style popups and prefer a simple green/red
22
+ (customizable!) indicator on the modeline, spectator-emacs is for you.
23
+
24
+ ## Features
25
+
26
+ * Notifications on the emacs modeline
27
+ * Short summary report on mouse-over in the modeline indicator
28
+ * Easily switch to the results buffer with just a click on the
29
+ modeline indicator
30
+ * Org formatted RSpec results with the aid of RSpec Org Formatter
31
+ * Summary extraction can be customized to work with different RSpec
32
+ output formats
33
+ * all the features offered by Spectator
34
+
35
+ ## Install
36
+ ```
37
+ $ gem install spectator-emacs
38
+ ```
39
+ ## Examples
40
+
41
+ To run `spectator-emacs`, just run it!
42
+ ```
43
+ $ spectator-emacs
44
+ ```
45
+ To customize it, create a .spectator-emacs file in your project root.
46
+ You can customize various aspects of how spectator-emacs works:
47
+
48
+ * Enotify host (default: localhost)
49
+ * Enotify port (default: 5000)
50
+ * The notification message that will appear on the emacs modeline
51
+ (default: 'F' for failures, 'P' for pending, 'S' for success)
52
+ * The notification faces used to display the icons in the modeline
53
+ (default: `enotify-success-face` for success,
54
+ `enotify-failure-face`for failures, `enotify-warning-face` for
55
+ success with pending examples)
56
+ * The Enotify slot id to register for notifications
57
+
58
+ An example `.spectator-emacs' file:
59
+
60
+
61
+ ```ruby
62
+ require 'spectator/emacs'
63
+
64
+ @runner = Spectator::ERunner.new(:enotify_port => 5001,
65
+ :notification_messages => {
66
+ :failure => "failure",
67
+ :success => "success",
68
+ :pending => "pending"
69
+ },
70
+ :slot_id => "project foobar"
71
+ :notification_face => {
72
+ :pending => :font_lock_warning_face,
73
+ # see the docs for detail on Symbol#keyword
74
+ :success => :success.keyword,
75
+ :failure => :failure
76
+ }) do |runner|
77
+ # This code will be executed before entering the main loop.
78
+
79
+ def format_summary(examples, failures, pending)
80
+ summary = "#{examples} examples"
81
+ summary << ", #{failures} failures" if failures > 0
82
+ summary << ", #{pending} pending" if pending > 0
83
+ summary << "."
84
+ summary
85
+ end
86
+
87
+ # The default summary extraction method works with
88
+ # the standard documentation formatter, or any formatter
89
+ # that puts the summary on the last line and with the
90
+ # same format of the documentation formatter.
91
+ # It uses the helper function
92
+ # Spectator::Spec#extract_rspec_stats, which can be
93
+ # useful if the summary is expressed with the same
94
+ # pattern but on a line other than the last.
95
+ # For example, the RSpecOrgFormatter puts the summary on
96
+ # the 6th-last line.
97
+ #
98
+ def extract_rspec_org_summary(output)
99
+ runner.extract_rspec_stats(output, -6)
100
+ end
101
+
102
+ # Suppose rspec is using a custom formatter that
103
+ # puts the summary in a format in the last lines
104
+ # with a format like the following:
105
+ #
106
+ # Examples: 123
107
+ # Errors: 12
108
+ # Pending: 2
109
+ #
110
+ def runner.extract_rspec_summary(output)
111
+ summary_lines = summary[-3..-1]
112
+ examples = summary[-3].split(':')[1].to_i
113
+ errors = summary[-2].split(':')[1].to_i
114
+ pending = summary[-1].split(':')[1].to_i
115
+ stats = {
116
+ :examples => examples,
117
+ :failures => failures,
118
+ :pending => pending,
119
+ :summary => format_summary(examples, failures, pending)
120
+ }
121
+ stats.merge(:status => rspec_status(stats))
122
+ stats
123
+ end
124
+ end
125
+ ```
126
+
127
+
128
+
129
+ ## Requirements
130
+
131
+ `spectator-emacs` requires a working Emacs installation. You need to
132
+ install [Enotify][enotify], which can be found in the [MELPA][melpa]
133
+ repository.
134
+
135
+ You also need to load the `enotify-spectator-emacs` Enotify plugin.
136
+
137
+ Put this in your .emacs:
138
+
139
+ ```lisp
140
+ (require 'enotify)
141
+ (enotify-minor-mode t)
142
+ (add-to-list 'load-path "path/to/enotify-spectator-emacs")
143
+ (require 'enotify-spectator-emacs)
144
+ ```
145
+
146
+ ## Copyright
147
+
148
+ Copyright (c) 2013 Alessandro Piras
149
+
150
+ See LICENSE.txt for details.
151
+
152
+ [enotify]:http://github.com/laynor/enotify
153
+ [spectator]:http://github.com/elia/spectator
154
+ [RSpecOrgFormatter]:http://github.org/laynor/rspec_org_formatter
155
+ [melpa]:http://melpa.milkbox.net/
data/Rakefile ADDED
@@ -0,0 +1,52 @@
1
+ # encoding: utf-8
2
+
3
+ require 'rubygems'
4
+ require 'rake'
5
+ require 'rake/clean'
6
+ require 'yard'
7
+ $:.unshift 'lib'
8
+ begin
9
+ gem 'rubygems-tasks', '~> 0.2'
10
+ require 'rubygems/tasks'
11
+
12
+ Gem::Tasks.new
13
+ rescue LoadError => e
14
+ warn e.message
15
+ warn "Run `gem install rubygems-tasks` to install Gem::Tasks."
16
+ end
17
+
18
+ begin
19
+ gem 'rdoc', '~> 3.0'
20
+ require 'rdoc/task'
21
+
22
+ RDoc::Task.new do |rdoc|
23
+ rdoc.title = "spectator-emacs"
24
+ end
25
+ rescue LoadError => e
26
+ warn e.message
27
+ warn "Run `gem install rdoc` to install 'rdoc/task'."
28
+ end
29
+ task :doc => :rdoc
30
+
31
+ begin
32
+ gem 'rspec', '~> 2.4'
33
+ require 'rspec/core/rake_task'
34
+
35
+ RSpec::Core::RakeTask.new
36
+ rescue LoadError => e
37
+ task :spec do
38
+ abort "Please run `gem install rspec` to install RSpec."
39
+ end
40
+ end
41
+
42
+ task :test => :spec
43
+ task :default => :spec
44
+
45
+ desc "Run spectator-emacs"
46
+ task :'spectator-emacs' do
47
+ load "bin/spectator-emacs"
48
+ end
49
+
50
+ ## YARD stuff
51
+ YARD::Rake::YardocTask.new
52
+ CLOBBER.include('doc', '.yardoc')
@@ -0,0 +1,40 @@
1
+ #!/usr/env/bin ruby
2
+
3
+ require 'spectator/emacs'
4
+ require 'docopt'
5
+ CONFIGFILE = '.spectator-emacs'
6
+ THISFILE = File.basename __FILE__
7
+ doc = <<DOCOPT
8
+ Listen to file changes and run RSpec, sending notifications to Emacs via Enotify.
9
+
10
+ Usage:
11
+ #{THISFILE} [--config <filename>]
12
+ #{THISFILE} -h | --help
13
+ #{THISFILE} --version
14
+
15
+ Options:
16
+ -h --help Show this screen.
17
+ --config <filename> Configuration file to read [default: .spectator-emacs]
18
+ --version Show version.
19
+
20
+ DOCOPT
21
+
22
+ begin
23
+ require 'pp'
24
+ args = Docopt::docopt(doc, :version => Spectator::Emacs::VERSION)
25
+ config_file = args['--config']
26
+ pp args
27
+ if File.exists? config_file
28
+ content = File.read(config_file)
29
+ eval(content) if not content.nil?
30
+ end
31
+
32
+ @runner ||= Spectator::ERunner.new
33
+ rescue Docopt::Exit => e
34
+ puts e.message
35
+ end
36
+
37
+
38
+ # Local Variables:
39
+ # mode: ruby
40
+ # End:
@@ -0,0 +1,6 @@
1
+ module Spectator
2
+ module Emacs
3
+ # spectator-emacs version
4
+ VERSION = "0.1.0"
5
+ end
6
+ end
@@ -0,0 +1,460 @@
1
+ require 'spectator/emacs/version'
2
+ require 'spectator'
3
+ require 'socket'
4
+ require 'open4'
5
+
6
+ class Object
7
+ # Returns a string representing the object as a lisp sexp.
8
+ def to_lisp
9
+ to_s
10
+ end
11
+ end
12
+
13
+ class Symbol
14
+ # Returns a string that represents the symbol as a lisp
15
+ # symbol. Underscores are converted to dashes.
16
+ #
17
+ # Example:
18
+ #
19
+ # ```
20
+ # :foo_bar.to_lisp => 'foo-bar'
21
+ # ```
22
+ def to_lisp
23
+ to_s.gsub "_", "-"
24
+ end
25
+
26
+ # Returns a symbol with the same name prefixed by a colon. This is
27
+ # convenient when converting a symbol with the {#to_lisp} method.
28
+ #
29
+ # Example:
30
+ #
31
+ # ```
32
+ # :foo_bar.keyword.to_lisp => ':foo-bar'
33
+ # ```
34
+ def keyword
35
+ if self[0] == ':'
36
+ self
37
+ else
38
+ ":#{to_s}".to_sym
39
+ end
40
+ end
41
+ end
42
+
43
+ class String
44
+ # Returns a string that represents a lisp string.
45
+ # This is basically just an alias for {String#inspect}
46
+ def to_lisp
47
+ inspect
48
+ end
49
+ end
50
+
51
+ class Array
52
+ # Returns a string that represents the array as a lisp list.
53
+ #
54
+ # Example:
55
+ #
56
+ # ```
57
+ # [:foo, 123, "bar"].to_lisp => '(foo 123 "bar")'
58
+ def to_lisp
59
+ sexp_array = map { |el| el.to_lisp }
60
+ "(#{sexp_array.join ' '})"
61
+ end
62
+ end
63
+
64
+ class Hash
65
+ # Creates and returns a new hash tabke with the same keys and values
66
+ # and tags it to be rendered as an association list by the to_lisp
67
+ # method.
68
+ #
69
+ # For example, ```{:a => :b, :x => 1}``` would be rendered as
70
+ #
71
+ # ```
72
+ # ((a . b) (x . 1))
73
+ # ```
74
+ def as_alist
75
+ merge(:__render_as => :alist)
76
+ end
77
+
78
+ # Creates and returns a new hash tabke with the same keys and values
79
+ # but tagged to be rendered as a flat list by the to_lisp method.
80
+ #
81
+ # For example, ```{:a => :b, :x => 1}``` would be rendered as
82
+ #
83
+ # ```
84
+ # (a b x 1)
85
+ # ```
86
+ def as_flat_list
87
+ merge(:__render_as => :flat )
88
+ end
89
+
90
+ # Creates and returns a new hash table with the same keys and values
91
+ # but tagged to be rendered as a property list by the to_lisp method.
92
+ # The keys must be symbols, and they will be rendered as keywords.
93
+ #
94
+ # For example, ```{:a => :b, :x => 1}``` would be rendered as
95
+ #
96
+ # ```
97
+ # (:a b :x 1)
98
+ # ```
99
+ def as_plist
100
+ merge(:__render_as => :plist)
101
+ end
102
+
103
+ # Returns a symbol indicating how the hash will be rendered by the
104
+ # to_lisp method. The possible values are :flat, :alist, :plist.
105
+ def rendering_type
106
+ self[:__render_as] or :plist
107
+ end
108
+
109
+ # Renders the hash as a list, depending on how it has been tagged.
110
+ # If the hash has not been tagged, it will be rendered as a property
111
+ # list, see as_plist.
112
+ def to_lisp
113
+ def pjoin(string_list)
114
+ "(#{string_list.join ' '})"
115
+ end
116
+ h = self.clone
117
+ h.delete(:__render_as)
118
+ case rendering_type
119
+ when :alist
120
+ pjoin(h.map { |k, v| "(#{k.to_lisp} . #{v.to_lisp})" })
121
+ when :flat
122
+ pjoin(h.map { |k, v| "#{k.to_lisp} #{v.to_lisp}" })
123
+ when :plist
124
+ pjoin(h.map { |k, v| "#{k.keyword.to_lisp} #{v.to_lisp}" })
125
+ end
126
+ end
127
+ end
128
+
129
+
130
+ module Spectator
131
+ # This exception is thrown when an error occurs when trying to
132
+ # extract the rspec result summary (number of examples ran, number
133
+ # of failures, number of pending examples) from its output.
134
+ class SummaryExtractionError < RuntimeError
135
+ end
136
+
137
+ module Specs
138
+ # Summarizes the rspec results as one of `:failure, :pending, :success`.
139
+ #
140
+ # @param [Hash] rspec_stats A Hash table with keys ```:examples, :failures, :pending, :summary, :status```.
141
+ # See {#extract_rspec_summary} for details about the meaning of the key/value pairs in this table.
142
+ def rspec_status(rspec_stats)
143
+ if rspec_stats[:failures] > 0
144
+ :failure
145
+ elsif rspec_stats[:pending] > 0
146
+ :pending
147
+ else
148
+ :success
149
+ end
150
+ end
151
+
152
+ # Returns a hash that summarizes the rspec results.
153
+ #
154
+ # @param [String] output the rspec output
155
+ # @param [Integer] line_number the line number of the summary in the rspec
156
+ # output. It can be negative: -1 indicates the last line, -2
157
+ # indicates the second last line and so on.
158
+ # @return [Hash] a hash table with keys ```:examples, :failures, :pending, :summary, :status```.
159
+ #
160
+ # * **:examples** => number of examples ran
161
+ # * **:failures** => number of failed examples
162
+ # * **:pending** => number of pending examples ran
163
+ # * **:summary** => the summary string from which the above have been extracted
164
+ # * **:status** => one of ```:failure, :pending, :success```
165
+ def extract_rspec_stats(output, line_number)
166
+ summary_line = output.split("\n")[line_number]
167
+ summary_regex = /^(\d*)\sexamples?,\s(\d*)\s(errors?|failures?)[^\d]*((\d*)\spending)?/
168
+ matchdata = summary_line.match(summary_regex)
169
+ raise SummaryExtractionError.new if matchdata.nil?
170
+ _, examples, failures, _, pending = matchdata.to_a
171
+ stats = {:examples => examples.to_i, :failures => failures.to_i, :pending => pending.to_i, :summary => summary_line}
172
+ stats.merge(:status => rspec_status(stats))
173
+ end
174
+
175
+ # Returns a hash that summarizes the rspec results.
176
+ #
177
+ # Redefine this method if you are using a non standard rspec formatter,
178
+ # see the {file:README.md} for details.
179
+ # @param [String] output the rspec output
180
+ # @return [Hash] a hash table with keys ```:examples, :failures, :pending, :summary, :status```.
181
+ #
182
+ # * **`:examples`**: number of examples ran
183
+ # * **`:failures`**: number of failed examples
184
+ # * **`:pending`**: number of pending examples ran
185
+ # * **`:summary`**: the summary string from which the above have been extracted
186
+ # * **`:status`**: one of
187
+ #
188
+ # ```
189
+ # :success, :pending, :failure
190
+ # ```
191
+ def extract_rspec_summary(output)
192
+ begin
193
+ extract_rspec_stats output, @summary_line_number
194
+ rescue SummaryExtractionError
195
+ puts "--- Error while extracting summary with the default method.".red
196
+ print "--- Summary line number: ".yellow
197
+ @summary_line_number = STDIN.gets.to_i
198
+ extract_rspec_summary output
199
+ end
200
+ end
201
+
202
+ # Runs a command and returns a hash containing exit status,
203
+ # standard output and standard error contents.
204
+ #
205
+ # @return [Hash] a hash table with keys `:status, :stdout, :stderr`.
206
+ def run(cmd)
207
+ puts "=== running: #{cmd} ".ljust(terminal_columns, '=').cyan
208
+ pid, _, stdout, stderr = Open4::popen4 cmd
209
+ _, status = Process::waitpid2 pid
210
+ puts "===".ljust(terminal_columns, '=').cyan
211
+ {:status => status, :stdout => stdout.read.strip, :stderr => stderr.read.strip}
212
+ end
213
+
214
+ # Sends a notification to emacs via Enotify
215
+ #
216
+ # @param [String] rspec_output The rspec command output
217
+ # @param [Hash] stats A Hash table with keys ```:examples, :failures, :pending, :summary, :status```.
218
+ # See {#extract_rspec_summary} for details about the meaning of the key/value pairs in this table.
219
+ def rspec_send_results(rspec_output, stats)
220
+ begin
221
+ print "--- Sending notification to #{@enotify_host}:#{@enotify_port}" \
222
+ " through #{@enotify_slot_id}... ".cyan
223
+ enotify_notify rspec_output, stats
224
+ puts "Success!".green
225
+ rescue SocketError
226
+ puts "Failed!".red
227
+ enotify_connect
228
+ rspec_send_results rspec_output, stats
229
+ end
230
+ end
231
+
232
+ # Checks if the commands `bundle exec rspec` and `rspec` actually
233
+ # run the same program, and sets the `@bundle` instance variable
234
+ # accordingly.
235
+ #
236
+ # This is meant to speed up the execution of `rspec`.
237
+ def check_if_bundle_needed
238
+ if `bundle exec #{rspec_command} -v` == `#{rspec_command} -v`
239
+ @bundle = ""
240
+ else
241
+ @bundle = "bundle exec "
242
+ end
243
+ end
244
+
245
+ # Runs the `rspec` command with the given options, and notifies Emacs of the results.
246
+ #
247
+ # @param [String] options The command line arguments to pass to rspec.
248
+ def rspec(options)
249
+ unless options.empty?
250
+ results = run("#{@bundle}#{rspec_command} --failure-exit-code 99 #{options}")
251
+ status = results[:status].exitstatus
252
+ if status == 1
253
+ puts "An error occurred when running the tests".red
254
+ puts "RSpec output:"
255
+ puts "STDERR:"
256
+ puts results[:stderr]
257
+ puts "-" * 80
258
+ puts "STDOUT:"
259
+ puts results[:stdout]
260
+ else
261
+ begin
262
+ stats = extract_rspec_summary results[:stdout]
263
+ puts(stats[:summary].send(results[:status] == 0 ? :green : :red))
264
+ # enotify_notify results[:stdout], stats
265
+ rspec_send_results results[:stdout], stats
266
+ rescue StandardError => e
267
+ puts "ERROR extracting summary from rspec output: #{e}".red
268
+ puts e.backtrace
269
+ puts "RSpec output:"
270
+ puts "STDERR:"
271
+ puts results[:stderr]
272
+ puts "-" * 80
273
+ puts "STDOUT:"
274
+ puts results[:stdout]
275
+ print "Exit? (y/N)"
276
+ answer = STDIN.gets
277
+ abort "Execution aborted by the user" if answer.strip.downcase == 'y'
278
+ end
279
+ end
280
+ end
281
+ end
282
+ end
283
+
284
+ # This module contains all the functions used to interact with the Enotify emacs mode-line notification system.
285
+ module Emacs
286
+ # Sends a message to the Enotify host.
287
+ #
288
+ # @param [Object] object the object to be serialized as a lisp
289
+ # object (with the {Object#to_lisp} method) and sent as a message.
290
+ def enotify_send(object)
291
+ sexp = object.to_lisp
292
+ @sock.puts "|#{sexp.length}|#{sexp}"
293
+ end
294
+
295
+ # Registers the slot named `@enotify_slot_id` with Enotify.
296
+ def enotify_register
297
+ enotify_send :register => @enotify_slot_id, :handler_fn => :enotify_rspec_result_message_handler
298
+ end
299
+
300
+ # Sends a notification to the enotify host with the RSpec results.
301
+ #
302
+ # @param [String] stdout the rspec command output.
303
+ # @param [Hash] stats the extracted summary of the results. For
304
+ # details, see the return value of
305
+ # {Spectator::Specs#extract_rspec_summary} for details.
306
+ def enotify_notify(stdout, stats)
307
+ #stats = extract_rspec_stats stdout
308
+ status = stats[:status]
309
+ message = {
310
+ :id => @enotify_slot_id,
311
+ :notification => {
312
+ :text => @notification_messages[status],
313
+ :face => @notification_face[status],
314
+ :help => format_tooltip(stats),
315
+ :mouse_1 => :enotify_rspec_mouse_1_handler
316
+ },
317
+ :data => stdout
318
+ }
319
+
320
+ enotify_send message
321
+ end
322
+
323
+ # Checks whether the string is made by whitespace characters.
324
+ #
325
+ # @param [String] string the string to be checked
326
+ # @return [Boolean] non nil if the string is blank, nil otherwise.
327
+ def blank_string?(string)
328
+ string =~ /\A\s*\n?\z/
329
+ end
330
+
331
+ # Interactively retries to connect to the Enotify host, asking a
332
+ # new *host:port* value.
333
+ def rescue_sock_error
334
+ print "--- Enter Enotify host [localhost:5000]: ".yellow
335
+ host_and_port = STDIN.gets.strip
336
+ if blank_string?(host_and_port)
337
+ @enotify_host, @enotify_port = ['localhost', @default_options[:enotify_port]]
338
+ else
339
+ @enotify_host, @enotify_port = host_and_port.split(/\s:\s/)
340
+ @enotify_port = @enotify_port.to_i
341
+ end
342
+ enotify_connect
343
+ end
344
+
345
+ # Creates a connection to the Enotify host.
346
+ def enotify_connect
347
+ begin
348
+ print "=== Connecting to emacs... ".cyan
349
+ @sock = TCPSocket.new(@enotify_host, @enotify_port)
350
+ enotify_register
351
+ puts "Success!".green
352
+ rescue SocketError, Errno::ECONNREFUSED => e
353
+ puts "Failed!".red
354
+ rescue_sock_error
355
+ end
356
+ end
357
+
358
+ # Formats the text that will be used as a tooltip for the modeline
359
+ # *"icon"*.
360
+ #
361
+ # @param [Hash] stats the extracted summary of the results. For
362
+ # details, see the return value of
363
+ # {Spectator::Specs#extract_rspec_summary} for details.
364
+ def format_tooltip(stats)
365
+ t = Time.now
366
+ "#{t.year}-#{t.month}-#{t.day} -- #{t.hour}:#{t.min}:#{t.sec}\n" +
367
+ "#{stats[:examples]} examples, #{stats[:failures]} failures" +
368
+ ((stats[:pending] > 0) ? ", #{stats[:pending]} pending.\n" : ".\n") +
369
+ "\nmouse-1: switch to rspec output buffer"
370
+ end
371
+
372
+ end
373
+
374
+
375
+ # This is the class that implements the main loop of spectator-emacs.
376
+ # To run spectator-emacs, just create a new ERunner object.
377
+ class ERunner < Runner
378
+ include Specs
379
+ include Emacs
380
+ # Creates a new instance of ERunner. This implements the main loop of `spectator-emacs`.
381
+ # See the {file:README.md} for examples on how to customize the default behavior.
382
+ # @param [Hash] options possible options are:
383
+ #
384
+ # ##### :enotify_port (Fixnum)
385
+ # the port the Enotify host is listening to.
386
+ # ##### :enotify_host`({String})
387
+ # the host name or IP address where Enotify is running.
388
+ # ##### :notification_messages ({Hash})
389
+ # a hash with keys `:success, :failure, :pending` containing the
390
+ # relative modeline *icons* strings.
391
+ # Defaults to `{:success => "S", :failure => "F", :pending => "P"}`.
392
+ # ##### :notification_face ({Hash})
393
+ # A hash table with keys `:success, :failure, :pending` containing
394
+ # the faces to apply to the notification *icon* in the Emacs modeline.
395
+ # Values must be {Symbol}s, like for example `:font_lock_constant_face`
396
+ # in order to use Emacs' `font-lock-warning-face`.
397
+ # Defaults to
398
+ #
399
+ # ```
400
+ # {:success => :\':success\', :failure => :\':failure\', :pending => :\':warning\'}
401
+ # ```
402
+ # @yield [ERunner] Gives a reference of the ERunner object just created to the block
403
+ # Use this block when you need to customize the behavior of spectator-emacs.
404
+ # For example, if you need a custom summary extraction method, you can create
405
+ # the runner object as follows in your `.spectator-emacs` script:
406
+ #
407
+ # ```ruby
408
+ # @runner = ERunner.new do |runner|
409
+ # def runner.extract_rspec_summary(output)
410
+ # ## your summary extraction code here
411
+ # ## ...
412
+ # end
413
+ # end
414
+ # ```
415
+ def initialize(options={}, &block)
416
+ @default_options = {
417
+ :enotify_port => 5000,
418
+ :enotify_host => 'localhost',
419
+ :notification_messages => {:failure => "F", :success => "S", :pending => "P"},
420
+ :notification_face => {
421
+ :failure => :failure.keyword,
422
+ :success => :success.keyword,
423
+ :pending => :warning.keyword
424
+ }
425
+ }
426
+ options = @default_options.merge options
427
+ @cli_args = ARGV.to_a
428
+ puts "======= OPTIONS ======="
429
+ options.each {|k, v| puts "#{k} => #{v}"}
430
+ @enotify_host = options[:enotify_host]
431
+ @enotify_port = options[:enotify_port]
432
+ @notification_messages = options[:notification_messages]
433
+ @notification_face = options[:notification_face]
434
+ @summary_line_number = options[:summary_line] || -1
435
+ @enotify_slot_id = options[:slot_id] ||
436
+ ((File.basename Dir.pwd).split('_').map {|s| s.capitalize}).join.gsub('-','/')
437
+ check_if_bundle_needed
438
+ enotify_connect
439
+ yield self if block_given?
440
+ # TODO: load .spectator-emacs
441
+ # contents = File::read('.spectator-emacs')
442
+ # eval(contents)
443
+ super()
444
+ end
445
+ end
446
+ end
447
+ #####################################
448
+ # require 'spectator/emacs'
449
+
450
+ # Spectator::ERunner.new(:enotify_port => 5001, :enotify_host => 'localhost') do |runner|
451
+ # def runner.extract_rspec_stats(results, line)
452
+ # ## define new spec extraction routine
453
+ # ## it must return a hash like this one:
454
+ # ## {:examples => 10, # number of examples executed
455
+ # ## :failures => 4, # number of failures
456
+ # ## :pending => 1, # number of pending examples
457
+ # ## :status => :failure # one of :pending, :succes, :failure
458
+ # ## }
459
+ # end
460
+ # end
@@ -0,0 +1,8 @@
1
+ require 'spec_helper'
2
+ require 'spectator/emacs'
3
+
4
+ describe Spectator::Emacs do
5
+ it "should have a VERSION constant" do
6
+ subject.const_get('VERSION').should_not be_empty
7
+ end
8
+ end
@@ -0,0 +1,68 @@
1
+ require 'spectator/emacs'
2
+
3
+ describe 'Spectator' do
4
+ describe 'EmacsInteraction' do
5
+ describe "Lisp sexp helpers" do
6
+ describe "Symbol#keyword" do
7
+ it "should be present" do
8
+ :foobar.should respond_to(:keyword)
9
+ end
10
+ it "should add a ':' if the symbol name does not begin with ':'" do
11
+ :foobar.keyword.should == :':foobar'
12
+ end
13
+ it "should not add a ':' if the symbol name already begins with ':'" do
14
+ sym = :':foobar'
15
+ sym.keyword.should == sym
16
+ end
17
+ end
18
+ describe "Object#to_lisp" do
19
+ it "should correctly represent a number" do
20
+ 20.times do
21
+ num = rand(1..1000)
22
+ num.to_lisp.should == "#{num}"
23
+ end
24
+ end
25
+
26
+ it "should correctly represent a symbol" do
27
+ symbol = :foobar
28
+ symbol.to_lisp.should == "foobar"
29
+ end
30
+
31
+ it "should convert underscores to dashes when converting a symbol" do
32
+ symbol = :foo_bar
33
+ symbol.to_lisp.should == "foo-bar"
34
+ end
35
+
36
+ it "should correctly represent a string" do
37
+ "asdf\nfoobar".to_lisp.should == '"asdf\nfoobar"'
38
+ end
39
+
40
+ it "should correctly represent an array as a list" do
41
+ [1,2,3,4].to_lisp.should == '(1 2 3 4)'
42
+ [1,2, [3, 4], 5, 6].to_lisp.should == '(1 2 (3 4) 5 6)'
43
+ [:a, 1, :b, 2].to_lisp.should == '(a 1 b 2)'
44
+ end
45
+
46
+ describe "Hash#to_lisp" do
47
+ before(:each) do
48
+ @hash = {:a => [1,2,3], :b => 1, :c => "asdf"}
49
+ end
50
+ it "should correctly represent a hash as a plist" do
51
+ @hash.as_plist.to_lisp.should == '(:a (1 2 3) :b 1 :c "asdf")'
52
+ end
53
+ it "should correctly represent a hash as an alist" do
54
+ @hash.as_alist.to_lisp.should == '((a . (1 2 3)) (b . 1) (c . "asdf"))'
55
+ end
56
+ it "should correctly represent a hash as a flat list" do
57
+ @hash.as_flat_list.to_lisp.should == '(a (1 2 3) b 1 c "asdf")'
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
63
+ describe "ERunner" do
64
+ it "should inherit from Runner" do
65
+ Spectator::ERunner.superclass.should be(Spectator::Runner)
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,5 @@
1
+ gem 'rspec', '~> 2.4'
2
+ require 'rspec'
3
+ require 'spectator/emacs/version'
4
+
5
+ include Spectator::Emacs
@@ -0,0 +1,44 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ require File.expand_path('../lib/spectator/emacs/version', __FILE__)
4
+
5
+ Gem::Specification.new do |gem|
6
+ gem.name = "spectator-emacs"
7
+ gem.version = Spectator::Emacs::VERSION
8
+ gem.summary = %q{A Spectator monkey-patch that displays notifications on the emacs modeline.}
9
+ gem.description = <<-DESCRIPTION
10
+ spectator-emacs is a Spectator extension that provides discreet
11
+ notificatoins in the Emacs modeline, via the Enotify Emacs
12
+ notification system.
13
+
14
+ == Features ==
15
+ * Notifications on the emacs modeline
16
+ * Short summary report on mouse-over in the modeline indicator
17
+ * Easily switch to the results buffer with just a click on the
18
+ modeline indicator
19
+ * Org formatted RSpec results with the aid of RSpec Org Formatter
20
+ * Summary extraction can be customized to work with different RSpec
21
+ output formats
22
+ * all the features offered by Spectator
23
+ DESCRIPTION
24
+ gem.license = "MIT"
25
+ gem.authors = ["Alessandro Piras"]
26
+ gem.email = "laynor@gmail.com"
27
+ gem.homepage = "https://github.com/laynor/spectator-emacs#readme"
28
+
29
+ gem.files = `git ls-files`.split($/)
30
+ gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
31
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
32
+ gem.require_paths = ['lib']
33
+
34
+ gem.add_development_dependency 'rdoc', '~> 3.0'
35
+ gem.add_development_dependency 'rspec', '~> 2.4'
36
+ gem.add_development_dependency 'rubygems-tasks', '~> 0.2'
37
+ gem.add_development_dependency 'yard'
38
+ gem.add_development_dependency 'redcarpet'
39
+ gem.add_dependency 'rspec'
40
+ gem.add_dependency 'spectator', '~> 1.2'
41
+ gem.add_dependency 'open4'
42
+ gem.add_dependency 'rb-inotify', '~> 0.8.8'
43
+ gem.add_dependency 'docopt'
44
+ end
metadata ADDED
@@ -0,0 +1,238 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: spectator-emacs
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Alessandro Piras
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2013-02-03 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: rdoc
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ~>
20
+ - !ruby/object:Gem::Version
21
+ version: '3.0'
22
+ type: :development
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ~>
28
+ - !ruby/object:Gem::Version
29
+ version: '3.0'
30
+ - !ruby/object:Gem::Dependency
31
+ name: rspec
32
+ requirement: !ruby/object:Gem::Requirement
33
+ none: false
34
+ requirements:
35
+ - - ~>
36
+ - !ruby/object:Gem::Version
37
+ version: '2.4'
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: '2.4'
46
+ - !ruby/object:Gem::Dependency
47
+ name: rubygems-tasks
48
+ requirement: !ruby/object:Gem::Requirement
49
+ none: false
50
+ requirements:
51
+ - - ~>
52
+ - !ruby/object:Gem::Version
53
+ version: '0.2'
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.2'
62
+ - !ruby/object:Gem::Dependency
63
+ name: yard
64
+ requirement: !ruby/object:Gem::Requirement
65
+ none: false
66
+ requirements:
67
+ - - ! '>='
68
+ - !ruby/object:Gem::Version
69
+ version: '0'
70
+ type: :development
71
+ prerelease: false
72
+ version_requirements: !ruby/object:Gem::Requirement
73
+ none: false
74
+ requirements:
75
+ - - ! '>='
76
+ - !ruby/object:Gem::Version
77
+ version: '0'
78
+ - !ruby/object:Gem::Dependency
79
+ name: redcarpet
80
+ requirement: !ruby/object:Gem::Requirement
81
+ none: false
82
+ requirements:
83
+ - - ! '>='
84
+ - !ruby/object:Gem::Version
85
+ version: '0'
86
+ type: :development
87
+ prerelease: false
88
+ version_requirements: !ruby/object:Gem::Requirement
89
+ none: false
90
+ requirements:
91
+ - - ! '>='
92
+ - !ruby/object:Gem::Version
93
+ version: '0'
94
+ - !ruby/object:Gem::Dependency
95
+ name: rspec
96
+ requirement: !ruby/object:Gem::Requirement
97
+ none: false
98
+ requirements:
99
+ - - ! '>='
100
+ - !ruby/object:Gem::Version
101
+ version: '0'
102
+ type: :runtime
103
+ prerelease: false
104
+ version_requirements: !ruby/object:Gem::Requirement
105
+ none: false
106
+ requirements:
107
+ - - ! '>='
108
+ - !ruby/object:Gem::Version
109
+ version: '0'
110
+ - !ruby/object:Gem::Dependency
111
+ name: spectator
112
+ requirement: !ruby/object:Gem::Requirement
113
+ none: false
114
+ requirements:
115
+ - - ~>
116
+ - !ruby/object:Gem::Version
117
+ version: '1.2'
118
+ type: :runtime
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ none: false
122
+ requirements:
123
+ - - ~>
124
+ - !ruby/object:Gem::Version
125
+ version: '1.2'
126
+ - !ruby/object:Gem::Dependency
127
+ name: open4
128
+ requirement: !ruby/object:Gem::Requirement
129
+ none: false
130
+ requirements:
131
+ - - ! '>='
132
+ - !ruby/object:Gem::Version
133
+ version: '0'
134
+ type: :runtime
135
+ prerelease: false
136
+ version_requirements: !ruby/object:Gem::Requirement
137
+ none: false
138
+ requirements:
139
+ - - ! '>='
140
+ - !ruby/object:Gem::Version
141
+ version: '0'
142
+ - !ruby/object:Gem::Dependency
143
+ name: rb-inotify
144
+ requirement: !ruby/object:Gem::Requirement
145
+ none: false
146
+ requirements:
147
+ - - ~>
148
+ - !ruby/object:Gem::Version
149
+ version: 0.8.8
150
+ type: :runtime
151
+ prerelease: false
152
+ version_requirements: !ruby/object:Gem::Requirement
153
+ none: false
154
+ requirements:
155
+ - - ~>
156
+ - !ruby/object:Gem::Version
157
+ version: 0.8.8
158
+ - !ruby/object:Gem::Dependency
159
+ name: docopt
160
+ requirement: !ruby/object:Gem::Requirement
161
+ none: false
162
+ requirements:
163
+ - - ! '>='
164
+ - !ruby/object:Gem::Version
165
+ version: '0'
166
+ type: :runtime
167
+ prerelease: false
168
+ version_requirements: !ruby/object:Gem::Requirement
169
+ none: false
170
+ requirements:
171
+ - - ! '>='
172
+ - !ruby/object:Gem::Version
173
+ version: '0'
174
+ description: ! " spectator-emacs is a Spectator extension that provides discreet\n
175
+ \ notificatoins in the Emacs modeline, via the Enotify Emacs\n notification system.\n\n
176
+ \ == Features ==\n * Notifications on the emacs modeline\n * Short summary report
177
+ on mouse-over in the modeline indicator\n * Easily switch to the results buffer
178
+ with just a click on the\n modeline indicator\n * Org formatted RSpec results
179
+ with the aid of RSpec Org Formatter\n * Summary extraction can be customized to
180
+ work with different RSpec\n output formats\n * all the features offered by Spectator\n"
181
+ email: laynor@gmail.com
182
+ executables:
183
+ - spectator-emacs
184
+ extensions: []
185
+ extra_rdoc_files: []
186
+ files:
187
+ - .document
188
+ - .gitignore
189
+ - .rspec
190
+ - .yardopts
191
+ - ChangeLog.rdoc
192
+ - Gemfile
193
+ - Gemfile.lock
194
+ - LICENSE.txt
195
+ - README.md
196
+ - Rakefile
197
+ - bin/spectator-emacs
198
+ - lib/spectator/emacs.rb
199
+ - lib/spectator/emacs/version.rb
200
+ - spec/emacs_spec.rb
201
+ - spec/lib/spectator/emacs_spec.rb
202
+ - spec/spec_helper.rb
203
+ - spectator-emacs.gemspec
204
+ homepage: https://github.com/laynor/spectator-emacs#readme
205
+ licenses:
206
+ - MIT
207
+ post_install_message:
208
+ rdoc_options: []
209
+ require_paths:
210
+ - lib
211
+ required_ruby_version: !ruby/object:Gem::Requirement
212
+ none: false
213
+ requirements:
214
+ - - ! '>='
215
+ - !ruby/object:Gem::Version
216
+ version: '0'
217
+ segments:
218
+ - 0
219
+ hash: -2575753694497142155
220
+ required_rubygems_version: !ruby/object:Gem::Requirement
221
+ none: false
222
+ requirements:
223
+ - - ! '>='
224
+ - !ruby/object:Gem::Version
225
+ version: '0'
226
+ segments:
227
+ - 0
228
+ hash: -2575753694497142155
229
+ requirements: []
230
+ rubyforge_project:
231
+ rubygems_version: 1.8.24
232
+ signing_key:
233
+ specification_version: 3
234
+ summary: A Spectator monkey-patch that displays notifications on the emacs modeline.
235
+ test_files:
236
+ - spec/emacs_spec.rb
237
+ - spec/lib/spectator/emacs_spec.rb
238
+ - spec/spec_helper.rb