saucer 0.6.5 → 1.0.0.alpha
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +6 -0
- data/Gemfile +3 -1
- data/LICENSE.txt +1 -1
- data/README.md +70 -59
- data/Rakefile +5 -3
- data/lib/saucer.rb +17 -4
- data/lib/saucer/asset_management.rb +113 -0
- data/lib/saucer/custom_commands.rb +21 -0
- data/lib/saucer/data_collection.rb +47 -0
- data/lib/saucer/job_update.rb +26 -0
- data/lib/saucer/options.rb +85 -0
- data/lib/saucer/runners.rb +5 -0
- data/lib/saucer/runners/cucumber.rb +23 -0
- data/lib/saucer/runners/rspec.rb +23 -0
- data/lib/saucer/runners/unknown.rb +23 -0
- data/lib/saucer/session.rb +134 -0
- data/lib/saucer/version.rb +3 -1
- data/saucer.gemspec +25 -25
- metadata +38 -57
- data/lib/saucer/annotations.rb +0 -44
- data/lib/saucer/api.rb +0 -57
- data/lib/saucer/config/common.rb +0 -23
- data/lib/saucer/config/sauce.rb +0 -67
- data/lib/saucer/config/selenium.rb +0 -39
- data/lib/saucer/driver.rb +0 -48
- data/lib/saucer/parallel.rb +0 -57
- data/lib/saucer/patches/sauce_whisk.rb +0 -7
- data/lib/saucer/platform_configuration.rb +0 -22
- data/lib/saucer/sauce.rb +0 -20
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 0a0d5ca72570b0c717d295613491312bec954800
|
4
|
+
data.tar.gz: b12996df6767e459a2746a3eb3dcc7776eb42468
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 34570556581c60312af0867069b7b3b3080f7e28b5386f44b21bce4124f62c47a37a02c5a00eea0154d95c5b2cd8547a8b37c1cd1bc87bc6bd92c152a8c177f5
|
7
|
+
data.tar.gz: fad1c1d36deddf108d981c114ae6e50f83bd54e3b61d9b4219470e5b08c6904cba57fa0c6b07d704f609f4f0bd98b7d9058e854ecfc70babb97b030087a9dd71
|
data/CHANGELOG.md
ADDED
@@ -0,0 +1,6 @@
|
|
1
|
+
### 1.0.0.alpha (2019-05-16)
|
2
|
+
* Complete Revamp of the Project
|
3
|
+
* Implement Options class to set customized values
|
4
|
+
* Implement Session class to directly interface with Sauce
|
5
|
+
* Automatically update Test Name, Build Name, Pass/Fail
|
6
|
+
* Automatically add custom data: Ruby version, OS, gems
|
data/Gemfile
CHANGED
data/LICENSE.txt
CHANGED
data/README.md
CHANGED
@@ -1,94 +1,105 @@
|
|
1
1
|
# Saucer
|
2
2
|
|
3
|
-
|
3
|
+
Make running your tests on Sauce Labs easier with these helpful wrappers and convenience methods
|
4
4
|
|
5
5
|
## Disclaimer
|
6
6
|
*This code is provided on an "AS-IS” basis without warranty of any kind, either express or implied, including without limitation any implied warranties of condition, uninterrupted use, merchantability, fitness for a particular purpose, or non-infringement. Your tests and testing environments may require you to modify this framework. Issues regarding this framework should be submitted through GitHub. For questions regarding Sauce Labs integration, please see the Sauce Labs documentation at https://wiki.saucelabs.com/. This framework is not maintained by Sauce Labs Support.*
|
7
7
|
|
8
8
|
## Installation
|
9
9
|
|
10
|
-
|
10
|
+
In your Gemfile:
|
11
|
+
|
12
|
+
`gem 'saucer'
|
13
|
+
|
14
|
+
In your project:
|
11
15
|
|
12
16
|
```ruby
|
13
|
-
|
17
|
+
require 'saucer'
|
14
18
|
```
|
15
19
|
|
16
20
|
## Usage
|
17
21
|
|
18
|
-
####
|
19
|
-
|
20
|
-
Can optionally pass in a `Config::Selenium` instance with any of the
|
21
|
-
[supported Test Configuration Options](https://wiki.saucelabs.com/display/DOCS/Test+Configuration+Options)
|
22
|
-
Note that Ruby syntax is a `Symbol` with snake_case and not `String` with camelCase
|
22
|
+
#### Starting the Driver
|
23
|
+
Use Saucer to start your sessions
|
23
24
|
```ruby
|
24
|
-
|
25
|
-
@driver =
|
25
|
+
@session = Saucer::Session.begin
|
26
|
+
@driver = @session.driver
|
26
27
|
```
|
27
|
-
|
28
|
-
#### Cucumber
|
29
|
-
RSpec doesn't need to be concerned with this, but Cucumber needs an extra step in `env.rb`:
|
28
|
+
Optionally you can create options with various parameters to pass into `Session.begin`
|
30
29
|
```ruby
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
After do |scenario|
|
37
|
-
Saucer::Config::Sauce.scenario = scenario
|
38
|
-
@driver.quit
|
39
|
-
end
|
30
|
+
options = Saucer::Options.new(browser_name: 'Safari',
|
31
|
+
browser_version: '12.0',
|
32
|
+
platform_name: 'macOS 10.14')
|
33
|
+
@session = Saucer::Session.begin(options)
|
34
|
+
@driver = @session.driver
|
40
35
|
```
|
41
36
|
|
42
|
-
####
|
43
|
-
|
44
|
-
will every spec in the spec directory in 4 processes on the default Sauce platform (Linux with Chrome v48)
|
45
|
-
|
37
|
+
#### Finishing the session
|
38
|
+
You can still quit the driver yourself if you'd like
|
46
39
|
```ruby
|
47
|
-
|
40
|
+
@driver.quit
|
48
41
|
```
|
49
|
-
|
50
|
-
To Specify basic number of processes, a specific subdirectory (Cucumber or RSpec), and
|
51
|
-
reporting output file:
|
52
|
-
|
42
|
+
You get some automatic data population if you end the Session
|
53
43
|
```ruby
|
54
|
-
|
55
|
-
path: 'features/foo',
|
56
|
-
output: 'foo').run
|
44
|
+
@session.end
|
57
45
|
```
|
58
46
|
|
59
|
-
|
60
|
-
To
|
61
|
-
|
47
|
+
#### Automatic Data Population
|
48
|
+
To automatically pass in the test name, populate pass/fail and provide exception information:
|
49
|
+
RSpec doesn't need to do anything, but Cucumber will need to specify the scenario information
|
50
|
+
in the `env.rb` or `hooks.rb` file.
|
62
51
|
```ruby
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
platforms = configs.map { |c| Saucer::PlatformConfiguration.new(c) }
|
69
|
-
|
70
|
-
Saucer::Parallel.new(platforms: platforms).run
|
52
|
+
Before do |scenario|
|
53
|
+
options = Saucer::Options.new(scenario: scenario)
|
54
|
+
session = Saucer::Session.begin(options)
|
55
|
+
@driver = session.driver
|
71
56
|
end
|
72
57
|
```
|
73
58
|
|
74
|
-
|
75
|
-
|
76
|
-
```
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
59
|
+
#### Session Commands
|
60
|
+
Saucer provides a number of custom methods as part of the session instance:
|
61
|
+
```ruby
|
62
|
+
# Add useful information to a test after initializing a session
|
63
|
+
@session.comment = "Foo"
|
64
|
+
@session.tags = %w[foo bar]
|
65
|
+
@session.tags << 'foobar'
|
66
|
+
@session.data = {foo: 'bar'}
|
67
|
+
@session.data[:bar] = 'foo'
|
68
|
+
|
69
|
+
# These things should be set automatically, but can be set manually
|
70
|
+
@session.name = 'Test Name'
|
71
|
+
@session.build = 'Build Name'
|
72
|
+
@session.result = 'passed'
|
73
|
+
@session.result = 'failed'
|
74
|
+
|
75
|
+
# Special Features that might be useful
|
76
|
+
@session.stop_network
|
77
|
+
@session.start_network
|
78
|
+
@session.breakpoint
|
79
|
+
|
80
|
+
# This will cause an error, but is available as an option
|
81
|
+
session.stop
|
82
|
+
|
83
|
+
# These things can be done after the session has ended
|
84
|
+
@session.save_screenshots
|
85
|
+
@session.save_log(log_type: :selenium)
|
86
|
+
@session.save_log(log_type: :sauce)
|
87
|
+
@session.save_log(log_type: :automator)
|
88
|
+
@session.save_logs
|
89
|
+
@session.save_video
|
90
|
+
@session.save_assets
|
91
|
+
@session.delete_assets
|
86
92
|
```
|
87
93
|
|
94
|
+
#### Additional API Interactions
|
95
|
+
A more fully featured wrapping of the Sauce API is planned for upcoming releases.
|
96
|
+
For now, make use of [SauceWhisk](https://github.com/saucelabs/sauce_whisk).
|
97
|
+
|
88
98
|
## Contributing
|
89
99
|
|
90
|
-
Bug reports and pull requests are welcome on GitHub at https://github.com/
|
100
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/titusfortner/saucer.
|
91
101
|
|
92
|
-
## License
|
102
|
+
## License & Copyright
|
93
103
|
|
94
104
|
The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
|
105
|
+
see LICENSE.txt for full details and copyright.
|
data/Rakefile
CHANGED
data/lib/saucer.rb
CHANGED
@@ -1,6 +1,19 @@
|
|
1
|
-
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
2
3
|
require 'selenium-webdriver'
|
3
|
-
require
|
4
|
-
require
|
5
|
-
|
4
|
+
require 'saucer/options'
|
5
|
+
require 'saucer/data_collection'
|
6
|
+
require 'saucer/asset_management'
|
7
|
+
require 'saucer/custom_commands'
|
8
|
+
require 'saucer/job_update'
|
9
|
+
require 'saucer/runners'
|
10
|
+
require 'saucer/session'
|
11
|
+
require 'saucer/version'
|
12
|
+
|
13
|
+
module Saucer
|
14
|
+
class AuthenticationError < StandardError
|
15
|
+
end
|
6
16
|
|
17
|
+
class APIError < StandardError
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,113 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Saucer
|
4
|
+
module AssetManagement
|
5
|
+
def screenshots
|
6
|
+
start_time = Time.now
|
7
|
+
begin
|
8
|
+
urls = SauceWhisk::Jobs.job_assets(job_id)['screenshot_urls']
|
9
|
+
raise APIError, 'No screenshots found' if urls.nil?
|
10
|
+
rescue APIError
|
11
|
+
retry if Time.now - start_time < 5
|
12
|
+
end
|
13
|
+
job.screenshots
|
14
|
+
end
|
15
|
+
|
16
|
+
def save_screenshots(path = nil)
|
17
|
+
screenshots&.each do |screenshot|
|
18
|
+
save_file(path: path, file_name: screenshot.name, data: screenshot.data.body, type: 'screenshots')
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
# TODO: refactor leveraging `details.end_time`
|
23
|
+
def log(log_type = nil)
|
24
|
+
log_type ||= :sauce
|
25
|
+
valid = {sauce: 'log.json',
|
26
|
+
selenium: 'selenium-server.log',
|
27
|
+
automator: 'automator.log'}
|
28
|
+
unless valid.key?(log_type)
|
29
|
+
raise ArgumentError,
|
30
|
+
"#{log_type} is not a valid log type; use one of #{valid.keys}"
|
31
|
+
end
|
32
|
+
|
33
|
+
start_time = Time.now
|
34
|
+
begin
|
35
|
+
response = asset(valid[log_type]).body
|
36
|
+
raise APIError, "Can not retrieve log: #{response['message']}" if JSON.parse(response).key?('message')
|
37
|
+
rescue NoMethodError
|
38
|
+
# Sauce Log is Special
|
39
|
+
JSON.parse(response).map do |hash|
|
40
|
+
hash.map do |key, value|
|
41
|
+
"#{key}: #{value}"
|
42
|
+
end.join("\n")
|
43
|
+
end.join("\n\n")
|
44
|
+
rescue JSON::ParserError, NoMethodError
|
45
|
+
response
|
46
|
+
rescue APIError
|
47
|
+
retry if Time.now - start_time < 5
|
48
|
+
raise
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def save_log(path: nil, log_type: nil)
|
53
|
+
log_type ||= :sauce
|
54
|
+
save_file(path: path, file_name: "#{log_type}.log", data: log(log_type), type: 'logs')
|
55
|
+
end
|
56
|
+
|
57
|
+
def save_logs(path: nil)
|
58
|
+
%i[sauce selenium automator].each do |type|
|
59
|
+
save_log(path: path, log_type: type)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def video_stream
|
64
|
+
start_time = Time.now
|
65
|
+
begin
|
66
|
+
response = asset('video.mp4').body
|
67
|
+
raise APIError, "Can not retrieve video: #{response['message']}" if JSON.parse(response).key?('message')
|
68
|
+
rescue JSON::ParserError
|
69
|
+
response
|
70
|
+
rescue APIError
|
71
|
+
retry if Time.now - start_time < 5
|
72
|
+
raise
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
def save_video(path = nil)
|
77
|
+
save_file(path: path, file_name: 'video.mp4', data: video_stream, type: 'videos')
|
78
|
+
end
|
79
|
+
|
80
|
+
def save_assets
|
81
|
+
save_logs
|
82
|
+
save_video
|
83
|
+
save_screenshots
|
84
|
+
end
|
85
|
+
|
86
|
+
def delete_assets
|
87
|
+
start_time = Time.now
|
88
|
+
begin
|
89
|
+
response = JSON.parse(SauceWhisk::Assets.delete(job_id).data.body)
|
90
|
+
raise APIError, "Can not delete assets: #{response}" if response.is_a?(String)
|
91
|
+
rescue APIError
|
92
|
+
retry if Time.now - start_time < 5
|
93
|
+
raise
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
private
|
98
|
+
|
99
|
+
def save_file(path:, data:, file_name:, type:)
|
100
|
+
path ||= File.expand_path("../../../assets/#{job_id}/#{type}/#{file_name}", __FILE__)
|
101
|
+
|
102
|
+
file_name = path[%r{[^/]*$}]
|
103
|
+
base_path = path.gsub(file_name, '')
|
104
|
+
|
105
|
+
FileUtils.mkdir_p(base_path) unless File.exist?(base_path)
|
106
|
+
File.write(path, data)
|
107
|
+
end
|
108
|
+
|
109
|
+
def asset(asset)
|
110
|
+
SauceWhisk::Jobs.fetch_asset(@job_id, asset)
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Saucer
|
4
|
+
module CustomCommands
|
5
|
+
def comment=(comment)
|
6
|
+
@driver.execute_script("sauce: context=#{comment}")
|
7
|
+
end
|
8
|
+
|
9
|
+
def stop_network
|
10
|
+
@driver.execute_script('sauce: stop network')
|
11
|
+
end
|
12
|
+
|
13
|
+
def start_network
|
14
|
+
@driver.execute_script('sauce: start network')
|
15
|
+
end
|
16
|
+
|
17
|
+
def breakpoint
|
18
|
+
@driver.execute_script('sauce: break')
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Saucer
|
4
|
+
class DataCollection
|
5
|
+
PAGE_OBJECTS = %w[site_prism page-object watirsome watir_drops].freeze
|
6
|
+
|
7
|
+
def gems
|
8
|
+
Bundler.definition.specs.map(&:name).each_with_object({}) do |gem_name, hash|
|
9
|
+
name = Bundler.environment.specs.to_hash[gem_name]
|
10
|
+
next if name.empty?
|
11
|
+
|
12
|
+
hash[gem_name] = name.first.version
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def page_objects
|
17
|
+
page_objects_gems = PAGE_OBJECTS & gems.keys
|
18
|
+
if page_objects_gems.size > 1
|
19
|
+
'multiple'
|
20
|
+
elsif page_objects_gems.empty?
|
21
|
+
'unknown'
|
22
|
+
else
|
23
|
+
page_objects_gems.first
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def selenium_version
|
28
|
+
gems['selenium-webdriver']
|
29
|
+
end
|
30
|
+
|
31
|
+
def test_library
|
32
|
+
if gems['watir'] && gems['capybara']
|
33
|
+
'multiple'
|
34
|
+
elsif gems['capybara']
|
35
|
+
'capybara'
|
36
|
+
elsif gems['watir']
|
37
|
+
'watir'
|
38
|
+
else
|
39
|
+
'unknown'
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def test_runner(runner)
|
44
|
+
gems[runner.name.to_s] ? "#{runner.name} v#{gems[runner.name.to_s]}" : 'Unknown'
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Saucer
|
4
|
+
module JobUpdate
|
5
|
+
def name=(name)
|
6
|
+
job.name = name
|
7
|
+
end
|
8
|
+
|
9
|
+
def build=(name)
|
10
|
+
job.build = name
|
11
|
+
end
|
12
|
+
|
13
|
+
def visibility=(value)
|
14
|
+
valid = %i[public public_restricted share team private]
|
15
|
+
raise ArgumentError, "#{value} is not a valid visibility value; use one of #{valid}" unless valid.include?(value)
|
16
|
+
|
17
|
+
job.visibility = value.to_s
|
18
|
+
end
|
19
|
+
|
20
|
+
def save
|
21
|
+
job.tags = tags unless tags.empty?
|
22
|
+
job.custom_data = data unless data.empty?
|
23
|
+
SauceWhisk::Jobs.save(job)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|