From 9cbcb2ba9f8dabe50173f6a2d6f5f6716df72d4b Mon Sep 17 00:00:00 2001 From: Chris Hendrix Date: Thu, 23 Jul 2015 23:59:13 -0700 Subject: [PATCH] Adds "upload to s3" capabilities if configured - Introduces an S3Saver that's a transparent facade over a Saver - Refactors all saver instantiation behind saver "factory method" which dynamically chooses whether to give you an S3Saver depending on if you have configured s3 upload - Defaults to 'us-east-1' region if not specified --- README.md | 25 +++++ capybara-screenshot.gemspec | 1 + lib/capybara-screenshot.rb | 17 +++- lib/capybara-screenshot/cucumber.rb | 2 +- lib/capybara-screenshot/minitest.rb | 2 +- lib/capybara-screenshot/rspec.rb | 2 +- lib/capybara-screenshot/s3_saver.rb | 64 +++++++++++++ lib/capybara-screenshot/spinach.rb | 2 +- lib/capybara-screenshot/testunit.rb | 2 +- spec/unit/capybara-screenshot_spec.rb | 26 +++++ spec/unit/s3_saver_spec.rb | 132 ++++++++++++++++++++++++++ 11 files changed, 268 insertions(+), 7 deletions(-) create mode 100644 lib/capybara-screenshot/s3_saver.rb create mode 100644 spec/unit/s3_saver_spec.rb diff --git a/README.md b/README.md index dd48aa8..ee8bfb6 100644 --- a/README.md +++ b/README.md @@ -174,6 +174,31 @@ Capybara.save_and_open_page_path = "/file/path" ``` +Uploading screenshots to S3 +-------------------------- +You can configure capybara-screenshot to automatically save your screenshots to an AWS S3 bucket. + +First, install the `aws-sdk` gem or add it to your Gemfile + +```ruby +gem 'capybara-screenshot', :group => :test +gem 'aws-sdk', :group => :test +``` + +Next, configure capybara-screenshot with your S3 credentials, the bucket to save to, and an optional region (default: `us-east-1`). + +```ruby +Capybara::Screenshot.s3_configuration = { + s3_client_credentials: { + access_key_id: "my_access_key_id", + secret_access_key: "my_secret_access_key", + region: "eu-central-1" + }, + bucket_name: "my_screenshots" +} +``` + + Pruning old screenshots automatically -------------------------- By default screenshots are saved indefinitely, if you want them to be automatically pruned on a new failure, then you can specify one of the following prune strategies as follows: diff --git a/capybara-screenshot.gemspec b/capybara-screenshot.gemspec index dfaec78..643c09a 100644 --- a/capybara-screenshot.gemspec +++ b/capybara-screenshot.gemspec @@ -30,6 +30,7 @@ Gem::Specification.new do |s| s.add_development_dependency 'test-unit' s.add_development_dependency 'spinach' s.add_development_dependency 'minitest' + s.add_development_dependency 'aws-sdk' s.files = `git ls-files`.split("\n") s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n") diff --git a/lib/capybara-screenshot.rb b/lib/capybara-screenshot.rb index 3c42efb..a8f6f6a 100644 --- a/lib/capybara-screenshot.rb +++ b/lib/capybara-screenshot.rb @@ -9,6 +9,7 @@ class << self attr_accessor :webkit_options attr_writer :final_session_name attr_accessor :prune_strategy + attr_accessor :s3_configuration end self.autosave_on_failure = true @@ -18,6 +19,7 @@ class << self self.append_random = false self.webkit_options = {} self.prune_strategy = :keep_all + self.s3_configuration = {} def self.append_screenshot_path=(value) $stderr.puts "WARNING: Capybara::Screenshot.append_screenshot_path is deprecated. " + @@ -26,7 +28,7 @@ def self.append_screenshot_path=(value) end def self.screenshot_and_save_page - saver = Saver.new(Capybara, Capybara.page) + saver = new_saver(Capybara, Capybara.page) if saver.save {:html => saver.html_path, :image => saver.screenshot_path} end @@ -35,7 +37,7 @@ def self.screenshot_and_save_page def self.screenshot_and_open_image require "launchy" - saver = Saver.new(Capybara, Capybara.page, false) + saver = new_saver(Capybara, Capybara.page, false) if saver.save Launchy.open saver.screenshot_path {:html => nil, :image => saver.screenshot_path} @@ -90,6 +92,17 @@ def self.reset_prune_history @pruned_previous_screenshots = nil end + def self.new_saver(*args) + saver = Saver.new(*args) + + unless s3_configuration.empty? + require 'capybara-screenshot/s3_saver' + saver = S3Saver.new_with_configuration(saver, s3_configuration) + end + + return saver + end + private # If the path isn't set, default to the current directory diff --git a/lib/capybara-screenshot/cucumber.rb b/lib/capybara-screenshot/cucumber.rb index fbcbb9a..8644bc8 100644 --- a/lib/capybara-screenshot/cucumber.rb +++ b/lib/capybara-screenshot/cucumber.rb @@ -10,7 +10,7 @@ Capybara.using_session(Capybara::Screenshot.final_session_name) do filename_prefix = Capybara::Screenshot.filename_prefix_for(:cucumber, scenario) - saver = Capybara::Screenshot::Saver.new(Capybara, Capybara.page, true, filename_prefix) + saver = Capybara::Screenshot.new_saver(Capybara, Capybara.page, true, filename_prefix) saver.save saver.output_screenshot_path diff --git a/lib/capybara-screenshot/minitest.rb b/lib/capybara-screenshot/minitest.rb index 6d6b244..1a64c30 100644 --- a/lib/capybara-screenshot/minitest.rb +++ b/lib/capybara-screenshot/minitest.rb @@ -13,7 +13,7 @@ def after_teardown Capybara.using_session(Capybara::Screenshot.final_session_name) do filename_prefix = Capybara::Screenshot.filename_prefix_for(:minitest, self) - saver = Capybara::Screenshot::Saver.new(Capybara, Capybara.page, true, filename_prefix) + saver = Capybara::Screenshot.new_saver(Capybara, Capybara.page, true, filename_prefix) saver.save saver.output_screenshot_path end diff --git a/lib/capybara-screenshot/rspec.rb b/lib/capybara-screenshot/rspec.rb index 7ea064d..69d81df 100644 --- a/lib/capybara-screenshot/rspec.rb +++ b/lib/capybara-screenshot/rspec.rb @@ -53,7 +53,7 @@ def after_failed_example(example) if Capybara.page.current_url != '' && Capybara::Screenshot.autosave_on_failure && example.exception filename_prefix = Capybara::Screenshot.filename_prefix_for(:rspec, example) - saver = Capybara::Screenshot::Saver.new(Capybara, Capybara.page, true, filename_prefix) + saver = Capybara::Screenshot.new_saver(Capybara, Capybara.page, true, filename_prefix) saver.save example.metadata[:screenshot] = {} diff --git a/lib/capybara-screenshot/s3_saver.rb b/lib/capybara-screenshot/s3_saver.rb new file mode 100644 index 0000000..50ce423 --- /dev/null +++ b/lib/capybara-screenshot/s3_saver.rb @@ -0,0 +1,64 @@ +require 'aws-sdk' + +module Capybara + module Screenshot + class S3Saver + DEFAULT_REGION = 'us-east-1' + + def initialize(saver, s3_client, bucket_name) + @saver = saver + @s3_client = s3_client + @bucket_name = bucket_name + end + + def self.new_with_configuration(saver, configuration) + default_s3_client_credentials = { + region: DEFAULT_REGION + } + + s3_client_credentials = default_s3_client_credentials.merge( + configuration.fetch(:s3_client_credentials) + ) + + s3_client = Aws::S3::Client.new(s3_client_credentials) + bucket_name = configuration.fetch(:bucket_name) + + new(saver, s3_client, bucket_name) + rescue KeyError + raise "Invalid S3 Configuration #{configuration}. Please refer to the documentation for the necessary configurations." + end + + def save_and_upload_screenshot + save_and do |local_file_path| + File.open(local_file_path) do |file| + s3_client.put_object( + bucket: bucket_name, + key: File.basename(local_file_path), + body: file + ) + end + end + end + alias_method :save, :save_and_upload_screenshot + + def method_missing(method, *args) + # Need to use @saver instead of S3Saver#saver attr_reader method because + # using the method goes into infinite loop. Maybe attr_reader implements + # its methods via method_missing? + @saver.send(method, *args) + end + + private + attr_reader :saver, + :s3_client, + :bucket_name + + def save_and + saver.save + + yield(saver.html_path) if block_given? && saver.html_saved? + yield(saver.screenshot_path) if block_given? && saver.screenshot_saved? + end + end + end +end diff --git a/lib/capybara-screenshot/spinach.rb b/lib/capybara-screenshot/spinach.rb index a49e4dd..7b8b34e 100644 --- a/lib/capybara-screenshot/spinach.rb +++ b/lib/capybara-screenshot/spinach.rb @@ -9,7 +9,7 @@ def self.fail_with_screenshot(step_data, exception, location, step_definitions) if Capybara::Screenshot.autosave_on_failure Capybara.using_session(Capybara::Screenshot.final_session_name) do filename_prefix = Capybara::Screenshot.filename_prefix_for(:spinach, step_data) - saver = Capybara::Screenshot::Saver.new(Capybara, Capybara.page, true, filename_prefix) + saver = Capybara::Screenshot.new_saver(Capybara, Capybara.page, true, filename_prefix) saver.save saver.output_screenshot_path end diff --git a/lib/capybara-screenshot/testunit.rb b/lib/capybara-screenshot/testunit.rb index 5a7cd61..9ba4574 100644 --- a/lib/capybara-screenshot/testunit.rb +++ b/lib/capybara-screenshot/testunit.rb @@ -27,7 +27,7 @@ def notify_fault_with_screenshot(fault, *args) Capybara.using_session(Capybara::Screenshot.final_session_name) do filename_prefix = Capybara::Screenshot.filename_prefix_for(:testunit, fault) - saver = Capybara::Screenshot::Saver.new(Capybara, Capybara.page, true, filename_prefix) + saver = Capybara::Screenshot.new_saver(Capybara, Capybara.page, true, filename_prefix) saver.save saver.output_screenshot_path end diff --git a/spec/unit/capybara-screenshot_spec.rb b/spec/unit/capybara-screenshot_spec.rb index f9ca68c..a6e3ca2 100644 --- a/spec/unit/capybara-screenshot_spec.rb +++ b/spec/unit/capybara-screenshot_spec.rb @@ -65,6 +65,32 @@ end end + describe '.new_saver' do + it 'passes through to get a new Saver if the user has not configured s3' do + saver_double = double('saver') + args = double('args') + expect(Capybara::Screenshot::Saver).to receive(:new).with(args).and_return(saver_double) + + expect(Capybara::Screenshot.new_saver(args)).to eq(saver_double) + end + + it 'wraps the returned saver in an S3 saver if it has been configured' do + require 'capybara-screenshot/s3_saver' + + saver_double = double('saver') + args = double('args') + s3_saver_double = double('s3_saver') + s3_configuration = { hello: 'world' } + + Capybara::Screenshot.s3_configuration = s3_configuration + + expect(Capybara::Screenshot::Saver).to receive(:new).with(args).and_return(saver_double) + expect(Capybara::Screenshot::S3Saver).to receive(:new_with_configuration).with(saver_double, s3_configuration).and_return(s3_saver_double) + + expect(Capybara::Screenshot.new_saver(args)).to eq(s3_saver_double) + end + end + describe '#prune' do before do Capybara::Screenshot.reset_prune_history diff --git a/spec/unit/s3_saver_spec.rb b/spec/unit/s3_saver_spec.rb new file mode 100644 index 0000000..e091baf --- /dev/null +++ b/spec/unit/s3_saver_spec.rb @@ -0,0 +1,132 @@ +require 'spec_helper' +require 'capybara-screenshot/s3_saver' + +describe Capybara::Screenshot::S3Saver do + let(:saver) { double('saver') } + let(:bucket_name) { double('bucket_name') } + let(:s3_client) { double('s3_client') } + + let(:s3_saver) { Capybara::Screenshot::S3Saver.new(saver, s3_client, bucket_name) } + + describe '.new_with_configuration' do + let(:access_key_id) { double('access_key_id') } + let(:secret_access_key) { double('secret_access_key') } + let(:s3_client_credentials_using_defaults) { + { + access_key_id: access_key_id, + secret_access_key: secret_access_key + } + } + + let(:region) { double('region') } + let(:s3_client_credentials) { + s3_client_credentials_using_defaults.merge(region: region) + } + + it 'destructures the configuration into its components' do + allow(Aws::S3::Client).to receive(:new).and_return(s3_client) + allow(Capybara::Screenshot::S3Saver).to receive(:new) + + Capybara::Screenshot::S3Saver.new_with_configuration(saver, { + s3_client_credentials: s3_client_credentials, + bucket_name: bucket_name + }) + + expect(Aws::S3::Client).to have_received(:new).with(s3_client_credentials) + expect(Capybara::Screenshot::S3Saver).to have_received(:new).with(saver, s3_client, bucket_name) + end + + it 'defaults the region to us-east-1' do + default_region = 'us-east-1' + + allow(Aws::S3::Client).to receive(:new).and_return(s3_client) + allow(Capybara::Screenshot::S3Saver).to receive(:new) + + Capybara::Screenshot::S3Saver.new_with_configuration(saver, { + s3_client_credentials: s3_client_credentials_using_defaults, + bucket_name: bucket_name + }) + + expect(Aws::S3::Client).to have_received(:new).with( + s3_client_credentials.merge(region: default_region) + ) + + expect(Capybara::Screenshot::S3Saver).to have_received(:new).with(saver, s3_client, bucket_name) + end + end + + describe '#save' do + before do + allow(saver).to receive(:html_saved?).and_return(false) + allow(saver).to receive(:screenshot_saved?).and_return(false) + allow(saver).to receive(:save) + end + + it 'calls save on the underlying saver' do + expect(saver).to receive(:save) + + s3_saver.save + end + + it 'uploads the html' do + html_path = '/foo/bar.html' + expect(saver).to receive(:html_path).and_return(html_path) + expect(saver).to receive(:html_saved?).and_return(true) + + html_file = double('html_file') + + expect(File).to receive(:open).with(html_path).and_yield(html_file) + + expect(s3_client).to receive(:put_object).with( + bucket: bucket_name, + key: 'bar.html', + body: html_file + ) + + s3_saver.save + end + + it 'uploads the screenshot' do + screenshot_path = '/baz/bim.jpg' + expect(saver).to receive(:screenshot_path).and_return(screenshot_path) + expect(saver).to receive(:screenshot_saved?).and_return(true) + + screenshot_file = double('screenshot_file') + + expect(File).to receive(:open).with(screenshot_path).and_yield(screenshot_file) + + expect(s3_client).to receive(:put_object).with( + bucket: bucket_name, + key: 'bim.jpg', + body: screenshot_file + ) + + s3_saver.save + end + end + + # Needed because we cannot depend on Verifying Doubles + # in older RSpec versions + describe 'an actual saver' do + it 'implements the methods needed by the s3 saver' do + instance_methods = Capybara::Screenshot::Saver.instance_methods + + expect(instance_methods).to include(:save) + expect(instance_methods).to include(:html_saved?) + expect(instance_methods).to include(:html_path) + expect(instance_methods).to include(:screenshot_saved?) + expect(instance_methods).to include(:screenshot_path) + end + end + + describe 'any other method' do + it 'transparently passes through to the saver' do + allow(saver).to receive(:foo_bar) + + args = double('args') + s3_saver.foo_bar(*args) + + expect(saver).to have_received(:foo_bar).with(*args) + end + end +end \ No newline at end of file