mini_autobot 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (41) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +26 -0
  3. data/.rspec +2 -0
  4. data/.ruby-version +1 -0
  5. data/Gemfile +3 -0
  6. data/Gemfile.lock +191 -0
  7. data/LICENSE +22 -0
  8. data/README.md +632 -0
  9. data/bin/mini_autobot +5 -0
  10. data/lib/mini_autobot.rb +44 -0
  11. data/lib/mini_autobot/connector.rb +288 -0
  12. data/lib/mini_autobot/console.rb +15 -0
  13. data/lib/mini_autobot/emails.rb +5 -0
  14. data/lib/mini_autobot/emails/mailbox.rb +15 -0
  15. data/lib/mini_autobot/endeca/base.rb +6 -0
  16. data/lib/mini_autobot/init.rb +63 -0
  17. data/lib/mini_autobot/logger.rb +12 -0
  18. data/lib/mini_autobot/page_objects.rb +22 -0
  19. data/lib/mini_autobot/page_objects/base.rb +264 -0
  20. data/lib/mini_autobot/page_objects/overlay/base.rb +76 -0
  21. data/lib/mini_autobot/page_objects/widgets/base.rb +47 -0
  22. data/lib/mini_autobot/parallel.rb +197 -0
  23. data/lib/mini_autobot/runner.rb +91 -0
  24. data/lib/mini_autobot/settings.rb +78 -0
  25. data/lib/mini_autobot/test_case.rb +233 -0
  26. data/lib/mini_autobot/test_cases.rb +7 -0
  27. data/lib/mini_autobot/utils.rb +10 -0
  28. data/lib/mini_autobot/utils/assertion_helper.rb +35 -0
  29. data/lib/mini_autobot/utils/castable.rb +103 -0
  30. data/lib/mini_autobot/utils/data_generator_helper.rb +145 -0
  31. data/lib/mini_autobot/utils/endeca_helper.rb +46 -0
  32. data/lib/mini_autobot/utils/loggable.rb +16 -0
  33. data/lib/mini_autobot/utils/overlay_and_widget_helper.rb +78 -0
  34. data/lib/mini_autobot/utils/page_object_helper.rb +209 -0
  35. data/lib/mini_autobot/version.rb +3 -0
  36. data/lib/minitap/minitest5_rent.rb +22 -0
  37. data/lib/minitest/autobot_settings_plugin.rb +77 -0
  38. data/lib/tapout/custom_reporters/fancy_tap_reporter.rb +94 -0
  39. data/lib/yard/tagged_test_case_handler.rb +61 -0
  40. data/mini_autobot.gemspec +38 -0
  41. metadata +299 -0
data/bin/mini_autobot ADDED
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env ruby
2
+ require 'mini_autobot'
3
+
4
+ MiniAutobot::Runner.after_run { MiniAutobot::Connector.finalize!(:force) }
5
+ MiniAutobot::Runner.run!(ARGV)
@@ -0,0 +1,44 @@
1
+ require 'bundler/setup'
2
+
3
+ envs = [:default]
4
+ envs << ENV['AUTOBOT_BUNDLE'].to_sym if ENV.key?('AUTOBOT_BUNDLE')
5
+ envs << ENV['APPLICATION_ENV'].to_sym if ENV.key?('APPLICATION_ENV')
6
+
7
+ Bundler.setup(*envs)
8
+ require 'minitest'
9
+ require 'yaml'
10
+ require 'erb'
11
+ require 'faker'
12
+ require 'selenium/webdriver'
13
+ require 'rest-client'
14
+
15
+ require 'cgi'
16
+ require 'pathname'
17
+
18
+ require 'active_support/core_ext/date_time/conversions'
19
+ require 'active_support/core_ext/hash'
20
+ require 'active_support/core_ext/module/attr_internal'
21
+ require 'active_support/core_ext/module/attribute_accessors'
22
+ require 'active_support/core_ext/numeric/time'
23
+ require 'active_support/core_ext/object/blank'
24
+ require 'active_support/core_ext/object/conversions'
25
+ require 'active_support/core_ext/object/try'
26
+ require 'active_support/core_ext/object/with_options'
27
+ require 'active_support/core_ext/string/access'
28
+ require 'active_support/core_ext/string/conversions'
29
+ require 'active_support/core_ext/string/inflections'
30
+ require 'active_support/core_ext/string/starts_ends_with'
31
+ require 'active_support/core_ext/string/strip'
32
+ require 'active_support/inflector'
33
+ require 'active_support/logger'
34
+
35
+ require_relative 'minitap/minitest5_rent'
36
+
37
+ ActiveSupport::Inflector.inflections(:en) do |inflector|
38
+ inflector.acronym 'PDP'
39
+ inflector.acronym 'SRP'
40
+ end
41
+
42
+ Time::DATE_FORMATS[:month_day_year] = "%m/%d/%Y"
43
+
44
+ require_relative 'mini_autobot/init'
@@ -0,0 +1,288 @@
1
+ module MiniAutobot
2
+
3
+ # A connector provides a thin layer that combines configuration files and
4
+ # access to the WebDriver. It's a thin layer in that, other than #initialize,
5
+ # it is a drop-in replacement for WebDriver calls.
6
+ #
7
+ # For example, if you usually access a method as `@driver.find_element`, you
8
+ # can still access them as the same method under `@connector.find_element`.
9
+ class Connector
10
+
11
+ # Simple configuration container for all profiles. Struct is not used here
12
+ # because it contaminates the class with Enumerable methods, which will
13
+ # cause #method_missing in Connector to get confused.
14
+ class Config
15
+ attr_accessor :connector, :env
16
+
17
+ def ==(other)
18
+ self.class == other.class && self.connector == other.connector && self.env == other.env
19
+ end
20
+
21
+ alias_method :eql?, :==
22
+
23
+ # Hashing mechanism should only look at the connector and environment values
24
+ def hash
25
+ @connector.hash ^ @env.hash
26
+ end
27
+
28
+ # Initialize a new configuration object. This object should never be
29
+ # instantiated directly.
30
+ #
31
+ # @api private
32
+ def initialize(connector, env)
33
+ @connector = connector
34
+ @env = env
35
+ end
36
+
37
+ end
38
+
39
+ class <<self # :nodoc:
40
+ protected
41
+ attr_accessor :finalization_queue
42
+ end
43
+
44
+ self.finalization_queue = Queue.new
45
+
46
+ # Finalize connectors in the pool that are no longer used, and then clear
47
+ # the pool if it should be empty.
48
+ def self.finalize!(force = false)
49
+ return if MiniAutobot.settings.reuse_driver? && !force
50
+
51
+ if Thread.current[:active_connector]
52
+ self.finalization_queue << Thread.current[:active_connector]
53
+ Thread.current[:active_connector] = nil
54
+ end
55
+
56
+ return unless MiniAutobot.settings.auto_finalize?
57
+
58
+ Thread.new do
59
+ while connector = self.finalization_queue.pop
60
+ begin
61
+ connector.finalize!
62
+ rescue => e
63
+ MiniAutobot.logger.error("Could not finalize Connector(##{connector.object_id}): #{e.message}")
64
+ end
65
+ end
66
+ end
67
+ end
68
+
69
+ # Given a connector profile and an environment profile, this method will
70
+ # instantiate a connector object with the correct WebDriver instance and
71
+ # settings.
72
+ #
73
+ # @raise ArgumentError
74
+ # @param connector [#to_s] the name of the connector profile to use.
75
+ # @param env [#to_s] the name of the environment profile to use.
76
+ # @return [Connector] an initialized connector object
77
+ def self.get(connector_id, env_id)
78
+ # Ensure arguments are at least provided
79
+ raise ArgumentError, "A connector must be provided" if connector_id.blank?
80
+ raise ArgumentError, "An environment must be provided" if env_id.blank?
81
+
82
+ # Find the connector and environment profiles
83
+ connector_cfg = self.load(MiniAutobot.root.join('config/mini_autobot', 'connectors'), connector_id)
84
+ env_cfg = self.load(MiniAutobot.root.join('config/mini_autobot', 'environments'), env_id)
85
+ cfg = Config.new(connector_cfg, env_cfg)
86
+
87
+ if Thread.current[:active_connector] && !MiniAutobot.settings.reuse_driver?
88
+ self.finalization_queue << active_connector
89
+ Thread.current[:active_connector] = nil
90
+ end
91
+
92
+ # If the current thread already has an active connector, and the connector
93
+ # is of the same type requested, reuse it after calling `reset!`
94
+ active_connector = Thread.current[:active_connector]
95
+ if active_connector.present?
96
+ if active_connector.config == cfg
97
+ active_connector.reset!
98
+ else
99
+ self.finalization_queue << active_connector
100
+ active_connector = nil
101
+ end
102
+ end
103
+
104
+ # Reuse or instantiate
105
+ Thread.current[:active_connector] = active_connector || Connector.new(cfg)
106
+ end
107
+
108
+ # Retrieve the default connector for the current environment.
109
+ #
110
+ # @raise ArgumentError
111
+ # @return [Connector] an initialized connector object
112
+ def self.get_default
113
+ connector = MiniAutobot.settings.connector
114
+ env = MiniAutobot.settings.env
115
+ MiniAutobot.logger.debug("Retrieving connector with settings (#{connector}, #{env})")
116
+
117
+ # Get a connector instance and use it in the new page object
118
+ self.get(connector, env)
119
+ end
120
+
121
+ # Equivalent to @driver.browser
122
+ def self.browser_name
123
+ Thread.current[:active_connector].browser
124
+ end
125
+
126
+ # Load profile from a specific path using the selector(s) specified.
127
+ #
128
+ # @raise ArgumentError
129
+ # @param path [#to_path, #to_s] the path in which to find the profile
130
+ # @param selector [String] semicolon-delimited selector set
131
+ # @return [Hash] immutable configuration values
132
+ def self.load(path, selector)
133
+ overrides = selector.to_s.split(/:/)
134
+ name = overrides.shift
135
+ filepath = path.join("#{name}.yml")
136
+ raise ArgumentError, "Cannot load profile #{name.inspect} because #{filepath.inspect} does not exist" unless filepath.exist?
137
+
138
+ cfg = YAML.load(ERB.new(File.read(filepath)).result)
139
+ cfg = self.resolve(cfg, overrides)
140
+ cfg.freeze
141
+ end
142
+
143
+ # Resolve a set of profile overrides.
144
+ #
145
+ # @param cfg [Hash] the configuration structure optionally containing a
146
+ # key of `:overrides`
147
+ # @param overrides [Enumerable<String>]
148
+ # @return [Hash] the resolved configuration
149
+ def self.resolve(cfg, overrides)
150
+ cfg = cfg.dup.with_indifferent_access
151
+
152
+ if options = cfg.delete(:overrides)
153
+ # Evaluate each override in turn, allowing each override to--well,
154
+ # override--anything coming before it
155
+ overrides.each do |override|
156
+ if tree = options[override]
157
+ cfg.deep_merge!(tree)
158
+ end
159
+ end
160
+ end
161
+
162
+ cfg
163
+ end
164
+
165
+ attr_reader :config
166
+
167
+ # Perform cleanup on the connector and driver.
168
+ def finalize!
169
+ @driver.quit
170
+ true
171
+ end
172
+
173
+ # Initialize a new connector with a set of configuration files.
174
+ #
175
+ # @see Connector.get
176
+ # @api private
177
+ def initialize(config)
178
+ @config = config
179
+
180
+ # Load and configure the WebDriver, if necessary
181
+ if concon = config.connector
182
+ driver_config = { }
183
+ driver = concon[:driver]
184
+ raise ArgumentError, "Connector driver must not be empty" if driver.nil?
185
+
186
+ # Handle hub-related options, like hub URLs (for remote execution)
187
+ if hub = concon[:hub]
188
+ builder = URI.parse(hub[:url])
189
+ builder.user = hub[:user] if hub.has_key?(:user)
190
+ builder.password = hub[:pass] if hub.has_key?(:pass)
191
+
192
+ MiniAutobot.logger.debug("Connector(##{self.object_id}): targeting remote #{builder.to_s}")
193
+ driver_config[:url] = builder.to_s
194
+ end
195
+
196
+ # Handle driver-related timeouts
197
+ if timeouts = concon[:timeouts]
198
+ client = Selenium::WebDriver::Remote::Http::Default.new
199
+ client.timeout = timeouts[:driver]
200
+ driver_config[:http_client] = client
201
+ end
202
+
203
+ # Handle archetypal capability lists
204
+ if archetype = concon[:archetype]
205
+ MiniAutobot.logger.debug("Connector(##{self.object_id}): using #{archetype.inspect} as capabilities archetype")
206
+ caps = Selenium::WebDriver::Remote::Capabilities.send(archetype)
207
+ if caps_set = concon[:capabilities]
208
+ caps.merge!(caps_set)
209
+ end
210
+ driver_config[:desired_capabilities] = caps
211
+ end
212
+
213
+ # Load Firefox profile if specified - applicable only when using the firefoxdriver
214
+ if profile = concon[:profile]
215
+ driver_config[:profile] = profile
216
+ end
217
+
218
+ # Initialize the driver and declare explicit browser timeouts
219
+ MiniAutobot.logger.debug("Connector(##{self.object_id}): using WebDriver(#{driver.inspect}, #{driver_config.inspect})")
220
+ @driver = Selenium::WebDriver.for(driver.to_sym, driver_config)
221
+
222
+ # Resize browser window for local browser with 'resolution'
223
+ if concon[:resolution]
224
+ width = concon[:resolution].split(/x/)[0].to_i
225
+ height = concon[:resolution].split(/x/)[1].to_i
226
+ @driver.manage.window.resize_to(width, height)
227
+ end
228
+
229
+ # setTimeout is undefined for safari driver so skip these steps for it
230
+ unless @driver.browser == :safari
231
+ if timeouts = concon[:timeouts]
232
+ @driver.manage.timeouts.implicit_wait = timeouts[:implicit_wait] if timeouts[:implicit_wait]
233
+ @driver.manage.timeouts.page_load = timeouts[:page_load] if timeouts[:page_load]
234
+ @driver.manage.timeouts.script_timeout = timeouts[:script_timeout] if timeouts[:script_timeout]
235
+ end
236
+ end
237
+ end
238
+ end
239
+
240
+ # Forward any other method call to the configuration container; if that
241
+ # fails, forward it to the WebDriver. The WebDriver will take care of any
242
+ # method resolution errors.
243
+ #
244
+ # @param name [#to_sym] symbol representing the method call
245
+ # @param args [*Object] arguments to be passed along
246
+ def method_missing(name, *args, &block)
247
+ if @config.respond_to?(name)
248
+ @config.send(name, *args, *block)
249
+ else
250
+ MiniAutobot.logger.debug("Connector(##{self.object_id})->#{name}(#{args.map { |a| a.inspect }.join(', ')})")
251
+ @driver.send(name, *args, &block)
252
+ end
253
+ end
254
+
255
+ # Resets the current session by deleting all cookies and clearing all local
256
+ # and session storage. Local and session storage are only cleared if the
257
+ # underlying driver supports it, and even then, only if the storage
258
+ # supports atomic clearing.
259
+ #
260
+ # @return [Boolean]
261
+ def reset!
262
+ @driver.manage.delete_all_cookies
263
+ @driver.try(:local_storage).try(:clear)
264
+ @driver.try(:session_storage).try(:clear)
265
+ true
266
+ end
267
+
268
+ # Forward unhandled message checks to the configuration and driver.
269
+ #
270
+ # @param name [#to_sym]
271
+ # @return [Boolean]
272
+ def respond_to?(name)
273
+ super || @config.respond_to?(name) || @driver.respond_to?(name)
274
+ end
275
+
276
+ # Compose a URL from the provided +path+ and the environment profile. The
277
+ # latter contains things like the hostname, port, SSL settings.
278
+ #
279
+ # @param path [#to_s] the path to append after the root URL.
280
+ # @return [URI] the composed URL.
281
+ def url_for(path)
282
+ root = @config.env[:root]
283
+ raise ArgumentError, "The 'root' attribute is missing from the environment profile" unless root
284
+ URI.join(root, path)
285
+ end
286
+
287
+ end
288
+ end
@@ -0,0 +1,15 @@
1
+ module MiniAutobot
2
+ class Console < TestCase
3
+
4
+ def self.bootstrap!
5
+ MiniAutobot.settings.tags << [:__dummy__]
6
+ end
7
+
8
+ test :dummy, tags: [:__dummy__, :non_regression], serial: 'DUMMY' do
9
+ require 'pry'
10
+ assert_respond_to binding, :pry
11
+ binding.pry
12
+ end
13
+
14
+ end
15
+ end
@@ -0,0 +1,5 @@
1
+ module MiniAutobot
2
+ module Emails; end
3
+ end
4
+
5
+ require_relative 'emails/mailbox'
@@ -0,0 +1,15 @@
1
+ require 'net/imap'
2
+
3
+ module MiniAutobot
4
+ module Emails
5
+
6
+ class Mailbox
7
+
8
+ def initialize(**opts)
9
+ end
10
+
11
+ end
12
+
13
+ end
14
+ end
15
+
@@ -0,0 +1,6 @@
1
+ module MiniAutobot
2
+ module Endeca
3
+ class Base
4
+ end
5
+ end
6
+ end
@@ -0,0 +1,63 @@
1
+ # The base module for everything MiniAutobot and is the container for other
2
+ # modules and classes in the hierarchy:
3
+ #
4
+ # * `Connector` provides support for drivers and connector profiles;
5
+ # * `Emails` introduces email-specific drivers for MiniAutobot;
6
+ # * `PageObjects` provides a hierarchy of page objects, page modules, widgets,
7
+ # and overlays;
8
+ # * `Settings` provides support for internal MiniAutobot settings; and
9
+ # * `Utils` provides an overarching module for miscellaneous helper modules.
10
+ module MiniAutobot
11
+
12
+ def self.logger
13
+ @@logger ||= MiniAutobot::Logger.new($stdout)
14
+ end
15
+
16
+ def self.logger=(value)
17
+ @@logger = value
18
+ end
19
+
20
+ def self.settings
21
+ @@settings ||= Settings.new
22
+ end
23
+
24
+ def self.settings=(options)
25
+ self.settings.merge!(options)
26
+ end
27
+
28
+ # Root directory of the automation repository.
29
+ # Automation repo can use it to refer to files within itself,
30
+ # and this gem also uses it to refer to config files of automation,
31
+ # for example:
32
+ #
33
+ # File.read(MiniAutobot.root.join('config/mini_autobot', 'data.yml'))
34
+ #
35
+ # will return the contents of `automation_root/config/mini_autobot/data.yml`.
36
+ #
37
+ # @return [Pathname] A reference to the root directory, ready to be used
38
+ # in directory and file path calculations.
39
+ def self.root
40
+ @@__root__ ||= Pathname.new(File.expand_path('.'))
41
+ end
42
+
43
+ # Absolute path of root directory of this gem
44
+ # can be used both within this gem and in automation repo
45
+ def self.gem_root
46
+ @@__gem_root__ ||= Pathname.new(File.realpath(File.join(File.dirname(__FILE__), '..', '..')))
47
+ end
48
+
49
+ end
50
+
51
+ require_relative 'runner'
52
+ require_relative 'logger'
53
+ require_relative 'utils'
54
+
55
+ require_relative 'connector'
56
+ require_relative 'page_objects'
57
+ require_relative 'parallel'
58
+ require_relative 'settings'
59
+
60
+ require_relative 'emails'
61
+
62
+ require_relative 'test_case'
63
+ require_relative 'console'