selfie_formatter 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 40373d4d7ab3ef4ab64d87bd40dccdacb111fe37
4
+ data.tar.gz: 15a59ad88f51a8fa3c0938a7b12aa22c66adc70e
5
+ SHA512:
6
+ metadata.gz: 05ef8afcc20dda9caaa4b427b861ea0f87f093a53df828a6f9c56bea2bbc7a71d2997f3d851d77b70d05939f669b8f25541c7c8a2354dd74bfa10d3a943c52f1
7
+ data.tar.gz: 4fba84dc01175dab9f59a7e8f20c174f34065679d9e8e4cb6bab7885d80081590f2de3f0071be5409b2cf131e09de7816ce91184f11e1b8da3ada263b06f1e2a
@@ -0,0 +1,11 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
10
+ *#
11
+ *~
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in selfie_formatter.gemspec
4
+ gemspec
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2016 Skye Shaw (sshaw)
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
@@ -0,0 +1,52 @@
1
+ # The RSpec Selfie Formatter
2
+
3
+ An RSpec Formatter for the new generation of programmers.
4
+
5
+ ![Selfie Formatter Animation](example.gif)
6
+
7
+ The Selfie Formatter takes photos of you while your tests run and uses them to track
8
+ progress and format the results.
9
+
10
+ Currently only works on OS X with iTerm2 >= 3.0. **Warning** see [known issues](#known-issues).
11
+
12
+ ## Installation
13
+
14
+ ImageMagick is required
15
+
16
+ ```
17
+ brew install imagemagick --with-fontconfig
18
+ ```
19
+
20
+ Then
21
+
22
+ ```
23
+ gem install selfie_formatter
24
+ ```
25
+
26
+ Or, in your `Gemfile`
27
+
28
+ ```ruby
29
+ gem "selfie_formatter", :group => "test"
30
+ ```
31
+
32
+ ## Usage
33
+
34
+ ```
35
+ rspec -f SelfieFormatter
36
+ ```
37
+
38
+ ## Known Issues
39
+
40
+ 1. Photos are taken via [imagesnap](https://github.com/rharder/imagesnap), which is a fine program but can quickly eat up memory.
41
+ Upwards of 500 MB after 10 or 15 seconds.
42
+
43
+ 1. Photos are taken every 300ms. Unused photos are cleaned up after every test completes but if a single test takes a while to
44
+ complete photos can start to eat up disk space.
45
+
46
+ 1. Spec numbers are added to the top left of each image. They will not show up if the background is dark.
47
+
48
+ At some point I may write something that does not [fake the `Camera` interface via `fork`](https://github.com/sshaw/selfie_formatter/blob/34f1999391695ce7633d79638a0903e1eb612e9e/lib/selfie/camera.rb). [imagesnap](https://github.com/rharder/imagesnap) and [CaptureCamera](https://github.com/fernyb/CaptureCamera) are good starting points.
49
+
50
+ ## License
51
+
52
+ The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
Binary file
Binary file
@@ -0,0 +1,7 @@
1
+ require "selfie/camera"
2
+ require "selfie/cursor"
3
+ require "selfie/iterm2/image"
4
+
5
+ module Selfie
6
+ Error = Class.new(StandardError)
7
+ end
@@ -0,0 +1,61 @@
1
+ module Selfie
2
+ class Camera
3
+ CAPTURE_COMMAND = File.expand_path("../../imagesnap", __FILE__) << " -qw 0.8 -t 0.3"
4
+
5
+ def initialize(outdir, options = {})
6
+ @outdir = outdir
7
+ @glob = File.join(@outdir, "snapshot-*.jpg")
8
+ @captured = []
9
+ @pid = nil
10
+ end
11
+
12
+ # The camera interface we'd like to have (and may at some point).
13
+ # Until then, we fake it.
14
+ def on
15
+ return false if @pid
16
+
17
+ # TODO: signals
18
+ begin
19
+ @pid = spawn(CAPTURE_COMMAND, :chdir => @outdir, [:out, :err] => File::NULL)
20
+ rescue SystemCallError => e
21
+ raise Error, "cannot spawn photo capture process: #{e}"
22
+ end
23
+
24
+ # Wait for camera to warm up, -w option is not enough :(
25
+ sleep 2.5
26
+
27
+ true
28
+ end
29
+
30
+ def off
31
+ return false unless @pid
32
+
33
+ begin
34
+ Process.kill("TERM", @pid)
35
+ Process.wait(@pid)
36
+ rescue Errno::ESRCH, Errno::ECHILD
37
+ # kill, wait cannot find pid
38
+ end
39
+
40
+ # final cleanup
41
+ FileUtils.rm_f(Dir[@glob] - @captured)
42
+
43
+ @pid = nil
44
+ true
45
+ end
46
+
47
+ # Some hacks to make it seem like capture() takes a picture
48
+ # without accumulating a ton of unused images.
49
+ def capture
50
+ sleep 0.2
51
+
52
+ images = Dir[@glob]
53
+ return if images.none? || images.last == @captured.last
54
+
55
+ @captured << images.last
56
+ FileUtils.rm_f(images - @captured)
57
+
58
+ @captured.last
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,49 @@
1
+ require "tty-cursor"
2
+
3
+ module Selfie
4
+ class Cursor
5
+ include TTY::Cursor
6
+
7
+ def initialize(output)
8
+ raise ArgumentError, "output must be a terminal" unless output.tty?
9
+ @output = output
10
+ @settings = `stty -g 2>/dev/null`
11
+ end
12
+
13
+ %w[up down forward back move_to hide show clear_screen].each do |name|
14
+ define_method(name) { |*n| @output << super(*n) }
15
+ end
16
+
17
+ # Calculates how many columns and rows the code in the given block moved the cursor
18
+ def distance
19
+ raise ArgumentError, "block required" unless block_given?
20
+
21
+ beg_pos = position
22
+ yield
23
+ end_pos = position
24
+ return unless end_pos && beg_pos
25
+
26
+ # width, height
27
+ [ end_pos[1] - beg_pos[1], end_pos[0] - beg_pos[0] ]
28
+ end
29
+
30
+ def position
31
+ # TODO: use terminos instead?
32
+ `stty -echo -icanon -cread`
33
+ @output << current
34
+
35
+ position = ""
36
+ while ch = $stdin.getc
37
+ position << ch
38
+ break if ch == "R"
39
+ end
40
+
41
+ return unless position =~ %r|\[(\d+)\;(\d+)|
42
+
43
+ [ $1.to_i, $2.to_i ]
44
+ ensure
45
+ # TODO: restore if killed..?
46
+ `stty -g #@settings`
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,47 @@
1
+ require "base64"
2
+
3
+ module Selfie
4
+ module ITerm2
5
+ class Image
6
+ attr :path
7
+
8
+ def initialize(path, options = {})
9
+ @path = path
10
+ @options = options.dup
11
+ @options[:inline] = 1
12
+ @image = Base64.encode64(File.read(path))
13
+ end
14
+
15
+ def to_s(options = {})
16
+ sprintf("%s1337;File=%s:%s%s", start_esc, format_options(options), @image, end_esc)
17
+ end
18
+
19
+ private
20
+
21
+ def screen_tty?
22
+ $stdout.tty? && ENV.include?("TERM") && ENV["TERM"].start_with?("screen")
23
+ end
24
+
25
+ # From imgls: https://raw.githubusercontent.com/gnachman/iTerm2/master/tests/imgls
26
+ def start_esc
27
+ @screen == true ? "\ePtmux;\e\e]" : "\e]"
28
+ end
29
+
30
+ def end_esc
31
+ @screen == true ? "\a\e\\" : "\a"
32
+ end
33
+ # --
34
+
35
+ def format_options(options)
36
+ @options.merge(options).map do |name, value|
37
+ if name == :preserve_aspect_ratio
38
+ name = "preserveAspectRatio"
39
+ value = value == false ? 0 : 1
40
+ end
41
+
42
+ sprintf "%s=%s", name, value
43
+ end.join(";")
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,3 @@
1
+ module Selfie
2
+ VERSION = "0.0.1"
3
+ end
@@ -0,0 +1,193 @@
1
+ require "rspec"
2
+
3
+ # This is needed by the Reporter. RSpec doesn't require it.
4
+ require "rspec/core/formatters/console_codes"
5
+
6
+ require "fileutils"
7
+ require "mini_magick"
8
+
9
+ require "selfie"
10
+
11
+ class SelfieFormatter
12
+ include Selfie
13
+
14
+ DEFAULT_COLUMNS = 80
15
+
16
+ RSpec::Core::Formatters.register self, :dump_summary, :dump_failures, :dump_pending, :close, :example_passed, :example_failed, :example_pending, :start, :stop
17
+
18
+ def self.output_directory
19
+ @output_directory ||= RSpec.configuration.selfie_output_directory || File.join(Dir.pwd, "selfies")
20
+ end
21
+
22
+ def self.output_directory=(path)
23
+ @output_directory = path
24
+ end
25
+
26
+ def initialize(output, options = {})
27
+ raise ArgumentError, "can only render to a terminal" unless output.tty?
28
+
29
+ @output = output
30
+ @options = options
31
+ @cursor = Cursor.new(@output)
32
+ @camera = Camera.new(photo_dir)
33
+
34
+ @columns = `stty size 2>/dev/null`.split(" ").last.to_i
35
+ @columns = DEFAULT_COLUMNS if @columns == 0
36
+
37
+ @offset = 1
38
+ @spec_count = 0
39
+
40
+ @failed = {}
41
+ @pending = {}
42
+
43
+ @main_img_height = nil
44
+ @main_img_width = nil
45
+ end
46
+
47
+ def start(notification)
48
+ @camera.on
49
+ @cursor.hide
50
+ @cursor.clear_screen
51
+ @cursor.move_to(0, 0)
52
+ end
53
+
54
+ def stop(notification)
55
+ @camera.off
56
+
57
+ # Make sure we move down past the "film strip" before the summary is printed
58
+ pos = @cursor.position
59
+ if pos && pos[1] != 1
60
+ @cursor.down(@main_img_height)
61
+ end
62
+
63
+ @output << "\n\n"
64
+ end
65
+
66
+ def close(n)
67
+ @cursor.show
68
+ @output.flush
69
+ end
70
+
71
+ def example_passed(notification)
72
+ image = capture("green")
73
+ display_progess(image) if image
74
+ end
75
+
76
+ def example_failed(notification)
77
+ image = capture("red")
78
+ return unless image
79
+
80
+ @failed[notification.example] = image
81
+ display_progess(image)
82
+ end
83
+
84
+ def example_pending(notification)
85
+ image = capture("yellow")
86
+ return unless image
87
+
88
+ @pending[notification.example] = image
89
+ display_progess(image)
90
+ end
91
+
92
+ def dump_pending(notification)
93
+ if notification.pending_notifications.any?
94
+ @output << "\nPending:\n"
95
+ dump_notifications(notification.pending_notifications, @pending)
96
+ end
97
+ end
98
+
99
+ def dump_failures(notification)
100
+ if notification.failure_notifications.any?
101
+ @output << "\nFailures:\n"
102
+ dump_notifications(notification.failure_notifications, @failed)
103
+ end
104
+ end
105
+
106
+ def dump_summary(summary)
107
+ @output.puts summary.fully_formatted
108
+ end
109
+
110
+ private
111
+
112
+ def dump_notifications(notifications, images)
113
+ notifications.each_with_index do |notification, i|
114
+ notice = notification.fully_formatted(i + 1)
115
+ if images[notification.example]
116
+ # Replace the example's number with its photo
117
+ notice.sub!(%r|\A\s*\d+\)\s+(.+)$|, "\n%s\\1\n" % images[notification.example].to_s(:width => 12))
118
+ end
119
+
120
+ @output << notice
121
+ end
122
+ end
123
+
124
+ def photo_dir
125
+ path = File.join(SelfieFormatter.output_directory, Time.now.strftime("%Y-%m-%d-%H%M%S"))
126
+ FileUtils.mkdir_p(path)
127
+ path
128
+ end
129
+
130
+ def capture(color)
131
+ @spec_count += 1
132
+
133
+ path = @camera.capture
134
+ return unless path
135
+
136
+ transform(path, color)
137
+ ITerm2::Image.new(path)
138
+ end
139
+
140
+ def transform(path, color)
141
+ # TODO: options
142
+ # TODO: put number in a white box
143
+ image = MiniMagick::Image.new(path)
144
+ image.combine_options do |i|
145
+ i.resize "250x250>"
146
+ i.border "4x4"
147
+ i.bordercolor color
148
+ i.pointsize '26'
149
+ i.weight 'Bold'
150
+ # for 26 point
151
+ # count isn't available on Notification -of something?!
152
+ i.annotate "+10+30", @spec_count.to_s
153
+ # for 32 point
154
+ #i.annotate "+15+40", "#1000"
155
+ end
156
+ rescue MiniMagick::Error => e
157
+ raise Error, "image transformation failed: #{e}"
158
+ end
159
+
160
+ def display_progess(image)
161
+ # TODO: subclass?
162
+ # if SelfieFormatter.film_strip?
163
+ film_strip_formatter(image)
164
+ end
165
+
166
+ def film_strip_formatter(image)
167
+ output_image(image)
168
+ adjust_cursor
169
+ end
170
+
171
+ def output_image(image)
172
+ str = image.to_s
173
+ if @main_img_height && @main_img_width
174
+ @output << str
175
+ else
176
+ @main_img_width, @main_img_height = @cursor.distance { @output << str }
177
+ raise Error, "formatting failed: cannot determine cursor position" unless @main_img_width && @main_img_height
178
+ end
179
+ end
180
+
181
+ def adjust_cursor
182
+ # Check if the next image fits on the current row
183
+ @offset += 1
184
+ if @offset * @main_img_width <= @columns
185
+ @cursor.up(@main_img_height)
186
+ else
187
+ @offset = 1
188
+ @output << "\n"
189
+ end
190
+ end
191
+ end
192
+
193
+ RSpec.configuration.add_setting :selfie_output_directory
@@ -0,0 +1,30 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'selfie/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "selfie_formatter"
8
+ spec.version = Selfie::VERSION
9
+ spec.authors = ["Skye Shaw"]
10
+ spec.email = ["skye.shaw@gmail.com"]
11
+
12
+ spec.summary = "RSpec Formatter that takes photos of you while your tests run and uses them to track progress and format the results."
13
+ spec.description =<<-DESC
14
+ An RSpec Formatter for the new generation for programmers.
15
+ Selfie Formatter takes photos of you while your tests run and uses them to track progress and format the results.
16
+ Currently only works on OS X with iTerm2 >= 3.0.
17
+ DESC
18
+
19
+ spec.homepage = "https://github.com/sshaw/selfie_formatter"
20
+ spec.license = "MIT"
21
+
22
+ spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
23
+ spec.require_paths = ["lib"]
24
+
25
+ spec.add_dependency "rspec", "~> 3.0"
26
+ spec.add_dependency "mini_magick", "~> 4.0"
27
+ spec.add_dependency "tty-cursor", "~> 0.3"
28
+ spec.add_development_dependency "bundler", "~> 1.10"
29
+ spec.add_development_dependency "rake", "~> 10.0"
30
+ end
metadata ADDED
@@ -0,0 +1,132 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: selfie_formatter
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Skye Shaw
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2016-06-22 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rspec
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '3.0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '3.0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: mini_magick
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '4.0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '4.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: tty-cursor
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '0.3'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '0.3'
55
+ - !ruby/object:Gem::Dependency
56
+ name: bundler
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '1.10'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '1.10'
69
+ - !ruby/object:Gem::Dependency
70
+ name: rake
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '10.0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '10.0'
83
+ description: |2
84
+ An RSpec Formatter for the new generation for programmers.
85
+ Selfie Formatter takes photos of you while your tests run and uses them to track progress and format the results.
86
+ Currently only works on OS X with iTerm2 >= 3.0.
87
+ email:
88
+ - skye.shaw@gmail.com
89
+ executables: []
90
+ extensions: []
91
+ extra_rdoc_files: []
92
+ files:
93
+ - ".gitignore"
94
+ - Gemfile
95
+ - LICENSE.txt
96
+ - README.md
97
+ - example.gif
98
+ - lib/imagesnap
99
+ - lib/selfie.rb
100
+ - lib/selfie/camera.rb
101
+ - lib/selfie/cursor.rb
102
+ - lib/selfie/iterm2/image.rb
103
+ - lib/selfie/version.rb
104
+ - lib/selfie_formatter.rb
105
+ - selfie_formatter.gemspec
106
+ homepage: https://github.com/sshaw/selfie_formatter
107
+ licenses:
108
+ - MIT
109
+ metadata: {}
110
+ post_install_message:
111
+ rdoc_options: []
112
+ require_paths:
113
+ - lib
114
+ required_ruby_version: !ruby/object:Gem::Requirement
115
+ requirements:
116
+ - - ">="
117
+ - !ruby/object:Gem::Version
118
+ version: '0'
119
+ required_rubygems_version: !ruby/object:Gem::Requirement
120
+ requirements:
121
+ - - ">="
122
+ - !ruby/object:Gem::Version
123
+ version: '0'
124
+ requirements: []
125
+ rubyforge_project:
126
+ rubygems_version: 2.4.8
127
+ signing_key:
128
+ specification_version: 4
129
+ summary: RSpec Formatter that takes photos of you while your tests run and uses them
130
+ to track progress and format the results.
131
+ test_files: []
132
+ has_rdoc: