lapis_lazuli 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (79) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +18 -0
  3. data/Gemfile +4 -0
  4. data/Gemfile.lock +42 -0
  5. data/LICENSE +30 -0
  6. data/README.md +74 -0
  7. data/Rakefile +1 -0
  8. data/bin/lapis_lazuli +3 -0
  9. data/lapis_lazuli.gemspec +32 -0
  10. data/lib/lapis_lazuli/api.rb +52 -0
  11. data/lib/lapis_lazuli/argparse.rb +128 -0
  12. data/lib/lapis_lazuli/ast.rb +160 -0
  13. data/lib/lapis_lazuli/browser/error.rb +93 -0
  14. data/lib/lapis_lazuli/browser/find.rb +500 -0
  15. data/lib/lapis_lazuli/browser/interaction.rb +91 -0
  16. data/lib/lapis_lazuli/browser/screenshots.rb +70 -0
  17. data/lib/lapis_lazuli/browser/wait.rb +158 -0
  18. data/lib/lapis_lazuli/browser.rb +246 -0
  19. data/lib/lapis_lazuli/cli.rb +110 -0
  20. data/lib/lapis_lazuli/cucumber.rb +25 -0
  21. data/lib/lapis_lazuli/generators/cucumber/template/.gitignore +6 -0
  22. data/lib/lapis_lazuli/generators/cucumber/template/Gemfile +37 -0
  23. data/lib/lapis_lazuli/generators/cucumber/template/README.md +27 -0
  24. data/lib/lapis_lazuli/generators/cucumber/template/config/config.yml +29 -0
  25. data/lib/lapis_lazuli/generators/cucumber/template/config/cucumber.yml +34 -0
  26. data/lib/lapis_lazuli/generators/cucumber/template/features/example.feature +11 -0
  27. data/lib/lapis_lazuli/generators/cucumber/template/features/step_definitions/interaction_steps.rb +20 -0
  28. data/lib/lapis_lazuli/generators/cucumber/template/features/step_definitions/validation_steps.rb +21 -0
  29. data/lib/lapis_lazuli/generators/cucumber/template/features/support/env.rb +12 -0
  30. data/lib/lapis_lazuli/generators/cucumber/template/features/support/transition.rb +12 -0
  31. data/lib/lapis_lazuli/generators/cucumber.rb +128 -0
  32. data/lib/lapis_lazuli/generic/xpath.rb +49 -0
  33. data/lib/lapis_lazuli/options.rb +28 -0
  34. data/lib/lapis_lazuli/placeholders.rb +36 -0
  35. data/lib/lapis_lazuli/proxy.rb +179 -0
  36. data/lib/lapis_lazuli/runtime.rb +88 -0
  37. data/lib/lapis_lazuli/scenario.rb +88 -0
  38. data/lib/lapis_lazuli/storage.rb +59 -0
  39. data/lib/lapis_lazuli/version.rb +10 -0
  40. data/lib/lapis_lazuli/versions.rb +40 -0
  41. data/lib/lapis_lazuli/world/annotate.rb +45 -0
  42. data/lib/lapis_lazuli/world/api.rb +35 -0
  43. data/lib/lapis_lazuli/world/browser.rb +75 -0
  44. data/lib/lapis_lazuli/world/config.rb +292 -0
  45. data/lib/lapis_lazuli/world/error.rb +141 -0
  46. data/lib/lapis_lazuli/world/hooks.rb +109 -0
  47. data/lib/lapis_lazuli/world/logging.rb +53 -0
  48. data/lib/lapis_lazuli/world/proxy.rb +59 -0
  49. data/lib/lapis_lazuli/world/variable.rb +139 -0
  50. data/lib/lapis_lazuli.rb +75 -0
  51. data/test/.gitignore +8 -0
  52. data/test/Gemfile +42 -0
  53. data/test/README.md +35 -0
  54. data/test/config/config.yml +37 -0
  55. data/test/config/cucumber.yml +37 -0
  56. data/test/features/annotation.feature +23 -0
  57. data/test/features/browser.feature +10 -0
  58. data/test/features/button.feature +38 -0
  59. data/test/features/click.feature +35 -0
  60. data/test/features/error.feature +30 -0
  61. data/test/features/find.feature +92 -0
  62. data/test/features/har.feature +9 -0
  63. data/test/features/modules.feature +14 -0
  64. data/test/features/step_definitions/interaction_steps.rb +154 -0
  65. data/test/features/step_definitions/validation_steps.rb +350 -0
  66. data/test/features/support/env.rb +21 -0
  67. data/test/features/text_field.feature +32 -0
  68. data/test/features/timing.feature +47 -0
  69. data/test/features/variable.feature +11 -0
  70. data/test/features/xpath.feature +41 -0
  71. data/test/server/start.rb +17 -0
  72. data/test/server/www/button.html +22 -0
  73. data/test/server/www/error_html.html +9 -0
  74. data/test/server/www/find.html +66 -0
  75. data/test/server/www/javascript_error.html +12 -0
  76. data/test/server/www/text_fields.html +15 -0
  77. data/test/server/www/timing.html +32 -0
  78. data/test/server/www/xpath.html +22 -0
  79. metadata +295 -0
@@ -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
@@ -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