unobtainium 0.0.2 → 0.1.0

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 194953d1b77be115316598397b69e99037647085
4
- data.tar.gz: 9194ccf829bcf5c6e98027251716b8598ea1979b
3
+ metadata.gz: 9464c7e860902fed8b6f41813261a19368aea2aa
4
+ data.tar.gz: f9498b642d79da6d09cbebd854bda897c6b737d1
5
5
  SHA512:
6
- metadata.gz: 2e43f762d2c90323173cdfeedc2d57e764114558282066bd28a9208ed1fb175deb71917b23cbc135e8f476770e5c396414a572dcb2814ecd5e93449999cb1a8a
7
- data.tar.gz: 58fd4916229bf0528304c9cb18541f5b6f2431e958da9f7154b70f1a553ffc04e26be791a02442c0e858ce45e1d42e98bb8c8f9f48e6f9a7fa6866843432baac
6
+ metadata.gz: 07d45f57984c9ffe4d7c12d8fa7a189a6e22e0638a4dab0bed898f423ba73a9ae0f484247d0c23824513bf93f3eedfc32b7e4212e6515d751611c2b1c34eaee7
7
+ data.tar.gz: c1378459dc8affa7f5d2a2ce96cfcf5d240b2bf45c83158be0a1d3bf81795ae5024821c99eb3cd3e00617e606cea21c6b43f7ea3eda29b70213a43c5a78f3d30
data/.codeclimate.yml ADDED
@@ -0,0 +1,33 @@
1
+ ---
2
+ engines:
3
+ bundler-audit:
4
+ enabled: true
5
+ duplication:
6
+ enabled: true
7
+ exclude_fingerprints:
8
+ - d85d6f12c93d79ccd43868b8315d8816
9
+ - a8e2b4ccb258f16eda697c5f98e21823
10
+ - 442a316695836b4f4693fe3786cd3396
11
+ - 1c24bb5da72323796b645814cc006684
12
+ config:
13
+ languages:
14
+ - ruby
15
+ - javascript
16
+ - python
17
+ - php
18
+ fixme:
19
+ enabled: true
20
+ rubocop:
21
+ enabled: true
22
+ ratings:
23
+ paths:
24
+ - Gemfile.lock
25
+ - "**.inc"
26
+ - "**.js"
27
+ - "**.jsx"
28
+ - "**.module"
29
+ - "**.php"
30
+ - "**.py"
31
+ - "**.rb"
32
+ exclude_paths:
33
+ - spec/
data/.rubocop.yml CHANGED
@@ -52,3 +52,6 @@ Style/IfUnlessModifier:
52
52
 
53
53
  Style/TrailingCommaInLiteral:
54
54
  Enabled: false
55
+
56
+ Style/FirstParameterIndentation:
57
+ Enabled: false
data/.travis.yml ADDED
@@ -0,0 +1,8 @@
1
+ language: ruby
2
+ rvm:
3
+ - 2.0
4
+ - 2.1
5
+ - 2.2
6
+ addons:
7
+ code_climate:
8
+ repo_token: 5dcf014e3a26ded8ffe8ff8bc70f98d49523e4851613123d8359035d44937953
data/Gemfile CHANGED
@@ -2,3 +2,5 @@ source 'https://rubygems.org'
2
2
 
3
3
  # Specify your gem's dependencies in unobtainium.gemspec
4
4
  gemspec
5
+
6
+ gem "codeclimate-test-reporter", group: :test, require: nil
data/Gemfile.lock CHANGED
@@ -1,16 +1,35 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- unobtainium (0.0.1)
4
+ unobtainium (0.1.0)
5
5
 
6
6
  GEM
7
7
  remote: https://rubygems.org/
8
8
  specs:
9
9
  ast (2.2.0)
10
+ codeclimate-test-reporter (0.5.0)
11
+ simplecov (>= 0.7.1, < 1.0.0)
12
+ diff-lcs (1.2.5)
13
+ docile (1.1.5)
14
+ json (1.8.3)
10
15
  parser (2.3.0.7)
11
16
  ast (~> 2.2)
12
17
  powerpack (0.1.1)
13
18
  rainbow (2.1.0)
19
+ rake (11.1.2)
20
+ rspec (3.4.0)
21
+ rspec-core (~> 3.4.0)
22
+ rspec-expectations (~> 3.4.0)
23
+ rspec-mocks (~> 3.4.0)
24
+ rspec-core (3.4.4)
25
+ rspec-support (~> 3.4.0)
26
+ rspec-expectations (3.4.0)
27
+ diff-lcs (>= 1.2.0, < 2.0)
28
+ rspec-support (~> 3.4.0)
29
+ rspec-mocks (3.4.1)
30
+ diff-lcs (>= 1.2.0, < 2.0)
31
+ rspec-support (~> 3.4.0)
32
+ rspec-support (3.4.1)
14
33
  rubocop (0.39.0)
15
34
  parser (>= 2.3.0.7, < 3.0)
16
35
  powerpack (~> 0.1)
@@ -18,6 +37,11 @@ GEM
18
37
  ruby-progressbar (~> 1.7)
19
38
  unicode-display_width (~> 1.0, >= 1.0.1)
20
39
  ruby-progressbar (1.7.5)
40
+ simplecov (0.11.2)
41
+ docile (~> 1.1.0)
42
+ json (~> 1.8)
43
+ simplecov-html (~> 0.10.0)
44
+ simplecov-html (0.10.0)
21
45
  unicode-display_width (1.0.3)
22
46
 
23
47
  PLATFORMS
@@ -25,7 +49,11 @@ PLATFORMS
25
49
 
26
50
  DEPENDENCIES
27
51
  bundler (~> 1.11)
52
+ codeclimate-test-reporter
53
+ rake (~> 11.1)
54
+ rspec (~> 3.4)
28
55
  rubocop (~> 0.39)
56
+ simplecov (~> 0.11)
29
57
  unobtainium!
30
58
 
31
59
  BUNDLED WITH
data/README.md CHANGED
@@ -13,6 +13,63 @@ Some additional useful functionality for the maintenance of test suites is
13
13
  also added.
14
14
 
15
15
  [![Gem Version](https://badge.fury.io/rb/unobtainium.svg)](https://badge.fury.io/rb/unobtainium)
16
+ [![Build status](https://travis-ci.org/jfinkhaeuser/unobtainium.svg?branch=master)](https://travis-ci.org/jfinkhaeuser/unobtainium)
17
+ [![Code Climate](https://codeclimate.com/github/jfinkhaeuser/unobtainium/badges/gpa.svg)](https://codeclimate.com/github/jfinkhaeuser/unobtainium)
18
+ [![Test Coverage](https://codeclimate.com/github/jfinkhaeuser/unobtainium/badges/coverage.svg)](https://codeclimate.com/github/jfinkhaeuser/unobtainium/coverage)
19
+
20
+ # Usage
21
+
22
+ You can use unobtainium on its own, or use it as part of a
23
+ [cucumber](https://cucumber.io/) test suite.
24
+
25
+ Unobtainium's functionality is in standalone classes, but it's all combined in
26
+ the `Unobtainium::World` module.
27
+
28
+ - The `PathedHash` class extends `Hash` by allowing paths to nested values, e.g.:
29
+ ```ruby
30
+ h = PathedHash.new { "foo" => { "bar" => 42 }}
31
+ h["foo.bar"] == 42 # true
32
+ ```
33
+ - The `Config` class is a `PathedHash`, but also reads JSON or YAML files to
34
+ initialize itself with values, and allows config paths to be overridden by
35
+ environment variables: `FOO_BAR` overrides the "foo.bar" path.
36
+
37
+ You can use JSON as environment variable values.
38
+ - The `Runtime` class is a singleton and a `Hash`-like container (but simpler),
39
+ that destroys all of its contents at the end of a script, calling custom
40
+ destructors if required. That allows for clean teardown and avoids everything
41
+ to have to implement the Singleton pattern itself.
42
+ - The `Driver` class, of course, wraps either of Appium or Selenium drivers:
43
+ ```ruby
44
+ drv = Driver.create(:firefox) # uses Selenium
45
+ drv = Driver.create(:android) # uses Appium
46
+
47
+ drv.navigate.to "..." # delegates to Selenium or Appium
48
+ ```
49
+
50
+ ## World
51
+
52
+ The World module combines all of the above by providing a simple entry point
53
+ for everything:
54
+
55
+ - `World.config_file` can be set to the path of a config file to be loaded,
56
+ defaulting to `config/config.yml`.
57
+ - `World#config` is a `Config` instance containing the above file's contents.
58
+ - `World#driver` returns a Driver, initialized to the settings contained in
59
+ the configuration file.
60
+
61
+ For a simple usage example of the World module, see the [cuke](./cuke)
62
+ subdirectory (used with cucumber).
63
+
64
+ ## Configuration File
65
+
66
+ The configuration file knows two configuration variables:
67
+
68
+ - `driver` is expected to be a string, specifying the driver to use as if it
69
+ was passed to `Driver.create` (see above), e.g. "android", "chrome", etc.
70
+ - `drivers` (note the trailing s) is a Hash. Under each key you can nest an
71
+ options hash you might otherwise pass to `Driver.create` as the second
72
+ parameter.
16
73
 
17
74
  # Credits
18
75
  This gem is inspired by [LapisLazuli](https://github.com/spriteCloud/lapis-lazuli),
data/Rakefile ADDED
@@ -0,0 +1,17 @@
1
+ # Rubocop
2
+ require 'rubocop/rake_task'
3
+ RuboCop::RakeTask.new
4
+
5
+ # Rspec
6
+ require 'rspec/core/rake_task'
7
+ RSpec::Core::RakeTask.new(:spec)
8
+
9
+ # Combined test task
10
+ desc "Test the code"
11
+ task :test do
12
+ Rake::Task[:rubocop].invoke
13
+ Rake::Task[:spec].invoke
14
+ end
15
+
16
+ # Default is the test task
17
+ task default: :test
data/cuke/.gitignore ADDED
@@ -0,0 +1,2 @@
1
+ C:\\nppdf32Log\\debuglog.txt
2
+ config/*-local.yml
data/cuke/Gemfile ADDED
@@ -0,0 +1,13 @@
1
+ source 'http://rubygems.org'
2
+
3
+ # Cucumber
4
+ gem 'cucumber'
5
+
6
+ # For drivers
7
+ gem 'appium_lib'
8
+ gem 'selenium-webdriver'
9
+
10
+ # unobtainium itself; test code should point to the current repo and branch
11
+ repo = `git config --get remote.origin.url`.strip
12
+ branch = `git rev-parse --abbrev-ref HEAD`.strip
13
+ gem 'unobtainium', git: repo, branch: branch
data/cuke/README.md ADDED
@@ -0,0 +1,2 @@
1
+ This directory contains tests for the integration with cucumber. You can also
2
+ run limited tests with the Selenium and Appium drivers from here.
@@ -0,0 +1,6 @@
1
+ ---
2
+ drivers:
3
+ firefox:
4
+ android:
5
+ browser: chrome
6
+ driver: firefox
@@ -0,0 +1,12 @@
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
+ Given(/^I navigate to the best website in the world$/) do
11
+ driver.navigate.to "http://finkhaeuser.de"
12
+ end
@@ -0,0 +1,11 @@
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 'unobtainium'
10
+
11
+ World(Unobtainium::World)
@@ -0,0 +1,4 @@
1
+ Feature: World
2
+
3
+ Scenario: Driver and configuration loading
4
+ Given I navigate to the best website in the world
data/lib/unobtainium.rb CHANGED
@@ -10,3 +10,6 @@
10
10
  require 'unobtainium/version'
11
11
 
12
12
  require 'unobtainium/driver'
13
+ require 'unobtainium/config'
14
+ require 'unobtainium/runtime'
15
+ require 'unobtainium/world'
@@ -14,9 +14,9 @@ require 'unobtainium/pathed_hash'
14
14
  module Unobtainium
15
15
  ##
16
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.
17
+ # - it loads configuration files and turns them into pathed hashes, and
18
+ # - it treats environment variables as overriding anything contained in
19
+ # the configuration file.
20
20
  #
21
21
  # For configuration file loading, a named configuration file will be laoaded
22
22
  # if present. A file with the same name but '-local' appended before the
@@ -34,7 +34,8 @@ module Unobtainium
34
34
  #
35
35
  # Note: if your configuration file's top-level structure is an array, it will
36
36
  # be returned as a hash with a 'config' key that maps to your file's contents.
37
- #
37
+ # That means that if you are trying to merge a hash with an array config, the
38
+ # result may be unexpected.
38
39
  class Config < PathedHash
39
40
  # Very simple YAML parser
40
41
  class YAMLParser
@@ -97,29 +98,22 @@ module Unobtainium
97
98
  # Load base and local configuration files
98
99
  base, config = load_base_config(path)
99
100
  _, 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!"
101
+ if local_config.nil?
102
+ return Config.new(config)
107
103
  end
108
104
 
109
105
  # 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
106
+ merger = proc do |_, v1, v2|
107
+ # rubocop:disable Style/GuardClause
108
+ if v1.is_a? Hash and v2.is_a? Hash
109
+ next v1.merge(v2, &merger)
110
+ elsif v1.is_a? Array and v2.is_a? Array
111
+ next v1 + v2
116
112
  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}"
113
+ next v2
114
+ # rubocop:enable Style/GuardClause
122
115
  end
116
+ config.merge!(local_config, &merger)
123
117
 
124
118
  return Config.new(config)
125
119
  end
@@ -143,7 +137,7 @@ module Unobtainium
143
137
  # Parse the contents.
144
138
  config = FILE_TO_PARSER[base.extname].parse(contents)
145
139
 
146
- return base, config
140
+ return base, hashify(config)
147
141
  end
148
142
 
149
143
  def load_local_config(base)
@@ -151,9 +145,8 @@ module Unobtainium
151
145
  local = Pathname.new(base.dirname)
152
146
  local = local.join(base.basename(base.extname).to_s + "-local" +
153
147
  base.extname)
154
- puts local
155
148
  if not local.exist?
156
- return Config.new(config)
149
+ return local, nil
157
150
  end
158
151
 
159
152
  # We know the local override file exists, but we do want to let any errors
@@ -163,7 +156,17 @@ module Unobtainium
163
156
 
164
157
  local_config = FILE_TO_PARSER[base.extname].parse(contents)
165
158
 
166
- return local, local_config
159
+ return local, hashify(local_config)
160
+ end
161
+
162
+ def hashify(data)
163
+ if data.nil?
164
+ return {}
165
+ end
166
+ if data.is_a? Array
167
+ data = { ARRAY_KEY => data }
168
+ end
169
+ return data
167
170
  end
168
171
  end # class << self
169
172
  end # class Config
@@ -132,7 +132,7 @@ module Unobtainium
132
132
 
133
133
  options = nil
134
134
  if args.length > 1
135
- if not args[1].is_a? Hash
135
+ if not args[1].nil? and not args[1].is_a? Hash
136
136
  raise ArgumentError, "The second argument is expected to be an options "\
137
137
  "hash!"
138
138
  end
@@ -28,7 +28,7 @@ module Unobtainium
28
28
  # Create our own finalizer
29
29
  ObjectSpace.define_finalizer(self) do
30
30
  @objects.keys.each do |key|
31
- unset(key)
31
+ delete(key)
32
32
  end
33
33
  end
34
34
  end
@@ -53,8 +53,8 @@ module Unobtainium
53
53
  # If a destructor is passed, it is used to destroy the *new* object only.
54
54
  # If no destructor is passed and the object responds to a :destroy method, that
55
55
  # method is called.
56
- def set(name, object, destructor = nil)
57
- unset(name)
56
+ def store(name, object, destructor = nil)
57
+ delete(name)
58
58
 
59
59
  @objects[name] = [object, destructor]
60
60
 
@@ -65,8 +65,8 @@ module Unobtainium
65
65
  # Store the object returned by the block, if any. If no object is returned
66
66
  # or no block is given, this function does nothing.
67
67
  #
68
- # Otherwise it works much like :set above.
69
- def set_with(name, destructor = nil, &block)
68
+ # Otherwise it works much like :store above.
69
+ def store_with(name, destructor = nil, &block)
70
70
  object = nil
71
71
  if not block.nil?
72
72
  object = yield
@@ -76,12 +76,30 @@ module Unobtainium
76
76
  return
77
77
  end
78
78
 
79
- return set(name, object, destructor)
79
+ return store(name, object, destructor)
80
80
  end
81
81
 
82
82
  ##
83
- # Unsets (and destroys) any object found under the given name.
84
- def unset(name)
83
+ # Like :store, but only stores the object if none exists for that key yet.
84
+ def store_if(name, object, destructor = nil)
85
+ if has?(name)
86
+ return self[name]
87
+ end
88
+ return store(name, object, destructor)
89
+ end
90
+
91
+ ##
92
+ # Like :store_if, but as a block version similar to :store_with.
93
+ def store_with_if(name, destructor = nil, &block)
94
+ if has?(name)
95
+ return self[name]
96
+ end
97
+ return store_with(name, destructor, &block)
98
+ end
99
+
100
+ ##
101
+ # Deletes (and destroys) any object found under the given name.
102
+ def delete(name)
85
103
  if not @objects.key?(name)
86
104
  return
87
105
  end
@@ -95,14 +113,23 @@ module Unobtainium
95
113
  # Returns the object with the given name, or the default value if no such
96
114
  # object exists.
97
115
  def fetch(name, default = nil)
98
- return @objects.fetch(name, default)
116
+ return @objects.fetch(name)[0]
117
+ rescue KeyError
118
+ if default.nil?
119
+ raise
120
+ end
121
+ return default
99
122
  end
100
123
 
101
124
  ##
102
125
  # Similar to :fetch, but always returns nil for an object that could not
103
126
  # be found.
104
127
  def [](name)
105
- return @objects[name]
128
+ val = @objects[name]
129
+ if val.nil?
130
+ return nil
131
+ end
132
+ return val[0]
106
133
  end
107
134
 
108
135
  private
@@ -111,8 +138,7 @@ module Unobtainium
111
138
  # Destroy the given object with the destructor provided.
112
139
  def destroy(object, destructor)
113
140
  if not destructor.nil?
114
- destructor.call
115
- return
141
+ return destructor.call(object)
116
142
  end
117
143
 
118
144
  if not object.respond_to?(:destroy)
@@ -7,5 +7,5 @@
7
7
  # All rights reserved.
8
8
  #
9
9
  module Unobtainium
10
- VERSION = "0.0.2".freeze
10
+ VERSION = "0.1.0".freeze
11
11
  end
@@ -0,0 +1,79 @@
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 'unobtainium'
10
+
11
+ require 'unobtainium/driver'
12
+ require 'unobtainium/config'
13
+ require 'unobtainium/runtime'
14
+
15
+ module Unobtainium
16
+ ##
17
+ # The World module combines other modules, defining simpler entry points
18
+ # into the gem's functionality.
19
+ module World
20
+ ##
21
+ # Modules can have class methods, too.
22
+ module ClassMethods
23
+ # Configuration related
24
+ def config_file=(name)
25
+ @config_file = name
26
+ end
27
+
28
+ def config_file
29
+ return @config_file || "config/config.yml"
30
+ end
31
+ end # module ClassMethods
32
+ extend ClassMethods
33
+
34
+ ##
35
+ # Return the global configuration, loaded from :config_file
36
+ def config
37
+ return ::Unobtainium::Runtime.instance.store_with_if(:config) do
38
+ ::Unobtainium::Config.load_config(::Unobtainium::World.config_file)
39
+ end
40
+ end
41
+
42
+ ##
43
+ # Returns a driver instance with the given options. If no options are
44
+ # provided, options from the global configuration are used.
45
+ def driver(label = nil, options = nil)
46
+ # Make sure we have a label for the driver
47
+ if label.nil?
48
+ label = config["driver"]
49
+ end
50
+
51
+ # Make sure we have options matching the driver
52
+ if options.nil?
53
+ options = config["drivers.#{label}"]
54
+ end
55
+
56
+ # Create a key for the label and options. This should always
57
+ # return the same key for the same label and options.
58
+ key = { label: label, options: options }
59
+ require 'digest/sha1'
60
+ key = Digest::SHA1.hexdigest(key.to_s)
61
+ key = "driver-#{key}"
62
+
63
+ # Only create a driver with this exact configuration once
64
+ dtor = ::Unobtainium::World.method(:driver_destructor)
65
+ return ::Unobtainium::Runtime.instance.store_with_if(key, dtor) do
66
+ ::Unobtainium::Driver.create(label, options)
67
+ end
68
+ end
69
+
70
+ class << self
71
+ def driver_destructor(the_driver = nil)
72
+ if the_driver.nil?
73
+ return
74
+ end
75
+ the_driver.close
76
+ end
77
+ end
78
+ end # module World
79
+ end # module Unobtainium
@@ -0,0 +1,82 @@
1
+ require 'spec_helper'
2
+ require_relative '../lib/unobtainium/config'
3
+
4
+ describe ::Unobtainium::Config do
5
+ before :each do
6
+ @data_path = File.join(File.dirname(__FILE__), 'data')
7
+ end
8
+
9
+ it "fails to load a nonexistent file" do
10
+ expect { ::Unobtainium::Config.load_config("_nope_.yaml") }.to \
11
+ raise_error Errno::ENOENT
12
+ end
13
+
14
+ it "is asked to load an unrecognized extension" do
15
+ expect { ::Unobtainium::Config.load_config("_nope_.cfg") }.to \
16
+ raise_error ArgumentError
17
+ end
18
+
19
+ it "loads a yaml config with a top-level hash correctly" do
20
+ config = File.join(@data_path, 'hash.yml')
21
+ cfg = ::Unobtainium::Config.load_config(config)
22
+
23
+ expect(cfg["foo"]).to eql "bar"
24
+ expect(cfg["baz"]).to eql "quux"
25
+ end
26
+
27
+ it "loads a yaml config with a top-level array correctly" do
28
+ config = File.join(@data_path, 'array.yaml')
29
+ cfg = ::Unobtainium::Config.load_config(config)
30
+
31
+ expect(cfg["config"]).to eql %w(foo bar)
32
+ end
33
+
34
+ it "loads a JSON config correctly" do
35
+ config = File.join(@data_path, 'test.json')
36
+ cfg = ::Unobtainium::Config.load_config(config)
37
+
38
+ expect(cfg["foo"]).to eql "bar"
39
+ expect(cfg["baz"]).to eql 42
40
+ end
41
+
42
+ it "merges a hashed config correctly" do
43
+ config = File.join(@data_path, 'hashmerge.yml')
44
+ cfg = ::Unobtainium::Config.load_config(config)
45
+
46
+ expect(cfg["asdf"]).to eql 1
47
+ expect(cfg["foo.bar"]).to eql "baz"
48
+ expect(cfg["foo.quux"]).to eql [1, 42]
49
+ expect(cfg["foo.baz"]).to eql 3.14
50
+ expect(cfg["blargh"]).to eql false
51
+ end
52
+
53
+ it "merges an array config correctly" do
54
+ config = File.join(@data_path, 'arraymerge.yaml')
55
+ cfg = ::Unobtainium::Config.load_config(config)
56
+
57
+ expect(cfg["config"]).to eql %w(foo bar baz)
58
+ end
59
+
60
+ it "merges an array and hash config" do
61
+ config = File.join(@data_path, 'mergefail.yaml')
62
+ cfg = ::Unobtainium::Config.load_config(config)
63
+
64
+ expect(cfg["config"]).to eql %w(array in main config)
65
+ expect(cfg["local"]).to eql "override is a hash"
66
+ end
67
+
68
+ it "overrides configuration variables from the environment" do
69
+ config = File.join(@data_path, 'hash.yml')
70
+ cfg = ::Unobtainium::Config.load_config(config)
71
+
72
+ ENV["BAZ"] = "override"
73
+ expect(cfg["foo"]).to eql "bar"
74
+ expect(cfg["baz"]).to eql "override"
75
+ end
76
+
77
+ it "treats an empty YAML file as an empty hash" do
78
+ config = File.join(@data_path, 'empty.yml')
79
+ cfg = ::Unobtainium::Config.load_config(config)
80
+ expect(cfg).to be_empty
81
+ end
82
+ end
@@ -0,0 +1,3 @@
1
+ ---
2
+ - foo
3
+ - bar
@@ -0,0 +1,2 @@
1
+ ---
2
+ - baz
@@ -0,0 +1,3 @@
1
+ ---
2
+ - foo
3
+ - bar
@@ -0,0 +1 @@
1
+ ---
@@ -0,0 +1,3 @@
1
+ ---
2
+ foo: bar
3
+ baz: quux
@@ -0,0 +1,4 @@
1
+ ---
2
+ blargh: false
3
+ foo:
4
+ baz: 3.14
@@ -0,0 +1,7 @@
1
+ ---
2
+ asdf: 1
3
+ foo:
4
+ bar: baz
5
+ quux:
6
+ - 1
7
+ - 42
@@ -0,0 +1,2 @@
1
+ ---
2
+ local: override is a hash
@@ -0,0 +1,5 @@
1
+ ---
2
+ - array
3
+ - in
4
+ - main
5
+ - config
@@ -0,0 +1,4 @@
1
+ {
2
+ "foo": "bar",
3
+ "baz": 42
4
+ }
@@ -0,0 +1,5 @@
1
+ ---
2
+ drivers:
3
+ mock:
4
+ option: value
5
+ driver: mock
@@ -0,0 +1,65 @@
1
+ require 'spec_helper'
2
+ require_relative '../lib/unobtainium/driver'
3
+ require_relative './mock_driver.rb'
4
+
5
+ class FakeDriver
6
+ end # class FakeDriver
7
+
8
+ describe ::Unobtainium::Driver do
9
+ before :each do
10
+ ::Unobtainium::Driver.register_implementation(MockDriver, "mock_driver.rb")
11
+ end
12
+
13
+ it "refuses to register a driver with missing methods" do
14
+ expect do
15
+ ::Unobtainium::Driver.register_implementation(FakeDriver, __FILE__)
16
+ end.to raise_error(LoadError)
17
+ end
18
+
19
+ it "refuses to register the same driver twice from different locations" do
20
+ expect do
21
+ ::Unobtainium::Driver.register_implementation(MockDriver, __FILE__)
22
+ end.to raise_error(LoadError)
23
+ end
24
+
25
+ it "verifies arguments" do
26
+ expect { ::Unobtainium::Driver.create }.to raise_error(ArgumentError)
27
+
28
+ expect do
29
+ ::Unobtainium::Driver.create(:mock, 1)
30
+ end.to raise_error(ArgumentError)
31
+
32
+ expect do
33
+ ::Unobtainium::Driver.create(:mock, [])
34
+ end.to raise_error(ArgumentError)
35
+
36
+ expect do
37
+ ::Unobtainium::Driver.create(:mock, "foo")
38
+ end.to raise_error(ArgumentError)
39
+ end
40
+
41
+ it "creates no driver with an unknown label" do
42
+ expect { ::Unobtainium::Driver.create(:nope) }.to raise_error(LoadError)
43
+ end
44
+
45
+ it "fails preconditions correctly" do
46
+ expect do
47
+ ::Unobtainium::Driver.create(:raise_mock)
48
+ end.to raise_error(RuntimeError)
49
+ end
50
+
51
+ it "creates a driver correctly" do
52
+ ::Unobtainium::Driver.create(:mock)
53
+ end
54
+
55
+ it "delegates to created driver class" do
56
+ drv = ::Unobtainium::Driver.create(:mock, foo: 42)
57
+ expect(drv.respond_to?(:passed_options)).to be_truthy
58
+ _ = drv.passed_options
59
+ end
60
+
61
+ it "passes options through correctly" do
62
+ drv = ::Unobtainium::Driver.create(:mock, foo: 42)
63
+ expect(drv.passed_options).to eql foo: 42
64
+ end
65
+ end
@@ -0,0 +1,27 @@
1
+ class Mock
2
+ attr_accessor :passed_options
3
+
4
+ def initialize(opts)
5
+ @passed_options = opts
6
+ end
7
+ end
8
+
9
+ # rubocop:disable Style/GuardClause
10
+ class MockDriver
11
+ class << self
12
+ def matches?(label)
13
+ label == :mock || label == :raise_mock
14
+ end
15
+
16
+ def ensure_preconditions(label, _)
17
+ if label == :raise_mock
18
+ raise "something"
19
+ end
20
+ end
21
+
22
+ def create(_, options)
23
+ Mock.new(options)
24
+ end
25
+ end
26
+ end # class MockDriver
27
+ # rubocop:enable Style/GuardClause
@@ -0,0 +1,63 @@
1
+ require 'spec_helper'
2
+ require_relative '../lib/unobtainium/pathed_hash'
3
+
4
+ describe ::Unobtainium::PathedHash do
5
+ describe "#initialize" do
6
+ it "can be constructed without values" do
7
+ ph = ::Unobtainium::PathedHash.new
8
+ expect(ph.empty?).to eql true
9
+ end
10
+
11
+ it "can be constructed with values" do
12
+ ph = ::Unobtainium::PathedHash.new(foo: 42)
13
+ expect(ph.empty?).to eql false
14
+ expect(ph[:foo]).to eql 42
15
+ end
16
+ end
17
+
18
+ describe "Hash-like" do
19
+ it "responds to Hash functions" do
20
+ ph = ::Unobtainium::PathedHash.new
21
+ [:invert, :delete, :fetch].each do |meth|
22
+ expect(ph.respond_to?(meth)).to eql true
23
+ end
24
+ end
25
+
26
+ it "can be used like a hash" do
27
+ ph = ::Unobtainium::PathedHash.new(foo: 42)
28
+ inverted = ph.invert
29
+ expect(inverted.empty?).to eql false
30
+ expect(inverted[42]).to eql :foo
31
+ end
32
+ end
33
+
34
+ it "can recursively read entries via a path" do
35
+ sample = {
36
+ "foo" => 42,
37
+ "bar" => {
38
+ "baz" => "quux",
39
+ "blah" => [1, 2],
40
+ }
41
+ }
42
+ ph = ::Unobtainium::PathedHash.new(sample)
43
+
44
+ expect(ph["foo"]).to eql 42
45
+ expect(ph["bar.baz"]).to eql "quux"
46
+ expect(ph["bar.blah"]).to eql [1, 2]
47
+
48
+ expect(ph["nope"]).to eql nil
49
+ expect(ph["bar.nope"]).to eql nil
50
+ end
51
+
52
+ it "can recursively write entries via a path" do
53
+ ph = ::Unobtainium::PathedHash.new
54
+ ph["foo.bar"] = 42
55
+ expect(ph["foo.bar"]).to eql 42
56
+ end
57
+
58
+ it "has the same string representation as the hash it's initialized from" do
59
+ h = { foo: 42 }
60
+ ph = ::Unobtainium::PathedHash.new(h)
61
+ expect(ph.to_s).to eql h.to_s
62
+ end
63
+ end
@@ -0,0 +1,110 @@
1
+ require 'spec_helper'
2
+ require_relative '../lib/unobtainium/runtime'
3
+
4
+ # rubocop:disable Style/ClassVars
5
+ class Foo
6
+ @@instances = 0
7
+
8
+ class << self
9
+ def instances
10
+ @@instances ||= 0
11
+ end
12
+
13
+ def instances=(value)
14
+ @@instances = value
15
+ end
16
+ end
17
+
18
+ def initialize
19
+ Foo.instances += 1
20
+ end
21
+
22
+ def destroy
23
+ Foo.instances -= 1
24
+ end
25
+ end # class Foo
26
+ # rubocop:enable Style/ClassVars
27
+
28
+ describe ::Unobtainium::Runtime do
29
+ it "is a singleton" do
30
+ expect { ::Unobtainium::Runtime.new }.to raise_error(NoMethodError)
31
+ first = ::Unobtainium::Runtime.instance
32
+ second = ::Unobtainium::Runtime.instance
33
+ expect(first).to eql second
34
+ end
35
+
36
+ it "can store objects" do
37
+ ::Unobtainium::Runtime.instance.store("foo", 42)
38
+
39
+ expect(::Unobtainium::Runtime.instance.has?("foo")).to be_truthy
40
+ expect(::Unobtainium::Runtime.instance.length).to eql 1
41
+ expect(::Unobtainium::Runtime.instance.fetch("foo")).to eql 42
42
+ expect(::Unobtainium::Runtime.instance["foo"]).to eql 42
43
+ end
44
+
45
+ it "deals well with default values" do
46
+ expect(::Unobtainium::Runtime.instance["bar"]).to be_nil
47
+ expect { ::Unobtainium::Runtime.instance.fetch("bar") }.to raise_error(
48
+ KeyError)
49
+ expect(::Unobtainium::Runtime.instance.fetch("bar", 123)).to eql 123
50
+ end
51
+
52
+ it "destroys deleted objects" do
53
+ expect(Foo.instances).to eql 0
54
+
55
+ ::Unobtainium::Runtime.instance.store("foo", Foo.new)
56
+ expect(Foo.instances).to eql 1
57
+
58
+ ::Unobtainium::Runtime.instance.delete("foo")
59
+ expect(Foo.instances).to eql 0
60
+ end
61
+
62
+ it "can use custom destructors" do
63
+ called = false
64
+ ::Unobtainium::Runtime.instance.store("foo", 666, proc { called = true })
65
+ expect(called).to be_falsy
66
+
67
+ ::Unobtainium::Runtime.instance.delete("foo")
68
+ expect(called).to be_truthy
69
+ end
70
+
71
+ it "can store objects created from a block" do
72
+ ::Unobtainium::Runtime.instance.store_with("foo") { 123 }
73
+ expect(::Unobtainium::Runtime.instance.fetch("foo")).to eql 123
74
+ end
75
+
76
+ it "ignores nil objects created from a block" do
77
+ ::Unobtainium::Runtime.instance.store_with("_nope_") { nil }
78
+ expect { ::Unobtainium::Runtime.instance.fetch("_nope_") }.to raise_error(
79
+ KeyError)
80
+ end
81
+
82
+ it "stores objects with :store_if" do
83
+ ::Unobtainium::Runtime.instance.store_if("store_if", 42)
84
+ expect(::Unobtainium::Runtime.instance.fetch("store_if")).to eql 42
85
+ end
86
+
87
+ it "does not overwrite objects with :store_if" do
88
+ ::Unobtainium::Runtime.instance.store("foo", 42)
89
+ ::Unobtainium::Runtime.instance.store_if("foo", 123)
90
+ expect(::Unobtainium::Runtime.instance.fetch("foo")).to eql 42
91
+ end
92
+
93
+ it "stores objects with :store_with_if" do
94
+ ::Unobtainium::Runtime.instance.store_with_if("store_with_if") do
95
+ 42
96
+ end
97
+ expect(::Unobtainium::Runtime.instance.fetch("store_with_if")).to eql 42
98
+ end
99
+
100
+ it "does not overwrite objects with :store_with_if" do
101
+ ::Unobtainium::Runtime.instance.store("foo", 42)
102
+ called = false
103
+ ::Unobtainium::Runtime.instance.store_with_if("foo") do
104
+ called = true
105
+ 123
106
+ end
107
+ expect(::Unobtainium::Runtime.instance.fetch("foo")).to eql 42
108
+ expect(called).to be_falsy
109
+ end
110
+ end
@@ -0,0 +1,7 @@
1
+ require 'simplecov'
2
+ SimpleCov.start do
3
+ add_filter 'unobtainium/drivers'
4
+ end
5
+
6
+ require "codeclimate-test-reporter"
7
+ CodeClimate::TestReporter.start
@@ -0,0 +1,33 @@
1
+ require 'spec_helper'
2
+ require_relative '../lib/unobtainium/world'
3
+ require_relative './mock_driver.rb'
4
+
5
+ class Tester
6
+ include ::Unobtainium::World
7
+ end # class Tester
8
+
9
+ describe ::Unobtainium::World do
10
+ before :each do
11
+ # Set configuration
12
+ path = File.join(File.dirname(__FILE__), 'data', 'world.yml')
13
+ ::Unobtainium::World.config_file = path
14
+
15
+ # Load MockDriver
16
+ ::Unobtainium::Driver.register_implementation(MockDriver, "mock_driver.rb")
17
+
18
+ # Create tester object
19
+ @tester = Tester.new
20
+ end
21
+
22
+ it "loads the global config" do
23
+ expect(@tester.config["drivers.mock.option"]).to eql "value"
24
+ end
25
+
26
+ it "creates a mock driver parameters" do
27
+ expect(@tester.driver.respond_to?(:passed_options)).to be_truthy
28
+ end
29
+
30
+ it "passed the config file options to the driver" do
31
+ expect(@tester.driver.passed_options["option"]).to eql "value"
32
+ end
33
+ end
data/unobtainium.gemspec CHANGED
@@ -37,12 +37,15 @@ Gem::Specification.new do |spec|
37
37
  spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
38
38
  spec.require_paths = ["lib"]
39
39
 
40
- spec.required_ruby_version = '>= 1.9'
40
+ spec.required_ruby_version = '>= 2.0'
41
41
 
42
42
  spec.requirements = "Either or all of 'selenium-webdriver', 'appium_lib'"
43
43
 
44
44
  spec.add_development_dependency "bundler", "~> 1.11"
45
45
  spec.add_development_dependency "rubocop", "~> 0.39"
46
+ spec.add_development_dependency "rake", "~> 11.1"
47
+ spec.add_development_dependency "rspec", "~> 3.4"
48
+ spec.add_development_dependency "simplecov", "~> 0.11"
46
49
  end
47
50
  # rubocop:enable Style/SpaceAroundOperators
48
51
  # rubocop:enable Style/UnneededPercentQ, Style/ExtraSpacing
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.2
4
+ version: 0.1.0
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-08 00:00:00.000000000 Z
11
+ date: 2016-04-11 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -38,6 +38,48 @@ dependencies:
38
38
  - - "~>"
39
39
  - !ruby/object:Gem::Version
40
40
  version: '0.39'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rake
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '11.1'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '11.1'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rspec
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '3.4'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '3.4'
69
+ - !ruby/object:Gem::Dependency
70
+ name: simplecov
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '0.11'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '0.11'
41
83
  description: "\n Unobtainium wraps Selenium and Appium in a simple driver abstraction
42
84
  so that\n test code can more easily cover desktop browsers, mobile browsers and
43
85
  mobile\n apps.\n\n Some additional useful functionality for the maintenance
@@ -48,12 +90,22 @@ executables: []
48
90
  extensions: []
49
91
  extra_rdoc_files: []
50
92
  files:
93
+ - ".codeclimate.yml"
51
94
  - ".gitignore"
52
95
  - ".rubocop.yml"
96
+ - ".travis.yml"
53
97
  - Gemfile
54
98
  - Gemfile.lock
55
99
  - LICENSE
56
100
  - README.md
101
+ - Rakefile
102
+ - cuke/.gitignore
103
+ - cuke/Gemfile
104
+ - cuke/README.md
105
+ - cuke/config/config.yml
106
+ - cuke/features/step_definitions/steps.rb
107
+ - cuke/features/support/env.rb
108
+ - cuke/features/world.feature
57
109
  - lib/unobtainium.rb
58
110
  - lib/unobtainium/config.rb
59
111
  - lib/unobtainium/driver.rb
@@ -62,6 +114,25 @@ files:
62
114
  - lib/unobtainium/pathed_hash.rb
63
115
  - lib/unobtainium/runtime.rb
64
116
  - lib/unobtainium/version.rb
117
+ - lib/unobtainium/world.rb
118
+ - spec/config_spec.rb
119
+ - spec/data/array.yaml
120
+ - spec/data/arraymerge-local.yaml
121
+ - spec/data/arraymerge.yaml
122
+ - spec/data/empty.yml
123
+ - spec/data/hash.yml
124
+ - spec/data/hashmerge-local.yml
125
+ - spec/data/hashmerge.yml
126
+ - spec/data/mergefail-local.yaml
127
+ - spec/data/mergefail.yaml
128
+ - spec/data/test.json
129
+ - spec/data/world.yml
130
+ - spec/driver_spec.rb
131
+ - spec/mock_driver.rb
132
+ - spec/pathed_hash_spec.rb
133
+ - spec/runtime_spec.rb
134
+ - spec/spec_helper.rb
135
+ - spec/world_spec.rb
65
136
  - unobtainium.gemspec
66
137
  homepage: https://github.com/jfinkhaeuser/unobtainium
67
138
  licenses:
@@ -75,7 +146,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
75
146
  requirements:
76
147
  - - ">="
77
148
  - !ruby/object:Gem::Version
78
- version: '1.9'
149
+ version: '2.0'
79
150
  required_rubygems_version: !ruby/object:Gem::Requirement
80
151
  requirements:
81
152
  - - ">="
@@ -88,5 +159,23 @@ rubygems_version: 2.4.5.1
88
159
  signing_key:
89
160
  specification_version: 4
90
161
  summary: 'Obtain the unobtainable: test code covering multiple platforms'
91
- test_files: []
162
+ test_files:
163
+ - spec/config_spec.rb
164
+ - spec/data/array.yaml
165
+ - spec/data/arraymerge-local.yaml
166
+ - spec/data/arraymerge.yaml
167
+ - spec/data/empty.yml
168
+ - spec/data/hash.yml
169
+ - spec/data/hashmerge-local.yml
170
+ - spec/data/hashmerge.yml
171
+ - spec/data/mergefail-local.yaml
172
+ - spec/data/mergefail.yaml
173
+ - spec/data/test.json
174
+ - spec/data/world.yml
175
+ - spec/driver_spec.rb
176
+ - spec/mock_driver.rb
177
+ - spec/pathed_hash_spec.rb
178
+ - spec/runtime_spec.rb
179
+ - spec/spec_helper.rb
180
+ - spec/world_spec.rb
92
181
  has_rdoc: