lapis_lazuli 0.6.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +18 -0
- data/Gemfile +4 -0
- data/Gemfile.lock +42 -0
- data/LICENSE +30 -0
- data/README.md +74 -0
- data/Rakefile +1 -0
- data/bin/lapis_lazuli +3 -0
- data/lapis_lazuli.gemspec +32 -0
- data/lib/lapis_lazuli/api.rb +52 -0
- data/lib/lapis_lazuli/argparse.rb +128 -0
- data/lib/lapis_lazuli/ast.rb +160 -0
- data/lib/lapis_lazuli/browser/error.rb +93 -0
- data/lib/lapis_lazuli/browser/find.rb +500 -0
- data/lib/lapis_lazuli/browser/interaction.rb +91 -0
- data/lib/lapis_lazuli/browser/screenshots.rb +70 -0
- data/lib/lapis_lazuli/browser/wait.rb +158 -0
- data/lib/lapis_lazuli/browser.rb +246 -0
- data/lib/lapis_lazuli/cli.rb +110 -0
- data/lib/lapis_lazuli/cucumber.rb +25 -0
- data/lib/lapis_lazuli/generators/cucumber/template/.gitignore +6 -0
- data/lib/lapis_lazuli/generators/cucumber/template/Gemfile +37 -0
- data/lib/lapis_lazuli/generators/cucumber/template/README.md +27 -0
- data/lib/lapis_lazuli/generators/cucumber/template/config/config.yml +29 -0
- data/lib/lapis_lazuli/generators/cucumber/template/config/cucumber.yml +34 -0
- data/lib/lapis_lazuli/generators/cucumber/template/features/example.feature +11 -0
- data/lib/lapis_lazuli/generators/cucumber/template/features/step_definitions/interaction_steps.rb +20 -0
- data/lib/lapis_lazuli/generators/cucumber/template/features/step_definitions/validation_steps.rb +21 -0
- data/lib/lapis_lazuli/generators/cucumber/template/features/support/env.rb +12 -0
- data/lib/lapis_lazuli/generators/cucumber/template/features/support/transition.rb +12 -0
- data/lib/lapis_lazuli/generators/cucumber.rb +128 -0
- data/lib/lapis_lazuli/generic/xpath.rb +49 -0
- data/lib/lapis_lazuli/options.rb +28 -0
- data/lib/lapis_lazuli/placeholders.rb +36 -0
- data/lib/lapis_lazuli/proxy.rb +179 -0
- data/lib/lapis_lazuli/runtime.rb +88 -0
- data/lib/lapis_lazuli/scenario.rb +88 -0
- data/lib/lapis_lazuli/storage.rb +59 -0
- data/lib/lapis_lazuli/version.rb +10 -0
- data/lib/lapis_lazuli/versions.rb +40 -0
- data/lib/lapis_lazuli/world/annotate.rb +45 -0
- data/lib/lapis_lazuli/world/api.rb +35 -0
- data/lib/lapis_lazuli/world/browser.rb +75 -0
- data/lib/lapis_lazuli/world/config.rb +292 -0
- data/lib/lapis_lazuli/world/error.rb +141 -0
- data/lib/lapis_lazuli/world/hooks.rb +109 -0
- data/lib/lapis_lazuli/world/logging.rb +53 -0
- data/lib/lapis_lazuli/world/proxy.rb +59 -0
- data/lib/lapis_lazuli/world/variable.rb +139 -0
- data/lib/lapis_lazuli.rb +75 -0
- data/test/.gitignore +8 -0
- data/test/Gemfile +42 -0
- data/test/README.md +35 -0
- data/test/config/config.yml +37 -0
- data/test/config/cucumber.yml +37 -0
- data/test/features/annotation.feature +23 -0
- data/test/features/browser.feature +10 -0
- data/test/features/button.feature +38 -0
- data/test/features/click.feature +35 -0
- data/test/features/error.feature +30 -0
- data/test/features/find.feature +92 -0
- data/test/features/har.feature +9 -0
- data/test/features/modules.feature +14 -0
- data/test/features/step_definitions/interaction_steps.rb +154 -0
- data/test/features/step_definitions/validation_steps.rb +350 -0
- data/test/features/support/env.rb +21 -0
- data/test/features/text_field.feature +32 -0
- data/test/features/timing.feature +47 -0
- data/test/features/variable.feature +11 -0
- data/test/features/xpath.feature +41 -0
- data/test/server/start.rb +17 -0
- data/test/server/www/button.html +22 -0
- data/test/server/www/error_html.html +9 -0
- data/test/server/www/find.html +66 -0
- data/test/server/www/javascript_error.html +12 -0
- data/test/server/www/text_fields.html +15 -0
- data/test/server/www/timing.html +32 -0
- data/test/server/www/xpath.html +22 -0
- metadata +295 -0
data/lib/lapis_lazuli/generators/cucumber/template/features/step_definitions/interaction_steps.rb
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
################################################################################
|
2
|
+
# Copyright <%= config[:year] %> spriteCloud B.V. All rights reserved.
|
3
|
+
# Generated by LapisLazuli, version <%= config[:lapis_lazuli][:version] %>
|
4
|
+
# Author: "<%= config[:user] %>" <<%= config[:email] %>>
|
5
|
+
|
6
|
+
Given(/^I navigate to (.*) in (.*)$/) do |site,language|
|
7
|
+
config_name = "#{site.downcase}.#{language.downcase}"
|
8
|
+
if has_env?(config_name)
|
9
|
+
url = env(config_name)
|
10
|
+
browser.goto url
|
11
|
+
else
|
12
|
+
error(:env => config_name)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
Given(/^I search for "(.*?)"$/) do |query|
|
17
|
+
searchbox = browser.find(:text_field => {:name => "q"})
|
18
|
+
searchbox.clear rescue log.debug "Could not clear searchbox"
|
19
|
+
searchbox.send_keys(query)
|
20
|
+
end
|
data/lib/lapis_lazuli/generators/cucumber/template/features/step_definitions/validation_steps.rb
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
################################################################################
|
2
|
+
# Copyright <%= config[:year] %> spriteCloud B.V. All rights reserved.
|
3
|
+
# Generated by LapisLazuli, version <%= config[:lapis_lazuli][:version] %>
|
4
|
+
# Author: "<%= config[:user] %>" <<%= config[:email] %>>
|
5
|
+
|
6
|
+
Then(/I see "([^"]*)" on the page/) do |string|
|
7
|
+
# Note: The following is *really* slow, as it'll apply the regex to all
|
8
|
+
# elements in the page, one after the other. Of course, if any element
|
9
|
+
# includes the regex, all its parent elements also will, so you have
|
10
|
+
# tons of matches to process.
|
11
|
+
#
|
12
|
+
# browser.wait(:text => /#{string}/i)
|
13
|
+
|
14
|
+
# Instead, you will want to search only the root element for some
|
15
|
+
# text, e.g.
|
16
|
+
#
|
17
|
+
# browser.wait(:html => {:text => /#{string}/i})
|
18
|
+
|
19
|
+
# There's a shortcut for that in find/wait:
|
20
|
+
browser.wait(:html => /#{string}/i)
|
21
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
################################################################################
|
2
|
+
# Copyright <%= config[:year] %> spriteCloud B.V. All rights reserved.
|
3
|
+
# Generated by LapisLazuli, version <%= config[:lapis_lazuli][:version] %>
|
4
|
+
# Author: "<%= config[:user] %>" <<%= config[:email] %>>
|
5
|
+
require 'lapis_lazuli'
|
6
|
+
require 'lapis_lazuli/cucumber'
|
7
|
+
|
8
|
+
LapisLazuli::WorldModule::Config.config_file = "config/config.yml"
|
9
|
+
World(LapisLazuli)
|
10
|
+
|
11
|
+
# Transition function from old codebase to new
|
12
|
+
load 'features/support/transition.rb'
|
@@ -0,0 +1,12 @@
|
|
1
|
+
##
|
2
|
+
# All function in this file SHOULD not be used.
|
3
|
+
# There only included for backwards compatibility
|
4
|
+
#
|
5
|
+
|
6
|
+
##
|
7
|
+
# Creating a link for easy debugging afterwards
|
8
|
+
def create_link(name, url)
|
9
|
+
log.info("[DEPRECATED] [feature/support/transition.rb] create_link")
|
10
|
+
#Lets just send the url without the parameters to prevent html display problems
|
11
|
+
"<a href='#{url}' target='_blank'>#{name}</a>"
|
12
|
+
end
|
@@ -0,0 +1,128 @@
|
|
1
|
+
#
|
2
|
+
# LapisLazuli
|
3
|
+
# https://github.com/spriteCloud/lapis-lazuli
|
4
|
+
#
|
5
|
+
# Copyright (c) 2013-2014 spriteCloud B.V. and other LapisLazuli contributors.
|
6
|
+
# All rights reserved.
|
7
|
+
#
|
8
|
+
|
9
|
+
require 'lapis_lazuli/version'
|
10
|
+
|
11
|
+
require 'thor/group'
|
12
|
+
|
13
|
+
module LapisLazuli
|
14
|
+
module Generators
|
15
|
+
|
16
|
+
PROJECT_PATHS = [
|
17
|
+
'config',
|
18
|
+
'features',
|
19
|
+
File.join('features', 'step_definitions'),
|
20
|
+
File.join('features', 'support'),
|
21
|
+
'log',
|
22
|
+
'results',
|
23
|
+
'screenshots',
|
24
|
+
]
|
25
|
+
|
26
|
+
|
27
|
+
ALLOWED_HIDDEN = [
|
28
|
+
'.gitignore'
|
29
|
+
]
|
30
|
+
|
31
|
+
|
32
|
+
|
33
|
+
class Cucumber < Thor::Group
|
34
|
+
include Thor::Actions
|
35
|
+
|
36
|
+
argument :path, :type => :string
|
37
|
+
|
38
|
+
# Bug in Thor: this seems to need to be in place both here, and in the CLI
|
39
|
+
# class - here to actually work, and in the CLI class to be shown in the help.
|
40
|
+
class_option :branch, :aliases => "-b", :type => :string, :default => nil
|
41
|
+
|
42
|
+
|
43
|
+
|
44
|
+
def create_directory_structure
|
45
|
+
empty_directory(path)
|
46
|
+
PROJECT_PATHS.each do |p|
|
47
|
+
empty_directory(File.join(path, p))
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
|
52
|
+
|
53
|
+
def copy_template
|
54
|
+
opts = {
|
55
|
+
:year => Time.now.year,
|
56
|
+
:user => Cucumber.get_username(self),
|
57
|
+
:email => Cucumber.get_email(self),
|
58
|
+
:lapis_lazuli => {
|
59
|
+
:version => LapisLazuli::VERSION,
|
60
|
+
:dependency => '"' + LapisLazuli::VERSION + '"',
|
61
|
+
},
|
62
|
+
:project => {
|
63
|
+
:name => File.basename(path),
|
64
|
+
},
|
65
|
+
}
|
66
|
+
|
67
|
+
# If a branch was specified on the CLI, we have to update the dependency
|
68
|
+
# string.
|
69
|
+
if options.has_key?("branch")
|
70
|
+
opts[:lapis_lazuli][:dependency] = ":github => 'spriteCloud/lapis-lazuli', :branch => '#{options["branch"]}'"
|
71
|
+
end
|
72
|
+
|
73
|
+
require 'facets/string/lchomp'
|
74
|
+
require 'find'
|
75
|
+
Find.find(Cucumber.source_root) do |name|
|
76
|
+
# Skip the source root itself
|
77
|
+
next if name == Cucumber.source_root
|
78
|
+
|
79
|
+
# Find the relative path and file name component
|
80
|
+
relative = name.lchomp(Cucumber.source_root + File::SEPARATOR)
|
81
|
+
filename = File.basename(relative)
|
82
|
+
|
83
|
+
# Ignore hidden files UNLESS they're listed in the allowed hidden
|
84
|
+
# files.
|
85
|
+
if filename.start_with?('.')
|
86
|
+
next if not ALLOWED_HIDDEN.include?(filename)
|
87
|
+
end
|
88
|
+
|
89
|
+
# Create directories as empty directories, and treat every file as
|
90
|
+
# a template.
|
91
|
+
if File.directory?(name)
|
92
|
+
empty_directory(File.join(path, relative))
|
93
|
+
else
|
94
|
+
template(relative, File.join(path, relative), opts)
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
|
100
|
+
|
101
|
+
def self.source_root
|
102
|
+
File.join(File.dirname(__FILE__), "cucumber", "template")
|
103
|
+
end
|
104
|
+
|
105
|
+
|
106
|
+
|
107
|
+
def self.run_helper(cuke, command, default)
|
108
|
+
begin
|
109
|
+
cuke.run(command, {:capture => true}).strip || default
|
110
|
+
rescue
|
111
|
+
default
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
|
116
|
+
|
117
|
+
def self.get_username(cuke)
|
118
|
+
run_helper(cuke, 'git config --get user.name', 'spriteCloud B.V.')
|
119
|
+
end
|
120
|
+
|
121
|
+
|
122
|
+
|
123
|
+
def self.get_email(cuke)
|
124
|
+
run_helper(cuke, 'git config --get user.email', 'info@spritecloud.com')
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
#
|
2
|
+
# LapisLazuli
|
3
|
+
# https://github.com/spriteCloud/lapis-lazuli
|
4
|
+
#
|
5
|
+
# Copyright (c) 2013-2014 spriteCloud B.V. and other LapisLazuli contributors.
|
6
|
+
# All rights reserved.
|
7
|
+
#
|
8
|
+
module LapisLazuli
|
9
|
+
module GenericModule
|
10
|
+
|
11
|
+
##
|
12
|
+
# Helper functions for XPath composition
|
13
|
+
module XPath
|
14
|
+
|
15
|
+
|
16
|
+
##
|
17
|
+
# Return an xpath contains clause for e.g. checking wether an element's
|
18
|
+
# class attribute node contains a string. The optional third parameter
|
19
|
+
# determines how substrings are separated in the attribute node; the
|
20
|
+
# default is space for matching class names.
|
21
|
+
# Note that enclosing [ and ] are not included in the return value; this
|
22
|
+
# lets you more easily use and()/or()/not() operators.
|
23
|
+
def xp_contains(node, needle, separator = ' ')
|
24
|
+
contains = "contains(concat('#{separator}', normalize-space(#{node}), '#{separator}'), '#{separator}#{needle}#{separator}')"
|
25
|
+
return contains
|
26
|
+
end
|
27
|
+
|
28
|
+
##
|
29
|
+
# Constructs xpath and clause
|
30
|
+
def xp_and(first, second)
|
31
|
+
return "(#{first} and #{second})"
|
32
|
+
end
|
33
|
+
|
34
|
+
##
|
35
|
+
# Constructs xpath or clause
|
36
|
+
def xp_or(first, second)
|
37
|
+
return "(#{first} or #{second})"
|
38
|
+
end
|
39
|
+
|
40
|
+
##
|
41
|
+
# Constructs xpath or clause
|
42
|
+
def xp_not(expr)
|
43
|
+
return "not(#{expr})"
|
44
|
+
end
|
45
|
+
|
46
|
+
|
47
|
+
end # module XPath
|
48
|
+
end # module GenericModule
|
49
|
+
end # module LapisLazuli
|
@@ -0,0 +1,28 @@
|
|
1
|
+
#
|
2
|
+
# LapisLazuli
|
3
|
+
# https://github.com/spriteCloud/lapis-lazuli
|
4
|
+
#
|
5
|
+
# Copyright (c) 2013-2014 spriteCloud B.V. and other LapisLazuli contributors.
|
6
|
+
# All rights reserved.
|
7
|
+
#
|
8
|
+
module LapisLazuli
|
9
|
+
##
|
10
|
+
# Configuration options and their default values
|
11
|
+
CONFIG_OPTIONS = {
|
12
|
+
"close_browser_after" => ["feature", "Close the browser after every scenario, feature, etc. Possible values are 'feature', 'scenario', 'end' and 'never'."],
|
13
|
+
"error_strings" => [nil, "List of strings that indicate errors when detected on a web page."],
|
14
|
+
"default_env" => [nil, "Indicates which environment specific configuration to load when no test environment is provided explicitly."],
|
15
|
+
"test_env" => [nil, "Indicates which environment specific configuration to load in this test run."],
|
16
|
+
"browser" => ['firefox', "Indicates the browser in which to run tests. Possible values are 'firefox', 'chrome', 'safari', 'ie', 'ios'."],
|
17
|
+
"email_domain" => ["google.com", "The domain name used when generating email addresses. See the `placeholders` command for more information."],
|
18
|
+
"screenshot_on_failure" => [true, "Toggle whether failed scenarios should result in a screenshot being taken automatically."],
|
19
|
+
"screenshot_dir" => [".#{File::SEPARATOR}screenshots", "Location prefix for the screenshot path."],
|
20
|
+
"screenshot_scheme" => ["old", "Naming scheme for screenshots. Possible values are 'old' and 'new'. This option will be deprecated in the near future, and only the new scheme will be supported."],
|
21
|
+
"breakpoint_on_error" => [false, "If the error() function is used to create errors, should the debugger be started?"],
|
22
|
+
"step_pause_time" => [0, "(Deprecated) Number of seconds to wait after each cucumber step is executed."],
|
23
|
+
"log_dir" => [".#{File::SEPARATOR}logs", "Location for log files; they'll be named like the configuration file but with the '.log' extension."],
|
24
|
+
"log_file" => [nil, "Location of log file; overrides 'log_dir'."],
|
25
|
+
"log_level" => ['DEBUG', "Log level; see ruby Logger class for details."],
|
26
|
+
"storage_dir" => [".#{File::SEPARATOR}storage", "Location prefix where to output test information file with the '.json' extension."]
|
27
|
+
}
|
28
|
+
end # module LapisLazuli
|
@@ -0,0 +1,36 @@
|
|
1
|
+
#
|
2
|
+
# LapisLazuli
|
3
|
+
# https://github.com/spriteCloud/lapis-lazuli
|
4
|
+
#
|
5
|
+
# Copyright (c) 2013-2014 spriteCloud B.V. and other LapisLazuli contributors.
|
6
|
+
# All rights reserved.
|
7
|
+
#
|
8
|
+
#
|
9
|
+
module LapisLazuli
|
10
|
+
##
|
11
|
+
# Placeholders and their meanings.
|
12
|
+
# The first value is a string to be eval'd to determine the value the
|
13
|
+
# placeholder is to be replaced with.
|
14
|
+
# The second value describes the meaning.
|
15
|
+
PLACEHOLDERS = {
|
16
|
+
:timestamp => ['time[:timestamp]', 'The local time at which the test run started.'],
|
17
|
+
:iso_timestamp => ['time[:iso_timestamp]', 'The UTC time at which the test run started.'],
|
18
|
+
:iso_short => ['time[:iso_short]', 'A shorter version of the UTC time above.'],
|
19
|
+
:epoch => ['time[:epoch]', 'An integer representation of the local time above, relative to the epoch.'],
|
20
|
+
:email => ['"test_#{uuid}@#{email_domain}"', 'A unique email for the test run (contains the UUID).'],
|
21
|
+
:uuid => ['uuid', 'A UUID for the test run.'],
|
22
|
+
:scenario_id => ['scenario.id', 'A unique identifier for the current scenario based on the title, in filesystem safe form.'],
|
23
|
+
:scenario_timestamp => ['scenario.time[:timestamp]', 'Same as timestamp, but relative to the start of the scenario.'],
|
24
|
+
:scenario_iso_timestamp => ['scenario.time[:iso_timestamp]', 'Same as iso_timestamp, but relative to the start of the scenario.'],
|
25
|
+
:scenario_iso_short => ['scenario.time[:iso_short]', 'Same as iso_short, but relative to the start of the scenario.'],
|
26
|
+
:scenario_epoch => ['scenario.time[:epoch]', 'Same as epoch, but relative to the start of the scenario.'],
|
27
|
+
:scenario_email => ['"test_#{uuid}_scenario_#{scenario.uuid}@#{email_domain}"', 'Same as email, but contains the test run UUID and the scenario UUID.'],
|
28
|
+
:scenario_uuid => ['scenario.uuid', 'A UUID for the scenario.'],
|
29
|
+
:random => ['rand(9999)', 'A random integer <10,000.'],
|
30
|
+
:random_small => ['rand(99)', 'A random integer <100.'],
|
31
|
+
:random_lange => ['rand(999999)', 'A random integer <1,000,000.'],
|
32
|
+
:random_uuid => ['random_uuid', 'A random UUID.'],
|
33
|
+
:random_email => ['"test_#{uuid}_random_#{random_uuid}@#{email_domain}"', 'Same as email, but contains the test run and the random UUID.'],
|
34
|
+
:versions => ['LapisLazuli.software_versions.nil? ? "" : JSON.generate(LapisLazuli.software_versions)', 'A JSON serialized string of software versions found in e.g. the AfterConfiguration hook.']
|
35
|
+
}
|
36
|
+
end # module LapisLazuli
|
@@ -0,0 +1,179 @@
|
|
1
|
+
#
|
2
|
+
# LapisLazuli
|
3
|
+
# https://github.com/spriteCloud/lapis-lazuli
|
4
|
+
#
|
5
|
+
# Copyright (c) 2013-2014 spriteCloud B.V. and other LapisLazuli contributors.
|
6
|
+
# All rights reserved.
|
7
|
+
#
|
8
|
+
|
9
|
+
require 'socket'
|
10
|
+
require 'timeout'
|
11
|
+
require "lapis_lazuli/api"
|
12
|
+
|
13
|
+
module LapisLazuli
|
14
|
+
##
|
15
|
+
# Proxy class to map to sc-proxy
|
16
|
+
class Proxy
|
17
|
+
attr_reader :is_scproxy, :api, :ip, :scproxy_port, :port
|
18
|
+
|
19
|
+
##
|
20
|
+
# Create a new LL Proxy
|
21
|
+
# What is the ip/port of the master?
|
22
|
+
def initialize(ip, port, scproxy=true)
|
23
|
+
# Save the information
|
24
|
+
@ip = ip
|
25
|
+
@is_scproxy = scproxy
|
26
|
+
if scproxy
|
27
|
+
@scproxy_port = port
|
28
|
+
else
|
29
|
+
@port = port
|
30
|
+
end
|
31
|
+
# We should have a master
|
32
|
+
if !is_port_open?(ip, port)
|
33
|
+
raise "Proxy not online"
|
34
|
+
end
|
35
|
+
if @is_scproxy
|
36
|
+
# Create an API connection to the master
|
37
|
+
@api = API.new()
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def has_session?()
|
42
|
+
return !@port.nil? && is_port_open?(@ip, @port);
|
43
|
+
end
|
44
|
+
|
45
|
+
##
|
46
|
+
# Creates a new session with the proxy
|
47
|
+
def create()
|
48
|
+
# Do we already have a connection?
|
49
|
+
if @is_scproxy and self.has_session?
|
50
|
+
# Close it before starting a new one
|
51
|
+
self.close()
|
52
|
+
end
|
53
|
+
# Create a new
|
54
|
+
if @is_scproxy and @api
|
55
|
+
# Let the master create a new proxy
|
56
|
+
response = self.proxy_new :master => true
|
57
|
+
# Did we get on?
|
58
|
+
if response["status"] == true
|
59
|
+
@port = response["result"]["port"]
|
60
|
+
else
|
61
|
+
# Show the error
|
62
|
+
raise response["message"]
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
if @port.nil?
|
67
|
+
raise "Coult not create a new proxy"
|
68
|
+
end
|
69
|
+
|
70
|
+
return @port
|
71
|
+
end
|
72
|
+
|
73
|
+
##
|
74
|
+
# Close the session with the proxy
|
75
|
+
def close()
|
76
|
+
# If we don't have one we don't do anything
|
77
|
+
return if !@is_scproxy or !self.has_session?
|
78
|
+
|
79
|
+
# Send the call to the master
|
80
|
+
response = self.proxy_close :port => @port, :master => true
|
81
|
+
|
82
|
+
# Did we close it?
|
83
|
+
if response["status"] == true
|
84
|
+
# Clear our session
|
85
|
+
@port = nil
|
86
|
+
else
|
87
|
+
# Show an error
|
88
|
+
raise response["message"]
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
##
|
93
|
+
# Check if a TCP port is open on a host
|
94
|
+
def is_port_open?(ip, port)
|
95
|
+
begin
|
96
|
+
# Timeout is important
|
97
|
+
Timeout::timeout(1) do
|
98
|
+
begin
|
99
|
+
# Create the socket and close it
|
100
|
+
s = TCPSocket.new(ip, port)
|
101
|
+
s.close
|
102
|
+
return true
|
103
|
+
# If it fails the port is closed
|
104
|
+
rescue Errno::ECONNREFUSED, Errno::EHOSTUNREACH
|
105
|
+
end
|
106
|
+
end
|
107
|
+
rescue Timeout::Error
|
108
|
+
end
|
109
|
+
|
110
|
+
# Sorry port is closed
|
111
|
+
return false
|
112
|
+
end
|
113
|
+
|
114
|
+
##
|
115
|
+
# Map any missing method to the API object
|
116
|
+
#
|
117
|
+
# Example
|
118
|
+
# proxy.har_get
|
119
|
+
# proxy.proxy_close :port => 10002
|
120
|
+
def method_missing(meth, *args, &block)
|
121
|
+
# Only for spritecloud proxies
|
122
|
+
if !@is_scproxy
|
123
|
+
raise "Incorrect method: #{meth}"
|
124
|
+
end
|
125
|
+
|
126
|
+
# We should have no arguments or a Hash
|
127
|
+
if args.length > 1 or (args.length == 1 and not args[0].is_a? Hash)
|
128
|
+
raise "Incorrect arguments: #{args}"
|
129
|
+
end
|
130
|
+
settings = args[0] || {}
|
131
|
+
|
132
|
+
# A custom block or arguments?
|
133
|
+
block = block_given? ? block : Proc.new do |req|
|
134
|
+
if args.length == 1
|
135
|
+
settings.each do |key,value|
|
136
|
+
req.params[key.to_s] = value.to_s
|
137
|
+
end
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
# Pick the master proxy or the proxy for this session
|
142
|
+
@api.set_conn("http://#{@ip}:#{(settings.has_key? :master) ? @scproxy_port : @port}/")
|
143
|
+
|
144
|
+
# Call the API
|
145
|
+
response = @api.get("/#{meth.to_s.gsub("_","/")}", nil, &block)
|
146
|
+
# Only return the body if we could parse the JSOn
|
147
|
+
if response.body.is_a? Hash
|
148
|
+
return response.body
|
149
|
+
else
|
150
|
+
# Got a serious issue here, label as code 500
|
151
|
+
return {
|
152
|
+
"code" => 500,
|
153
|
+
"status" => false,
|
154
|
+
"message" => "Incorrect response from proxy",
|
155
|
+
"result" => response
|
156
|
+
}
|
157
|
+
end
|
158
|
+
end
|
159
|
+
|
160
|
+
##
|
161
|
+
# During the end of the test run all data should be added to the storage
|
162
|
+
def destroy(world)
|
163
|
+
begin
|
164
|
+
# Is it a spriteCloud proxy?
|
165
|
+
if @is_scproxy
|
166
|
+
# Request HAR data
|
167
|
+
response = self.har_get
|
168
|
+
if response["status"] == true
|
169
|
+
# Add it to the storage
|
170
|
+
world.storage.set("har", response["result"])
|
171
|
+
end
|
172
|
+
end
|
173
|
+
self.close
|
174
|
+
rescue StandardError => err
|
175
|
+
world.log.debug("Failed to close the proxy: #{err}")
|
176
|
+
end
|
177
|
+
end
|
178
|
+
end
|
179
|
+
end
|
@@ -0,0 +1,88 @@
|
|
1
|
+
#
|
2
|
+
# LapisLazuli
|
3
|
+
# https://github.com/spriteCloud/lapis-lazuli
|
4
|
+
#
|
5
|
+
# Copyright (c) 2013-2014 spriteCloud B.V. and other LapisLazuli contributors.
|
6
|
+
# All rights reserved.
|
7
|
+
#
|
8
|
+
require 'singleton'
|
9
|
+
|
10
|
+
module LapisLazuli
|
11
|
+
|
12
|
+
##
|
13
|
+
# Simple singleton class (that therefore lives for the duration of the test's
|
14
|
+
# run time for managing objects whose lifetime should also be this long.
|
15
|
+
class Runtime
|
16
|
+
include Singleton
|
17
|
+
|
18
|
+
def initialize
|
19
|
+
@objects = {}
|
20
|
+
end
|
21
|
+
|
22
|
+
def has?(name)
|
23
|
+
return @objects.has_key? name
|
24
|
+
end
|
25
|
+
|
26
|
+
def set(world, name, object, destructor = nil)
|
27
|
+
if @objects.has_key? name
|
28
|
+
Runtime.destroy(world, name, destructor)
|
29
|
+
end
|
30
|
+
|
31
|
+
# Register a finalizer, so we can clean up the proxy again
|
32
|
+
ObjectSpace.define_finalizer(self, Runtime.destroy(world, name, destructor))
|
33
|
+
|
34
|
+
@objects[name] = object
|
35
|
+
end
|
36
|
+
|
37
|
+
def set_if(world, name, destructor = nil, &block)
|
38
|
+
if @objects.has_key? name
|
39
|
+
return @objects[name]
|
40
|
+
end
|
41
|
+
|
42
|
+
obj = block.call
|
43
|
+
|
44
|
+
set(world, name, obj, destructor)
|
45
|
+
|
46
|
+
return obj
|
47
|
+
end
|
48
|
+
|
49
|
+
def get(name)
|
50
|
+
return @objects[name]
|
51
|
+
end
|
52
|
+
|
53
|
+
|
54
|
+
private
|
55
|
+
def self.destroy(world, name, destructor)
|
56
|
+
Proc.new do
|
57
|
+
# If a destructor is given, call that.
|
58
|
+
if not destructor.nil?
|
59
|
+
return destructor.call(world)
|
60
|
+
end
|
61
|
+
|
62
|
+
# Next, try a has_foo?/foo.destroy combination
|
63
|
+
if world.respond_to? "has_#{name}?" and world.respond_to? name
|
64
|
+
if world.send("has_#{name}?")
|
65
|
+
return world.send(name).destroy(world)
|
66
|
+
end
|
67
|
+
return false
|
68
|
+
end
|
69
|
+
|
70
|
+
# If it only responds to a destroy function, then we can just
|
71
|
+
# call that.
|
72
|
+
if world.respond_to? name
|
73
|
+
return world.send(name).destroy(world)
|
74
|
+
end
|
75
|
+
|
76
|
+
# If all else fails, we have to log an error. We can't rely
|
77
|
+
# on log existing in world, though...
|
78
|
+
message = "No destructor available for #{name}."
|
79
|
+
if world.respond_to? :log
|
80
|
+
world.log.info(message)
|
81
|
+
else
|
82
|
+
puts message
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
end # class Runtime
|
88
|
+
end
|
@@ -0,0 +1,88 @@
|
|
1
|
+
#
|
2
|
+
# LapisLazuli
|
3
|
+
# https://github.com/spriteCloud/lapis-lazuli
|
4
|
+
#
|
5
|
+
# Copyright (c) 2013-2014 spriteCloud B.V. and other LapisLazuli contributors.
|
6
|
+
# All rights reserved.
|
7
|
+
#
|
8
|
+
require "securerandom"
|
9
|
+
require "lapis_lazuli/storage"
|
10
|
+
require "lapis_lazuli/ast"
|
11
|
+
|
12
|
+
module LapisLazuli
|
13
|
+
##
|
14
|
+
# Stores the Cucumber scenario
|
15
|
+
# Includes timing, running state and a name
|
16
|
+
class Scenario
|
17
|
+
include LapisLazuli::Ast
|
18
|
+
|
19
|
+
attr_reader :id, :time, :uuid, :data, :storage, :error
|
20
|
+
attr_accessor :running, :check_browser_errors
|
21
|
+
|
22
|
+
def initialize
|
23
|
+
@uuid = SecureRandom.hex
|
24
|
+
@storage = Storage.new
|
25
|
+
@running = false
|
26
|
+
@name = "start_of_test_run"
|
27
|
+
self.update_timestamp
|
28
|
+
end
|
29
|
+
|
30
|
+
##
|
31
|
+
# Update the scenario with a new one
|
32
|
+
def update(scenario)
|
33
|
+
@uuid = SecureRandom.hex
|
34
|
+
# Reset the fail attribute
|
35
|
+
@check_browser_errors = true
|
36
|
+
# The original scenario from cucumber
|
37
|
+
@data = scenario
|
38
|
+
# A name without special characters.
|
39
|
+
@id = clean(scenario_id(scenario))
|
40
|
+
self.update_timestamp
|
41
|
+
end
|
42
|
+
|
43
|
+
def update_timestamp
|
44
|
+
now = Time.now
|
45
|
+
# The current time
|
46
|
+
@time = {
|
47
|
+
:timestamp => now.strftime('%y%m%d_%H%M%S'),
|
48
|
+
:iso_timestamp => now.utc.strftime("%FT%TZ"),
|
49
|
+
:iso_short => now.utc.strftime("%y%m%dT%H%M%SZ"),
|
50
|
+
:epoch => now.to_i.to_s
|
51
|
+
}
|
52
|
+
end
|
53
|
+
|
54
|
+
def tags
|
55
|
+
if !@data.nil?
|
56
|
+
return @data.source_tag_names
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def scope(cleaned = false)
|
61
|
+
scope = nil
|
62
|
+
if @data.respond_to? :backtrace_line
|
63
|
+
scope = @data.backtrace_line
|
64
|
+
elsif @data.respond_to? :file_colon_line
|
65
|
+
scope = @data.file_colon_line
|
66
|
+
end
|
67
|
+
|
68
|
+
if scope.nil?
|
69
|
+
return nil
|
70
|
+
elsif cleaned
|
71
|
+
return clean [scope]
|
72
|
+
else
|
73
|
+
return scope
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
private
|
78
|
+
|
79
|
+
def clean(strings)
|
80
|
+
result = []
|
81
|
+
strings.each do |string|
|
82
|
+
clean_string = string.gsub(/[^\w\.\-]/, ' ').strip.squeeze(' ').gsub(" ","_")
|
83
|
+
result.push(clean_string)
|
84
|
+
end
|
85
|
+
return result.join("-").squeeze("-")
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|