unobtainium 0.0.1 → 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/Gemfile +0 -3
- data/Gemfile.lock +0 -23
- data/README.md +2 -0
- data/lib/unobtainium/config.rb +170 -0
- data/lib/unobtainium/driver.rb +79 -27
- data/lib/unobtainium/pathed_hash.rb +121 -0
- data/lib/unobtainium/runtime.rb +126 -0
- data/lib/unobtainium/version.rb +1 -1
- data/unobtainium.gemspec +6 -5
- metadata +11 -7
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 194953d1b77be115316598397b69e99037647085
|
4
|
+
data.tar.gz: 9194ccf829bcf5c6e98027251716b8598ea1979b
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 2e43f762d2c90323173cdfeedc2d57e764114558282066bd28a9208ed1fb175deb71917b23cbc135e8f476770e5c396414a572dcb2814ecd5e93449999cb1a8a
|
7
|
+
data.tar.gz: 58fd4916229bf0528304c9cb18541f5b6f2431e958da9f7154b70f1a553ffc04e26be791a02442c0e858ce45e1d42e98bb8c8f9f48e6f9a7fa6866843432baac
|
data/Gemfile
CHANGED
data/Gemfile.lock
CHANGED
@@ -6,21 +6,7 @@ PATH
|
|
6
6
|
GEM
|
7
7
|
remote: https://rubygems.org/
|
8
8
|
specs:
|
9
|
-
appium_lib (8.0.2)
|
10
|
-
awesome_print (~> 1.6)
|
11
|
-
json (~> 1.8)
|
12
|
-
nokogiri (~> 1.6.6)
|
13
|
-
selenium-webdriver (~> 2.49)
|
14
|
-
tomlrb (~> 1.1)
|
15
9
|
ast (2.2.0)
|
16
|
-
awesome_print (1.6.1)
|
17
|
-
childprocess (0.5.9)
|
18
|
-
ffi (~> 1.0, >= 1.0.11)
|
19
|
-
ffi (1.9.10)
|
20
|
-
json (1.8.3)
|
21
|
-
mini_portile2 (2.0.0)
|
22
|
-
nokogiri (1.6.7.2)
|
23
|
-
mini_portile2 (~> 2.0.0.rc2)
|
24
10
|
parser (2.3.0.7)
|
25
11
|
ast (~> 2.2)
|
26
12
|
powerpack (0.1.1)
|
@@ -32,23 +18,14 @@ GEM
|
|
32
18
|
ruby-progressbar (~> 1.7)
|
33
19
|
unicode-display_width (~> 1.0, >= 1.0.1)
|
34
20
|
ruby-progressbar (1.7.5)
|
35
|
-
rubyzip (1.2.0)
|
36
|
-
selenium-webdriver (2.53.0)
|
37
|
-
childprocess (~> 0.5)
|
38
|
-
rubyzip (~> 1.0)
|
39
|
-
websocket (~> 1.0)
|
40
|
-
tomlrb (1.2.0)
|
41
21
|
unicode-display_width (1.0.3)
|
42
|
-
websocket (1.2.2)
|
43
22
|
|
44
23
|
PLATFORMS
|
45
24
|
ruby
|
46
25
|
|
47
26
|
DEPENDENCIES
|
48
|
-
appium_lib
|
49
27
|
bundler (~> 1.11)
|
50
28
|
rubocop (~> 0.39)
|
51
|
-
selenium-webdriver
|
52
29
|
unobtainium!
|
53
30
|
|
54
31
|
BUNDLED WITH
|
data/README.md
CHANGED
@@ -12,6 +12,8 @@ so that test code can more easily cover:
|
|
12
12
|
Some additional useful functionality for the maintenance of test suites is
|
13
13
|
also added.
|
14
14
|
|
15
|
+
[![Gem Version](https://badge.fury.io/rb/unobtainium.svg)](https://badge.fury.io/rb/unobtainium)
|
16
|
+
|
15
17
|
# Credits
|
16
18
|
This gem is inspired by [LapisLazuli](https://github.com/spriteCloud/lapis-lazuli),
|
17
19
|
but vastly less complex, and aims to stay so.
|
@@ -0,0 +1,170 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
#
|
3
|
+
# unobtainium
|
4
|
+
# https://github.com/jfinkhaeuser/unobtainium
|
5
|
+
#
|
6
|
+
# Copyright (c) 2016 Jens Finkhaeuser and other unobtainium contributors.
|
7
|
+
# All rights reserved.
|
8
|
+
#
|
9
|
+
|
10
|
+
require 'pathname'
|
11
|
+
|
12
|
+
require 'unobtainium/pathed_hash'
|
13
|
+
|
14
|
+
module Unobtainium
|
15
|
+
##
|
16
|
+
# The Config class extends PathedHash by two main pieces of functionality:
|
17
|
+
# a) it loads configuration files and turns them into pathed hashes, and
|
18
|
+
# b) it treats environment variables as overriding anything contained in
|
19
|
+
# the configuration file.
|
20
|
+
#
|
21
|
+
# For configuration file loading, a named configuration file will be laoaded
|
22
|
+
# if present. A file with the same name but '-local' appended before the
|
23
|
+
# extension will be loaded as well, overriding any values in the original
|
24
|
+
# configuration file.
|
25
|
+
#
|
26
|
+
# For environment variable support, any environment variable named like a
|
27
|
+
# path into the configuration hash, but with separators transformed to
|
28
|
+
# underscore and all letters capitalized will override values from the
|
29
|
+
# configuration files under that path, i.e. FOO_BAR will override 'foo.bar'.
|
30
|
+
#
|
31
|
+
# Environment variables can contain JSON *only*; if the value can be parsed
|
32
|
+
# as JSON, it becomes a Hash in the configuration tree. If it cannot be parsed
|
33
|
+
# as JSON, it remains a string.
|
34
|
+
#
|
35
|
+
# Note: if your configuration file's top-level structure is an array, it will
|
36
|
+
# be returned as a hash with a 'config' key that maps to your file's contents.
|
37
|
+
#
|
38
|
+
class Config < PathedHash
|
39
|
+
# Very simple YAML parser
|
40
|
+
class YAMLParser
|
41
|
+
require 'yaml'
|
42
|
+
|
43
|
+
def self.parse(string)
|
44
|
+
YAML.load(string)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
# Very simple JSON parser
|
49
|
+
class JSONParser
|
50
|
+
require 'json'
|
51
|
+
|
52
|
+
def self.parse(string)
|
53
|
+
JSON.parse(string)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
PathedHash::READ_METHODS.each do |method|
|
58
|
+
# Wrap all read functions into something that checks for environment
|
59
|
+
# variables first.
|
60
|
+
define_method(method) do |*args, &block|
|
61
|
+
# We'll make it rather simple: since the first argument is a key, we
|
62
|
+
# will just transform it to the matching environment variable name,
|
63
|
+
# and see if that environment variable is set.
|
64
|
+
env_name = args[0].upcase.gsub(split_pattern, '_')
|
65
|
+
contents = ENV[env_name]
|
66
|
+
|
67
|
+
# No environment variable set? Fine, just do the usual thing.
|
68
|
+
if contents.nil?
|
69
|
+
return super(*args, &block)
|
70
|
+
end
|
71
|
+
|
72
|
+
# With an environment variable, we will try to parse it as JSON first.
|
73
|
+
begin
|
74
|
+
return JSONParser.parse(contents)
|
75
|
+
rescue JSON::ParserError
|
76
|
+
return contents
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
class << self
|
82
|
+
# Mapping of file name extensions to parser types.
|
83
|
+
FILE_TO_PARSER = {
|
84
|
+
'.yml' => YAMLParser,
|
85
|
+
'.yaml' => YAMLParser,
|
86
|
+
'.json' => JSONParser,
|
87
|
+
}.freeze
|
88
|
+
|
89
|
+
# If the config file contains an Array, this is what they key of the
|
90
|
+
# returned Hash will be.
|
91
|
+
ARRAY_KEY = 'config'.freeze
|
92
|
+
|
93
|
+
##
|
94
|
+
# Loads a configuration file with the given file name. The format is
|
95
|
+
# detected based on one of the extensions in FILE_TO_PARSER.
|
96
|
+
def load_config(path)
|
97
|
+
# Load base and local configuration files
|
98
|
+
base, config = load_base_config(path)
|
99
|
+
_, local_config = load_local_config(base)
|
100
|
+
|
101
|
+
# We can't sensibly merge arrays and hashes, so bail if the two classes
|
102
|
+
# don't match.
|
103
|
+
if config.class != local_config.class
|
104
|
+
raise ArgumentError, "Config file and local override file do not have "\
|
105
|
+
"the same top-level structure (hash or array), and therefore "\
|
106
|
+
"cannot be merged!"
|
107
|
+
end
|
108
|
+
|
109
|
+
# Merge
|
110
|
+
if config.is_a? Array
|
111
|
+
config = { ARRAY_KEY => config.push(*local_config) }
|
112
|
+
elsif config.is_a? Hash
|
113
|
+
# rubocop:disable Style/CaseEquality
|
114
|
+
merger = proc do |_, v1, v2|
|
115
|
+
Hash === v1 && Hash === v2 ? v1.merge(v2, &merger) : v2
|
116
|
+
end
|
117
|
+
# rubocop:enable Style/CaseEquality
|
118
|
+
config.merge!(local_config, &merger)
|
119
|
+
else
|
120
|
+
raise "Unexpected top-level structure in configuration file: "\
|
121
|
+
"#{config.class}"
|
122
|
+
end
|
123
|
+
|
124
|
+
return Config.new(config)
|
125
|
+
end
|
126
|
+
|
127
|
+
private
|
128
|
+
|
129
|
+
def load_base_config(path)
|
130
|
+
# Make sure the format is recognized early on.
|
131
|
+
base = Pathname.new(path)
|
132
|
+
formats = FILE_TO_PARSER.keys
|
133
|
+
if not formats.include?(base.extname)
|
134
|
+
raise ArgumentError, "Files with extension '#{base.extname}' are not"\
|
135
|
+
" recognized; please use one of #{formats}!"
|
136
|
+
end
|
137
|
+
|
138
|
+
# Don't check the path whether it exists - loading a nonexistent
|
139
|
+
# file will throw a nice error for the user to catch.
|
140
|
+
file = base.open
|
141
|
+
contents = file.read
|
142
|
+
|
143
|
+
# Parse the contents.
|
144
|
+
config = FILE_TO_PARSER[base.extname].parse(contents)
|
145
|
+
|
146
|
+
return base, config
|
147
|
+
end
|
148
|
+
|
149
|
+
def load_local_config(base)
|
150
|
+
# Now construct a file name for a local override.
|
151
|
+
local = Pathname.new(base.dirname)
|
152
|
+
local = local.join(base.basename(base.extname).to_s + "-local" +
|
153
|
+
base.extname)
|
154
|
+
puts local
|
155
|
+
if not local.exist?
|
156
|
+
return Config.new(config)
|
157
|
+
end
|
158
|
+
|
159
|
+
# We know the local override file exists, but we do want to let any errors
|
160
|
+
# go through that come with reading or parsing it.
|
161
|
+
file = local.open
|
162
|
+
contents = file.read
|
163
|
+
|
164
|
+
local_config = FILE_TO_PARSER[base.extname].parse(contents)
|
165
|
+
|
166
|
+
return local, local_config
|
167
|
+
end
|
168
|
+
end # class << self
|
169
|
+
end # class Config
|
170
|
+
end # module Unobtainium
|
data/lib/unobtainium/driver.rb
CHANGED
@@ -23,23 +23,77 @@ module Unobtainium
|
|
23
23
|
##
|
24
24
|
# Create a driver instance with the given arguments
|
25
25
|
def create(*args)
|
26
|
-
|
26
|
+
new(*args)
|
27
27
|
end
|
28
|
+
|
29
|
+
##
|
30
|
+
# Add a new driver implementation. The first parameter is the class
|
31
|
+
# itself, the second should be a file path pointing to the file where
|
32
|
+
# the class is defined. You would typically pass __FILE__ for the second
|
33
|
+
# parameter.
|
34
|
+
#
|
35
|
+
# Using file names lets us figure out whether the class is a duplicate,
|
36
|
+
# or merely a second registration of the same class.
|
37
|
+
def register_implementation(klass, path)
|
38
|
+
# We need to deal with absolute paths only
|
39
|
+
fpath = File.absolute_path(path)
|
40
|
+
|
41
|
+
# Figure out if the class implements all the methods we need; we're not
|
42
|
+
# checking for anything else.
|
43
|
+
klass_methods = klass.methods - klass.instance_methods - Object.methods
|
44
|
+
|
45
|
+
if DRIVER_METHODS - klass_methods != []
|
46
|
+
raise LoadError, "Driver #{klass.name} is not implementing all of "\
|
47
|
+
"the class methods #{DRIVER_METHODS}, aborting!"
|
48
|
+
end
|
49
|
+
|
50
|
+
# The second question is whether the same class is already known, or
|
51
|
+
# whether a class with the same name but under a different location is
|
52
|
+
# known.
|
53
|
+
if @@drivers.include?(klass) and @@drivers[klass] != fpath
|
54
|
+
raise LoadError, "Driver #{klass.name} is duplicated in file "\
|
55
|
+
"'#{fpath}'; previous definition is here: "\
|
56
|
+
"'#{@@drivers[klass]}'"
|
57
|
+
end
|
58
|
+
|
59
|
+
# If all of that was ok, we can register the implementation.
|
60
|
+
@@drivers[klass] = fpath
|
61
|
+
end
|
62
|
+
|
63
|
+
private :new
|
28
64
|
end # class << self
|
29
65
|
|
30
66
|
############################################################################
|
31
67
|
# Public methods
|
32
68
|
attr_reader :label, :options, :impl
|
33
69
|
|
70
|
+
##
|
71
|
+
# Map any missing method to the driver implementation
|
72
|
+
def respond_to?(meth)
|
73
|
+
if not @impl.nil? and @impl.respond_to?(meth)
|
74
|
+
return true
|
75
|
+
end
|
76
|
+
return super
|
77
|
+
end
|
78
|
+
|
79
|
+
def method_missing(meth, *args, &block)
|
80
|
+
if not @impl.nil? and @impl.respond_to?(meth)
|
81
|
+
return @impl.send(meth.to_s, *args, &block)
|
82
|
+
end
|
83
|
+
return super
|
84
|
+
end
|
85
|
+
|
86
|
+
private
|
87
|
+
|
34
88
|
##
|
35
89
|
# Initializer
|
36
90
|
def initialize(*args)
|
37
|
-
# Sanitize options
|
38
|
-
@label, @options = sanitize_options(*args)
|
39
|
-
|
40
91
|
# Load drivers
|
41
92
|
load_drivers
|
42
93
|
|
94
|
+
# Sanitize options
|
95
|
+
@label, @options = sanitize_options(*args)
|
96
|
+
|
43
97
|
# Determine the driver class, if any
|
44
98
|
driver_klass = get_driver(@label)
|
45
99
|
if not driver_klass
|
@@ -54,8 +108,6 @@ module Unobtainium
|
|
54
108
|
@impl = driver_klass.create(@label, @options)
|
55
109
|
end
|
56
110
|
|
57
|
-
private
|
58
|
-
|
59
111
|
# Class variables have their place, rubocop... still, err on the strict
|
60
112
|
# side and just skip this check here.
|
61
113
|
# rubocop:disable Style/ClassVars
|
@@ -70,18 +122,31 @@ module Unobtainium
|
|
70
122
|
].freeze
|
71
123
|
|
72
124
|
##
|
73
|
-
#
|
125
|
+
# Ensures arguments are according to expectations.
|
74
126
|
def sanitize_options(*args)
|
75
|
-
|
76
|
-
|
77
|
-
|
127
|
+
if args.empty?
|
128
|
+
raise ArgumentError, "Need at least one argument specifying the driver!"
|
129
|
+
end
|
130
|
+
|
131
|
+
label = args[0].to_sym
|
132
|
+
|
133
|
+
options = nil
|
134
|
+
if args.length > 1
|
135
|
+
if not args[1].is_a? Hash
|
136
|
+
raise ArgumentError, "The second argument is expected to be an options "\
|
137
|
+
"hash!"
|
138
|
+
end
|
139
|
+
options = args[1]
|
140
|
+
end
|
141
|
+
|
142
|
+
return label, options
|
78
143
|
end
|
79
144
|
|
80
145
|
##
|
81
|
-
# Load drivers.
|
146
|
+
# Load drivers; this loads all driver implementations included in this gem.
|
147
|
+
# You can register external implementations with the :register_implementation
|
148
|
+
# method.
|
82
149
|
def load_drivers
|
83
|
-
# TODO: add load path for external drivers, or let them be specified via
|
84
|
-
# the driver environment/config variables.
|
85
150
|
pattern = File.join(File.dirname(__FILE__), 'drivers', '*.rb')
|
86
151
|
Dir.glob(pattern).each do |fpath|
|
87
152
|
# Determine class name from file name
|
@@ -92,20 +157,7 @@ module Unobtainium
|
|
92
157
|
require fpath
|
93
158
|
klassname = 'Unobtainium::Drivers::' + fname
|
94
159
|
klass = Object.const_get(klassname)
|
95
|
-
|
96
|
-
|
97
|
-
if DRIVER_METHODS - klass_methods != []
|
98
|
-
raise LoadError, "Driver #{klassname} is not implementing all of "\
|
99
|
-
"#{DRIVER_METHODS}, aborting!"
|
100
|
-
end
|
101
|
-
|
102
|
-
if @@drivers.include?(klass) and @@drivers[klass] != fpath
|
103
|
-
raise LoadError, "Driver #{klassname} is duplicated in file "\
|
104
|
-
"'#{fpath}'; previous definition is here: "\
|
105
|
-
"'#{@@drivers[klass]}'"
|
106
|
-
end
|
107
|
-
@@drivers[klass] = fpath
|
108
|
-
|
160
|
+
Driver.register_implementation(klass, fpath)
|
109
161
|
rescue LoadError => err
|
110
162
|
raise LoadError, "#{err.message}: unknown problem loading driver, "\
|
111
163
|
"aborting!"
|
@@ -0,0 +1,121 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
#
|
3
|
+
# unobtainium
|
4
|
+
# https://github.com/jfinkhaeuser/unobtainium
|
5
|
+
#
|
6
|
+
# Copyright (c) 2016 Jens Finkhaeuser and other unobtainium contributors.
|
7
|
+
# All rights reserved.
|
8
|
+
#
|
9
|
+
|
10
|
+
module Unobtainium
|
11
|
+
|
12
|
+
##
|
13
|
+
# The PathedHash class wraps Hash by offering pathed access on top of
|
14
|
+
# regular access, i.e. instead of h["first"]["second"] you can write
|
15
|
+
# h["first.second"]
|
16
|
+
class PathedHash
|
17
|
+
##
|
18
|
+
# Initializer
|
19
|
+
def initialize(init = {})
|
20
|
+
@data = init
|
21
|
+
@separator = '.'
|
22
|
+
end
|
23
|
+
|
24
|
+
# The separator is the character or pattern splitting paths
|
25
|
+
attr_accessor :separator
|
26
|
+
|
27
|
+
READ_METHODS = [
|
28
|
+
:[], :default, :delete, :fetch, :has_key?, :include?, :key?, :member?,
|
29
|
+
].freeze
|
30
|
+
WRITE_METHODS = [
|
31
|
+
:[]=, :store,
|
32
|
+
].freeze
|
33
|
+
|
34
|
+
##
|
35
|
+
# Returns the pattern to split paths at
|
36
|
+
def split_pattern
|
37
|
+
/(?<!\\)#{Regexp.escape(@separator)}/
|
38
|
+
end
|
39
|
+
|
40
|
+
(READ_METHODS + WRITE_METHODS).each do |method|
|
41
|
+
# Wrap all accessor functions to deal with paths
|
42
|
+
define_method(method) do |*args, &block|
|
43
|
+
# With any of the dispatch methods, we know that the first argument has
|
44
|
+
# to be a key. We'll try to split it by the path separator.
|
45
|
+
components = args[0].to_s.split(split_pattern)
|
46
|
+
|
47
|
+
# This PathedHash is already the leaf-most Hash
|
48
|
+
if components.length == 1
|
49
|
+
return @data.send(method, *args, &block)
|
50
|
+
end
|
51
|
+
|
52
|
+
# Deal with other paths. The frustrating part here is that for nested
|
53
|
+
# hashes, only this outermost one is guaranteed to know anything about
|
54
|
+
# path splitting, so we'll have to recurse down to the leaf here.
|
55
|
+
#
|
56
|
+
# For write methods, we need to create intermediary hashes.
|
57
|
+
leaf = recursive_fetch(components, @data,
|
58
|
+
create: WRITE_METHODS.include?(method))
|
59
|
+
|
60
|
+
# If the leaf is nil, we can't send it any method without raising
|
61
|
+
# an error. We'll instead send the method to an empty hash, to mimic
|
62
|
+
# the correct behaviour.
|
63
|
+
if leaf.nil?
|
64
|
+
return {}.send(method, *args, &block)
|
65
|
+
end
|
66
|
+
|
67
|
+
# If we have a leaf, we want to send the requested method to that
|
68
|
+
# leaf.
|
69
|
+
copy = args.dup
|
70
|
+
copy[0] = components.last
|
71
|
+
return leaf.send(method, *copy, &block)
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
def to_s
|
76
|
+
@data.to_s
|
77
|
+
end
|
78
|
+
|
79
|
+
##
|
80
|
+
# Map any missing method to the driver implementation
|
81
|
+
def respond_to?(meth)
|
82
|
+
if not @data.nil? and @data.respond_to?(meth)
|
83
|
+
return true
|
84
|
+
end
|
85
|
+
return super
|
86
|
+
end
|
87
|
+
|
88
|
+
def method_missing(meth, *args, &block)
|
89
|
+
if not @data.nil? and @data.respond_to?(meth)
|
90
|
+
return @data.send(meth.to_s, *args, &block)
|
91
|
+
end
|
92
|
+
return super
|
93
|
+
end
|
94
|
+
|
95
|
+
private
|
96
|
+
|
97
|
+
##
|
98
|
+
# Given the path components, recursively fetch any but the last key.
|
99
|
+
def recursive_fetch(path, data, options = {})
|
100
|
+
# For the leaf element, we do nothing because that's where we want to
|
101
|
+
# dispatch to.
|
102
|
+
if path.length == 1
|
103
|
+
return data
|
104
|
+
end
|
105
|
+
|
106
|
+
# Split path into head and tail; for the next iteration, we'll look use only
|
107
|
+
# head, and pass tail on recursively.
|
108
|
+
head = path[0]
|
109
|
+
tail = path.slice(1, path.length)
|
110
|
+
|
111
|
+
# If we're a write function, then we need to create intermediary objects,
|
112
|
+
# i.e. what's at head if nothing is there.
|
113
|
+
if options[:create] and data.fetch(head, nil).nil?
|
114
|
+
data[head] = {}
|
115
|
+
end
|
116
|
+
|
117
|
+
# Ok, recurse.
|
118
|
+
return recursive_fetch(tail, data[head], options)
|
119
|
+
end
|
120
|
+
end # class PathedHash
|
121
|
+
end # module Unobtainium
|
@@ -0,0 +1,126 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
#
|
3
|
+
# unobtainium
|
4
|
+
# https://github.com/jfinkhaeuser/unobtainium
|
5
|
+
#
|
6
|
+
# Copyright (c) 2016 Jens Finkhaeuser and other unobtainium contributors.
|
7
|
+
# All rights reserved.
|
8
|
+
#
|
9
|
+
require 'singleton'
|
10
|
+
|
11
|
+
module Unobtainium
|
12
|
+
|
13
|
+
##
|
14
|
+
# The Runtime class is a singleton scoped to destroy itself when script
|
15
|
+
# execution stops. It's also an object map, which will destroy all object
|
16
|
+
# it contains when it destroys itself.
|
17
|
+
#
|
18
|
+
# Therefore, it can be used as a way to register object instances for
|
19
|
+
# destruction at script end.
|
20
|
+
class Runtime
|
21
|
+
include Singleton
|
22
|
+
|
23
|
+
##
|
24
|
+
# Initializer
|
25
|
+
def initialize
|
26
|
+
@objects = {}
|
27
|
+
|
28
|
+
# Create our own finalizer
|
29
|
+
ObjectSpace.define_finalizer(self) do
|
30
|
+
@objects.keys.each do |key|
|
31
|
+
unset(key)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
##
|
37
|
+
# Number of objects stored in the object map
|
38
|
+
def length
|
39
|
+
return @objects.length
|
40
|
+
end
|
41
|
+
|
42
|
+
##
|
43
|
+
# Does an object with the given name exist?
|
44
|
+
def has?(name)
|
45
|
+
return @objects.key?(name)
|
46
|
+
end
|
47
|
+
|
48
|
+
##
|
49
|
+
# Store the given object under the given name. This overwrites any objects
|
50
|
+
# already stored under that name, which are destroyed before the new object
|
51
|
+
# is stored.
|
52
|
+
#
|
53
|
+
# If a destructor is passed, it is used to destroy the *new* object only.
|
54
|
+
# If no destructor is passed and the object responds to a :destroy method, that
|
55
|
+
# method is called.
|
56
|
+
def set(name, object, destructor = nil)
|
57
|
+
unset(name)
|
58
|
+
|
59
|
+
@objects[name] = [object, destructor]
|
60
|
+
|
61
|
+
return object
|
62
|
+
end
|
63
|
+
|
64
|
+
##
|
65
|
+
# Store the object returned by the block, if any. If no object is returned
|
66
|
+
# or no block is given, this function does nothing.
|
67
|
+
#
|
68
|
+
# Otherwise it works much like :set above.
|
69
|
+
def set_with(name, destructor = nil, &block)
|
70
|
+
object = nil
|
71
|
+
if not block.nil?
|
72
|
+
object = yield
|
73
|
+
end
|
74
|
+
|
75
|
+
if object.nil?
|
76
|
+
return
|
77
|
+
end
|
78
|
+
|
79
|
+
return set(name, object, destructor)
|
80
|
+
end
|
81
|
+
|
82
|
+
##
|
83
|
+
# Unsets (and destroys) any object found under the given name.
|
84
|
+
def unset(name)
|
85
|
+
if not @objects.key?(name)
|
86
|
+
return
|
87
|
+
end
|
88
|
+
|
89
|
+
obj, dtor = @objects[name]
|
90
|
+
@objects.delete(name)
|
91
|
+
destroy(obj, dtor)
|
92
|
+
end
|
93
|
+
|
94
|
+
##
|
95
|
+
# Returns the object with the given name, or the default value if no such
|
96
|
+
# object exists.
|
97
|
+
def fetch(name, default = nil)
|
98
|
+
return @objects.fetch(name, default)
|
99
|
+
end
|
100
|
+
|
101
|
+
##
|
102
|
+
# Similar to :fetch, but always returns nil for an object that could not
|
103
|
+
# be found.
|
104
|
+
def [](name)
|
105
|
+
return @objects[name]
|
106
|
+
end
|
107
|
+
|
108
|
+
private
|
109
|
+
|
110
|
+
##
|
111
|
+
# Destroy the given object with the destructor provided.
|
112
|
+
def destroy(object, destructor)
|
113
|
+
if not destructor.nil?
|
114
|
+
destructor.call
|
115
|
+
return
|
116
|
+
end
|
117
|
+
|
118
|
+
if not object.respond_to?(:destroy)
|
119
|
+
return
|
120
|
+
end
|
121
|
+
|
122
|
+
object.send(:destroy)
|
123
|
+
end
|
124
|
+
end # class Runtime
|
125
|
+
|
126
|
+
end # module Unobtainium
|
data/lib/unobtainium/version.rb
CHANGED
data/unobtainium.gemspec
CHANGED
@@ -20,11 +20,8 @@ Gem::Specification.new do |spec|
|
|
20
20
|
spec.email = ["jens@finkhaeuser.de"]
|
21
21
|
spec.description = %q(
|
22
22
|
Unobtainium wraps Selenium and Appium in a simple driver abstraction so that
|
23
|
-
test code can more easily cover
|
24
|
-
|
25
|
-
- Desktop browsers
|
26
|
-
- Mobile browsers
|
27
|
-
- Mobile apps
|
23
|
+
test code can more easily cover desktop browsers, mobile browsers and mobile
|
24
|
+
apps.
|
28
25
|
|
29
26
|
Some additional useful functionality for the maintenance of test suites is
|
30
27
|
also added.
|
@@ -40,6 +37,10 @@ Gem::Specification.new do |spec|
|
|
40
37
|
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
41
38
|
spec.require_paths = ["lib"]
|
42
39
|
|
40
|
+
spec.required_ruby_version = '>= 1.9'
|
41
|
+
|
42
|
+
spec.requirements = "Either or all of 'selenium-webdriver', 'appium_lib'"
|
43
|
+
|
43
44
|
spec.add_development_dependency "bundler", "~> 1.11"
|
44
45
|
spec.add_development_dependency "rubocop", "~> 0.39"
|
45
46
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: unobtainium
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Jens Finkhaeuser
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2016-04-
|
11
|
+
date: 2016-04-08 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -39,9 +39,9 @@ dependencies:
|
|
39
39
|
- !ruby/object:Gem::Version
|
40
40
|
version: '0.39'
|
41
41
|
description: "\n Unobtainium wraps Selenium and Appium in a simple driver abstraction
|
42
|
-
so that\n test code can more easily cover
|
43
|
-
|
44
|
-
|
42
|
+
so that\n test code can more easily cover desktop browsers, mobile browsers and
|
43
|
+
mobile\n apps.\n\n Some additional useful functionality for the maintenance
|
44
|
+
of test suites is\n also added.\n "
|
45
45
|
email:
|
46
46
|
- jens@finkhaeuser.de
|
47
47
|
executables: []
|
@@ -55,9 +55,12 @@ files:
|
|
55
55
|
- LICENSE
|
56
56
|
- README.md
|
57
57
|
- lib/unobtainium.rb
|
58
|
+
- lib/unobtainium/config.rb
|
58
59
|
- lib/unobtainium/driver.rb
|
59
60
|
- lib/unobtainium/drivers/appium.rb
|
60
61
|
- lib/unobtainium/drivers/selenium.rb
|
62
|
+
- lib/unobtainium/pathed_hash.rb
|
63
|
+
- lib/unobtainium/runtime.rb
|
61
64
|
- lib/unobtainium/version.rb
|
62
65
|
- unobtainium.gemspec
|
63
66
|
homepage: https://github.com/jfinkhaeuser/unobtainium
|
@@ -72,13 +75,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
72
75
|
requirements:
|
73
76
|
- - ">="
|
74
77
|
- !ruby/object:Gem::Version
|
75
|
-
version: '
|
78
|
+
version: '1.9'
|
76
79
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
77
80
|
requirements:
|
78
81
|
- - ">="
|
79
82
|
- !ruby/object:Gem::Version
|
80
83
|
version: '0'
|
81
|
-
requirements:
|
84
|
+
requirements:
|
85
|
+
- Either or all of 'selenium-webdriver', 'appium_lib'
|
82
86
|
rubyforge_project:
|
83
87
|
rubygems_version: 2.4.5.1
|
84
88
|
signing_key:
|