shrimple 0.8.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.
@@ -0,0 +1,69 @@
1
+ require 'spec_helper'
2
+
3
+
4
+ # the other Shrimple specs exercise this class pretty well
5
+ # it would take a lot of mocking and stubbing to do it here too.
6
+
7
+
8
+ describe Shrimple::ProcessMonitor do
9
+ it "will add a process" do
10
+ processes = Shrimple::ProcessMonitor.new(1)
11
+ # this should not raise an exception
12
+ # not using expect(...).not_to raise_exception since that eats all raised expections.
13
+ processes._add(Object.new)
14
+ end
15
+
16
+ it "won't launch too many processes" do
17
+ processes = Shrimple::ProcessMonitor.new(0)
18
+ expect { processes._add(Object.new) }.to raise_exception(Shrimple::TooManyProcessesError)
19
+ end
20
+
21
+ it "can disable the process counter" do
22
+ processes = Shrimple::ProcessMonitor.new(-1)
23
+ processes._add(Object.new)
24
+ end
25
+
26
+ it "counts and kills multiple processes" do
27
+ expect(Shrimple.processes.count).to eq 0
28
+ process = Shrimple::Process.new(['sleep', '20'], StringIO.new, StringIO.new, StringIO.new)
29
+ process = Shrimple::Process.new(['sleep', '20'], StringIO.new, StringIO.new, StringIO.new)
30
+ process = Shrimple::Process.new(['sleep', '20'], StringIO.new, StringIO.new, StringIO.new)
31
+ process = Shrimple::Process.new(['sleep', '20'], StringIO.new, StringIO.new, StringIO.new)
32
+ expect(Shrimple.processes.count).to eq 4
33
+ Shrimple.processes.first.kill
34
+ expect(Shrimple.processes.count).to eq 3
35
+ # can't use Array#each since calling delete in the block causes it to screw up
36
+ Shrimple.processes.kill_all
37
+ expect(Shrimple.processes.count).to eq 0
38
+ end
39
+
40
+ it "waits for multiple processes" do
41
+ expect(Shrimple.processes.count).to eq 0
42
+ # these sleep durations might be too small, depends on machine load and scheduling.
43
+ # if you're seeing threads finishing in the wrong order, try increasing them 10X.
44
+ process1 = Shrimple::Process.new(['sleep', '.3'], StringIO.new, StringIO.new, StringIO.new)
45
+ process2 = Shrimple::Process.new(['sleep', '.1'], StringIO.new, StringIO.new, StringIO.new)
46
+ process3 = Shrimple::Process.new(['sleep', '.2'], StringIO.new, StringIO.new, StringIO.new)
47
+ expect(Shrimple.processes.count).to eq 3
48
+
49
+ child = Shrimple.processes.wait_next
50
+ expect(child).to eq process2
51
+ expect(child.finished?).to eq true
52
+ expect(child.success?).to eq true
53
+ expect(Shrimple.processes.count).to eq 2
54
+
55
+ child = Shrimple.processes.wait_next
56
+ expect(child).to eq process3
57
+ expect(Shrimple.processes.count).to eq 1
58
+
59
+ child = Shrimple.processes.wait_next
60
+ expect(child).to eq process1
61
+ expect(Shrimple.processes.count).to eq 0
62
+ end
63
+
64
+ it "handles waiting for zero processes" do
65
+ expect {
66
+ child = Shrimple.processes.wait_next
67
+ }.to raise_exception(ThreadsWait::ErrNoWaitingThread)
68
+ end
69
+ end
@@ -0,0 +1,85 @@
1
+ require 'spec_helper'
2
+
3
+ # run this to ensure there are no deadlock / process synchronization problems:
4
+ # while rspec spec/shrimple_process_spec.rb ; do echo -n ; done
5
+
6
+ describe Shrimple::Process do
7
+ let(:chin) { StringIO.new('small instring') }
8
+ let(:chout) { StringIO.new }
9
+ let(:cherr) { StringIO.new }
10
+
11
+ it "has a working drain method" do
12
+ bigin = StringIO.new('x' * 1024 * 1024) # at least 1 MB of data to test drain loop
13
+ process = Shrimple::Process.new('cat', bigin, chout, cherr)
14
+ process.stop
15
+ expect(chout.string).to eq bigin.string
16
+ expect(process.finished?).to eq true
17
+ end
18
+
19
+ it "waits until a sleeping command is finished" do
20
+ # pile a bunch of checks into this test so we only have to sleep once
21
+ expect(Shrimple.processes.count).to eq 0
22
+ claimed = nil
23
+
24
+ elapsed = time do
25
+ # echo -n doesn't work here because of platform variations
26
+ # and for some reason jruby requires the explicit subshell; mri launches it automatically
27
+ process = Shrimple::Process.new('/bin/sh -c "sleep 0.1 && printf done."', chin, chout, cherr)
28
+ expect(Shrimple.processes.count).to eq 1
29
+ process.stop
30
+ expect(process.start_time).not_to eq nil
31
+ expect(process.stop_time).not_to eq nil
32
+ claimed = process.stop_time - process.start_time
33
+ expect(chout.string).to eq 'done.'
34
+ expect(process.finished?).to eq true
35
+ expect(process.success?).to eq true
36
+ end
37
+
38
+ # ensure process elapsed time is in the ballpark
39
+ expect(elapsed).to be >= 0.1
40
+ expect(claimed).to be >= 0.1
41
+ expect(claimed).to be <= elapsed
42
+
43
+ expect(Shrimple.processes.count).to eq 0
44
+ expect(chout.closed_read?).to eq true
45
+ expect(cherr.closed_read?).to eq true
46
+ end
47
+
48
+ it "has a working kill method" do
49
+ elapsed = time do
50
+ process = Shrimple::Process.new(['sleep', '0.5'], chin, chout, cherr)
51
+
52
+ expect(process.finished?).to eq false
53
+ expect(process.killed?).to eq false
54
+ expect(process.success?).to eq false
55
+ expect(process.timed_out?).to eq false
56
+
57
+ process.kill
58
+
59
+ expect(process.finished?).to eq true
60
+ expect(process.killed?).to eq true
61
+ expect(process.success?).to eq false
62
+ expect(process.timed_out?).to eq false
63
+ end
64
+
65
+ expect(elapsed).to be < 0.5
66
+ expect(chout.closed_read?).to eq true
67
+ expect(cherr.closed_read?).to eq true
68
+ end
69
+
70
+ it "handles invalid commands" do
71
+ expect {
72
+ expect(Shrimple.processes.count).to eq 0
73
+ process = Shrimple::Process.new(['ThisCmdDoes.Not.Exist.'], chin, chout, cherr)
74
+ raise "we shouldn't get here"
75
+ }.to raise_error(/[Nn]o such file/)
76
+ expect(Shrimple.processes.count).to eq 0
77
+ end
78
+
79
+ it "has a working timeout" do
80
+ elapsed = time do
81
+ process = Shrimple::Process.new(['sleep', '10'], chin, chout, cherr, 0)
82
+ end
83
+ expect(elapsed).to be < 0.5
84
+ end
85
+ end
@@ -0,0 +1,205 @@
1
+ require 'spec_helper'
2
+ require 'dimensions'
3
+
4
+ # this file contains the time-consuming tests that shell out to phantomjs
5
+
6
+
7
+ def pdf_valid?(io)
8
+ # quick & dirty check
9
+ case io
10
+ when File
11
+ io.read[0...4] == "%PDF"
12
+ when String
13
+ io[0...4] == "%PDF" || File.open(io).read[0...4] == "%PDF"
14
+ end
15
+ end
16
+
17
+
18
+ def prepare_file outfile
19
+ # TODO: there MUST be a better way of handling file output in rspec
20
+ # (can't mock file ops because the output is coming from phantomjs)
21
+ File.delete(outfile) if File.exists?(outfile)
22
+ return '/tmp/' + outfile
23
+ end
24
+
25
+
26
+ describe Shrimple do
27
+ it "echoes its arguments" do
28
+ s = Shrimple.new(renderer: 'spec/parse_and_print_stdin.js')
29
+ output = s.render
30
+ result = JSON.parse(output.stdout)
31
+ expect(result['renderer']).to eq 'spec/parse_and_print_stdin.js'
32
+ expect(result['processed']).to eq true # added by the phantom script
33
+ expect(output.stderr).to eq ""
34
+ end
35
+
36
+
37
+ # well I give up. can't find an item settable by --config that I can read in js. :(
38
+ # https://github.com/ariya/phantomjs/issues/12265
39
+ #
40
+ # it "sets a command-line arg" do
41
+ # s = Shrimple.new
42
+ # s.config.loadImages = false
43
+ # s.config.autoLoadImages = false
44
+ # s.renderer = 'render_max_disk_cache.js'
45
+ # s.render
46
+ # end
47
+
48
+
49
+ it "renders text to a string" do
50
+ callback_param = nil
51
+ s = Shrimple.new
52
+ s.onSuccess = Proc.new do |result|
53
+ # make sure this process isn't removed from the process table
54
+ # until after this callback returns.
55
+ sleep(0.2)
56
+ expect(Shrimple.processes.count).to eq 1
57
+ callback_param = result
58
+ end
59
+ s.onError = Proc.new { fail }
60
+ result = s.render_text("file://#{example_html}")
61
+ output = result.stdout # TODO: get rid of this line
62
+ expect(output).to eq "Hello World!\n"
63
+ expect(callback_param).to eq result
64
+ end
65
+
66
+ it "renders text to a file" do
67
+ outfile = prepare_file('shrimple-test-output.txt')
68
+ s = Shrimple.new
69
+ s.render_text("file://#{example_html}", to: outfile)
70
+ output = File.read(outfile)
71
+ expect(output).to eq "Hello World!\n"
72
+ File.delete(outfile)
73
+ end
74
+
75
+ it "renders html to a string" do
76
+ s = Shrimple.new
77
+ result = s.render_html("file://#{example_html}")
78
+ output = result.stdout # TODO: get rid of this line
79
+ expect(output).to include "<h1>Hello World!</h1>"
80
+ end
81
+
82
+ it "handles a missing file" do
83
+ # also ensures failures's stderr appears in the exception
84
+ callback_param = nil
85
+ s = Shrimple.new
86
+ s.onSuccess = Proc.new { fail }
87
+ s.onError = Proc.new { |result| callback_param = result }
88
+ expect {
89
+ s.render_text("file://this-does-not-exist")
90
+ }.to raise_exception(Shrimple::PhantomError, /Unable to load.*this-does-not-exist/)
91
+ expect(callback_param).to be_a Shrimple::Phantom
92
+ end
93
+
94
+ it "handles a missing file in background mode" do
95
+ callback_param = nil
96
+ s = Shrimple.new(background: true)
97
+ s.onSuccess = Proc.new { fail }
98
+ s.onError = Proc.new { |result| callback_param = result }
99
+ result = s.render_text("file://this-does-not-exist")
100
+ child = Shrimple.processes.wait_next
101
+
102
+ expect(child).to eq result
103
+ expect(result.success?).to eq false
104
+ expect(result.stderr).to match(/Unable to load.*this-does-not-exist/)
105
+ expect(callback_param).to eq result
106
+ end
107
+
108
+ it "handles phantomjs complaining about a missing render script" do
109
+ s = Shrimple.new(renderer: 'this-does-not-exist')
110
+ expect {
111
+ s.render_text("file://#{example_html}")
112
+ }.to raise_exception(Shrimple::PhantomError, /Can't open 'this-does-not-exist'/)
113
+ end
114
+
115
+ it "handles phantomjs complaining about a missing render script in background mode" do
116
+ s = Shrimple.new(renderer: 'this-does-not-exist', background: true)
117
+ result = s.render_text("file://#{example_html}")
118
+ child = Shrimple.processes.wait_next
119
+
120
+ expect(child).to eq result
121
+ expect(result.success?).to eq false
122
+ expect(result.stderr).to match(/Can't open 'this-does-not-exist'/)
123
+ end
124
+
125
+ # # it's hopeless: https://github.com/ariya/phantomjs/issues/10687
126
+ #
127
+ # it "handles a syntax error in a render script" do
128
+ # s = Shrimple.new(renderer: 'spec/syntax_error.js')
129
+ # expect {
130
+ # s.render_text("file://#{example_html}")
131
+ # }.to raise_exception(/Can't open 'this-does-not-exist'/)
132
+ # end
133
+
134
+ it "supports a debugging mode" do
135
+ # isn't there a better way of resetting global variables in rspec?
136
+ olderr = $stderr
137
+ begin
138
+ $stderr = StringIO.new
139
+ s = Shrimple.new(debug: true)
140
+ s.render_text("file://#{example_html}")
141
+
142
+ expect($stderr.string).to match /^COMMAND: \[.*phantomjs.*render.js"\]/
143
+ expect($stderr.string).to match /^STDIN: {.*"debug":true.*}/
144
+ ensure
145
+ $stderr = olderr
146
+ end
147
+ end
148
+
149
+ it "renders a pdf to a file" do
150
+ outfile = prepare_file('shrimple-test-output.pdf')
151
+ s = Shrimple.new(to: outfile)
152
+ s.render_pdf "file://#{example_html}"
153
+ expect(File.exists? outfile).to eq true
154
+ expect(pdf_valid?(File.new(outfile))).to eq true
155
+ end
156
+
157
+ it "renders a png to a file" do
158
+ outfile = prepare_file('shrimple-test-output.png')
159
+ s = Shrimple.new
160
+ p = s.render_png "file://#{example_html}", output: outfile
161
+
162
+ expect(File.exists? outfile).to eq true
163
+ dimensions = Dimensions.dimensions(outfile)
164
+ expect(dimensions[0]).to eq 400 # phantomjs default width
165
+ expect(dimensions[1]).to eq 300 # phantomjs default height
166
+
167
+ # when dimensions allows reading the filetype, add that check here
168
+ # https://github.com/cleanio/dimensions/commit/c61ad05c354feb1063bfbdc97c1ec5456c9ad43a
169
+ end
170
+
171
+ it "renders a png to a stream" do
172
+ s = Shrimple.new(page: {viewportSize: { width: 555, height: 555 }} )
173
+ s.page.zoomFactor = 0.75
174
+ output = s.render_png "file://#{example_html}"
175
+
176
+ # todo: would be great if we could attach Dimensions straight to the io object reading the results
177
+ # instead of needing to flush the result to a memory buffer and wrapping that in a new stringio
178
+ dimensions = Dimensions(StringIO.new(output.stdout))
179
+ expect(dimensions.width).to eq 555
180
+ expect(dimensions.height).to eq 555
181
+ end
182
+
183
+ it "renders a jpeg to a file" do
184
+ outfile = prepare_file('shrimple-test-output.jpg')
185
+ s = Shrimple.new
186
+ s.page.viewportSize = { width: 320, height: 240 }
187
+ s.output = outfile
188
+ output = s.render_jpeg "file://#{example_html}"
189
+
190
+ expect(File.exists? outfile).to eq true
191
+ dimensions = Dimensions.dimensions(outfile)
192
+ expect(dimensions[0]).to eq 320
193
+ expect(dimensions[1]).to eq 240
194
+ end
195
+
196
+ it "renders a gif to memory" do
197
+ s = Shrimple.new
198
+ s.page.viewportSize = { width: 213, height: 214 }
199
+ output = s.render_gif "file://#{example_html}"
200
+
201
+ dimensions = Dimensions(StringIO.new(output.stdout))
202
+ expect(dimensions.width).to eq 213
203
+ expect(dimensions.height).to eq 214
204
+ end
205
+ end
@@ -0,0 +1,119 @@
1
+ require 'spec_helper'
2
+
3
+ # Mostly tests the Shrimple API. Other specs test the internals.
4
+
5
+
6
+ describe Shrimple do
7
+ # we send this in every request until Phantom fixes its bug, see default_config.rb
8
+ let(:custom_headers) { {"page" => {"customHeaders"=>{"Accept-Encoding"=>"identity"}}} }
9
+
10
+ it "automatically finds the executable and renderer" do
11
+ s = Shrimple.new
12
+ expect(File.executable? s.executable).to be true
13
+ expect(File.exists? s.renderer).to be true
14
+ end
15
+
16
+ it "can be told the executable and renderer" do
17
+ # these don't need to be real executables since they're never called
18
+ s = Shrimple.new(executable: '/bin/sh', renderer: example_html)
19
+ expect(s.executable).to eq '/bin/sh'
20
+ expect(s.renderer).to eq example_html
21
+ end
22
+
23
+ it "dies if specified executable can't be found" do
24
+ s = Shrimple.new(executable: '/bin/THIS_FILE_DOES.not.Exyst')
25
+ expect { s.render 'http://be.com' }.to raise_exception(/[Nn]o such file/)
26
+ end
27
+
28
+ it "dies if default executable can't be found" do
29
+ expect { Shrimple.new.render('http://be.com', executable: nil) }.to raise_exception(/PhantomJS not found/)
30
+ end
31
+
32
+ it "allows a bunch of different ways to set options" do
33
+ s = Shrimple.new(executable: '/bin/sh', renderer: example_html, render: {quality: 50})
34
+
35
+ s.executable = '/bin/cat'
36
+ s.page.paperSize.orientation = 'landscape'
37
+ s[:page][:settings][:userAgent] = 'webkitalike'
38
+ s.options.page.zoomFactor = 0.25
39
+
40
+ mock_phantom = Object.new
41
+ expect(mock_phantom).to receive(:wait).once
42
+
43
+ allow(Shrimple::Phantom).to receive(:new).once do |opts|
44
+ expect(opts.to_hash).to eq(Hashie::Mash.new({
45
+ input: 'infile',
46
+ output: 'outfile',
47
+ executable: '/bin/cat',
48
+ renderer: example_html,
49
+ render: { quality: 50 },
50
+ page: {
51
+ paperSize: { orientation: 'landscape' },
52
+ settings: { userAgent: 'webkitalike' },
53
+ zoomFactor: 0.25
54
+ }
55
+ }).merge(custom_headers).to_hash)
56
+ mock_phantom
57
+ end
58
+
59
+ s.render 'infile', to: 'outfile'
60
+ end
61
+
62
+ it "runs in the background" do
63
+ s = Shrimple.new(executable: '/bin/cat', renderer: 'tt.js', background: true)
64
+
65
+ mock_phantom = Object.new
66
+ expect(mock_phantom).not_to receive(:wait)
67
+ allow(Shrimple::Phantom).to receive(:new).once.and_return(mock_phantom)
68
+
69
+ p = s.render 'infile'
70
+ end
71
+
72
+ it "special-cases input as the first argument" do
73
+ s = Shrimple.new
74
+ s.merge!(executable: nil, renderer: nil)
75
+ # can either start with a value for input
76
+ expect(s.get_full_options("input", to: "output")).
77
+ to eq({'input' => 'input', 'output' => 'output'}.merge(custom_headers))
78
+ # or just use hashes all the way through
79
+ expect(s.get_full_options(input: "eenput", output: "ootput")).
80
+ to eq({'input' => 'eenput', 'output' => 'ootput'}.merge(custom_headers))
81
+ end
82
+
83
+ it "has options with indifferent access" do
84
+ s = Shrimple.new
85
+ s.merge!('executable' => nil, renderer: nil)
86
+ expect(s.get_full_options(executable: 'symbol', 'executable' => 'string')).to eq({'executable' => 'string'}.merge(custom_headers))
87
+ s.merge!(executable: 'symbol')
88
+ expect(s.get_full_options(executable: 'symbol')).to eq({'executable' => 'symbol'}.merge(custom_headers))
89
+ expect(Shrimple.compact!(s.to_hash)).to eq({'executable' => 'symbol'}.merge(custom_headers))
90
+ end
91
+
92
+ it "has a working compact" do
93
+ expect(Shrimple.compact!({
94
+ a: nil,
95
+ b: { c: nil },
96
+ d: { e: { f: "", g: 1 } },
97
+ h: false
98
+ })).to eq({
99
+ d: { e: { g: 1 }},
100
+ h: false
101
+ })
102
+
103
+ expect(Shrimple.compact!({})).to eq({})
104
+ end
105
+
106
+ it "has a working deep_dup" do
107
+ x = { a: 1, b: { c: 2, d: false, e:[1,2,3] }}
108
+ y = Shrimple.deep_dup(x)
109
+
110
+ x[:a] = 2
111
+ x[:b].delete(:e)
112
+ x[:b][:d] = true
113
+ x.delete(:b)
114
+
115
+ # y should be unchanged since we dup'd it
116
+ expect(x).to eq({a: 2})
117
+ expect(y).to eq({a: 1, b: { c: 2, d: false, e: [1, 2, 3] }})
118
+ end
119
+ end