Test_Framework1.0 1.6.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 03e552c62291411f5305cad227dedfff3fc2a85e
4
+ data.tar.gz: 45dbb238a2a92e8adc5b4f542b65dee646007780
5
+ SHA512:
6
+ metadata.gz: 702918fb710b1d20b89c19478a77a776611eac6f8061a9da245ca6e5fa20e7f4ba74bb2de4d0aca9bac750f19db4fbe50c1c22975915f980d40e03d63b6f8da7
7
+ data.tar.gz: e517fbf5f86ec9c2f61b8d826256705d068fd5718b9437fe94a6743a89c601bda78996ff220aeddbfe57469057b224d93b5eddca0855b4ab54a23ce0c2e4f651
data/LICENSE ADDED
@@ -0,0 +1,19 @@
1
+ Copyright (c) 2013 Medidata Solutions Worldwide
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ of this software and associated documentation files (the "Software"), to deal
5
+ in the Software without restriction, including without limitation the rights
6
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ copies of the Software, and to permit persons to whom the Software is
8
+ furnished to do so, subject to the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be included in
11
+ all copies or substantial portions of the Software.
12
+
13
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,41 @@
1
+ # MediTAF
2
+
3
+ Test frameworks are one of the key requirements for implementing a successful automated software testing solution. A good test framework reduces maintenance costs, and speeds up test development. Currently at Medidata, we have several test frameworks that are successfully used in various projects. However, they have been designed with a focus on a product or a project, rendering them unusable in cross-product or multi-interface automation projects.
4
+
5
+ MediTAF is a generic ruby-based test automation framework intended to allow automation engineers and testers to design end-to-end automated integration tests for distributed systems. Specifically, MediTAF will allow testers to automate using two different interfaces i.e. UI and web services.
6
+
7
+
8
+ ## Key Features
9
+
10
+ * **UI testing:** MediTAF uses Selenium to allow users to write tests for the web user interface. For Selenium implementation, the framework uses the Page Object pattern. The Page Object pattern represents screens of a web application in a series of objects and encapsulates the features represented by a web page. It reduces the duplication of code and improves the maintainability of the automated tests. MediTAF uses a ruby gem called **site_prism** (an extension to **Capybara**) as an interface to **selenium-webdriver** to implement the Page Object pattern.
11
+ * **Services testing:** MediTAF provides an interface to connect with the services. Testers will be able to make calls to the service APIs, save results and perform comparisons with the expected results. Framework provides wrapper classes so nothing changes in the implementation. The framework also allows for future expansion to other services without affecting existing implementations.
12
+ * **Logging and Exception Handling:** The framework has an exception handler with multiple severity levels that can be logged to a configured file.
13
+
14
+
15
+ ## Advantages of using MediTAF
16
+
17
+ * Enables testers to automate tests across multiple Medidata interfaces and applications.
18
+ * The framework interprets the **Cucumber/Gherkin** language for the creation and execution of requirement/feature files.
19
+ * Allows for central location repository of features and step definitions - [MIST](https://github.com/mdsol/MIST.git) ( MediTAF Integration Steps and Tests )
20
+ * Enhances collaboration among testers, developers, and product managers to facilitate and simplify cross-team and cross-platform testing efforts.
21
+ * Reduces maintenance by minimizing duplication of code.
22
+ * Provides a way for standardized logging and validation output across multiple products.
23
+ * Allows easy execution of end-to-end automated tests.
24
+
25
+
26
+ ## Home Page
27
+ [https://github.com/mdsol/MediTAF.git](https://github.com/mdsol/MediTAF.git)
28
+
29
+ ## Documentation
30
+ [Medinet - MediTAF](https://sites.google.com/a/mdsol.com/knowledgebase/home/departments/r-d/sqa/utilities/meditaf)
31
+
32
+ ## Report Issues
33
+ Refer to JIRA Story [MCC-96074](https://medidata.atlassian.net/browse/MCC-96074) and add sub-tasks for issues.
34
+
35
+ ## Installation Instructions
36
+
37
+ MediTAF is released privately as a gem, and it works with MIST. For more information and installation instructions for MIST, please refer to the git repository at [https://github.com/mdsol/MIST.git](https://github.com/mdsol/MIST.git).
38
+
39
+ ## Copyright
40
+
41
+ Copyright © 2013-2015 Medidata Solutions, Inc. All Rights Reserved.
data/lib/MediTAF.rb ADDED
@@ -0,0 +1,14 @@
1
+ $:.unshift File.dirname(__FILE__)
2
+
3
+ module MediTAF
4
+ def self.root
5
+ File.expand_path '../..', __FILE__
6
+ end
7
+ end
8
+
9
+ require 'MediTAF/version'
10
+ require 'MediTAF/services'
11
+ require 'MediTAF/ui'
12
+ require 'MediTAF/utils/meditaf_faker'
13
+ require 'MediTAF/utils/sticky'
14
+ require 'MediTAF/utils/mail'
@@ -0,0 +1,22 @@
1
+ require 'MediTAF/utils/exceptions'
2
+
3
+ module MediTAF
4
+ # Enables MediTAF to interact with available resources. It depends on resource adapters that implement
5
+ # a set of require methods. The underlying adapters have the knowledge of the type resource with which
6
+ # it communicates.
7
+ module Services
8
+ # @return [ResourcesMgr] a new instance of ResourceMgr
9
+ def self.new
10
+ ResourcesMgr.new
11
+ end
12
+
13
+ class ServiceConfigurationMissing < MediTAF::Utils::Exceptions::MediTAFException; end
14
+ class ResourceAdapterLoadError < MediTAF::Utils::Exceptions::MediTAFException; end
15
+ class ResourceAdapterMissing < MediTAF::Utils::Exceptions::MediTAFException; end
16
+ class ResourceAdapterMethodMissing < MediTAF::Utils::Exceptions::MediTAFException; end
17
+ class ResourceAdapterConfigurationMissing < MediTAF::Utils::Exceptions::MediTAFException; end
18
+ class ResourceAdapterRequestError < MediTAF::Utils::Exceptions::MediTAFException; end
19
+ end
20
+ end
21
+
22
+ require_relative 'services/resources_mgr'
@@ -0,0 +1,7 @@
1
+ module MediTAF
2
+ module Services
3
+ # Reserved namespace for specific, underlying resource adapters
4
+ module Clients
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,115 @@
1
+ require 'euresource'
2
+
3
+ module MediTAF
4
+ module Services
5
+ module Clients
6
+ # The specific resource adapter for Euresource. It depends on the following configurations
7
+ # * services.euresource
8
+ # * mauth_url
9
+ # * app_uuid
10
+ # * key_file : location of file containing private key
11
+ # * eureka_url
12
+ # * stage
13
+ # * mauth_logger (optional)
14
+ # * eureka_logger (optional)
15
+ # * authenticate_responses (optional)
16
+ class EuresourceAdapter
17
+
18
+ # verifies needs configuration items exist for eureka-client
19
+ def initialize
20
+ errs = []
21
+ errs << 'euresource configuration' unless euresource_config
22
+ if errs.empty?
23
+ errs << 'eureka_url' unless euresource_config['eureka_url']
24
+ errs << 'mauth_url' unless euresource_config['mauth_url']
25
+ errs << 'app_uuid' unless euresource_config['app_uuid']
26
+ errs << 'key_file' unless euresource_config['key_file']
27
+ errs << 'stage' unless euresource_config['stage']
28
+ errs << 'authenticate_responses' if euresource_config['authenticate_responses'].nil?
29
+ end
30
+ unless errs.empty?
31
+ raise ResourceAdapterConfigurationMissing, 'Euresource Adapter Configurations: cannot find ' + errs.join(', ')
32
+ end
33
+ end
34
+
35
+ # require configurations for eureka-client
36
+ def configure
37
+ cfg = euresource_config
38
+ Euresource.configure do
39
+ config.stage_defaults do |defaults|
40
+ defaults.mauth(cfg['mauth_url']) do |mauth_config|
41
+ mauth_config.app_uuid cfg['app_uuid']
42
+ mauth_config.private_key_file cfg['key_file']
43
+
44
+ # Only set if you want different logger for MAuth
45
+ mauth_config.logger cfg['mauth_logger'].constantize.new if cfg['mauth_logger']
46
+ end
47
+ defaults.use MAuth::Faraday::RequestSigner, defaults.mauth_config
48
+
49
+ # Turns on develop mode for local development, which adds extra logging and forces API deployments to Eureka
50
+ # without having to delete prior versions of an API document for local development while making changes to an
51
+ # API document.
52
+ defaults.develop_mode false
53
+
54
+ # Rails logger will be used for all logging if this is not set in Rails.
55
+ defaults.logger EuresourceLogger.new if cfg['eureka_logger']
56
+
57
+ #Un-comment this option if you want to skip authenticating eureka responses
58
+ defaults.mauth_authenticate_responses cfg['authenticate_responses']
59
+
60
+ defaults.use FaradayMiddleware::RackCompatible, CacheComplainer::Complainer
61
+ end
62
+
63
+ # This applies all default from above to configure a Eureka::Client instance for a specific eureka stage
64
+ config.stage(cfg['eureka_url'], cfg['stage']) do |builder|
65
+ builder.deployment_base_uri 'http://localhost:3000'
66
+ builder.faraday_adapter :typhoeus
67
+ end
68
+ end
69
+ end
70
+
71
+ # @param resource [Symbol] the resource to get
72
+ # @return [Euresource::Base] The anonymous class.
73
+ def load(resource)
74
+ Euresource.class_for_resource(resource)
75
+ rescue => e
76
+ raise ResourceAdapterLoadError, "Can't load resource #{resource}. Inner Exception: #{e.to_s}"
77
+ end
78
+
79
+ # @note not implemented
80
+ # checks if the resource is deployed within eureka
81
+ # @param resource [Symbol] the resource to check
82
+ def deployed?(resource)
83
+ end
84
+
85
+ # @note not implemented
86
+ # check if the resource is consumable
87
+ # @param resource [Symbol] the resource to check
88
+ def connected?(resource)
89
+ end
90
+
91
+ # @return [String] the stage value from the configuration
92
+ def stage
93
+ euresource_config['stage']
94
+ end
95
+
96
+ # @return [Symbol] the euresource clients by stage from euresource configuration
97
+ def clients_by_stage
98
+ Euresource.config.clients_by_stage
99
+ end
100
+
101
+ # @return [Symbol] the default stage from euresource configuration
102
+ def default_stage
103
+ Euresource.config.default_stage
104
+ end
105
+
106
+ private
107
+
108
+ def euresource_config
109
+ raise ServiceConfigurationMissing, "services not found in configuration" unless MediTAF::Utils::Configuration['services']
110
+ MediTAF::Utils::Configuration['services']['euresource']
111
+ end
112
+ end
113
+ end
114
+ end
115
+ end
@@ -0,0 +1,125 @@
1
+ require 'mauth-client'
2
+ require 'faraday'
3
+ require 'faraday_middleware'
4
+
5
+ module MediTAF
6
+ module Services
7
+ module Clients
8
+ # The specific resource adapter for MAuthClient. It depends on the following configurations
9
+ # * services.mauth
10
+ # * mauth_url
11
+ # * mauth_api_version
12
+ # * app_uuid
13
+ # * key_file : location of file containing private key
14
+ # * mauth_logger (optional)
15
+ # * authenticate_responses (optional)
16
+ class MauthAdapter
17
+
18
+ # verifies needed configuration items exist for mauth-client
19
+ # @raise [ResourceAdapterConfigurationMissing] lists the missing required configuration items
20
+ def initialize
21
+ errs = []
22
+ errs << 'mauth configuration' unless mauth_config
23
+ if errs.empty?
24
+ errs << 'mauth_api_version' unless mauth_config[:mauth_api_version]
25
+ errs << 'mauth_url' unless mauth_config[:mauth_baseurl]
26
+ errs << 'app_uuid' unless mauth_config[:app_uuid]
27
+ errs << 'key_file' unless mauth_config[:private_key_file]
28
+ errs << 'authenticate_responses' if mauth_config[:authenticate_response].nil?
29
+ end
30
+ unless errs.empty?
31
+ raise ResourceAdapterConfigurationMissing, 'Mauth Adapter Configurations: cannot find ' + errs.join(', ')
32
+ end
33
+ end
34
+
35
+ # configure a new MAuth::Client based on configuration items
36
+ # @note configures MAuth::Client to return all response in JSON format
37
+ def configure
38
+ @mauth_client = ::MAuth::Client.new(mauth_config)
39
+
40
+ @connection = Faraday.new do |builder|
41
+ builder.use MAuth::Faraday::MAuthClientUserAgent, "MediTAF Mauth Client Adapter"
42
+ builder.use MAuth::Faraday::RequestSigner, :mauth_client => @mauth_client
43
+ builder.use MAuth::Faraday::ResponseAuthenticator, :mauth_client => @mauth_client if mauth_config[:authenticate_response]
44
+ builder.use FaradayMiddleware::ParseJson, content_type: /\bjson$/
45
+ builder.use FaradayMiddleware::ParseXml, content_type: /\bxml$/
46
+ builder.adapter Faraday.default_adapter
47
+ end
48
+ end
49
+
50
+ # loads a new MauthClient object for request on base_url
51
+ # @param args [Hash] arguments
52
+ # @option args [String] :baseurl the base url of this resource
53
+ # @return [MediTAF::Services::Clients::MauthClient]
54
+ # @raise [ResourceAdapterLoadError] when instantiating a new MauthClient
55
+ def load(args)
56
+ raise MauthClientBaseURLMissing, "supply a base url" unless args.is_a?(Hash) && args.has_key?(:baseurl)
57
+ MauthClient.new(@connection, args[:baseurl])
58
+ end
59
+
60
+ private
61
+
62
+ def mauth_config
63
+ raise ServiceConfigurationMissing, "services not found in configuration" unless MediTAF::Utils::Configuration['services']
64
+ raise ServiceConfigurationMissing, "euresource not found in configuration" unless MediTAF::Utils::Configuration['services']['mauth']
65
+ unless @mauth_config
66
+ cfg = MediTAF::Utils::Configuration['services']['mauth']
67
+ @mauth_config = {}
68
+ @mauth_config[:mauth_baseurl] = cfg['mauth_url']
69
+ @mauth_config[:private_key_file] = cfg['key_file']
70
+ @mauth_config[:app_uuid] = cfg['app_uuid']
71
+ @mauth_config[:mauth_api_version] = cfg['mauth_api_version']
72
+ @mauth_config[:authenticate_response] = cfg['authenticate_responses']
73
+ end
74
+ @mauth_config
75
+ rescue => e
76
+ nil
77
+ end
78
+ end
79
+
80
+ # A MauthClient object set to a specific base_url and uses the common MAuth::Client object
81
+ # @note only MauthAdapter object instantiate objects of this type
82
+ class MauthClient
83
+ # @param connection [Faraday] defines the MAuth::Faraday parameters/handlers/response formats
84
+ # @param base_url [String] the base url for all paths
85
+ # @raise [MauthClientBaseURLMissing] when the no base url is given
86
+ def initialize(connection, base_url)
87
+ @connection, @base_url = connection, base_url
88
+ end
89
+
90
+ # @param [Hash] opts the arguments to do an HTTP request
91
+ # @option opts [Symbol] :verb :get, :post, :put, or :delete
92
+ # @option opts [Symbol] :resource
93
+ # @option opts [Symbol] :body
94
+ # @option opts [Symbol] :content_type
95
+ # @return [Faraday::Response]
96
+ # @raise [ResourceAdapterRequestError]
97
+ def request(opts)
98
+ headers = {}
99
+ headers['Content-Type'] = opts[:content_type] if opts[:content_type]
100
+ headers['Content-Type'] = 'application/json' if opts[:content_type].nil? && opts[:body]
101
+ headers['Accept'] = opts[:accept] || 'text/html,application/xhtml+xml,application/xml,application/json'
102
+ @connection.run_request(opts[:verb], @base_url + '/' + opts[:resource], opts[:body], headers)
103
+ rescue MAuth::InauthenticError, MAuth::UnableToAuthenticateError => e
104
+ raise ResourceAdapterRequestError, "Failed to #{opts[:verb]} #{opts[:resource]}. Inner Exception: #{e.to_s}"
105
+ rescue => e
106
+ raise ResourceAdapterRequestError, "#{opts}. Inner Exception: #{e.to_s}"
107
+ end
108
+
109
+ # @note not implemented
110
+ # checks if the resource is deployed within eureka
111
+ # @param resource [Symbol] the resource to check
112
+ def deployed?(resource)
113
+ end
114
+
115
+ # @note not implemented
116
+ # check if the resource is consumable
117
+ # @param resource [Symbol] the resource to check
118
+ def connected?(resource)
119
+ end
120
+ end
121
+
122
+ class MauthClientBaseURLMissing < MediTAF::Utils::Exceptions::MediTAFException; end
123
+ end
124
+ end
125
+ end
@@ -0,0 +1,110 @@
1
+ # ResourcesMgr manages the requested resources.
2
+ # the manager needs a specific adapter from where it will interact with resources
3
+ # if no adapter is specified in the configuration i.e. ['services']['adapter']
4
+ # then 'euresource' adapter is used the default
5
+ # all resources are expressed as method
6
+ # each accessed resources exposes the underlying adapter implementation
7
+
8
+ require 'MediTAF/utils/configuration'
9
+ require 'active_support/core_ext/string/inflections'
10
+
11
+ module MediTAF
12
+ module Services
13
+ class ResourcesMgr
14
+ include Logging
15
+
16
+ attr_reader :adapters
17
+ attr_reader :resources
18
+ attr_reader :stage
19
+
20
+ # @note loads euresource adapter by default, otherwise attempts to load a specific adapter. adapters are
21
+ # specified in *MediTAF_configuration.services.adapter* value. the location of the adapter defaults
22
+ # to services/clients within MediTAF, otherwise it can be specified in *MediTAF_configuration.services.adapter_home*
23
+ # value. when MediTAF attempts to load the adapter and no location has been specified, MediTAF assumes that the
24
+ # adapter has been load into the environment.
25
+ # @note the adapter must be in namespace *MediTAF::Services::Clients*
26
+ # @raise [ResourceAdapterMethodMissing] when adapter does not respond to configure
27
+ # @raise [ResourceAdapterLoadError] when adapter could not be loaded
28
+ def initialize
29
+ @resources = {}
30
+ @adapters = {}
31
+
32
+ raise ServiceConfigurationMissing, "services not found in configuration" unless MediTAF::Utils::Configuration['services']
33
+
34
+ adapter_home = MediTAF::Utils::Configuration['services']['adapter_home']
35
+ adapter_home ||= "#{MediTAF.root}/lib/MediTAF/services/clients"
36
+
37
+ adapters = MediTAF::Utils::Configuration['services']['adapters']
38
+
39
+ if adapters
40
+ adapters.split(/ *, */).each do |adapter|
41
+ begin
42
+ require "#{adapter_home}/#{adapter}_adapter" if File.exist? "#{adapter_home}/#{adapter}_adapter.rb"
43
+ @adapters[adapter.to_sym] = "MediTAF::Services::Clients::#{adapter.camelize}Adapter".constantize.new
44
+ @adapters[adapter.to_sym].configure
45
+ rescue NoMethodError => e
46
+ raise ResourceAdapterMethodMissing, %Q|"#{adapter.camelize}Adapter" is missing required configure method|
47
+ rescue NameError => e
48
+ raise ResourceAdapterLoadError, %Q|Couldn't load resource adapter "#{adapter.camelize}Adapter". Inner Exception: #{e.to_s}|
49
+ end
50
+ end
51
+ end
52
+ end
53
+
54
+ # removes all or one resource from the resources hash
55
+ # @param resource [Symbol] the resource to remove
56
+ def delete(resource=nil)
57
+ if resource
58
+ @resources.delete(resource) if @resources.include?(resource)
59
+ else
60
+ @resources.clear
61
+ end
62
+ end
63
+
64
+ # checks if a resource has been deployed
65
+ # @note not implemented
66
+ # @param resource [Symbol] the resource to check
67
+ def deployed?(resource)
68
+ raise "not implemented"
69
+ end
70
+
71
+ # checks if a resource is consumable
72
+ # @note not implemented
73
+ # @param resource [Symbol] the resource to check
74
+ def connected?(resource)
75
+ raise "not implemented"
76
+ end
77
+
78
+ # @param adapter [Symbol] the adapter to get the stage from
79
+ # @return [Symbol] gets the current stage set for the adapter
80
+ def stage(adapter=:euresource)
81
+ @adapters[adapter].stage.to_sym if @adapters.has_key? adapter
82
+ end
83
+
84
+ # @param adapter [Symbol] the adapter to get the stage from
85
+ # @return [Symbol] gets the default stage set for the adapter
86
+ def default_stage(adapter=:euresource)
87
+ @adapters[adapter].default_stage if @adapters.has_key? adapter
88
+ end
89
+
90
+ # @param resource [Symbol] resource as a method name
91
+ # @param args [Object] args are passed to underlying adapter
92
+ # @return [Object] the specific resource adapter object
93
+ # @raise [ResourceAdapterMethodMissing] when adapter does not respond to load
94
+ def method_missing(resource, *args, &block)
95
+ opts = args[0] || {}
96
+ unless @resources.include?(resource)
97
+ unless @adapters.has_key? opts[:adapter]
98
+ raise ResourceAdapterMissing, %Q/'#{opts[:adapter]}' adapter not found/
99
+ end
100
+ adapter = @adapters[ opts[:adapter] ]
101
+ opts.delete(:adapter)
102
+ @resources[resource] = ( opts.nil? || opts.empty?) ? adapter.load(resource) : adapter.load(opts)
103
+ end
104
+ @resources[resource]
105
+ rescue NoMethodError => e
106
+ raise ResourceAdapterMethodMissing, "required load method missing"
107
+ end
108
+ end
109
+ end
110
+ end
data/lib/MediTAF/ui.rb ADDED
@@ -0,0 +1,25 @@
1
+ # Module ui namespace for all ui related capabilities
2
+ require 'MediTAF/utils/exceptions'
3
+
4
+ module MediTAF
5
+ # Enables MediTAF to interact with the UI via the page object model design pattern. It manages applications and their
6
+ # associated pages (i.e. page objects).
7
+ module UI
8
+ # @param pages_root [String] specifies location of applications' pages. default location: pages
9
+ def self.new(pages_root='pages')
10
+ Applications.new('./' + Dir.glob(File.join("**", pages_root)).first)
11
+ end
12
+
13
+ # Applications manager was unable to find a directory with the name of the application
14
+ class AppLoadError < MediTAF::Utils::Exceptions::MediTAFException; end
15
+
16
+ # Pages Manager (i.e. the Application) was unable to find a file presentation the page model
17
+ class PageLoadError < MediTAF::Utils::Exceptions::MediTAFException; end
18
+
19
+ # BaseURL for launching the application is missing.
20
+ class BaseURLMissing < MediTAF::Utils::Exceptions::MediTAFException; end
21
+
22
+ end
23
+ end
24
+
25
+ require_relative 'ui/applications'
@@ -0,0 +1,72 @@
1
+ require 'active_support/core_ext/string/inflections'
2
+
3
+ module MediTAF
4
+ module UI
5
+ # Manages all of the application's pages in the application's root directory
6
+ class Application
7
+ attr_reader :app_root
8
+ attr_reader :pages
9
+ attr_reader :current_page
10
+
11
+ # @param app_root [String] path to the application's page models
12
+ def initialize(app_root)
13
+ @pages = {}
14
+ @app_root = app_root
15
+ end
16
+
17
+ # references the page as a method name
18
+ # @param page [Symbol] the page name as a symbol
19
+ # @return [Object] the current page
20
+ def method_missing(page, *args, &block)
21
+ @current_page =
22
+ if @pages.include?(page)
23
+ @pages[page]
24
+ elsif in_namespace?(page)
25
+ new_page(@app_root + '/' + page.to_s)
26
+ else
27
+ load_page(page)
28
+ end
29
+ end
30
+
31
+ # iterates over the loaded pages collection
32
+ # @param block [Proc]
33
+ def each(&block)
34
+ @pages.each(&block) if @pages
35
+ end
36
+
37
+ # @return [Symbol] the current page in use
38
+ def current_page
39
+ @current_page
40
+ end
41
+
42
+ private
43
+
44
+ # @param page [Symbol] the page name as a symbol
45
+ # @return [MediTAF::UI:BasePage] the newly accessed page
46
+ # @raise [PageLoadError] when manager cannot fully load the requested page
47
+ def load_page(page)
48
+ raise "File #{@app_root}/#{page.to_s}.rb not found" unless Dir["#{@app_root}/#{page.to_s}.rb"].detect do |f|
49
+ !File.directory?(f) && File.basename(f, ".*").downcase.to_sym == page
50
+ end
51
+ class_path = @app_root + '/' + page.to_s
52
+ require class_path
53
+ new_page(class_path)
54
+ rescue => e
55
+ raise PageLoadError, "Couldn't load page #{page.to_s}. Inner Exception: #{e.to_s}"
56
+ end
57
+
58
+ def new_page(class_path)
59
+ @pages[class_path.split('/').last.to_sym] = /^.+\/([^\/]+\/[^\/]+)$/.match(class_path)[1].camelize.constantize.new
60
+ rescue => e
61
+ raise PageLoadError, "Couldn't instantiate page #{page.to_s}. Inner Exception #{e.to_s}"
62
+ end
63
+
64
+ def in_namespace?(page)
65
+ ns = /^.+\/[^\/]+\/([^\/]+)$/.match(@app_root)[1].camelize.constantize
66
+ ns.constants.detect { |c| ns.const_get(c).is_a?(Class) && c == page.to_s.camelize.to_sym }
67
+ rescue => e
68
+ nil
69
+ end
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,60 @@
1
+ # Class Applications manages all configured applications in a single browser session
2
+
3
+ require_relative '../ui/application'
4
+
5
+ module MediTAF
6
+ module UI
7
+ # Manages all of the applications in the applications root directory
8
+ class Applications
9
+ include Enumerable
10
+
11
+ attr_reader :apps_root
12
+ attr_reader :apps
13
+ attr_reader :current_app
14
+
15
+ # @param apps_root [String] root location of applications with their defined pages
16
+ def initialize(apps_root)
17
+ @apps = {}
18
+ @apps_root = apps_root
19
+ end
20
+
21
+ # only empties the applications internal apps collection
22
+ def close
23
+ @apps.clear
24
+ end
25
+
26
+ # references an application as a method name
27
+ # @param app [Symbol] the application name as a symbol
28
+ # @return [Application] the current application
29
+ def method_missing(app, *args, &block)
30
+ @current_app = @apps.include?(app) ? @apps[app] : load_app(app)
31
+ end
32
+
33
+ # iterates over the loaded applications collection
34
+ # @param block [Proc]
35
+ def each(&block)
36
+ @apps.each(&block) if @apps
37
+ end
38
+
39
+ # @return [Symbol] the current application in use
40
+ def current_app
41
+ @current_app
42
+ end
43
+
44
+ private
45
+
46
+ # @param app [Symbol] the application name as a symbol
47
+ # @return [Application] the newly accessed application
48
+ # @raise [AppLoadError] when manager cannot fully load the requested app
49
+ def load_app(app)
50
+ raise "Directory #{@apps_root}/#{app} not found" unless Dir["#{@apps_root}/*"].detect do |f|
51
+ File.directory?(f) && File.basename(f).downcase.to_sym == app
52
+ end
53
+ new_app = Application.new("#{@apps_root}/#{app.to_s.downcase}")
54
+ @apps[app] = new_app
55
+ rescue => e
56
+ raise AppLoadError, "Couldn't load #{app}. Inner Exception: #{e.to_s}"
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,84 @@
1
+ require 'MediTAF/utils/exceptions'
2
+
3
+ module MediTAF
4
+ # Representation of the meditaf_config.yml configuration file.
5
+ module Utils
6
+ module Configuration
7
+
8
+ class << self
9
+ # @param filepath [String] path/to/file
10
+ # @raise [ConfigurationNotFoundError] when YAML cannot load the file
11
+ def new(filepath = nil)
12
+ filepath ||= Dir.glob(File.join("**", "meditaf_config.yml")).first
13
+ yml_hashes = []
14
+
15
+ # load main yaml file
16
+ yml_hashes << YAML.load_file(filepath)
17
+ raise FileEmpty, "#{filepath} is empty. Nothing to config, YAY!" unless yml_hashes[0]
18
+
19
+ # load additional yml files
20
+ if yml_hashes[0]['modules']['config_files']
21
+ yml_hashes[0]['modules']['config_files'].split(/ *, */).each { |f| yml_hashes << YAML.load_file(f) }
22
+ modules = yml_hashes.shift
23
+ added = yml_hashes.each_with_object({}) { |oh, nh| nh.merge!(oh) }
24
+ modules['modules'].merge!(added)
25
+ end
26
+ modules ||= yml_hashes[0]
27
+
28
+ # merge all yml hashes into one hash
29
+ @modules = Settings.new('modules', modules['modules'] )
30
+ rescue Errno::ENOENT => e
31
+ raise FileNotFound, "missing configuration. check you have meditaf_config in the 'config' or " +
32
+ "project root directory and is valid."
33
+ rescue Psych::SyntaxError => e
34
+ raise Error, "Configuration Error : #{e}"
35
+ end
36
+
37
+ # backward compatibility for older style references of Configuration['modules']['xxx']
38
+ def [](key)
39
+ (key == 'modules') ? @modules : @modules['modules'][key]
40
+ end
41
+ end
42
+
43
+ class Settings
44
+ def initialize(key, settings)
45
+ @key = key
46
+ @settings = settings
47
+ self
48
+ end
49
+
50
+ def [](key)
51
+ # backward compatibility for older style references of $config['modules']['xxx']
52
+ return self if key == 'modules' && @key == 'modules'
53
+ (@settings[key].is_a?(Hash)) ? Settings.new(@key + '/' + key, @settings[key]) : @settings[key] if @settings.has_key?(key)
54
+ end
55
+
56
+ def include?(key)
57
+ @settings.has_key?(key)
58
+ end
59
+
60
+ private
61
+
62
+ def []=(key, value)
63
+ @settings[key] = value
64
+ end
65
+
66
+ def delete(key)
67
+ @settings.delete(key) if @settings.has_key?(key)
68
+ end
69
+ end
70
+
71
+ # When the specified configuration file has not configuration items i.e. EMPTY
72
+ class FileEmpty < StandardError; end
73
+
74
+ # Specified configuration file or meditaf_config.yml file not in config directory or project root directory
75
+ class FileNotFound < StandardError; end
76
+
77
+ # General configuration error: usually invalid YAML syntax
78
+ class Error < StandardError; end
79
+
80
+ # When request modules key is not found in configuration
81
+ class ItemNotFound < StandardError; end
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,21 @@
1
+ require 'MediTAF/utils/logger'
2
+
3
+ module MediTAF
4
+ module Utils
5
+ module Exceptions
6
+
7
+ # MediTAF Exception with logging
8
+ class MediTAFException < StandardError
9
+ include MediTAF::Utils::Logger
10
+
11
+ # @param message [String] error message.
12
+ def initialize(message = 'MediTAF Exception') # Default Message
13
+ log.error message
14
+ super(message)
15
+ end
16
+
17
+ end
18
+
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,63 @@
1
+
2
+ require 'logging'
3
+
4
+ module MediTAF
5
+ module Utils
6
+ # Logging Mechanism based on Logging gem
7
+ module Logger
8
+
9
+ # @return [Logging::Logger]
10
+ def log
11
+ @log ||= Logger.logger_for(self.class.name)
12
+ end
13
+
14
+ attr_reader :loggers
15
+ @loggers = {}
16
+
17
+ class << self
18
+
19
+ # for unit testing purposes only
20
+ def loggers
21
+ @loggers
22
+ end
23
+
24
+ # @param classname [Class] the class for which to log
25
+ # @return [Logging::Logger]
26
+ def logger_for(classname)
27
+ @loggers[classname] ||= configure_logger_for(classname)
28
+ end
29
+
30
+ # @param classname [Class] the class for which to log
31
+ # @return [Logging::Logger]
32
+ def configure_logger_for(classname)
33
+ raise LoggingConfigurationMissing, 'logging not found in configuration' unless MediTAF::Utils::Configuration['logging']
34
+ raise LoggingFilePathMissing, 'logging filepath not found in configuration' unless MediTAF::Utils::Configuration['logging']['filepath']
35
+ raise LoggingLevelMissing, 'logging level not found in configuration' unless MediTAF::Utils::Configuration['logging']['level']
36
+
37
+ config = MediTAF::Utils::Configuration['logging']
38
+ log = Logging.logger[classname]
39
+
40
+ log.add_appenders(
41
+ Logging.appenders.stdout('stdout', :layout => Logging.layouts.pattern(:pattern => '[%d] %-5l %c: %m\n')),
42
+ Logging.appenders.file("#{config['filepath']}", :layout => Logging.layouts.pattern(:pattern => '[%d] %-5l %c: %m\n'))
43
+ )
44
+
45
+ # valid levels are debug, info, warn, error, fatal
46
+ log.level = config['level'].to_sym
47
+ log
48
+ rescue LoggingConfigurationMissing, LoggingFilePathMissing, LoggingLevelMissing => e
49
+ raise e
50
+ rescue => e
51
+ raise LoggingConfigurationError, "Inner Exception: #{e.to_s}"
52
+ end
53
+
54
+ end
55
+
56
+ class LoggingConfigurationError < StandardError; end
57
+ class LoggingConfigurationMissing < StandardError; end
58
+ class LoggingFilePathMissing < StandardError; end
59
+ class LoggingLevelMissing < StandardError; end
60
+
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,118 @@
1
+ require 'gmail'
2
+ require 'MediTAF/utils/exceptions'
3
+
4
+ module MediTAF
5
+ module Utils
6
+ module Email
7
+ class Mail < Gmail::Client::Base
8
+
9
+ attr_reader :gmail
10
+
11
+ def initialize(username, options = {})
12
+ @gmail = Gmail.new(username, options)
13
+ end
14
+
15
+ # finds the unread invite email from iMedidata
16
+ # @param from [String] the from email address.
17
+ # @param subject [String] the subject of the email.
18
+ # @param newest [Boolean] the subject of the email.
19
+ # @param date [String] the date of the email. Defaults to today's date.
20
+ # @param to [String] the recepient email address.
21
+ def find_email(from, subject=nil, newest=true, date=Time.now, to=nil)
22
+ email_options={}
23
+ email_options[:from]= "#{from}" if from
24
+ email_options[:to]= "#{to}" if to
25
+ email_options[:on]= Date.parse("#{date}") if date
26
+ Message.new do
27
+
28
+ @gmail.inbox.find(:unread,email_options).each do |email|
29
+ subject.nil? || email.message.subject == subject
30
+ end.send(newest ? :last : :first)
31
+
32
+ end
33
+ end
34
+
35
+ # finds and deletes the email
36
+ # @param from [String] the from email address.
37
+ # @param subject [String] the subject of the email.
38
+ # @param date [String] the date of the email. Defaults to today's date.
39
+ def delete(from, subject=nil, date=Time.now)
40
+ raise SubjectNil if subject.nil?
41
+ @gmail.inbox.find(:all, on: Date.parse("#{date}"), from: "#{from}", subject: subject).each do |email|
42
+ email.delete!
43
+ end
44
+ end
45
+
46
+ # logs out of Mail
47
+ def logout
48
+ @gmail.logout
49
+ end
50
+
51
+ # whether or not you are logged in
52
+ def logged_in?
53
+ @gmail.logged_in?
54
+ end
55
+
56
+ # sends mail using configured email address
57
+ # @param addr [String] the address to send to
58
+ # @param title [String] the subject of the email
59
+ # @param contents [String] the glob of the email
60
+ def send_mail(addr, title, contents)
61
+ if logged_in?
62
+ @gmail.deliver do
63
+ to addr
64
+ subject title
65
+ html_part do
66
+ content_type 'text/html; charset=UTF-8'
67
+ body contents
68
+ end
69
+ delivery_method :smtp, {address: 'smtp.lab1.hdc.mdsol.com', port: 80} if `hostname` =~ /hdc505lb/
70
+ end
71
+ end
72
+ end
73
+
74
+ end
75
+
76
+ class Message
77
+ attr_reader :email, :url, :body
78
+
79
+ def initialize()
80
+ if block_given?
81
+ @email = yield
82
+ raise InviteEmailNotFoundError, 'Invitation email was not found' if @email.nil?
83
+ @body = @email.html_part.body.dup
84
+ end
85
+ self
86
+ end
87
+
88
+ def email
89
+ @email
90
+ end
91
+
92
+ def subject
93
+ @email.message.subject
94
+ end
95
+
96
+ def body
97
+ raise InviteBodyNotFoundError, 'Invitation body was not found' if @body.nil?
98
+ @body.decoded
99
+ end
100
+
101
+ def url
102
+ body.scan(/https?:\/\/\S+imedidata\.\S+\/users\S+\/activation/).first.delete("'")
103
+ rescue => e
104
+ raise URLNotFoundError, "URL is missing or does not contain a valid invite link. Inner Exception: #{e.to_s}"
105
+ end
106
+ end
107
+
108
+ class InviteEmailNotFoundError < MediTAF::Utils::Exceptions::MediTAFException;
109
+ end
110
+ class InviteBodyNotFoundError < MediTAF::Utils::Exceptions::MediTAFException;
111
+ end
112
+ class URLNotFoundError < MediTAF::Utils::Exceptions::MediTAFException;
113
+ end
114
+ class SubjectNil < MediTAF::Utils::Exceptions::MediTAFException;
115
+ end
116
+ end
117
+ end
118
+ end
@@ -0,0 +1,122 @@
1
+ # MeditafFaker is a module that provides random fake data for uid codes, random numbers, timestamp based on the Faker gem
2
+ #
3
+ #*********************************************************************************************************************S**c*#
4
+ require 'MediTAF/utils/configuration'
5
+ require 'faker'
6
+
7
+ require 'i18n'
8
+
9
+ I18n.enforce_available_locales = true
10
+ I18n.reload!
11
+
12
+ module MediTAF
13
+ module Utils
14
+ class Base < Faker::Base
15
+
16
+ class << self
17
+ def stringify(param, *params)
18
+ "#{param}#{params.join()}"
19
+ end
20
+
21
+ protected
22
+ def random_generator
23
+ rand(1000..9999)
24
+ end
25
+ end
26
+ end
27
+
28
+ # TimeStamp: a class that provides the current date time format
29
+ class MediTAFFaker < Base
30
+ class << self
31
+
32
+ ##using Faker address
33
+ def address
34
+ Faker::Address
35
+ end
36
+
37
+ # using Faker name
38
+ def name
39
+ Faker::Name
40
+ end
41
+
42
+ # using Faker code
43
+ def code
44
+ Faker::Code
45
+ end
46
+
47
+ # using Faker number
48
+ def number
49
+ Faker::Number
50
+ end
51
+
52
+ # using Faker phone_number
53
+ def phone_number
54
+ Faker::PhoneNumber
55
+ end
56
+
57
+ # Default representation of the current date format
58
+ def timestamp
59
+ DateTime.now.strftime()
60
+ end
61
+
62
+ # Default representation of the current date format
63
+ def medidata_date_format
64
+ DateTime.now.strftime("%d-%b-%Y")
65
+ end
66
+
67
+ # Long representation of the current date format - Missing Year
68
+ def timestamp_long
69
+ DateTime.now.strftime("%m%d%H%M%S%6N")
70
+ end
71
+
72
+ # Short representation of the current date format with minutes, seconds and micro seconds
73
+ def timestamp_short
74
+ DateTime.now.strftime("%M%S%6N")
75
+ end
76
+
77
+ # Complete representation of the current date format including picosecons
78
+ def timestamp_complete
79
+ DateTime.now.strftime("%m-%d-%Y:%H%M%S.%12N")
80
+ end
81
+
82
+ # Random: a class that displays a random string in the MD5 format based on DateTime as string
83
+ def random
84
+ random_generator.to_s
85
+ end
86
+
87
+ # UID: a class that displays a random string in the MD5 format based on DateTime as string
88
+ def uid
89
+ Digest::MD5.hexdigest(DateTime.now.strftime("%m-%d-%Y:%H%M%S.%12N")).to_s
90
+ end
91
+
92
+ # Code: a class that inherits from Faker:Code. It provides UID (32/40-bit length).
93
+ def uid_md5(no_dash=true)
94
+ no_dash ? generate_32_bit_checkmateUID_without_dash : generate_32_bit_checkmateUID_with_dash
95
+ end
96
+
97
+ def uid_sha1(no_dash=true)
98
+ no_dash ? generate_40_bit_checkmateUID_without_dash : generate_40_bit_checkmateUID_with_dash
99
+ end
100
+
101
+ private
102
+
103
+ def generate_32_bit_checkmateUID_with_dash
104
+ regexify(/[a-f0-9]{8}-([a-f0-9]{4}-){3}[a-f0-9]{12}/)
105
+ end
106
+
107
+ def generate_32_bit_checkmateUID_without_dash
108
+ regexify(/[a-f0-9]{32}/)
109
+ end
110
+
111
+ def generate_40_bit_checkmateUID_with_dash
112
+ regexify(/[a-f0-9]{12}-([a-f0-9]{4}-){4}[a-f0-9]{12}/)
113
+ end
114
+
115
+ def generate_40_bit_checkmateUID_without_dash
116
+ regexify(/[a-f0-9]{40}/)
117
+ end
118
+
119
+ end
120
+ end
121
+ end
122
+ end
@@ -0,0 +1,31 @@
1
+ module MediTAF
2
+ module Utils
3
+ class Sticky < Hash
4
+
5
+ def initialize(default_value=nil)
6
+ super default_value
7
+ end
8
+
9
+ def [](key)
10
+ raise MediTAF::Utils::StickyKeyNotFound, "I don't have '#{key}' key" unless has_key? key
11
+ super key
12
+ end
13
+
14
+ def set_value(key, value)
15
+ self[key] = value
16
+ end
17
+
18
+ # @param data [String] reference to stored value
19
+ # @return [Object] the stored value at reference
20
+ # @raise [] when the reference is not found
21
+ def get_value(data)
22
+ result = data.dup
23
+ result.scan(/(\w+|\w)\s*/).each { |memo| result.sub!("#{memo[0]}", self["#{memo[0]}"]) if self["#{memo[0]}"] } if result.is_a? String
24
+ result
25
+ end
26
+ end
27
+
28
+ class StickyKeyNotFound < MediTAF::Utils::Exceptions::MediTAFException; end
29
+ end
30
+ end
31
+
@@ -0,0 +1,3 @@
1
+ module MediTAF
2
+ VERSION = "1.6.0"
3
+ end
metadata ADDED
@@ -0,0 +1,147 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: Test_Framework1.0
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.6.0
5
+ platform: ruby
6
+ authors:
7
+ - Medidata Test Automation Framework Team
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2016-07-16 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: require_all
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: 1.3.2
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: 1.3.2
27
+ - !ruby/object:Gem::Dependency
28
+ name: logging
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: 1.8.2
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: 1.8.2
41
+ - !ruby/object:Gem::Dependency
42
+ name: faker
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '1.3'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '1.3'
55
+ - !ruby/object:Gem::Dependency
56
+ name: gmail
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: multi_xml
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :runtime
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: selenium-webdriver
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '2.46'
90
+ type: :runtime
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '2.46'
97
+ description: " Test Automation Framework"
98
+ email:
99
+ - chriwilliams_23@yahoo.com
100
+ executables: []
101
+ extensions: []
102
+ extra_rdoc_files: []
103
+ files:
104
+ - LICENSE
105
+ - README.md
106
+ - lib/MediTAF.rb
107
+ - lib/MediTAF/services.rb
108
+ - lib/MediTAF/services/clients.rb
109
+ - lib/MediTAF/services/clients/euresource_adapter.rb
110
+ - lib/MediTAF/services/clients/mauth_adapter.rb
111
+ - lib/MediTAF/services/resources_mgr.rb
112
+ - lib/MediTAF/ui.rb
113
+ - lib/MediTAF/ui/application.rb
114
+ - lib/MediTAF/ui/applications.rb
115
+ - lib/MediTAF/utils/configuration.rb
116
+ - lib/MediTAF/utils/exceptions.rb
117
+ - lib/MediTAF/utils/logger.rb
118
+ - lib/MediTAF/utils/mail.rb
119
+ - lib/MediTAF/utils/meditaf_faker.rb
120
+ - lib/MediTAF/utils/sticky.rb
121
+ - lib/MediTAF/version.rb
122
+ homepage: ''
123
+ licenses:
124
+ - MIT
125
+ - LICENSE
126
+ metadata: {}
127
+ post_install_message:
128
+ rdoc_options: []
129
+ require_paths:
130
+ - lib
131
+ required_ruby_version: !ruby/object:Gem::Requirement
132
+ requirements:
133
+ - - ">="
134
+ - !ruby/object:Gem::Version
135
+ version: '0'
136
+ required_rubygems_version: !ruby/object:Gem::Requirement
137
+ requirements:
138
+ - - ">="
139
+ - !ruby/object:Gem::Version
140
+ version: '0'
141
+ requirements: []
142
+ rubyforge_project:
143
+ rubygems_version: 2.4.5.1
144
+ signing_key:
145
+ specification_version: 4
146
+ summary: Test Automation Framework
147
+ test_files: []