shrimple 0.8.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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