spectator-emacs 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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