xcmonkey 0.3.0 → 1.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 +4 -4
- data/.github/ISSUE_TEMPLATE/bug_report.md +26 -0
- data/.github/ISSUE_TEMPLATE/feature-request.md +26 -0
- data/.github/pull_request_template.md +5 -0
- data/.github/workflows/test.yml +3 -2
- data/.gitignore +3 -0
- data/README.md +48 -29
- data/bin/xcmonkey +17 -5
- data/lib/xcmonkey/describer.rb +16 -17
- data/lib/xcmonkey/driver.rb +74 -23
- data/lib/xcmonkey/repeater.rb +39 -0
- data/lib/xcmonkey/version.rb +2 -3
- data/lib/xcmonkey.rb +33 -35
- data/spec/driver_spec.rb +89 -16
- data/spec/repeater_spec.rb +51 -0
- data/spec/xcmonkey_spec.rb +49 -55
- data/xcmonkey.gemspec +1 -1
- metadata +6 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 7d3bd605b57ec05a83a3cca39707b5c2fc9529828131b9ac872f3dc478180bbb
|
|
4
|
+
data.tar.gz: 8793e64f73775a2c8de84d1b79d0aebd258df7c22b9c8f43de2618f63f306d4e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 4e4634518e71dac0ed206d4fec26fc707fb9dbea8116c87db1959695261e8ee311246db203317790433d18b5e5250074f1f17bf567101dfec6767a1ed5db0670
|
|
7
|
+
data.tar.gz: 0e696e9804b94e6106f203b9bcda008f57dbe7fbda78a68ff7d57a8cfa980e4b691b533f430bac855ae8ed56ae0e7ac5085f305853bae8f03652095871287a94
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: Bug report
|
|
3
|
+
about: Create a report to help us improve
|
|
4
|
+
title: ''
|
|
5
|
+
labels: ''
|
|
6
|
+
assignees: ''
|
|
7
|
+
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## What did you do?
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
## What did you expect to happen?
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
## What happened instead?
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
## Environment
|
|
20
|
+
|
|
21
|
+
- `xcmonkey` version:
|
|
22
|
+
- `idb` version:
|
|
23
|
+
- `xcode` version:
|
|
24
|
+
- `macOS` version:
|
|
25
|
+
|
|
26
|
+
## Additional context
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: Feature request
|
|
3
|
+
about: Got any ideas about new features? Let us know!
|
|
4
|
+
title: ''
|
|
5
|
+
labels: ''
|
|
6
|
+
assignees: ''
|
|
7
|
+
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## What are you trying to achieve?
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
## If possible, how can you achieve this currently?
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
## What would be the better way?
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
## Environment
|
|
20
|
+
|
|
21
|
+
- `xcmonkey` version:
|
|
22
|
+
- `idb` version:
|
|
23
|
+
- `xcode` version:
|
|
24
|
+
- `macOS` version:
|
|
25
|
+
|
|
26
|
+
## Additional context
|
data/.github/workflows/test.yml
CHANGED
|
@@ -17,10 +17,11 @@ jobs:
|
|
|
17
17
|
env:
|
|
18
18
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
19
19
|
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
|
|
20
|
+
PR_NUMBER: ${{ github.event.number }}
|
|
20
21
|
steps:
|
|
21
|
-
- uses: actions/checkout@v3.
|
|
22
|
+
- uses: actions/checkout@v3.3.0
|
|
22
23
|
|
|
23
|
-
- uses: actions/setup-python@v4.
|
|
24
|
+
- uses: actions/setup-python@v4.5.0
|
|
24
25
|
with:
|
|
25
26
|
python-version: 3.11
|
|
26
27
|
cache: 'pip'
|
data/.gitignore
CHANGED
data/README.md
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
|
|
12
12
|
## Description
|
|
13
13
|
|
|
14
|
-
*xcmonkey* is a tool for doing
|
|
14
|
+
*xcmonkey* is a tool for doing stress testing of iOS apps. It's inspired by and has similar goals to [*monkey*](https://developer.android.com/studio/test/monkey) on Android.
|
|
15
15
|
|
|
16
16
|
Under the hood, *xcmonkey* uses [iOS Development Bridge](https://fbidb.io/) as a driver, that's why it's pretty smart and can do a lot of things, such as taps, swipes and presses. All that comes «pseudo-random» because it has access to the screen hierarchy, and so can either do actions blindly (like tapping on random points) or precisely (like tapping on the existing elements).
|
|
17
17
|
|
|
@@ -39,10 +39,32 @@ gem 'xcmonkey'
|
|
|
39
39
|
### To run a stress test
|
|
40
40
|
|
|
41
41
|
```bash
|
|
42
|
-
|
|
43
|
-
|
|
42
|
+
xcmonkey test --udid "413EA256-CFFB-4312-94A6-12592BEE4CBA" --bundle-id "com.apple.Maps" --duration 100
|
|
43
|
+
|
|
44
|
+
12:44:19.343: Device info: {
|
|
45
|
+
"name": "iPhone 14 Pro",
|
|
46
|
+
"udid": "413EA256-CFFB-4312-94A6-12592BEE4CBA",
|
|
47
|
+
"state": "Booted",
|
|
48
|
+
"type": "simulator",
|
|
49
|
+
"os_version": "iOS 16.2",
|
|
50
|
+
"architecture": "x86_64",
|
|
51
|
+
"path": "/tmp/idb/413EA256-CFFB-4312-94A6-12592BEE4CBA_companion.sock",
|
|
52
|
+
"is_local": true,
|
|
53
|
+
"companion": "/tmp/idb/413EA256-CFFB-4312-94A6-12592BEE4CBA_companion.sock"
|
|
54
|
+
}
|
|
44
55
|
|
|
45
|
-
12:44:22.550: App info:
|
|
56
|
+
12:44:22.550: App info: {
|
|
57
|
+
"bundle_id": "com.apple.Maps",
|
|
58
|
+
"name": "Maps",
|
|
59
|
+
"install_type": "system",
|
|
60
|
+
"architectures": [
|
|
61
|
+
"x86_64",
|
|
62
|
+
"arm64"
|
|
63
|
+
],
|
|
64
|
+
"process_state": "Running",
|
|
65
|
+
"debuggable": false,
|
|
66
|
+
"pid": "49186"
|
|
67
|
+
}
|
|
46
68
|
|
|
47
69
|
12:44:23.203: Tap: {
|
|
48
70
|
"x": 53,
|
|
@@ -63,35 +85,32 @@ $ xcmonkey test --udid "413EA256-CFFB-4312-94A6-12592BEE4CBA" --bundle-id "com.a
|
|
|
63
85
|
}
|
|
64
86
|
```
|
|
65
87
|
|
|
88
|
+
### To repeat the stress test from generated session
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
xcmonkey repeat --session-path "./xcmonkey-session.json"
|
|
92
|
+
```
|
|
93
|
+
|
|
66
94
|
### To describe the required point
|
|
67
95
|
|
|
68
96
|
```bash
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
20:05:21.713: x:20 y:625 point info: {
|
|
73
|
-
"AXFrame": "{{19, 624.3}, {86, 130.6}}",
|
|
74
|
-
"AXUniqueId": "ShortcutsRowCell",
|
|
75
|
-
"frame": {
|
|
76
|
-
"y": 624.3,
|
|
77
|
-
"x": 19,
|
|
78
|
-
"width": 86,
|
|
79
|
-
"height": 130.6
|
|
80
|
-
},
|
|
81
|
-
"role_description": "button",
|
|
82
|
-
"AXLabel": "Home",
|
|
83
|
-
"content_required": false,
|
|
84
|
-
"type": "Button",
|
|
85
|
-
"title": null,
|
|
86
|
-
"help": null,
|
|
87
|
-
"custom_actions": [
|
|
97
|
+
xcmonkey describe -x 20 -y 625 --udid "413EA256-CFFB-4312-94A6-12592BEE4CBA"
|
|
98
|
+
```
|
|
88
99
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
100
|
+
## [fastlane](https://github.com/fastlane/fastlane) integration
|
|
101
|
+
|
|
102
|
+
To run *xcmonkey* from *fastlane*, add the following code to your `Fastfile`:
|
|
103
|
+
|
|
104
|
+
```ruby
|
|
105
|
+
require 'xcmonkey'
|
|
106
|
+
|
|
107
|
+
lane :test do
|
|
108
|
+
Xcmonkey.new(
|
|
109
|
+
udid: '413EA256-CFFB-4312-94A6-12592BEE4CBA',
|
|
110
|
+
bundle_id: 'com.apple.Maps',
|
|
111
|
+
duration: 100
|
|
112
|
+
).run
|
|
113
|
+
end
|
|
95
114
|
```
|
|
96
115
|
|
|
97
116
|
## Code of Conduct
|
data/bin/xcmonkey
CHANGED
|
@@ -3,11 +3,12 @@
|
|
|
3
3
|
require 'commander/import'
|
|
4
4
|
require_relative '../lib/xcmonkey'
|
|
5
5
|
require_relative '../lib/xcmonkey/describer'
|
|
6
|
+
require_relative '../lib/xcmonkey/repeater'
|
|
6
7
|
require_relative '../lib/xcmonkey/logger'
|
|
7
8
|
require_relative '../lib/xcmonkey/driver'
|
|
8
9
|
require_relative '../lib/xcmonkey/version'
|
|
9
10
|
|
|
10
|
-
|
|
11
|
+
class Xcmonkey
|
|
11
12
|
program :version, VERSION
|
|
12
13
|
program :description, 'xcmonkey is a tool for doing randomised UI testing of iOS apps'
|
|
13
14
|
|
|
@@ -18,25 +19,36 @@ module Xcmonkey
|
|
|
18
19
|
c.option('-b', '--bundle-id STRING', String, 'Set target bundle identifier')
|
|
19
20
|
c.option('-d', '--duration SECONDS', Integer, 'Test duration in seconds. Defaults to `60`')
|
|
20
21
|
c.option('-k', '--enable-simulator-keyboard', 'Should simulator keyboard be enabled? Defaults to `true`')
|
|
21
|
-
c.
|
|
22
|
-
|
|
22
|
+
c.option('-s', '--session-path STRING', String, 'Path where monkey testing session should be saved. Defaults to current directory')
|
|
23
|
+
c.action do |_, options|
|
|
23
24
|
params = {
|
|
24
25
|
udid: options.udid,
|
|
25
26
|
bundle_id: options.bundle_id,
|
|
26
27
|
duration: options.duration,
|
|
27
|
-
|
|
28
|
+
session_path: options.session_path,
|
|
29
|
+
enable_simulator_keyboard: options.enable_simulator_keyboard
|
|
28
30
|
}
|
|
29
31
|
Xcmonkey.new(params).run
|
|
30
32
|
end
|
|
31
33
|
end
|
|
32
34
|
|
|
35
|
+
command :repeat do |c|
|
|
36
|
+
c.syntax = 'xcmonkey repeat [options]'
|
|
37
|
+
c.description = 'Repeats given session'
|
|
38
|
+
c.option('-s', '--session-path STRING', String, 'Path to monkey testing session')
|
|
39
|
+
c.action do |_, options|
|
|
40
|
+
params = { session_path: options.session_path }
|
|
41
|
+
Repeater.new(params).run
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
33
45
|
command :describe do |c|
|
|
34
46
|
c.syntax = 'xcmonkey describe [options]'
|
|
35
47
|
c.description = 'Describes given point'
|
|
36
48
|
c.option('-u', '--udid STRING', String, 'Set device UDID')
|
|
37
49
|
c.option('-x', '--x STRING', 'Point `x` coordinate')
|
|
38
50
|
c.option('-y', '--y STRING', 'Point `y` coordinate')
|
|
39
|
-
c.action do |
|
|
51
|
+
c.action do |_, options|
|
|
40
52
|
params = {
|
|
41
53
|
udid: options.udid,
|
|
42
54
|
x: options.x,
|
data/lib/xcmonkey/describer.rb
CHANGED
|
@@ -1,22 +1,21 @@
|
|
|
1
1
|
class Describer
|
|
2
|
-
|
|
2
|
+
attr_accessor :x, :y, :driver
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
end
|
|
4
|
+
def initialize(params)
|
|
5
|
+
ensure_required_params(params)
|
|
6
|
+
self.x = params[:x]
|
|
7
|
+
self.y = params[:y]
|
|
8
|
+
self.driver = Driver.new(params)
|
|
9
|
+
end
|
|
11
10
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
11
|
+
def run
|
|
12
|
+
driver.ensure_device_exists
|
|
13
|
+
driver.describe_point(x, y)
|
|
14
|
+
end
|
|
16
15
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
16
|
+
def ensure_required_params(params)
|
|
17
|
+
Logger.error('UDID should be provided') if params[:udid].nil?
|
|
18
|
+
Logger.error('`x` point coordinate should be provided') if params[:x].nil? || params[:x].to_i.to_s != params[:x].to_s
|
|
19
|
+
Logger.error('`y` point coordinate should be provided') if params[:y].nil? || params[:y].to_i.to_s != params[:y].to_s
|
|
20
|
+
end
|
|
22
21
|
end
|
data/lib/xcmonkey/driver.rb
CHANGED
|
@@ -1,18 +1,31 @@
|
|
|
1
1
|
class Driver
|
|
2
|
-
attr_accessor :udid, :bundle_id, :
|
|
2
|
+
attr_accessor :udid, :bundle_id, :enable_simulator_keyboard, :session_duration, :session_path, :session_actions
|
|
3
3
|
|
|
4
4
|
def initialize(params)
|
|
5
5
|
self.udid = params[:udid]
|
|
6
6
|
self.bundle_id = params[:bundle_id]
|
|
7
|
-
self.
|
|
7
|
+
self.session_duration = params[:duration]
|
|
8
|
+
self.session_path = params[:session_path]
|
|
8
9
|
self.enable_simulator_keyboard = params[:enable_simulator_keyboard]
|
|
10
|
+
self.session_actions = params[:session_actions]
|
|
11
|
+
@session = { params: params, actions: [] }
|
|
9
12
|
ensure_driver_installed
|
|
10
13
|
end
|
|
11
14
|
|
|
15
|
+
def monkey_test_precondition
|
|
16
|
+
puts
|
|
17
|
+
ensure_device_exists
|
|
18
|
+
ensure_app_installed
|
|
19
|
+
terminate_app
|
|
20
|
+
open_home_screen(with_tracker: true)
|
|
21
|
+
launch_app
|
|
22
|
+
end
|
|
23
|
+
|
|
12
24
|
def monkey_test(gestures)
|
|
25
|
+
monkey_test_precondition
|
|
13
26
|
app_elements = describe_ui.shuffle
|
|
14
27
|
current_time = Time.now
|
|
15
|
-
while Time.now < current_time +
|
|
28
|
+
while Time.now < current_time + session_duration
|
|
16
29
|
el1_coordinates = central_coordinates(app_elements.first)
|
|
17
30
|
el2_coordinates = central_coordinates(app_elements.last)
|
|
18
31
|
case gestures.sample
|
|
@@ -40,7 +53,32 @@ class Driver
|
|
|
40
53
|
next
|
|
41
54
|
end
|
|
42
55
|
app_elements = describe_ui.shuffle
|
|
43
|
-
|
|
56
|
+
next unless app_elements.include?(@home_tracker)
|
|
57
|
+
|
|
58
|
+
save_session
|
|
59
|
+
Logger.error('App lost')
|
|
60
|
+
end
|
|
61
|
+
save_session
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def repeat_monkey_test
|
|
65
|
+
monkey_test_precondition
|
|
66
|
+
session_actions.each do |action|
|
|
67
|
+
case action['type']
|
|
68
|
+
when 'tap'
|
|
69
|
+
tap(coordinates: { x: action['x'], y: action['y'] })
|
|
70
|
+
when 'press'
|
|
71
|
+
press(coordinates: { x: action['x'], y: action['y'] }, duration: action['duration'])
|
|
72
|
+
when 'swipe'
|
|
73
|
+
swipe(
|
|
74
|
+
start_coordinates: { x: action['x'], y: action['y'] },
|
|
75
|
+
end_coordinates: { x: action['endX'], y: action['endY'] },
|
|
76
|
+
duration: action['duration']
|
|
77
|
+
)
|
|
78
|
+
else
|
|
79
|
+
next
|
|
80
|
+
end
|
|
81
|
+
Logger.error('App lost') if describe_ui.shuffle.include?(@home_tracker)
|
|
44
82
|
end
|
|
45
83
|
end
|
|
46
84
|
|
|
@@ -84,40 +122,40 @@ class Driver
|
|
|
84
122
|
end
|
|
85
123
|
|
|
86
124
|
def list_targets
|
|
87
|
-
@
|
|
88
|
-
@
|
|
125
|
+
@targets ||= `idb list-targets --json`.split("\n").map! { |target| JSON.parse(target) }
|
|
126
|
+
@targets
|
|
89
127
|
end
|
|
90
128
|
|
|
91
|
-
def
|
|
92
|
-
`idb list-
|
|
129
|
+
def list_apps
|
|
130
|
+
`idb list-apps --udid #{udid} --json`.split("\n").map! { |app| JSON.parse(app) }
|
|
93
131
|
end
|
|
94
132
|
|
|
95
133
|
def ensure_app_installed
|
|
96
|
-
|
|
134
|
+
return if list_apps.any? { |app| app['bundle_id'] == bundle_id }
|
|
135
|
+
|
|
136
|
+
Logger.error("App #{bundle_id} is not installed on device #{udid}")
|
|
97
137
|
end
|
|
98
138
|
|
|
99
139
|
def ensure_device_exists
|
|
100
|
-
device = list_targets.detect { |target| target
|
|
140
|
+
device = list_targets.detect { |target| target['udid'] == udid }
|
|
101
141
|
Logger.error("Can't find device #{udid}") if device.nil?
|
|
102
142
|
|
|
103
|
-
Logger.info('Device info:', payload: device)
|
|
104
|
-
if device
|
|
143
|
+
Logger.info('Device info:', payload: JSON.pretty_generate(device))
|
|
144
|
+
if device['type'] == 'simulator'
|
|
105
145
|
configure_simulator_keyboard
|
|
106
146
|
boot_simulator
|
|
107
147
|
end
|
|
108
148
|
end
|
|
109
149
|
|
|
110
|
-
def list_apps
|
|
111
|
-
`idb list-apps --udid #{udid}`
|
|
112
|
-
end
|
|
113
|
-
|
|
114
150
|
def tap(coordinates:)
|
|
115
151
|
Logger.info('Tap:', payload: JSON.pretty_generate(coordinates))
|
|
152
|
+
@session[:actions] << { type: :tap, x: coordinates[:x], y: coordinates[:y] } unless session_actions
|
|
116
153
|
`idb ui tap --udid #{udid} #{coordinates[:x]} #{coordinates[:y]}`
|
|
117
154
|
end
|
|
118
155
|
|
|
119
156
|
def press(coordinates:, duration:)
|
|
120
157
|
Logger.info("Press (#{duration}s):", payload: JSON.pretty_generate(coordinates))
|
|
158
|
+
@session[:actions] << { type: :press, x: coordinates[:x], y: coordinates[:y], duration: duration } unless session_actions
|
|
121
159
|
`idb ui tap --udid #{udid} --duration #{duration} #{coordinates[:x]} #{coordinates[:y]}`
|
|
122
160
|
end
|
|
123
161
|
|
|
@@ -126,6 +164,16 @@ class Driver
|
|
|
126
164
|
"Swipe (#{duration}s):",
|
|
127
165
|
payload: "#{JSON.pretty_generate(start_coordinates)} => #{JSON.pretty_generate(end_coordinates)}"
|
|
128
166
|
)
|
|
167
|
+
unless session_actions
|
|
168
|
+
@session[:actions] << {
|
|
169
|
+
type: :swipe,
|
|
170
|
+
x: start_coordinates[:x],
|
|
171
|
+
y: start_coordinates[:y],
|
|
172
|
+
endX: end_coordinates[:x],
|
|
173
|
+
endY: end_coordinates[:y],
|
|
174
|
+
duration: duration
|
|
175
|
+
}
|
|
176
|
+
end
|
|
129
177
|
coordinates = "#{start_coordinates[:x]} #{start_coordinates[:y]} #{end_coordinates[:x]} #{end_coordinates[:y]}"
|
|
130
178
|
`idb ui swipe --udid #{udid} --duration #{duration} #{coordinates}`
|
|
131
179
|
end
|
|
@@ -168,6 +216,10 @@ class Driver
|
|
|
168
216
|
rand(0.5..1.5).ceil(1)
|
|
169
217
|
end
|
|
170
218
|
|
|
219
|
+
def save_session
|
|
220
|
+
File.write("#{session_path}/xcmonkey-session.json", JSON.pretty_generate(@session))
|
|
221
|
+
end
|
|
222
|
+
|
|
171
223
|
private
|
|
172
224
|
|
|
173
225
|
def ensure_driver_installed
|
|
@@ -183,14 +235,13 @@ class Driver
|
|
|
183
235
|
end
|
|
184
236
|
|
|
185
237
|
def wait_until_app_launched
|
|
186
|
-
|
|
238
|
+
app_is_running = false
|
|
187
239
|
current_time = Time.now
|
|
188
|
-
while
|
|
189
|
-
app_info = list_apps.
|
|
190
|
-
|
|
191
|
-
end
|
|
240
|
+
while !app_is_running && Time.now < current_time + 5
|
|
241
|
+
app_info = list_apps.detect { |app| app['bundle_id'] == bundle_id }
|
|
242
|
+
app_is_running = app_info && app_info['process_state'] == 'Running'
|
|
192
243
|
end
|
|
193
|
-
Logger.error("Can't run the app #{bundle_id}")
|
|
194
|
-
Logger.info('App info:', payload: app_info)
|
|
244
|
+
Logger.error("Can't run the app #{bundle_id}") unless app_is_running
|
|
245
|
+
Logger.info('App info:', payload: JSON.pretty_generate(app_info))
|
|
195
246
|
end
|
|
196
247
|
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
class Repeater
|
|
2
|
+
attr_accessor :udid, :bundle_id, :enable_simulator_keyboard, :actions
|
|
3
|
+
|
|
4
|
+
def initialize(params)
|
|
5
|
+
validate_session(params[:session_path])
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
def run
|
|
9
|
+
params = {
|
|
10
|
+
udid: udid,
|
|
11
|
+
bundle_id: bundle_id,
|
|
12
|
+
enable_simulator_keyboard: enable_simulator_keyboard,
|
|
13
|
+
session_actions: actions
|
|
14
|
+
}
|
|
15
|
+
Driver.new(params).repeat_monkey_test
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def validate_session(session_path)
|
|
19
|
+
Logger.error("Provided session can't be found: #{session_path}") unless File.exist?(session_path)
|
|
20
|
+
|
|
21
|
+
session = JSON.parse(File.read(session_path))
|
|
22
|
+
|
|
23
|
+
if session['params'].nil?
|
|
24
|
+
Logger.error('Provided session is not valid: `params` should not be `nil`')
|
|
25
|
+
return
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
self.actions = session['actions']
|
|
29
|
+
Logger.error('Provided session is not valid: `actions` should not be `nil` or `empty`') if actions.nil? || actions.empty?
|
|
30
|
+
|
|
31
|
+
self.udid = session['params']['udid']
|
|
32
|
+
Logger.error('Provided session is not valid: `udid` should not be `nil`') if udid.nil?
|
|
33
|
+
|
|
34
|
+
self.bundle_id = session['params']['bundle_id']
|
|
35
|
+
Logger.error('Provided session is not valid: `bundle_id` should not be `nil`') if bundle_id.nil?
|
|
36
|
+
|
|
37
|
+
self.enable_simulator_keyboard = session['params']['enable_simulator_keyboard']
|
|
38
|
+
end
|
|
39
|
+
end
|
data/lib/xcmonkey/version.rb
CHANGED
data/lib/xcmonkey.rb
CHANGED
|
@@ -1,44 +1,42 @@
|
|
|
1
1
|
require 'json'
|
|
2
2
|
require 'colorize'
|
|
3
3
|
require_relative 'xcmonkey/describer'
|
|
4
|
+
require_relative 'xcmonkey/repeater'
|
|
4
5
|
require_relative 'xcmonkey/version'
|
|
5
6
|
require_relative 'xcmonkey/logger'
|
|
6
7
|
require_relative 'xcmonkey/driver'
|
|
7
8
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
9
|
+
class Xcmonkey
|
|
10
|
+
attr_accessor :driver
|
|
11
|
+
|
|
12
|
+
def initialize(params)
|
|
13
|
+
params[:session_path] = Dir.pwd if params[:session_path].nil?
|
|
14
|
+
params[:duration] = 60 if params[:duration].nil?
|
|
15
|
+
params[:enable_simulator_keyboard] = true if params[:enable_simulator_keyboard].nil?
|
|
16
|
+
ensure_required_params(params)
|
|
17
|
+
self.driver = Driver.new(params)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def run
|
|
21
|
+
driver.monkey_test(gestures)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def gestures
|
|
25
|
+
taps = [:precise_tap, :blind_tap] * 10
|
|
26
|
+
swipes = [:precise_swipe, :blind_swipe] * 5
|
|
27
|
+
presses = [:precise_press, :blind_press]
|
|
28
|
+
taps + swipes + presses
|
|
29
|
+
end
|
|
28
30
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
Logger.error('Duration must be Integer and not less than 1 second')
|
|
41
|
-
end
|
|
42
|
-
end
|
|
43
|
-
end
|
|
31
|
+
def ensure_required_params(params)
|
|
32
|
+
Logger.error('UDID should be provided') if params[:udid].nil?
|
|
33
|
+
|
|
34
|
+
Logger.error('Bundle identifier should be provided') if params[:bundle_id].nil?
|
|
35
|
+
|
|
36
|
+
Logger.error('Session path should be a directory') if params[:session_path].nil? || !File.directory?(params[:session_path])
|
|
37
|
+
|
|
38
|
+
if params[:duration].nil? || !params[:duration].kind_of?(Integer) || !params[:duration].positive?
|
|
39
|
+
Logger.error('Duration must be Integer and not less than 1 second')
|
|
40
|
+
end
|
|
41
|
+
end
|
|
44
42
|
end
|
data/spec/driver_spec.rb
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
describe Driver do
|
|
2
2
|
let(:udid) { `xcrun simctl list | grep " iPhone 14 Pro Max"`.split("\n")[0].split('(')[1].split(')')[0] }
|
|
3
3
|
let(:bundle_id) { 'com.apple.Maps' }
|
|
4
|
-
let(:driver) { described_class.new(udid: udid, bundle_id: bundle_id) }
|
|
4
|
+
let(:driver) { described_class.new(udid: udid, bundle_id: bundle_id, session_path: Dir.pwd) }
|
|
5
|
+
let(:driver_with_session) { described_class.new(udid: udid, bundle_id: bundle_id, session_actions: [{ type: 'tap', x: 0, y: 0 }]) }
|
|
5
6
|
|
|
6
7
|
it 'verifies that sumulator was booted' do
|
|
7
8
|
error_message = "Failed to boot #{udid}"
|
|
@@ -9,12 +10,6 @@ describe Driver do
|
|
|
9
10
|
expect { driver.boot_simulator }.not_to raise_error
|
|
10
11
|
end
|
|
11
12
|
|
|
12
|
-
it 'verifies that there are booted simulators' do
|
|
13
|
-
driver.boot_simulator
|
|
14
|
-
booted_simulators = driver.list_booted_simulators
|
|
15
|
-
expect(booted_simulators).not_to be_empty
|
|
16
|
-
end
|
|
17
|
-
|
|
18
13
|
it 'verifies that ui can be described' do
|
|
19
14
|
driver.boot_simulator
|
|
20
15
|
ui = driver.describe_ui
|
|
@@ -40,8 +35,8 @@ describe Driver do
|
|
|
40
35
|
|
|
41
36
|
it 'verifies that list of apps can be showed' do
|
|
42
37
|
driver.boot_simulator
|
|
43
|
-
|
|
44
|
-
expect(
|
|
38
|
+
app_exists = driver.list_apps.any? { |app| app['bundle_id'] == bundle_id }
|
|
39
|
+
expect(app_exists).to be(true)
|
|
45
40
|
end
|
|
46
41
|
|
|
47
42
|
it 'verifies that app installed' do
|
|
@@ -61,11 +56,11 @@ describe Driver do
|
|
|
61
56
|
end
|
|
62
57
|
|
|
63
58
|
it 'verifies that device exists' do
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
expect(Logger).
|
|
67
|
-
expect(Logger).to receive(:info).with('Device info:', payload: payload)
|
|
59
|
+
payload = driver.list_targets.detect { |target| target['udid'] == udid }
|
|
60
|
+
expect(Logger).not_to receive(:error)
|
|
61
|
+
expect(Logger).to receive(:info).with('Device info:', payload: JSON.pretty_generate(payload))
|
|
68
62
|
expect(driver).to receive(:boot_simulator)
|
|
63
|
+
expect(driver).to receive(:configure_simulator_keyboard)
|
|
69
64
|
expect { driver.ensure_device_exists }.not_to raise_error
|
|
70
65
|
end
|
|
71
66
|
|
|
@@ -150,28 +145,106 @@ describe Driver do
|
|
|
150
145
|
expect { driver.launch_app }.not_to raise_error
|
|
151
146
|
end
|
|
152
147
|
|
|
153
|
-
it 'verifies tap' do
|
|
148
|
+
it 'verifies tap in new session' do
|
|
154
149
|
driver.boot_simulator
|
|
155
150
|
coordinates = { x: 1, y: 1 }
|
|
156
151
|
expect(Logger).to receive(:info).with('Tap:', payload: JSON.pretty_generate(coordinates))
|
|
157
152
|
driver.tap(coordinates: coordinates)
|
|
153
|
+
expect(driver.instance_variable_get(:@session)[:actions]).not_to be_empty
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
it 'verifies tap in old session' do
|
|
157
|
+
driver_with_session.boot_simulator
|
|
158
|
+
coordinates = { x: 1, y: 1 }
|
|
159
|
+
expect(Logger).to receive(:info).with('Tap:', payload: JSON.pretty_generate(coordinates))
|
|
160
|
+
driver_with_session.tap(coordinates: coordinates)
|
|
161
|
+
expect(driver_with_session.instance_variable_get(:@session)[:actions]).to be_empty
|
|
158
162
|
end
|
|
159
163
|
|
|
160
|
-
it 'verifies press' do
|
|
164
|
+
it 'verifies press in new session' do
|
|
161
165
|
driver.boot_simulator
|
|
162
166
|
duration = 0.5
|
|
163
167
|
coordinates = { x: 1, y: 1 }
|
|
164
168
|
expect(Logger).to receive(:info).with("Press (#{duration}s):", payload: JSON.pretty_generate(coordinates))
|
|
165
169
|
driver.press(coordinates: coordinates, duration: duration)
|
|
170
|
+
expect(driver.instance_variable_get(:@session)[:actions]).not_to be_empty
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
it 'verifies press in old session' do
|
|
174
|
+
driver_with_session.boot_simulator
|
|
175
|
+
duration = 0.5
|
|
176
|
+
coordinates = { x: 1, y: 1 }
|
|
177
|
+
expect(Logger).to receive(:info).with("Press (#{duration}s):", payload: JSON.pretty_generate(coordinates))
|
|
178
|
+
driver_with_session.press(coordinates: coordinates, duration: duration)
|
|
179
|
+
expect(driver_with_session.instance_variable_get(:@session)[:actions]).to be_empty
|
|
166
180
|
end
|
|
167
181
|
|
|
168
|
-
it 'verifies swipe' do
|
|
182
|
+
it 'verifies swipe in new session' do
|
|
169
183
|
driver.boot_simulator
|
|
170
184
|
duration = 0.5
|
|
171
185
|
start_coordinates = { x: 1, y: 1 }
|
|
172
186
|
end_coordinates = { x: 2, y: 2 }
|
|
173
187
|
expect(Logger).to receive(:info).with("Swipe (#{duration}s):", payload: "#{JSON.pretty_generate(start_coordinates)} => #{JSON.pretty_generate(end_coordinates)}")
|
|
174
188
|
driver.swipe(start_coordinates: start_coordinates, end_coordinates: end_coordinates, duration: duration)
|
|
189
|
+
expect(driver.instance_variable_get(:@session)[:actions]).not_to be_empty
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
it 'verifies swipe in old session' do
|
|
193
|
+
driver_with_session.boot_simulator
|
|
194
|
+
duration = 0.5
|
|
195
|
+
start_coordinates = { x: 1, y: 1 }
|
|
196
|
+
end_coordinates = { x: 2, y: 2 }
|
|
197
|
+
expect(Logger).to receive(:info).with("Swipe (#{duration}s):", payload: "#{JSON.pretty_generate(start_coordinates)} => #{JSON.pretty_generate(end_coordinates)}")
|
|
198
|
+
driver_with_session.swipe(start_coordinates: start_coordinates, end_coordinates: end_coordinates, duration: duration)
|
|
199
|
+
expect(driver_with_session.instance_variable_get(:@session)[:actions]).to be_empty
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
it 'verifies that session can be saved' do
|
|
203
|
+
expect(File).to receive(:write)
|
|
204
|
+
driver.instance_variable_set(:@session, { params: {}, actions: [] })
|
|
205
|
+
driver.save_session
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
it 'verifies that monkey_test_precondition works fine' do
|
|
209
|
+
driver.monkey_test_precondition
|
|
210
|
+
app_info = driver.list_apps.detect { |app| app['bundle_id'] == bundle_id }
|
|
211
|
+
app_is_running = app_info && app_info['process_state'] == 'Running'
|
|
212
|
+
expect(app_is_running).to be(true)
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
it 'verifies that monkey_test works fine' do
|
|
216
|
+
params = { udid: udid, bundle_id: bundle_id, duration: 1, session_path: Dir.pwd }
|
|
217
|
+
driver = described_class.new(params)
|
|
218
|
+
expect(driver).to receive(:monkey_test_precondition)
|
|
219
|
+
driver.monkey_test(Xcmonkey.new(params).gestures)
|
|
220
|
+
expect(driver.instance_variable_get(:@session)[:actions]).not_to be_empty
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
it 'verifies that repeat_monkey_test works fine' do
|
|
224
|
+
session_actions = [
|
|
225
|
+
{ 'type' => 'tap', 'x' => 10, 'y' => 10 },
|
|
226
|
+
{ 'type' => 'press', 'x' => 11, 'y' => 11, 'duration' => 1.4 },
|
|
227
|
+
{ 'type' => 'swipe', 'x' => 12, 'y' => 12, 'endX' => 15, 'endY' => 15, 'duration' => 0.3 }
|
|
228
|
+
]
|
|
229
|
+
driver = described_class.new(udid: udid, bundle_id: bundle_id, session_actions: session_actions)
|
|
230
|
+
allow(Logger).to receive(:info).twice
|
|
231
|
+
expect(driver).to receive(:monkey_test_precondition)
|
|
232
|
+
expect(driver).to receive(:tap).with(coordinates: { x: 10, y: 10 })
|
|
233
|
+
expect(driver).to receive(:press).with(coordinates: { x: 11, y: 11 }, duration: 1.4)
|
|
234
|
+
expect(driver).to receive(:swipe).with(start_coordinates: { x: 12, y: 12 }, end_coordinates: { x: 15, y: 15 }, duration: 0.3)
|
|
235
|
+
driver.repeat_monkey_test
|
|
236
|
+
expect(driver.instance_variable_get(:@session)[:actions]).to be_empty
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
it 'verifies that unknown actions does not break repeat_monkey_test' do
|
|
240
|
+
driver = described_class.new(udid: udid, bundle_id: bundle_id, session_actions: [{ 'type' => 'test', 'x' => 10, 'y' => 10 }])
|
|
241
|
+
allow(Logger).to receive(:info).twice
|
|
242
|
+
expect(driver).to receive(:monkey_test_precondition)
|
|
243
|
+
expect(driver).not_to receive(:tap)
|
|
244
|
+
expect(driver).not_to receive(:press)
|
|
245
|
+
expect(driver).not_to receive(:swipe)
|
|
246
|
+
driver.repeat_monkey_test
|
|
247
|
+
expect(driver.instance_variable_get(:@session)[:actions]).to be_empty
|
|
175
248
|
end
|
|
176
249
|
|
|
177
250
|
it 'verifies that simulator was not booted' do
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
describe Repeater do
|
|
2
|
+
let(:session_path) { 'test/path/session.json' }
|
|
3
|
+
let(:session_file_content_full) { '{ "params": {"udid": "0", "bundle_id": "0", "enable_simulator_keyboard": true}, "actions": [{ "type": "tap", "x": 0, "y": 0 }] }' }
|
|
4
|
+
let(:session_file_content_without_params) { '{ "actions": [{ "type": "tap", "x": 0, "y": 0 }] }' }
|
|
5
|
+
let(:session_file_content_with_empty_actions) { '{ "params": {"udid": "0", "bundle_id": "0", "enable_simulator_keyboard": true}, "actions": [] }' }
|
|
6
|
+
let(:session_file_content_without_actions) { '{ "params": {"udid": "0", "bundle_id": "0", "enable_simulator_keyboard": true} }' }
|
|
7
|
+
let(:session_file_content_without_bundle_id) { '{ "params": {"udid": "0", "enable_simulator_keyboard": true}, "actions": [{ "type": "tap", "x": 0, "y": 0 }] }' }
|
|
8
|
+
let(:session_file_content_without_udid) { '{ "params": {"bundle_id": "0", "enable_simulator_keyboard": true}, "actions": [{ "type": "tap", "x": 0, "y": 0 }] }' }
|
|
9
|
+
|
|
10
|
+
it 'verifies that session cannot be validated without params' do
|
|
11
|
+
allow(File).to receive(:exist?).and_return(true)
|
|
12
|
+
allow(File).to receive(:read).and_return(session_file_content_without_params)
|
|
13
|
+
expect(Logger).to receive(:error).with('Provided session is not valid: `params` should not be `nil`')
|
|
14
|
+
described_class.new(session_path: session_path)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
it 'verifies that session cannot be validated without actions' do
|
|
18
|
+
allow(File).to receive(:exist?).and_return(true)
|
|
19
|
+
allow(File).to receive(:read).and_return(session_file_content_without_actions)
|
|
20
|
+
expect(Logger).to receive(:error).with('Provided session is not valid: `actions` should not be `nil` or `empty`')
|
|
21
|
+
described_class.new(session_path: session_path)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
it 'verifies that session cannot be validated with empty actions' do
|
|
25
|
+
allow(File).to receive(:exist?).and_return(true)
|
|
26
|
+
allow(File).to receive(:read).and_return(session_file_content_with_empty_actions)
|
|
27
|
+
expect(Logger).to receive(:error).with('Provided session is not valid: `actions` should not be `nil` or `empty`')
|
|
28
|
+
described_class.new(session_path: session_path)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
it 'verifies that session cannot be validated without bundle id' do
|
|
32
|
+
allow(File).to receive(:exist?).and_return(true)
|
|
33
|
+
allow(File).to receive(:read).and_return(session_file_content_without_bundle_id)
|
|
34
|
+
expect(Logger).to receive(:error).with('Provided session is not valid: `bundle_id` should not be `nil`')
|
|
35
|
+
described_class.new(session_path: session_path)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
it 'verifies that session cannot be validated without udid' do
|
|
39
|
+
allow(File).to receive(:exist?).and_return(true)
|
|
40
|
+
allow(File).to receive(:read).and_return(session_file_content_without_udid)
|
|
41
|
+
expect(Logger).to receive(:error).with('Provided session is not valid: `udid` should not be `nil`')
|
|
42
|
+
described_class.new(session_path: session_path)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
it 'verifies that session validation can pass' do
|
|
46
|
+
allow(File).to receive(:exist?).and_return(true)
|
|
47
|
+
allow(File).to receive(:read).and_return(session_file_content_full)
|
|
48
|
+
expect(Logger).not_to receive(:error)
|
|
49
|
+
described_class.new(session_path: session_path)
|
|
50
|
+
end
|
|
51
|
+
end
|
data/spec/xcmonkey_spec.rb
CHANGED
|
@@ -1,58 +1,52 @@
|
|
|
1
1
|
describe Xcmonkey do
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
expect(current_version).to be > Gem::Version.new('0.1.0')
|
|
52
|
-
end
|
|
53
|
-
|
|
54
|
-
it 'verifies gem name' do
|
|
55
|
-
expect(Xcmonkey::GEM_NAME).to eq('xcmonkey')
|
|
56
|
-
end
|
|
2
|
+
let(:params) { { udid: '123', bundle_id: 'example.com.app', duration: 10, session_path: Dir.pwd } }
|
|
3
|
+
let(:duration_error_msg) { 'Duration must be Integer and not less than 1 second' }
|
|
4
|
+
|
|
5
|
+
it 'verifies gestures' do
|
|
6
|
+
gestures = described_class.new(params).gestures
|
|
7
|
+
taps = [:precise_tap, :blind_tap] * 10
|
|
8
|
+
swipes = [:precise_swipe, :blind_swipe] * 5
|
|
9
|
+
presses = [:precise_press, :blind_press]
|
|
10
|
+
expect(gestures) =~ presses + taps + swipes
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
it 'verifies required params' do
|
|
14
|
+
expect(Logger).not_to receive(:error)
|
|
15
|
+
described_class.new(params)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
it 'verifies `udid` param is required' do
|
|
19
|
+
params[:udid] = nil
|
|
20
|
+
expect(Logger).to receive(:error).with('UDID should be provided')
|
|
21
|
+
described_class.new(params)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
it 'verifies `bundle_id` param is required' do
|
|
25
|
+
params[:bundle_id] = nil
|
|
26
|
+
expect(Logger).to receive(:error).with('Bundle identifier should be provided')
|
|
27
|
+
described_class.new(params)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
it 'verifies `duration` param is optional' do
|
|
31
|
+
params[:duration] = nil
|
|
32
|
+
expect(Logger).not_to receive(:error)
|
|
33
|
+
described_class.new(params)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
it 'verifies `duration` param cannot be equal to zero' do
|
|
37
|
+
params[:duration] = 0
|
|
38
|
+
expect(Logger).to receive(:error).with(duration_error_msg)
|
|
39
|
+
described_class.new(params)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
it 'verifies `duration` param cannot be negative' do
|
|
43
|
+
params[:duration] = -1
|
|
44
|
+
expect(Logger).to receive(:error).with(duration_error_msg)
|
|
45
|
+
described_class.new(params)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
it 'verifies version' do
|
|
49
|
+
current_version = Gem::Version.new(Xcmonkey::VERSION)
|
|
50
|
+
expect(current_version).to be > Gem::Version.new('0.1.0')
|
|
57
51
|
end
|
|
58
52
|
end
|
data/xcmonkey.gemspec
CHANGED
|
@@ -3,7 +3,7 @@ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
|
|
3
3
|
require "xcmonkey/version"
|
|
4
4
|
|
|
5
5
|
Gem::Specification.new do |spec|
|
|
6
|
-
spec.name =
|
|
6
|
+
spec.name = "xcmonkey"
|
|
7
7
|
spec.version = Xcmonkey::VERSION
|
|
8
8
|
spec.authors = ["alteral"]
|
|
9
9
|
spec.email = ["a.alterpesotskiy@mail.ru"]
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: xcmonkey
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version:
|
|
4
|
+
version: 1.1.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- alteral
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2023-01-
|
|
11
|
+
date: 2023-01-20 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: bundler
|
|
@@ -216,6 +216,8 @@ extra_rdoc_files: []
|
|
|
216
216
|
files:
|
|
217
217
|
- ".fasterer.yml"
|
|
218
218
|
- ".github/FUNDING.yml"
|
|
219
|
+
- ".github/ISSUE_TEMPLATE/bug_report.md"
|
|
220
|
+
- ".github/ISSUE_TEMPLATE/feature-request.md"
|
|
219
221
|
- ".github/dependabot.yml"
|
|
220
222
|
- ".github/pull_request_template.md"
|
|
221
223
|
- ".github/workflows/test.yml"
|
|
@@ -236,12 +238,14 @@ files:
|
|
|
236
238
|
- lib/xcmonkey/describer.rb
|
|
237
239
|
- lib/xcmonkey/driver.rb
|
|
238
240
|
- lib/xcmonkey/logger.rb
|
|
241
|
+
- lib/xcmonkey/repeater.rb
|
|
239
242
|
- lib/xcmonkey/version.rb
|
|
240
243
|
- requirements.txt
|
|
241
244
|
- sonar-project.properties
|
|
242
245
|
- spec/describer_spec.rb
|
|
243
246
|
- spec/driver_spec.rb
|
|
244
247
|
- spec/logger_spec.rb
|
|
248
|
+
- spec/repeater_spec.rb
|
|
245
249
|
- spec/spec_helper.rb
|
|
246
250
|
- spec/xcmonkey_spec.rb
|
|
247
251
|
- xcmonkey.gemspec
|