Skip to content

Commit

Permalink
Add ruby 3.3 support and update test suite to use a local server (#216)
Browse files Browse the repository at this point in the history
* Add test server to avoid external network calls
* Update test suite to use the local server to run tests
  • Loading branch information
afromankenobi authored Jan 15, 2024
1 parent 156ef96 commit 898599a
Show file tree
Hide file tree
Showing 7 changed files with 172 additions and 29 deletions.
5 changes: 4 additions & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ jobs:
strategy:
fail-fast: false
matrix:
ruby-version: ['3.2']
ruby-version: ['3.3']
node-version: ['14']
puppeteer-version: [
'14.4.1',
Expand All @@ -43,6 +43,9 @@ jobs:
- ruby-version: '3.2'
node-version: '18'
puppeteer-version: '19.5.2'
- ruby-version: '3.3'
node-version: '18'
puppeteer-version: '19.5.2'

steps:
- uses: actions/checkout@v3
Expand Down
5 changes: 4 additions & 1 deletion grover.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ Gem::Specification.new do |spec|
SUMMARY
spec.homepage = 'https://github.com/Studiosity/grover'
spec.license = 'MIT'
spec.required_ruby_version = ['>= 2.7.0', '< 3.3.0']
spec.required_ruby_version = ['>= 2.7.0', '< 3.4.0']

# Prevent pushing this gem to RubyGems.org by setting 'allowed_push_host', or
# delete this section to allow pushing this gem to any host.
Expand All @@ -32,14 +32,17 @@ Gem::Specification.new do |spec|
spec.add_dependency 'combine_pdf', '~> 1.0'
spec.add_dependency 'nokogiri', '~> 1.0'

spec.add_development_dependency 'childprocess'
spec.add_development_dependency 'mini_magick', '~> 4.12'
spec.add_development_dependency 'pdf-reader', '~> 2.11'
spec.add_development_dependency 'puma', '~> 6.4'
spec.add_development_dependency 'rack-test', '~> 1.1'
spec.add_development_dependency 'rake', '~> 13.0'
spec.add_development_dependency 'rspec', '~> 3.12'
spec.add_development_dependency 'rubocop', '~> 1.43'
spec.add_development_dependency 'rubocop-rake', '~> 0.6'
spec.add_development_dependency 'rubocop-rspec', '~> 2.18'
spec.add_development_dependency 'sinatra', '~> 3.2'
# Limit simplecov to 0.17.x due to https://github.com/codeclimate/test-reporter/issues/413
spec.add_development_dependency 'simplecov', '~> 0.17', '< 0.18'
end
53 changes: 26 additions & 27 deletions spec/grover/processor_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
describe '#convert' do
subject(:convert) { processor.convert method, url_or_html, options }

let(:url_or_html) { 'http://google.com' }
let(:url_or_html) { 'http://localhost:4567' }
let(:options) { {} }
let(:date) do
# New version of Chromium (v93) that comes with v10.2.0 of puppeteer uses a different date format
Expand Down Expand Up @@ -40,14 +40,11 @@
end

context 'when passing through a valid URL' do
# we need to add the language for test stability
# if not added explicitly, google can respond with a different locale
# based on IP address geo-lookup, timezone, etc.
let(:url_or_html) { 'https://www.google.com/?gl=us' }
let(:url_or_html) { 'http://localhost:4567' }

it { is_expected.to start_with "%PDF-1.4\n" }
it { expect(pdf_reader.page_count).to eq 1 }
it { expect(pdf_text_content).to include "I'm Feeling Lucky" }
it { expect(pdf_text_content).to include "I'm Feeling Grovery" }
end

context 'when passing through an invalid URL' do
Expand Down Expand Up @@ -133,6 +130,8 @@

context 'when first call to gets on stdout succeeds but second returns nil' do
before do
allow(stdin).to receive(:puts).with(any_args)

allow(stdout).to receive(:gets).and_return '["ok"]', nil
allow(stdin).to receive(:puts).with '["pdf","http://google.com",{}]'
allow(stderr).to receive(:read).and_return 'The reason the worker failed'
Expand All @@ -149,7 +148,7 @@
context 'when first call to gets on stdout succeeds but second returns an error' do
before do
allow(stdout).to receive(:gets).and_return '["ok"]', '["err","Some unknown thing happened"]'
allow(stdin).to receive(:puts).with '["pdf","http://google.com",{}]'
allow(stdin).to receive(:puts).with '["pdf","http://localhost:4567",{}]'
allow(stderr).to receive(:read).and_return 'The reason the worker failed'
end

Expand All @@ -169,7 +168,7 @@
context 'when the worker returns an invalid response' do
before do
allow(stdout).to receive(:gets).and_return '["ok"]', '["ok",invalid_response]'
allow(stdin).to receive(:puts).with '["pdf","http://google.com",{}]'
allow(stdin).to receive(:puts).with '["pdf","http://localhost:4567",{}]'
end

it 'raises an Error' do
Expand Down Expand Up @@ -353,7 +352,7 @@
end

context 'when requesting a URI requiring basic authentication' do
let(:url_or_html) { 'https://jigsaw.w3.org/HTTP/Basic/' }
let(:url_or_html) { 'http://localhost:4567/auth' }

it { expect(pdf_text_content).to eq 'Unauthorized access You are denied access to this resource.' }

Expand All @@ -365,17 +364,17 @@
end

context 'when passing through cookies option' do
let(:url_or_html) { 'https://cookierenderer-production.up.railway.app/' }
let(:url_or_html) { 'http://localhost:4567/cookie_renderer' }
let(:options) do
{
'cookies' => [
{
'name' => 'grover-test',
'value' => 'nom nom nom',
'domain' => 'cookierenderer-production.up.railway.app'
'domain' => 'localhost:4567'
},
{ 'name' => 'other-domain', 'value' => 'should not display', 'domain' => 'example.com' },
{ 'name' => 'escaped', 'value' => '%26%3D%3D', 'domain' => 'cookierenderer-production.up.railway.app' }
{ 'name' => 'escaped', 'value' => '%26%3D%3D', 'domain' => 'localhost:4567' }
]
}
end
Expand All @@ -386,21 +385,21 @@
end

context 'when passing through extra HTTP headers' do
let(:url_or_html) { 'http://cookierenderer-production.up.railway.app/?type=headers' }
let(:url_or_html) { 'http://localhost:4567/headers' }
let(:options) { { 'extraHTTPHeaders' => { 'grover-test' => 'yes it is' } } }

it { expect(pdf_text_content).to match(/Request contained (15|16) headers/) }
it { expect(pdf_text_content).to include '1. host cookierenderer-production.up.railway.app' }
it { expect(pdf_text_content).to include '4. grover-test yes it is' }
it { expect(pdf_text_content).to match(/Request contained \d+ headers/) }
it { expect(pdf_text_content).to include '1. host localhost:4567' }
it { expect(pdf_text_content).to include '5. grover-test yes it is' }
end

context 'when overloading the user agent' do
let(:url_or_html) { 'http://cookierenderer-production.up.railway.app/?type=headers' }
let(:url_or_html) { 'http://localhost:4567/headers' }
let(:options) { { 'userAgent' => 'Grover user agent' } }

it { expect(pdf_text_content).to match(/Request contained (14|15) headers/) }
it { expect(pdf_text_content).to include '1. host cookierenderer-production.up.railway.app' }
it { expect(pdf_text_content).to include 'user-agent Grover user agent' }
it { expect(pdf_text_content).to match(/Request contained \d+ headers/) }
it { expect(pdf_text_content).to include '1. host localhost:4567' }
it { expect(pdf_text_content).to include '4. user-agent Grover user agent' }
end
end

Expand Down Expand Up @@ -641,7 +640,7 @@
end

context 'when assets have redirects PDFs are generated successfully' do
it { expect(pdf_text_content).to match "#{date} Google" }
it { expect(pdf_text_content).to match "#{date} I'm Feeling Grovery" }
end

context 'with images' do
Expand Down Expand Up @@ -809,7 +808,7 @@
let(:timeout) { 1 }

if puppeteer_version_on_or_after? '10.4.0'
it 'will timeout when trying to convert to PDF' do
it 'times out when trying to convert to PDF' do
expect { convert }.to raise_error(
Grover::JavaScript::TimeoutError,
'waiting for Page.printToPDF failed: timeout 1ms exceeded'
Expand All @@ -830,7 +829,7 @@
let(:convert_timeout) { 1 }

if puppeteer_version_on_or_after? '10.4.0'
it 'will raise an error when trying to convert to PDF' do
it 'raises an error when trying to convert to PDF' do
expect { convert }.to raise_error(
Grover::JavaScript::TimeoutError,
'waiting for Page.printToPDF failed: timeout 1ms exceeded'
Expand All @@ -844,7 +843,7 @@
let(:timeout) { 10_000 }

if puppeteer_version_on_or_after? '10.4.0'
it 'will use the convert timeout over the timeout option' do
it 'uses the convert timeout over the timeout option' do
expect { convert }.to raise_error(
Grover::JavaScript::TimeoutError,
'waiting for Page.printToPDF failed: timeout 1ms exceeded'
Expand Down Expand Up @@ -876,7 +875,7 @@
let(:image) { MiniMagick::Image.read convert }

context 'when passing through a valid URL' do
let(:url_or_html) { 'https://media.gettyimages.com/photos/tabby-cat-selfie-picture-id1151094724?s=2048x2048' }
let(:url_or_html) { 'http://localhost:4567/cat' }

# default screenshot is PNG 800w x 600h
it { expect(convert.unpack('C*')).to start_with "\x89PNG\r\n\x1A\n".unpack('C*') }
Expand All @@ -887,8 +886,8 @@
# so we'll check it's mean colour is roughly what we expect
it do
expect(image.data.dig('imageStatistics', MiniMagick.imagemagick7? ? 'Overall' : 'all', 'mean').to_f).
to be_within(1).of(97.7473). # ImageMagick 6.9.3-1 (version used by Travis CI)
or be_within(1).of(161.497) # ImageMagick 6.9.10-84
to be_within(10).of(97.7473). # ImageMagick 6.9.3-1 (version used by Travis CI)
or be_within(10).of(161.497) # ImageMagick 6.9.10-84
end
end

Expand Down
9 changes: 9 additions & 0 deletions spec/spec_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,20 @@
require 'stringio'
require 'pdf-reader'
require 'mini_magick'
require_relative 'support/test_server'

RSpec.configure do |config|
config.order = 'random'
config.filter_run_excluding remote_browser: true

config.before(:suite) do
TestServer.start
end

config.after(:suite) do
TestServer.stop
end

include Rack::Test::Methods
end

Expand Down
Binary file added spec/support/public/cat.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
52 changes: 52 additions & 0 deletions spec/support/test_app.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# frozen_string_literal: true

require 'sinatra'
require 'rack/auth/basic'

helpers do
def protected!
return if authorized?

response['WWW-Authenticate'] = %(Basic realm="Restricted Area")
throw(:halt, [401, "Unauthorized access\nYou are denied access to this resource."])
end

def authorized?
@auth ||= Rack::Auth::Basic::Request.new(request.env)
@auth.provided? && @auth.basic? && @auth.credentials && @auth.credentials == %w[guest guest]
end
end

get '/' do
"I'm Feeling Grovery"
end

get '/cookie_renderer' do
cookie_info = request.cookies.map.with_index(1) do |(name, value), i|
"#{i}. #{name} #{value}"
end.join(', ')

"Request contained #{request.cookies.size} cookies: #{cookie_info}"
end

get '/auth' do
protected!
'Your browser made it!'
end

get '/headers' do
headers =
request.
env.
select { |k, _v| k.start_with?('HTTP_') }
headers_info =
headers.
map.with_index(1) { |(k, v), i| "#{i}. #{k.sub(/^HTTP_/, '').downcase.tr('_', '-')} #{v}" }.
join(', ')

"Request contained #{headers.size} headers: #{headers_info}"
end

get '/cat' do
'<html><body><img src="/cat.png" alt="Cat"></body></html>'
end
77 changes: 77 additions & 0 deletions spec/support/test_server.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
# frozen_string_literal: true

require 'childprocess'
require 'net/http'
require 'logger'

#
# Simple control interface for the TestApp Sinatra server which hosts the various HTTP interfaces required to
# test Grover
#
class TestServer
class << self
attr_reader :server

def start
@server ||= new # rubocop:disable Naming/MemoizedInstanceVariableName
end

def stop
@server.stop
@server = nil
end
end

attr_reader :process, :read_io, :server_url, :logger

SERVER_URL = 'http://localhost:4567'

def initialize
@logger = Logger.new($stdout)
start
logging_thread
wait_until_started
end

def stop
@process.stop
@read_io.close
@logging_thread.kill
end

private

def start
@process = ChildProcess.build('ruby', File.join(__dir__, 'test_app.rb'))
@read_io, write_io = IO.pipe
@process.io.stdout = write_io
@process.io.stderr = write_io
@process.start
write_io.close
end

def logging_thread
@logging_thread ||= Thread.new do
loop { print @read_io.readpartial(8192) }
rescue EOFError
logger.warn 'Server process has terminated'
end
end

def server_running?
response = Net::HTTP.get_response(URI(SERVER_URL))
response.is_a?(Net::HTTPSuccess)
rescue Errno::ECONNREFUSED, Errno::EHOSTUNREACH
logger.warn 'Server not running'
false
end

def wait_until_started(max_attempts = 20)
max_attempts.times do
return if server_running?

sleep 0.5
end
raise 'Server failed to start'
end
end

0 comments on commit 898599a

Please sign in to comment.