xcmonkey 0.1.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
+ SHA256:
3
+ metadata.gz: dd9f0e62a4820d7e9041cf5219b79941fd416829f2943c68a354ed5dd46dd6aa
4
+ data.tar.gz: 80debe535a05457eb8cce60b1c9f43feae95e00b1849206f1e8c2d0c2194ae44
5
+ SHA512:
6
+ metadata.gz: 8ef6ae85c92902d29d04d59b0d2ec4ebed20877054edaa8c00f767db88e3c6d5232f8603159b78478562a53b154a7249ac6721ab20b14524d755367daaf73100
7
+ data.tar.gz: 57e3829a912bb9d7997f8bfa94e7748cefbe9ea4130efaa3fe8167ae91b70d8e90bbb1d30a5dccf997717ac2c83626cf761947f826239d85883c7f2936791794
data/.fasterer.yml ADDED
@@ -0,0 +1,5 @@
1
+ speedups:
2
+ each_with_index_vs_while: false
3
+
4
+ exclude_paths:
5
+ - 'vendor/**/*.rb'
@@ -0,0 +1,5 @@
1
+ # These are supported funding model platforms
2
+
3
+ github: alteral
4
+ ko_fi: alteral
5
+ custom: [ 'https://revolut.me/alteral', 'https://paypal.me/aapesotskiy' ]
@@ -0,0 +1,13 @@
1
+ version: 2
2
+ updates:
3
+ - package-ecosystem: bundler
4
+ directory: "/"
5
+ schedule:
6
+ interval: weekly
7
+ open-pull-requests-limit: 1
8
+
9
+ - package-ecosystem: "github-actions"
10
+ directory: "/"
11
+ schedule:
12
+ interval: "weekly"
13
+ open-pull-requests-limit: 1
@@ -0,0 +1,9 @@
1
+ # Changes
2
+
3
+ ## References
4
+ - https://github.com/alteral/xcmonkey/issues/XXX
5
+
6
+ ## Risks
7
+ - [ ] None
8
+ - [ ] Low
9
+ - [ ] High
@@ -0,0 +1,29 @@
1
+ name: Test
2
+
3
+ on:
4
+ pull_request:
5
+ types: [opened, synchronize]
6
+
7
+ jobs:
8
+ chat:
9
+ name: Checks
10
+ timeout-minutes: 15
11
+ runs-on: macos-12
12
+ steps:
13
+ - uses: actions/checkout@v3.1.0
14
+ with:
15
+ fetch-depth: '0'
16
+ - uses: actions/cache@v3
17
+ id: bundler-cache
18
+ with:
19
+ path: vendor/bundle
20
+ key: ${{ runner.os }}-gems-${{ hashFiles('**/Gemfile.lock') }}
21
+ restore-keys: |
22
+ ${{ runner.os }}-gems-
23
+ - name: Bundler
24
+ run: |
25
+ gem install bundler
26
+ bundle config path vendor/bundle
27
+ bundle check || bundle install --jobs 4 --retry 3
28
+ - name: Rubocop
29
+ run: bundle exec rake
data/.gitignore ADDED
@@ -0,0 +1,15 @@
1
+ .DS_Store
2
+ /.idea/
3
+ /pkg/
4
+ /PageObjects/
5
+ /attach/
6
+ /Attachments/
7
+ /XCResults/
8
+ *.lock
9
+ *.xcuserdatad
10
+ *.png
11
+ *.html
12
+ port
13
+ /vendor/
14
+ coverage/
15
+ test-results/
data/.rspec ADDED
@@ -0,0 +1,5 @@
1
+ --require spec_helper
2
+ --color
3
+ --format d
4
+ --format RspecJunitFormatter
5
+ --out test-results/rspec/rspec.xml
data/.rubocop.yml ADDED
@@ -0,0 +1,198 @@
1
+ ---
2
+ require:
3
+ - rubocop/require_tools
4
+ - rubocop-performance
5
+ - rubocop-rspec
6
+ - rubocop-rake
7
+ AllCops:
8
+ TargetRubyVersion: 2.4
9
+ NewCops: enable
10
+ Include:
11
+ - "**/*.rb"
12
+ - "**/*file"
13
+ - "**/*.gemspec"
14
+ - "*/lib/assets/*Template"
15
+ - "*/lib/assets/*TemplateAndroid"
16
+ Exclude:
17
+ - "**/lib/assets/custom_action_template.rb"
18
+ - "./vendor/**/*"
19
+ - "**/lib/assets/DefaultFastfileTemplate"
20
+ - "**/lib/assets/MatchfileTemplate"
21
+ - "**/spec/fixtures/broken_files/broken_file.rb"
22
+ - "**/*.provisionprofile"
23
+ Style/MultipleComparison:
24
+ Enabled: false
25
+ Style/RedundantFetchBlock:
26
+ Enabled: false
27
+ Style/PercentLiteralDelimiters:
28
+ Enabled: false
29
+ Style/ClassCheck:
30
+ EnforcedStyle: kind_of?
31
+ Style/FrozenStringLiteralComment:
32
+ Enabled: false
33
+ Style/SafeNavigation:
34
+ Enabled: false
35
+ Performance/RegexpMatch:
36
+ Enabled: false
37
+ Performance/StringReplacement:
38
+ Enabled: false
39
+ Style/NumericPredicate:
40
+ Enabled: false
41
+ Metrics/BlockLength:
42
+ Enabled: false
43
+ Metrics/ModuleLength:
44
+ Enabled: false
45
+ Naming/VariableNumber:
46
+ Enabled: false
47
+ Style/MissingRespondToMissing:
48
+ Enabled: false
49
+ Style/MultilineBlockChain:
50
+ Enabled: false
51
+ Style/NumericLiteralPrefix:
52
+ Enabled: false
53
+ Style/TernaryParentheses:
54
+ Enabled: false
55
+ Style/EmptyMethod:
56
+ Enabled: false
57
+ Lint/UselessAssignment:
58
+ Exclude:
59
+ - "**/spec/**/*"
60
+ Require/MissingRequireStatement:
61
+ Enabled: false
62
+ Layout/FirstHashElementIndentation:
63
+ Enabled: false
64
+ Layout/IndentationWidth:
65
+ Enabled: false
66
+ Layout/IndentationStyle:
67
+ Enabled: false
68
+ Layout/IndentationConsistency:
69
+ Enabled: false
70
+ Layout/HashAlignment:
71
+ Enabled: false
72
+ Layout/DotPosition:
73
+ Enabled: false
74
+ Style/DoubleNegation:
75
+ Enabled: false
76
+ Style/SymbolArray:
77
+ Enabled: false
78
+ Layout/HeredocIndentation:
79
+ Enabled: false
80
+ Style/MixinGrouping:
81
+ Exclude:
82
+ - "**/spec/**/*"
83
+ Lint/SuppressedException:
84
+ Enabled: false
85
+ Lint/SymbolConversion:
86
+ Enabled: false
87
+ Lint/UnusedBlockArgument:
88
+ Enabled: false
89
+ Lint/AmbiguousBlockAssociation:
90
+ Enabled: false
91
+ Style/GlobalVars:
92
+ Enabled: false
93
+ Style/ClassAndModuleChildren:
94
+ Enabled: false
95
+ Style/SpecialGlobalVars:
96
+ Enabled: false
97
+ Metrics/AbcSize:
98
+ Enabled: false
99
+ Metrics/MethodLength:
100
+ Enabled: false
101
+ Metrics/CyclomaticComplexity:
102
+ Enabled: false
103
+ Style/WordArray:
104
+ MinSize: 19
105
+ Style/SignalException:
106
+ Enabled: false
107
+ Style/RedundantReturn:
108
+ Enabled: false
109
+ Style/IfUnlessModifier:
110
+ Enabled: false
111
+ Style/AndOr:
112
+ Enabled: true
113
+ EnforcedStyle: conditionals
114
+ Metrics/ClassLength:
115
+ Max: 320
116
+ Layout/LineLength:
117
+ Max: 370
118
+ Metrics/ParameterLists:
119
+ Max: 17
120
+ Metrics/PerceivedComplexity:
121
+ Max: 20
122
+ Style/GuardClause:
123
+ Enabled: false
124
+ Style/StringLiterals:
125
+ Enabled: false
126
+ Style/ConditionalAssignment:
127
+ Enabled: false
128
+ Style/RedundantSelf:
129
+ Enabled: false
130
+ Lint/UnusedMethodArgument:
131
+ Enabled: false
132
+ Lint/ParenthesesAsGroupedExpression:
133
+ Exclude:
134
+ - "**/spec/**/*"
135
+ Naming/PredicateName:
136
+ Enabled: false
137
+ Naming/MethodParameterName:
138
+ Enabled: false
139
+ Style/PerlBackrefs:
140
+ Enabled: false
141
+ Layout/SpaceAroundOperators:
142
+ Exclude:
143
+ - "**/spec/actions_specs/xcodebuild_spec.rb"
144
+ Naming/FileName:
145
+ Exclude:
146
+ - "**/Dangerfile"
147
+ - "**/Brewfile"
148
+ - "**/Gemfile"
149
+ - "**/Podfile"
150
+ - "**/Rakefile"
151
+ - "**/Fastfile"
152
+ - "**/Deliverfile"
153
+ - "**/Snapfile"
154
+ - "**/Pluginfile"
155
+ - "**/*.gemspec"
156
+ Style/Documentation:
157
+ Enabled: false
158
+ Style/MutableConstant:
159
+ Enabled: false
160
+ Style/ZeroLengthPredicate:
161
+ Enabled: false
162
+ Style/IfInsideElse:
163
+ Enabled: false
164
+ Style/CollectionMethods:
165
+ Enabled: false
166
+ Style/MethodCallWithArgsParentheses:
167
+ Enabled: true
168
+ AllowedMethods:
169
+ - require
170
+ - require_relative
171
+ - fastlane_require
172
+ - gem
173
+ - program
174
+ - command
175
+ - raise
176
+ - attr_accessor
177
+ - attr_reader
178
+ - desc
179
+ - lane
180
+ - private_lane
181
+ - platform
182
+ - to
183
+ - not_to
184
+ - describe
185
+ - it
186
+ - be
187
+ - context
188
+ - before
189
+ - after
190
+ - and
191
+ RSpec/ExampleLength:
192
+ Max: 20
193
+ RSpec/MultipleMemoizedHelpers:
194
+ Max: 10
195
+ RSpec/MessageSpies:
196
+ Enabled: false
197
+ RSpec/MultipleExpectations:
198
+ Enabled: false
@@ -0,0 +1,74 @@
1
+ # Contributor Covenant Code of Conduct
2
+
3
+ ## Our Pledge
4
+
5
+ In the interest of fostering an open and welcoming environment, we as
6
+ contributors and maintainers pledge to making participation in our project and
7
+ our community a harassment-free experience for everyone, regardless of age, body
8
+ size, disability, ethnicity, gender identity and expression, level of experience,
9
+ nationality, personal appearance, race, religion, or sexual identity and
10
+ orientation.
11
+
12
+ ## Our Standards
13
+
14
+ Examples of behavior that contributes to creating a positive environment
15
+ include:
16
+
17
+ * Using welcoming and inclusive language
18
+ * Being respectful of differing viewpoints and experiences
19
+ * Gracefully accepting constructive criticism
20
+ * Focusing on what is best for the community
21
+ * Showing empathy towards other community members
22
+
23
+ Examples of unacceptable behavior by participants include:
24
+
25
+ * The use of sexualized language or imagery and unwelcome sexual attention or
26
+ advances
27
+ * Trolling, insulting/derogatory comments, and personal or political attacks
28
+ * Public or private harassment
29
+ * Publishing others' private information, such as a physical or electronic
30
+ address, without explicit permission
31
+ * Other conduct which could reasonably be considered inappropriate in a
32
+ professional setting
33
+
34
+ ## Our Responsibilities
35
+
36
+ Project maintainers are responsible for clarifying the standards of acceptable
37
+ behavior and are expected to take appropriate and fair corrective action in
38
+ response to any instances of unacceptable behavior.
39
+
40
+ Project maintainers have the right and responsibility to remove, edit, or
41
+ reject comments, commits, code, wiki edits, issues, and other contributions
42
+ that are not aligned to this Code of Conduct, or to ban temporarily or
43
+ permanently any contributor for other behaviors that they deem inappropriate,
44
+ threatening, offensive, or harmful.
45
+
46
+ ## Scope
47
+
48
+ This Code of Conduct applies both within project spaces and in public spaces
49
+ when an individual is representing the project or its community. Examples of
50
+ representing a project or community include using an official project e-mail
51
+ address, posting via an official social media account, or acting as an appointed
52
+ representative at an online or offline event. Representation of a project may be
53
+ further defined and clarified by project maintainers.
54
+
55
+ ## Enforcement
56
+
57
+ Instances of abusive, harassing, or otherwise unacceptable behavior may be
58
+ reported by contacting the project team at a.alterpesotskiy@corp.mail.ru. All
59
+ complaints will be reviewed and investigated and will result in a response that
60
+ is deemed necessary and appropriate to the circumstances. The project team is
61
+ obligated to maintain confidentiality with regard to the reporter of an incident.
62
+ Further details of specific enforcement policies may be posted separately.
63
+
64
+ Project maintainers who do not follow or enforce the Code of Conduct in good
65
+ faith may face temporary or permanent repercussions as determined by other
66
+ members of the project's leadership.
67
+
68
+ ## Attribution
69
+
70
+ This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
71
+ available at [http://contributor-covenant.org/version/1/4][version]
72
+
73
+ [homepage]: http://contributor-covenant.org
74
+ [version]: http://contributor-covenant.org/version/1/4/
data/Gemfile ADDED
@@ -0,0 +1,6 @@
1
+ source "https://rubygems.org"
2
+
3
+ git_source(:github) { |repo_name| "https://github.com/#{repo_name}" }
4
+
5
+ # Specify your gem's dependencies in xcmonkey.gemspec
6
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2022 Alexey Alter-Pesotskiy
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,36 @@
1
+ # XCMonkey
2
+
3
+ A tool for doing randomised UI testing of iOS apps 🙈
4
+
5
+ ## Requirements
6
+
7
+ ```bash
8
+ brew tap facebook/fb
9
+ brew install idb-companion
10
+ ```
11
+
12
+ ## Installation
13
+
14
+ ```bash
15
+ brew install xcmonkey
16
+ ```
17
+
18
+ or
19
+
20
+ ```bash
21
+ gem install xcmonkey
22
+ ```
23
+
24
+ ## Usage
25
+
26
+ ### Test
27
+
28
+ ```bash
29
+ xcmonkey test --udid "30694803-2018-460F-BBA6-97D7911A1AC0" --bundle-id "com.example.app"
30
+ ```
31
+
32
+ ### Describe point
33
+
34
+ ```bash
35
+ xcmonkey describe -x 10 -y 10 --udid "20694801-2018-460F-BBA6-97D7911A1AC0"
36
+ ```
data/Rakefile ADDED
@@ -0,0 +1,13 @@
1
+ require 'bundler/gem_tasks'
2
+ require 'rspec/core/rake_task'
3
+ require 'rubocop/rake_task'
4
+
5
+ RSpec::Core::RakeTask.new
6
+ RuboCop::RakeTask.new(:rubocop)
7
+
8
+ desc 'Run Fasterer'
9
+ task :fasterer do
10
+ sh('bundle exec fasterer')
11
+ end
12
+
13
+ task(default: [:spec, :rubocop, :fasterer])
data/bin/console ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "xcmonkey"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require "irb"
14
+ IRB.start(__FILE__)
data/bin/setup ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
data/bin/xcmonkey ADDED
@@ -0,0 +1,46 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'commander/import'
4
+ require_relative '../lib/xcmonkey'
5
+ require_relative '../lib/xcmonkey/describer'
6
+ require_relative '../lib/xcmonkey/logger'
7
+ require_relative '../lib/xcmonkey/driver'
8
+ require_relative '../lib/xcmonkey/version'
9
+
10
+ module Xcmonkey
11
+ program :version, VERSION
12
+ program :description, 'A tool for doing randomised UI testing of iOS apps'
13
+
14
+ command :test do |c|
15
+ c.syntax = 'xcmonkey test [options]'
16
+ c.description = 'Runs monkey test'
17
+ c.option('-u', '--udid STRING', String, 'Set device UDID')
18
+ c.option('-b', '--bundle-id STRING', String, 'Set target bundle identifier')
19
+ c.option('-d', '--duration SECONDS', Integer, 'Test duration in seconds')
20
+ c.action do |args, options|
21
+ options.default(duration: 60)
22
+ params = {
23
+ udid: options.udid,
24
+ bundle_id: options.bundle_id,
25
+ duration: options.duration
26
+ }
27
+ Xcmonkey.new(params).run
28
+ end
29
+ end
30
+
31
+ command :describe do |c|
32
+ c.syntax = 'xcmonkey describe [options]'
33
+ c.description = 'Describes given point'
34
+ c.option('-u', '--udid STRING', String, 'Set device UDID')
35
+ c.option('-x', '--x STRING', 'Point `x` coordinate')
36
+ c.option('-y', '--y STRING', 'Point `y` coordinate')
37
+ c.action do |args, options|
38
+ params = {
39
+ udid: options.udid,
40
+ x: options.x,
41
+ y: options.y
42
+ }
43
+ Describer.new(params).run
44
+ end
45
+ end
46
+ end
data/fastlane/Fastfile ADDED
@@ -0,0 +1,26 @@
1
+ skip_docs
2
+
3
+ lane :release do
4
+ code_review
5
+ sh('bundle exec rake build')
6
+ gem_path = "pkg/xcmonkey-#{version}.gem"
7
+ sh("gem push ../#{gem_path}")
8
+ set_github_release(
9
+ repository_name: 'alteral/xcmonkey',
10
+ api_token: ENV.fetch("GITHUB_TOKEN", nil),
11
+ name: "XCMonkey v#{version}",
12
+ tag_name: "v#{version}",
13
+ description: "v#{version}",
14
+ commitish: git_branch,
15
+ upload_assets: [gem_path]
16
+ )
17
+ end
18
+
19
+ lane :code_review do
20
+ sh('bundle exec rake')
21
+ end
22
+
23
+ def version
24
+ version_path = '../lib/xcmonkey/version.rb'
25
+ File.read(version_path).scan(/\d+/).join('.')
26
+ end
@@ -0,0 +1,22 @@
1
+ class Describer
2
+ attr_accessor :udid, :x, :y, :driver
3
+
4
+ def initialize(params)
5
+ ensure_required_params(params)
6
+ self.udid = params[:udid]
7
+ self.x = params[:x]
8
+ self.y = params[:y]
9
+ self.driver = Driver.new(params)
10
+ end
11
+
12
+ def run
13
+ driver.ensure_device_exists
14
+ driver.describe_point(x, y)
15
+ end
16
+
17
+ def ensure_required_params(params)
18
+ Logger.error('UDID should be provided') if params[:udid].nil?
19
+ Logger.error('`x` point coordinate should be provided') if params[:x].nil? || params[:x].to_i.to_s != params[:x].to_s
20
+ Logger.error('`y` point coordinate should be provided') if params[:y].nil? || params[:y].to_i.to_s != params[:y].to_s
21
+ end
22
+ end
@@ -0,0 +1,125 @@
1
+ class Driver
2
+ attr_accessor :udid, :bundle_id, :duration
3
+
4
+ def initialize(params)
5
+ self.udid = params[:udid]
6
+ self.bundle_id = params[:bundle_id]
7
+ self.duration = params[:duration]
8
+ ensure_driver_installed
9
+ end
10
+
11
+ def monkey_test(gestures)
12
+ app_elements = describe_ui.shuffle
13
+ current_time = Time.now
14
+ while Time.now < current_time + duration
15
+ el1_coordinates = central_coordinates(app_elements.first)
16
+ el2_coordinates = central_coordinates(app_elements.last)
17
+ case gestures.sample
18
+ when :precise_tap
19
+ tap(coordinates: el1_coordinates)
20
+ when :blind_tap
21
+ x = (el1_coordinates[:x] - el2_coordinates[:x]).abs
22
+ y = (el1_coordinates[:y] - el2_coordinates[:y]).abs
23
+ tap(coordinates: { x: x, y: y })
24
+ when :swipe
25
+ swipe(start_coordinates: el1_coordinates, end_coordinates: el2_coordinates)
26
+ end
27
+ app_elements = describe_ui.shuffle
28
+ Logger.error('App lost') if app_elements.include?(@home_tracker)
29
+ end
30
+ end
31
+
32
+ def open_home_screen(return_tracker: false)
33
+ `idb ui button --udid #{udid} HOME`
34
+ detect_home_unique_element if return_tracker
35
+ end
36
+
37
+ def describe_ui
38
+ JSON.parse(`idb ui describe-all --udid #{udid}`)
39
+ end
40
+
41
+ def describe_point(x, y)
42
+ point_info = JSON.parse(`idb ui describe-point --udid #{udid} #{x} #{y}`)
43
+ Logger.info("x:#{x} y:#{y} point info:", payload: JSON.pretty_generate(point_info))
44
+ point_info
45
+ end
46
+
47
+ def launch_app
48
+ `idb launch --udid #{udid} #{bundle_id}`
49
+ end
50
+
51
+ def terminate_app
52
+ `idb terminate --udid #{udid} #{bundle_id} 2>/dev/null`
53
+ end
54
+
55
+ def boot_simulator
56
+ `idb boot #{udid}`
57
+ ensure_simulator_was_booted
58
+ end
59
+
60
+ def shutdown_simulator
61
+ `idb shutdown #{udid}`
62
+ end
63
+
64
+ def list_targets
65
+ @list_targets ||= `idb list-targets`.split("\n")
66
+ @list_targets
67
+ end
68
+
69
+ def list_booted_simulators
70
+ `idb list-targets`.split("\n").grep(/Booted/)
71
+ end
72
+
73
+ def ensure_app_installed
74
+ Logger.error("App #{bundle_id} is not installed on device #{udid}") unless list_apps.include?(bundle_id)
75
+ end
76
+
77
+ def ensure_device_exists
78
+ device = list_targets.detect { |target| target.include?(udid) }
79
+ Logger.error("Can't find device #{udid}") if device.nil?
80
+ Logger.info('Device info:', payload: device)
81
+ boot_simulator if device.include?('simulator')
82
+ end
83
+
84
+ def ensure_simulator_was_booted
85
+ sim = list_booted_simulators.detect { |target| target.include?(udid) }
86
+ Logger.error("Failed to boot #{udid}") if sim.nil?
87
+ end
88
+
89
+ def list_apps
90
+ `idb list-apps --udid #{udid}`
91
+ end
92
+
93
+ def tap(coordinates:)
94
+ Logger.info('Tap:', payload: JSON.pretty_generate(coordinates))
95
+ `idb ui tap --udid #{udid} #{coordinates[:x]} #{coordinates[:y]}`
96
+ end
97
+
98
+ def swipe(start_coordinates:, end_coordinates:)
99
+ Logger.info('Swipe:', payload: "#{JSON.pretty_generate(start_coordinates)} => #{JSON.pretty_generate(end_coordinates)}")
100
+ coordinates = "#{start_coordinates[:x]} #{start_coordinates[:y]} #{end_coordinates[:x]} #{end_coordinates[:y]}"
101
+ `idb ui swipe --udid #{udid} --duration 0.5 #{coordinates}`
102
+ end
103
+
104
+ def central_coordinates(element)
105
+ frame = element['frame']
106
+ {
107
+ x: (frame['x'] + (frame['width'] / 2)).to_i,
108
+ y: (frame['y'] + (frame['height'] / 2)).to_i
109
+ }
110
+ end
111
+
112
+ private
113
+
114
+ def ensure_driver_installed
115
+ Logger.error("'idb' doesn't seem to be installed") if `which idb`.strip.empty?
116
+ end
117
+
118
+ def detect_home_unique_element
119
+ @home_tracker ||= describe_ui.reverse.detect do |el|
120
+ sleep(1)
121
+ !el['AXUniqueId'].nil? && !el['AXUniqueId'].empty? && el['type'] == 'Button'
122
+ end
123
+ @home_tracker
124
+ end
125
+ end
@@ -0,0 +1,23 @@
1
+ class Logger
2
+ def self.info(message, payload: nil)
3
+ log(message, color: :light_cyan, payload: payload)
4
+ end
5
+
6
+ def self.warn(message, payload: nil)
7
+ log(message, color: :light_yellow, payload: payload)
8
+ end
9
+
10
+ def self.error(message, payload: nil)
11
+ log(message, color: :light_red, payload: payload)
12
+ Process.exit(1)
13
+ end
14
+
15
+ def self.log(message, color:, payload:)
16
+ message = "#{Time.now.strftime('%k:%M:%S.%L')}: #{message}".colorize(color)
17
+ if payload
18
+ print(message, ' ', payload.colorize(:light_green), "\n\n")
19
+ else
20
+ puts("#{message}\n\n")
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,4 @@
1
+ module Xcmonkey
2
+ VERSION = '0.1.0'
3
+ GEM_NAME = 'xcmonkey'
4
+ end
data/lib/xcmonkey.rb ADDED
@@ -0,0 +1,41 @@
1
+ require 'json'
2
+ require 'colorize'
3
+ require_relative 'xcmonkey/describer'
4
+ require_relative 'xcmonkey/version'
5
+ require_relative 'xcmonkey/logger'
6
+ require_relative 'xcmonkey/driver'
7
+
8
+ module Xcmonkey
9
+ class Xcmonkey
10
+ attr_accessor :udid, :bundle_id, :duration, :driver
11
+
12
+ def initialize(params)
13
+ ensure_required_params(params)
14
+ self.udid = params[:udid]
15
+ self.bundle_id = params[:bundle_id]
16
+ self.duration = params[:duration]
17
+ self.driver = Driver.new(params)
18
+ end
19
+
20
+ def run
21
+ driver.ensure_device_exists
22
+ driver.ensure_app_installed
23
+ driver.terminate_app
24
+ driver.open_home_screen(return_tracker: true)
25
+ driver.launch_app
26
+ driver.monkey_test(gestures)
27
+ end
28
+
29
+ def gestures
30
+ [:precise_tap, :blind_tap, :swipe]
31
+ end
32
+
33
+ def ensure_required_params(params)
34
+ Logger.error('UDID should be provided') if params[:udid].nil?
35
+ Logger.error('Bundle identifier should be provided') if params[:bundle_id].nil?
36
+ if params[:duration].nil? || !params[:duration].kind_of?(Integer) || !params[:duration].positive?
37
+ Logger.error('Duration must be Integer and not less than 1 second')
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,43 @@
1
+ describe Describer do
2
+ let(:udid) { `xcrun simctl list | grep " iPhone 14 Pro Max"`.split("\n")[0].split('(')[1].split(')')[0] }
3
+ let(:driver) { Driver.new(udid: udid) }
4
+
5
+ it 'verifies that point can be described (integer)' do
6
+ allow(Logger).to receive(:info)
7
+ driver.boot_simulator
8
+ point_info = described_class.new(udid: udid, x: 10, y: 10).run
9
+ expect(point_info).not_to be_empty
10
+ end
11
+
12
+ it 'verifies that point can be described (string)' do
13
+ allow(Logger).to receive(:info)
14
+ driver.boot_simulator
15
+ point_info = described_class.new(udid: udid, x: '10', y: '10').run
16
+ expect(point_info).not_to be_empty
17
+ end
18
+
19
+ it 'verifies `udid` param is required' do
20
+ expect(Logger).to receive(:error).with('UDID should be provided')
21
+ described_class.new(x: 10, y: 10)
22
+ end
23
+
24
+ it 'verifies `x` param is required' do
25
+ expect(Logger).to receive(:error).with('`x` point coordinate should be provided')
26
+ described_class.new(udid: udid, y: '10')
27
+ end
28
+
29
+ it 'verifies `y` param is required' do
30
+ expect(Logger).to receive(:error).with('`y` point coordinate should be provided')
31
+ described_class.new(udid: udid, x: '10')
32
+ end
33
+
34
+ it 'verifies `x` param is integer' do
35
+ expect(Logger).to receive(:error).with('`x` point coordinate should be provided')
36
+ described_class.new(udid: udid, x: 'test', y: '10')
37
+ end
38
+
39
+ it 'verifies `y` param is integer' do
40
+ expect(Logger).to receive(:error).with('`y` point coordinate should be provided')
41
+ described_class.new(udid: udid, x: '10', y: 'test')
42
+ end
43
+ end
@@ -0,0 +1,94 @@
1
+ describe Driver do
2
+ let(:udid) { `xcrun simctl list | grep " iPhone 14 Pro Max"`.split("\n")[0].split('(')[1].split(')')[0] }
3
+ let(:bundle_id) { 'com.apple.Maps' }
4
+ let(:driver) { described_class.new(udid: udid, bundle_id: bundle_id) }
5
+
6
+ it 'verifies that sumulator was booted' do
7
+ error_message = "Failed to boot #{udid}"
8
+ expect(Logger).not_to receive(:error).with(error_message, payload: nil)
9
+ expect(driver).to receive(:ensure_simulator_was_booted)
10
+ driver.boot_simulator
11
+ end
12
+
13
+ it 'verifies that there are booted simulators' do
14
+ driver.boot_simulator
15
+ booted_simulators = driver.list_booted_simulators
16
+ expect(booted_simulators).not_to be_empty
17
+ end
18
+
19
+ it 'verifies that ui can be described' do
20
+ driver.boot_simulator
21
+ ui = driver.describe_ui
22
+ expect(ui).not_to be_empty
23
+ end
24
+
25
+ it 'verifies that home screen can be opened' do
26
+ driver.boot_simulator
27
+ home_tracker = driver.open_home_screen(return_tracker: true)
28
+ expect(home_tracker).not_to be_empty
29
+ end
30
+
31
+ it 'verifies that list of targets can be showed' do
32
+ list_targets = driver.list_targets
33
+ expect(list_targets).not_to be_empty
34
+ end
35
+
36
+ it 'verifies that list of apps can be showed' do
37
+ driver.boot_simulator
38
+ list_apps = driver.list_apps
39
+ expect(list_apps).to include(bundle_id)
40
+ end
41
+
42
+ it 'verifies that app installed' do
43
+ driver.boot_simulator
44
+ error_message = "App #{bundle_id} is not installed on device #{udid}"
45
+ expect(Logger).not_to receive(:error).with(error_message, payload: nil)
46
+ expect { driver.ensure_app_installed }.not_to raise_error
47
+ end
48
+
49
+ it 'verifies that app is not installed' do
50
+ driver.boot_simulator
51
+ bundle_id = 'fake.app.bundle.id'
52
+ error_message = "App #{bundle_id} is not installed on device #{udid}"
53
+ driver = described_class.new(udid: udid, bundle_id: bundle_id)
54
+ expect(Logger).to receive(:log).with(error_message, color: :light_red, payload: nil)
55
+ expect { driver.ensure_app_installed }.to raise_error(SystemExit) { |e| expect(e.status).to eq(1) }
56
+ end
57
+
58
+ it 'verifies that device exists' do
59
+ error_message = "Can't find device #{udid}"
60
+ payload = driver.list_targets.detect { |target| target.include?(udid) }
61
+ expect(Logger).not_to receive(:error).with(error_message, payload: nil)
62
+ expect(Logger).to receive(:info).with('Device info:', payload: payload)
63
+ expect(driver).to receive(:boot_simulator)
64
+ expect { driver.ensure_device_exists }.not_to raise_error
65
+ end
66
+
67
+ it 'verifies that device does not exist' do
68
+ udid = '1234-5678'
69
+ error_message = "Can't find device #{udid}"
70
+ driver = described_class.new(udid: udid, bundle_id: bundle_id)
71
+ expect(Logger).to receive(:log).with(error_message, color: :light_red, payload: nil)
72
+ expect { driver.ensure_device_exists }.to raise_error(SystemExit) { |e| expect(e.status).to eq(1) }
73
+ end
74
+
75
+ it 'verifies that idb installed' do
76
+ error_message = "'idb' doesn't seem to be installed"
77
+ expect(Logger).not_to receive(:error).with(error_message, payload: nil)
78
+ expect { driver.send(:ensure_driver_installed) }.not_to raise_error
79
+ end
80
+
81
+ it 'verifies that element central coordinates can be found' do
82
+ element = JSON.parse('{ "frame": { "x": 0, "y": 0, "width": 100, "height": 100 } }')
83
+ expected_coordinates = { 'x': 50, 'y': 50 }
84
+ actual_coordinates = driver.central_coordinates(element)
85
+ expect(actual_coordinates).to eq(expected_coordinates)
86
+ end
87
+
88
+ it 'verifies that simulator was not booted' do
89
+ driver.shutdown_simulator
90
+ error_message = "Failed to boot #{udid}"
91
+ expect(Logger).to receive(:log).with(error_message, color: :light_red, payload: nil)
92
+ expect { driver.ensure_simulator_was_booted }.to raise_error(SystemExit) { |e| expect(e.status).to eq(1) }
93
+ end
94
+ end
@@ -0,0 +1,36 @@
1
+ describe Logger do
2
+ let(:message) { 'test' }
3
+ let(:payload) { '{"test": true}' }
4
+
5
+ it 'verifies info log without payload' do
6
+ expect(described_class).to receive(:log).with(message, color: :light_cyan, payload: nil)
7
+ described_class.info(message)
8
+ end
9
+
10
+ it 'verifies info log with payload' do
11
+ expect(described_class).to receive(:log).with(message, color: :light_cyan, payload: payload)
12
+ described_class.info(message, payload: payload)
13
+ end
14
+
15
+ it 'verifies warning log without payload' do
16
+ expect(described_class).to receive(:log).with(message, color: :light_yellow, payload: nil)
17
+ described_class.warn(message)
18
+ end
19
+
20
+ it 'verifies warning log with payload' do
21
+ expect(described_class).to receive(:log).with(message, color: :light_yellow, payload: payload)
22
+ described_class.warn(message, payload: payload)
23
+ end
24
+
25
+ it 'verifies error log without payload' do
26
+ expect(described_class).to receive(:log).with(message, color: :light_red, payload: nil)
27
+ expect { described_class.error(message) }
28
+ .to raise_error(SystemExit) { |e| expect(e.status).to eq(1) }
29
+ end
30
+
31
+ it 'verifies error log with payload' do
32
+ expect(described_class).to receive(:log).with(message, color: :light_red, payload: payload)
33
+ expect { described_class.error(message, payload: payload) }
34
+ .to raise_error(SystemExit) { |e| expect(e.status).to eq(1) }
35
+ end
36
+ end
@@ -0,0 +1,12 @@
1
+ $LOAD_PATH.unshift(File.expand_path('../lib', __dir__))
2
+
3
+ require 'simplecov'
4
+
5
+ # SimpleCov.minimum_coverage(80)
6
+ SimpleCov.start
7
+
8
+ # This module is only used to check the environment is currently a testing env
9
+ module SpecHelper
10
+ end
11
+
12
+ require 'xcmonkey'
@@ -0,0 +1,46 @@
1
+ describe Xcmonkey do
2
+ describe Xcmonkey::Xcmonkey do
3
+ let(:params) { { udid: '123', bundle_id: 'example.com.app', duration: 10 } }
4
+ let(:duration_error_msg) { 'Duration must be Integer and not less than 1 second' }
5
+
6
+ it 'verifies gestures' do
7
+ gestures = described_class.new(params).gestures
8
+ expect(gestures) =~ [:swipe, :precise_tap, :blind_tap]
9
+ end
10
+
11
+ it 'verifies required params' do
12
+ expect(Logger).not_to receive(:error)
13
+ described_class.new(params)
14
+ end
15
+
16
+ it 'verifies `udid` param is required' do
17
+ params[:udid] = nil
18
+ expect(Logger).to receive(:error).with('UDID should be provided')
19
+ described_class.new(params)
20
+ end
21
+
22
+ it 'verifies `bundle_id` param is required' do
23
+ params[:bundle_id] = nil
24
+ expect(Logger).to receive(:error).with('Bundle identifier should be provided')
25
+ described_class.new(params)
26
+ end
27
+
28
+ it 'verifies `duration` param is required' do
29
+ params[:duration] = nil
30
+ expect(Logger).to receive(:error).with(duration_error_msg)
31
+ described_class.new(params)
32
+ end
33
+
34
+ it 'verifies `duration` param cannot be equal to zero' do
35
+ params[:duration] = 0
36
+ expect(Logger).to receive(:error).with(duration_error_msg)
37
+ described_class.new(params)
38
+ end
39
+
40
+ it 'verifies `duration` param cannot be negative' do
41
+ params[:duration] = -1
42
+ expect(Logger).to receive(:error).with(duration_error_msg)
43
+ described_class.new(params)
44
+ end
45
+ end
46
+ end
data/xcmonkey.gemspec ADDED
@@ -0,0 +1,42 @@
1
+ lib = File.expand_path('lib', __dir__)
2
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
3
+ require "xcmonkey/version"
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = Xcmonkey::GEM_NAME
7
+ spec.version = Xcmonkey::VERSION
8
+ spec.authors = ["alteral"]
9
+ spec.email = ["a.alterpesotskiy@mail.ru"]
10
+
11
+ spec.summary = "A tool for doing randomised UI testing of iOS apps"
12
+ spec.homepage = "https://github.com/alteral/xcmonkey"
13
+ spec.license = "MIT"
14
+
15
+ if spec.respond_to?(:metadata)
16
+ spec.metadata["allowed_push_host"] = "https://rubygems.org"
17
+ else
18
+ raise "RubyGems 2.0 or newer is required to protect against public gem pushes."
19
+ end
20
+
21
+ spec.bindir = "bin"
22
+ spec.executables = ["xcmonkey"]
23
+ spec.files = `git ls-files -z`.split("\x0")
24
+ spec.require_paths = ['lib']
25
+
26
+ spec.required_ruby_version = '>= 2.4'
27
+ spec.add_development_dependency('bundler')
28
+ spec.add_development_dependency('fasterer', '0.9.0')
29
+ spec.add_development_dependency('fastlane')
30
+ spec.add_development_dependency('rake')
31
+ spec.add_development_dependency('rspec')
32
+ spec.add_development_dependency('rspec_junit_formatter')
33
+ spec.add_development_dependency('rubocop', '1.38')
34
+ spec.add_development_dependency('rubocop-performance')
35
+ spec.add_development_dependency('rubocop-rake', '0.6.0')
36
+ spec.add_development_dependency('rubocop-require_tools')
37
+ spec.add_development_dependency('rubocop-rspec', '2.15.0')
38
+ spec.add_development_dependency('simplecov')
39
+ spec.add_dependency("colorize", "~> 0.8.1")
40
+ spec.add_dependency("commander")
41
+ spec.metadata['rubygems_mfa_required'] = 'true'
42
+ end
metadata ADDED
@@ -0,0 +1,270 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: xcmonkey
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - alteral
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2022-12-27 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: fasterer
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - '='
32
+ - !ruby/object:Gem::Version
33
+ version: 0.9.0
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - '='
39
+ - !ruby/object:Gem::Version
40
+ version: 0.9.0
41
+ - !ruby/object:Gem::Dependency
42
+ name: fastlane
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rake
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :development
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: rspec
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :development
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: rspec_junit_formatter
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ - !ruby/object:Gem::Dependency
98
+ name: rubocop
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - '='
102
+ - !ruby/object:Gem::Version
103
+ version: '1.38'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - '='
109
+ - !ruby/object:Gem::Version
110
+ version: '1.38'
111
+ - !ruby/object:Gem::Dependency
112
+ name: rubocop-performance
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - ">="
116
+ - !ruby/object:Gem::Version
117
+ version: '0'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - ">="
123
+ - !ruby/object:Gem::Version
124
+ version: '0'
125
+ - !ruby/object:Gem::Dependency
126
+ name: rubocop-rake
127
+ requirement: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - '='
130
+ - !ruby/object:Gem::Version
131
+ version: 0.6.0
132
+ type: :development
133
+ prerelease: false
134
+ version_requirements: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - '='
137
+ - !ruby/object:Gem::Version
138
+ version: 0.6.0
139
+ - !ruby/object:Gem::Dependency
140
+ name: rubocop-require_tools
141
+ requirement: !ruby/object:Gem::Requirement
142
+ requirements:
143
+ - - ">="
144
+ - !ruby/object:Gem::Version
145
+ version: '0'
146
+ type: :development
147
+ prerelease: false
148
+ version_requirements: !ruby/object:Gem::Requirement
149
+ requirements:
150
+ - - ">="
151
+ - !ruby/object:Gem::Version
152
+ version: '0'
153
+ - !ruby/object:Gem::Dependency
154
+ name: rubocop-rspec
155
+ requirement: !ruby/object:Gem::Requirement
156
+ requirements:
157
+ - - '='
158
+ - !ruby/object:Gem::Version
159
+ version: 2.15.0
160
+ type: :development
161
+ prerelease: false
162
+ version_requirements: !ruby/object:Gem::Requirement
163
+ requirements:
164
+ - - '='
165
+ - !ruby/object:Gem::Version
166
+ version: 2.15.0
167
+ - !ruby/object:Gem::Dependency
168
+ name: simplecov
169
+ requirement: !ruby/object:Gem::Requirement
170
+ requirements:
171
+ - - ">="
172
+ - !ruby/object:Gem::Version
173
+ version: '0'
174
+ type: :development
175
+ prerelease: false
176
+ version_requirements: !ruby/object:Gem::Requirement
177
+ requirements:
178
+ - - ">="
179
+ - !ruby/object:Gem::Version
180
+ version: '0'
181
+ - !ruby/object:Gem::Dependency
182
+ name: colorize
183
+ requirement: !ruby/object:Gem::Requirement
184
+ requirements:
185
+ - - "~>"
186
+ - !ruby/object:Gem::Version
187
+ version: 0.8.1
188
+ type: :runtime
189
+ prerelease: false
190
+ version_requirements: !ruby/object:Gem::Requirement
191
+ requirements:
192
+ - - "~>"
193
+ - !ruby/object:Gem::Version
194
+ version: 0.8.1
195
+ - !ruby/object:Gem::Dependency
196
+ name: commander
197
+ requirement: !ruby/object:Gem::Requirement
198
+ requirements:
199
+ - - ">="
200
+ - !ruby/object:Gem::Version
201
+ version: '0'
202
+ type: :runtime
203
+ prerelease: false
204
+ version_requirements: !ruby/object:Gem::Requirement
205
+ requirements:
206
+ - - ">="
207
+ - !ruby/object:Gem::Version
208
+ version: '0'
209
+ description:
210
+ email:
211
+ - a.alterpesotskiy@mail.ru
212
+ executables:
213
+ - xcmonkey
214
+ extensions: []
215
+ extra_rdoc_files: []
216
+ files:
217
+ - ".fasterer.yml"
218
+ - ".github/FUNDING.yml"
219
+ - ".github/dependabot.yml"
220
+ - ".github/pull_request_template.md"
221
+ - ".github/workflows/Test.yml"
222
+ - ".gitignore"
223
+ - ".rspec"
224
+ - ".rubocop.yml"
225
+ - CODE_OF_CONDUCT.md
226
+ - Gemfile
227
+ - LICENSE
228
+ - README.md
229
+ - Rakefile
230
+ - bin/console
231
+ - bin/setup
232
+ - bin/xcmonkey
233
+ - fastlane/Fastfile
234
+ - lib/xcmonkey.rb
235
+ - lib/xcmonkey/describer.rb
236
+ - lib/xcmonkey/driver.rb
237
+ - lib/xcmonkey/logger.rb
238
+ - lib/xcmonkey/version.rb
239
+ - spec/describer_spec.rb
240
+ - spec/driver_spec.rb
241
+ - spec/logger_spec.rb
242
+ - spec/spec_helper.rb
243
+ - spec/xcmonkey_spec.rb
244
+ - xcmonkey.gemspec
245
+ homepage: https://github.com/alteral/xcmonkey
246
+ licenses:
247
+ - MIT
248
+ metadata:
249
+ allowed_push_host: https://rubygems.org
250
+ rubygems_mfa_required: 'true'
251
+ post_install_message:
252
+ rdoc_options: []
253
+ require_paths:
254
+ - lib
255
+ required_ruby_version: !ruby/object:Gem::Requirement
256
+ requirements:
257
+ - - ">="
258
+ - !ruby/object:Gem::Version
259
+ version: '2.4'
260
+ required_rubygems_version: !ruby/object:Gem::Requirement
261
+ requirements:
262
+ - - ">="
263
+ - !ruby/object:Gem::Version
264
+ version: '0'
265
+ requirements: []
266
+ rubygems_version: 3.3.7
267
+ signing_key:
268
+ specification_version: 4
269
+ summary: A tool for doing randomised UI testing of iOS apps
270
+ test_files: []