selfie_formatter 0.0.1

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.
@@ -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: