xcmonkey 0.1.2 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7a6aa190c1c4cfa28f97b20524c907da488aad2d78f2c1dda8f827899f6ee53f
4
- data.tar.gz: 534245a695ed0f030df459986c326fb356b3593baec3bc6750eb1cc622d32390
3
+ metadata.gz: 48bd9b79b2370e4f20366973e689af344f6c75729b361658fbc5bb2c02c38253
4
+ data.tar.gz: c207e8dbe7c820623f60a9c0b9e8ea8a7e5bce18bfe10f9d5c27e8928829889d
5
5
  SHA512:
6
- metadata.gz: 41415c8c576a3b6887ee87d44f4c4e571f1e7abda3a96f23abff52cebe77438be70ee76ab18f83a92b78cca2da429cf0c12305b2d3a8ca59b0b2e7e26405ec23
7
- data.tar.gz: 93d86451b2b7b8d2a755744ea47be375d042763ea550ddbdef5faff530bbf775e46ab24ede0f6d9dfe424f02ef4c2a923ccd3f342d1872cd6f4d82dcefae7cef
6
+ metadata.gz: 9c0f1f52ad5e8118799556e4e1ba3a49574c504cf2a840fe0369e06c6dbab260fd0a9c275ada9efb789a865759ac48f7d46f9348cde5b2cb53a44969b5189cf9
7
+ data.tar.gz: 761ffee247c8646bc24dee5fd3f473414402033d3c9a773fe87623579aef93155ecbafc3d412111396945ae485d74b28d6916e5e287754a82b15fcfc792b0c47
data/.github/FUNDING.yml CHANGED
@@ -1,3 +1,2 @@
1
1
  custom: [ 'https://revolut.me/alteral', 'https://paypal.me/aapesotskiy' ]
2
- github: alteral
3
2
  ko_fi: alteral
@@ -13,7 +13,7 @@ jobs:
13
13
  chat:
14
14
  name: Automated Code Review
15
15
  runs-on: macos-12
16
- timeout-minutes: 15
16
+ timeout-minutes: 30
17
17
  env:
18
18
  GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
19
19
  SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
data/README.md CHANGED
@@ -9,9 +9,13 @@
9
9
  <a href="/LICENSE"><img src="https://img.shields.io/badge/license-MIT-green.svg?style=flat" /></a>
10
10
  </p>
11
11
 
12
- *xcmonkey* is a tool for doing randomised UI testing of iOS apps.
12
+ ## Description
13
13
 
14
- ## Requirements
14
+ *xcmonkey* is a tool for doing randomised UI testing of iOS apps. It's inspired by and has similar goals to [*monkey*](https://developer.android.com/studio/test/monkey) on Android.
15
+
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
+
18
+ ## Prerequisites
15
19
 
16
20
  ```bash
17
21
  brew install facebook/fb/idb-companion
@@ -24,22 +28,28 @@ pip3.6 install fb-idb
24
28
  gem install xcmonkey
25
29
  ```
26
30
 
31
+ If you prefer to use [*bundler*](https://bundler.io/), add the following line to your `Gemfile`:
32
+
33
+ ```ruby
34
+ gem 'xcmonkey'
35
+ ```
36
+
27
37
  ## Usage
28
38
 
29
- ### Test
39
+ ### To run a stress test
30
40
 
31
41
  ```bash
32
- $ xcmonkey test --udid "413EA256-CFFB-4312-94A6-12592BEE4CBA" --bundle-id "com.apple.mobilesafari"
42
+ $ xcmonkey test --udid "413EA256-CFFB-4312-94A6-12592BEE4CBA" --bundle-id "com.apple.Maps" --duration 100
33
43
  12:44:19.343: Device info: iPhone 14 Pro | 413EA256-CFFB-4312-94A6-12592BEE4CBA | Booted | simulator | iOS 16.2 | x86_64 | /tmp/idb/413EA256-CFFB-4312-94A6-12592BEE4CBA_companion.sock
34
44
 
35
- 12:44:22.550: App info: com.apple.mobilesafari | MobileSafari | system | x86_64, arm64 | Running | Not Debuggable | pid=43398
45
+ 12:44:22.550: App info: com.apple.Maps | Maps | system | arm64, x86_64 | Running | Not Debuggable | pid=74636
36
46
 
37
47
  12:44:23.203: Tap: {
38
48
  "x": 53,
39
49
  "y": 749
40
50
  }
41
51
 
42
- 12:44:23.511: Swipe: {
52
+ 12:44:23.511: Swipe (0.5s): {
43
53
  "x": 196,
44
54
  "y": 426
45
55
  } => {
@@ -47,39 +57,37 @@ $ xcmonkey test --udid "413EA256-CFFB-4312-94A6-12592BEE4CBA" --bundle-id "com.a
47
57
  "y": 447
48
58
  }
49
59
 
50
- 12:44:24.355: Tap: {
60
+ 12:44:24.355: Press (1.2s): {
51
61
  "x": 143,
52
62
  "y": 323
53
63
  }
54
64
  ```
55
65
 
56
- ### Describe point
66
+ ### To describe the required point
57
67
 
58
68
  ```bash
59
- $ xcmonkey describe -x 125 -y 760 --udid "413EA256-CFFB-4312-94A6-12592BEE4CBA"
60
- 12:41:03.840: Device info: iPhone 14 Pro | 413EA256-CFFB-4312-94A6-12592BEE4CBA | Booted | simulator | iOS 16.2 | x86_64 | /tmp/idb/413EA256-CFFB-4312-94A6-12592BEE4CBA_companion.sock
69
+ $ xcmonkey describe -x 20 -y 625 --udid "413EA256-CFFB-4312-94A6-12592BEE4CBA"
70
+ 20:05:20.212: Device info: iPhone 14 Pro | 413EA256-CFFB-4312-94A6-12592BEE4CBA | Booted | simulator | iOS 16.2 | x86_64 | /tmp/idb/413EA256-CFFB-4312-94A6-12592BEE4CBA_companion.sock
61
71
 
62
- 12:41:05.342: x:125 y:760 point info: {
63
- "AXFrame": "{{120, 759}, {64, 64}}",
64
- "AXUniqueId": "Safari",
72
+ 20:05:21.713: x:20 y:625 point info: {
73
+ "AXFrame": "{{19, 624.3}, {86, 130.6}}",
74
+ "AXUniqueId": "ShortcutsRowCell",
65
75
  "frame": {
66
- "y": 759,
67
- "x": 120,
68
- "width": 64,
69
- "height": 64
76
+ "y": 624.3,
77
+ "x": 19,
78
+ "width": 86,
79
+ "height": 130.6
70
80
  },
71
81
  "role_description": "button",
72
- "AXLabel": "Safari",
82
+ "AXLabel": "Home",
73
83
  "content_required": false,
74
84
  "type": "Button",
75
85
  "title": null,
76
- "help": "Double tap to open",
86
+ "help": null,
77
87
  "custom_actions": [
78
- "Edit mode",
79
- "Today",
80
- "App Library"
88
+
81
89
  ],
82
- "AXValue": "",
90
+ "AXValue": "Add",
83
91
  "enabled": true,
84
92
  "role": "AXButton",
85
93
  "subrole": null
Binary file
data/bin/xcmonkey CHANGED
@@ -16,13 +16,15 @@ module Xcmonkey
16
16
  c.description = 'Runs monkey test'
17
17
  c.option('-u', '--udid STRING', String, 'Set device UDID')
18
18
  c.option('-b', '--bundle-id STRING', String, 'Set target bundle identifier')
19
- c.option('-d', '--duration SECONDS', Integer, 'Test duration in seconds')
19
+ c.option('-d', '--duration SECONDS', Integer, 'Test duration in seconds. Defaults to `60`')
20
+ c.option('-k', '--enable-simulator-keyboard', 'Should simulator keyboard be enabled? Defaults to `true`')
20
21
  c.action do |args, options|
21
- options.default(duration: 60)
22
+ options.default(duration: 60, enable_simulator_keyboard: true)
22
23
  params = {
23
24
  udid: options.udid,
24
25
  bundle_id: options.bundle_id,
25
- duration: options.duration
26
+ duration: options.duration,
27
+ simulator_keyboard: options.enable_simulator_keyboard
26
28
  }
27
29
  Xcmonkey.new(params).run
28
30
  end
@@ -1,10 +1,11 @@
1
1
  class Driver
2
- attr_accessor :udid, :bundle_id, :duration
2
+ attr_accessor :udid, :bundle_id, :duration, :enable_simulator_keyboard
3
3
 
4
4
  def initialize(params)
5
5
  self.udid = params[:udid]
6
6
  self.bundle_id = params[:bundle_id]
7
7
  self.duration = params[:duration]
8
+ self.enable_simulator_keyboard = params[:enable_simulator_keyboard]
8
9
  ensure_driver_installed
9
10
  end
10
11
 
@@ -18,11 +19,23 @@ class Driver
18
19
  when :precise_tap
19
20
  tap(coordinates: el1_coordinates)
20
21
  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)
22
+ tap(coordinates: random_coordinates)
23
+ when :precise_press
24
+ press(coordinates: el1_coordinates, duration: press_duration)
25
+ when :blind_press
26
+ press(coordinates: random_coordinates, duration: press_duration)
27
+ when :precise_swipe
28
+ swipe(
29
+ start_coordinates: el1_coordinates,
30
+ end_coordinates: el2_coordinates,
31
+ duration: swipe_duration
32
+ )
33
+ when :blind_swipe
34
+ swipe(
35
+ start_coordinates: random_coordinates,
36
+ end_coordinates: random_coordinates,
37
+ duration: swipe_duration
38
+ )
26
39
  else
27
40
  next
28
41
  end
@@ -31,9 +44,9 @@ class Driver
31
44
  end
32
45
  end
33
46
 
34
- def open_home_screen(return_tracker: false)
47
+ def open_home_screen(with_tracker: false)
35
48
  `idb ui button --udid #{udid} HOME`
36
- detect_home_unique_element if return_tracker
49
+ detect_home_unique_element if with_tracker
37
50
  end
38
51
 
39
52
  def describe_ui
@@ -57,13 +70,19 @@ class Driver
57
70
 
58
71
  def boot_simulator
59
72
  `idb boot #{udid}`
60
- ensure_simulator_was_booted
73
+ Logger.error("Failed to boot #{udid}") if device_info['state'] != 'Booted'
61
74
  end
62
75
 
63
76
  def shutdown_simulator
64
77
  `idb shutdown #{udid}`
65
78
  end
66
79
 
80
+ def configure_simulator_keyboard
81
+ shutdown_simulator
82
+ keyboard_status = enable_simulator_keyboard ? 0 : 1
83
+ `defaults write com.apple.iphonesimulator ConnectHardwareKeyboard #{keyboard_status}`
84
+ end
85
+
67
86
  def list_targets
68
87
  @list_targets ||= `idb list-targets`.split("\n")
69
88
  @list_targets
@@ -80,13 +99,12 @@ class Driver
80
99
  def ensure_device_exists
81
100
  device = list_targets.detect { |target| target.include?(udid) }
82
101
  Logger.error("Can't find device #{udid}") if device.nil?
83
- Logger.info('Device info:', payload: device)
84
- boot_simulator if device.include?('simulator')
85
- end
86
102
 
87
- def ensure_simulator_was_booted
88
- sim = list_booted_simulators.detect { |target| target.include?(udid) }
89
- Logger.error("Failed to boot #{udid}") if sim.nil?
103
+ Logger.info('Device info:', payload: device)
104
+ if device.include?('simulator')
105
+ configure_simulator_keyboard
106
+ boot_simulator
107
+ end
90
108
  end
91
109
 
92
110
  def list_apps
@@ -98,20 +116,58 @@ class Driver
98
116
  `idb ui tap --udid #{udid} #{coordinates[:x]} #{coordinates[:y]}`
99
117
  end
100
118
 
101
- def swipe(start_coordinates:, end_coordinates:)
102
- Logger.info('Swipe:', payload: "#{JSON.pretty_generate(start_coordinates)} => #{JSON.pretty_generate(end_coordinates)}")
119
+ def press(coordinates:, duration:)
120
+ Logger.info("Press (#{duration}s):", payload: JSON.pretty_generate(coordinates))
121
+ `idb ui tap --udid #{udid} --duration #{duration} #{coordinates[:x]} #{coordinates[:y]}`
122
+ end
123
+
124
+ def swipe(start_coordinates:, end_coordinates:, duration:)
125
+ Logger.info(
126
+ "Swipe (#{duration}s):",
127
+ payload: "#{JSON.pretty_generate(start_coordinates)} => #{JSON.pretty_generate(end_coordinates)}"
128
+ )
103
129
  coordinates = "#{start_coordinates[:x]} #{start_coordinates[:y]} #{end_coordinates[:x]} #{end_coordinates[:y]}"
104
- `idb ui swipe --udid #{udid} --duration 0.5 #{coordinates}`
130
+ `idb ui swipe --udid #{udid} --duration #{duration} #{coordinates}`
105
131
  end
106
132
 
107
133
  def central_coordinates(element)
108
134
  frame = element['frame']
135
+ x = (frame['x'] + (frame['width'] / 2)).abs.to_i
136
+ y = (frame['y'] + (frame['height'] / 2)).abs.to_i
137
+ {
138
+ x: x > screen_size[:width].to_i ? rand(0..screen_size[:width].to_i) : x,
139
+ y: y > screen_size[:height].to_i ? rand(0..screen_size[:height].to_i) : y
140
+ }
141
+ end
142
+
143
+ def random_coordinates
109
144
  {
110
- x: (frame['x'] + (frame['width'] / 2)).to_i,
111
- y: (frame['y'] + (frame['height'] / 2)).to_i
145
+ x: rand(0..screen_size[:width].to_i),
146
+ y: rand(0..screen_size[:height].to_i)
112
147
  }
113
148
  end
114
149
 
150
+ def device_info
151
+ @device_info ||= JSON.parse(`idb describe --udid #{udid} --json`)
152
+ @device_info
153
+ end
154
+
155
+ def screen_size
156
+ screen_dimensions = device_info['screen_dimensions']
157
+ {
158
+ width: screen_dimensions['width_points'],
159
+ height: screen_dimensions['height_points']
160
+ }
161
+ end
162
+
163
+ def swipe_duration
164
+ rand(0.1..0.7).ceil(1)
165
+ end
166
+
167
+ def press_duration
168
+ rand(0.5..1.5).ceil(1)
169
+ end
170
+
115
171
  private
116
172
 
117
173
  def ensure_driver_installed
@@ -1,4 +1,4 @@
1
1
  module Xcmonkey
2
- VERSION = '0.1.2'
2
+ VERSION = '0.3.0'
3
3
  GEM_NAME = 'xcmonkey'
4
4
  end
data/lib/xcmonkey.rb CHANGED
@@ -21,13 +21,16 @@ module Xcmonkey
21
21
  driver.ensure_device_exists
22
22
  driver.ensure_app_installed
23
23
  driver.terminate_app
24
- driver.open_home_screen(return_tracker: true)
24
+ driver.open_home_screen(with_tracker: true)
25
25
  driver.launch_app
26
26
  driver.monkey_test(gestures)
27
27
  end
28
28
 
29
29
  def gestures
30
- [:precise_tap, :blind_tap, :swipe]
30
+ taps = [:precise_tap, :blind_tap] * 10
31
+ swipes = [:precise_swipe, :blind_swipe] * 5
32
+ presses = [:precise_press, :blind_press]
33
+ taps + swipes + presses
31
34
  end
32
35
 
33
36
  def ensure_required_params(params)
data/spec/driver_spec.rb CHANGED
@@ -6,8 +6,7 @@ describe Driver do
6
6
  it 'verifies that sumulator was booted' do
7
7
  error_message = "Failed to boot #{udid}"
8
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
9
+ expect { driver.boot_simulator }.not_to raise_error
11
10
  end
12
11
 
13
12
  it 'verifies that there are booted simulators' do
@@ -24,10 +23,16 @@ describe Driver do
24
23
 
25
24
  it 'verifies that home screen can be opened' do
26
25
  driver.boot_simulator
27
- home_tracker = driver.open_home_screen(return_tracker: true)
26
+ home_tracker = driver.open_home_screen(with_tracker: true)
28
27
  expect(home_tracker).not_to be_empty
29
28
  end
30
29
 
30
+ it 'verifies that home screen can be opened without tracker' do
31
+ driver.boot_simulator
32
+ home_tracker = driver.open_home_screen(with_tracker: false)
33
+ expect(home_tracker).to be_nil
34
+ end
35
+
31
36
  it 'verifies that list of targets can be showed' do
32
37
  list_targets = driver.list_targets
33
38
  expect(list_targets).not_to be_empty
@@ -85,6 +90,58 @@ describe Driver do
85
90
  expect(actual_coordinates).to eq(expected_coordinates)
86
91
  end
87
92
 
93
+ it 'verifies that device info can be for booted simulator' do
94
+ driver.boot_simulator
95
+ expect(driver.device_info).not_to be_empty
96
+ end
97
+
98
+ it 'verifies that device info can be for not booted simulator' do
99
+ driver.shutdown_simulator
100
+ expect(driver.device_info).not_to be_empty
101
+ end
102
+
103
+ it 'verifies that screen size can be found' do
104
+ driver.boot_simulator
105
+ screen_size = driver.screen_size
106
+ expect(screen_size[:width]).to be > 0
107
+ expect(screen_size[:height]).to be > 0
108
+ end
109
+
110
+ it 'verifies that random coordinates can be found' do
111
+ driver.boot_simulator
112
+ coordinates = driver.random_coordinates
113
+ expect(coordinates[:x]).to be > 0
114
+ expect(coordinates[:y]).to be > 0
115
+ end
116
+
117
+ it 'verifies swipe duration' do
118
+ expect(driver.swipe_duration).to be_between(0.1, 0.7)
119
+ end
120
+
121
+ it 'verifies press duration' do
122
+ expect(driver.press_duration).to be_between(0.5, 1.5)
123
+ end
124
+
125
+ it 'verifies that simulator keyboard can be enabled' do
126
+ allow(driver).to receive(:is_simulator_keyboard_enabled?).and_return(false)
127
+ driver = described_class.new(udid: udid, bundle_id: bundle_id, enable_simulator_keyboard: true)
128
+ expect(driver).to receive(:shutdown_simulator)
129
+ driver.configure_simulator_keyboard
130
+ keyboard_state = `defaults read com.apple.iphonesimulator`.split("\n").grep(/ConnectHardwareKeyboard/)
131
+ expect(keyboard_state).not_to be_empty
132
+ expect(keyboard_state.first).to include('0')
133
+ end
134
+
135
+ it 'verifies that simulator keyboard can be disabled' do
136
+ allow(driver).to receive(:is_simulator_keyboard_enabled?).and_return(true)
137
+ driver = described_class.new(udid: udid, bundle_id: bundle_id, enable_simulator_keyboard: false)
138
+ expect(driver).to receive(:shutdown_simulator)
139
+ driver.configure_simulator_keyboard
140
+ keyboard_state = `defaults read com.apple.iphonesimulator`.split("\n").grep(/ConnectHardwareKeyboard/)
141
+ expect(keyboard_state).not_to be_empty
142
+ expect(keyboard_state.first).to include('1')
143
+ end
144
+
88
145
  it 'verifies that app can be launched' do
89
146
  expect(Logger).not_to receive(:error)
90
147
  expect(Logger).to receive(:info)
@@ -93,10 +150,35 @@ describe Driver do
93
150
  expect { driver.launch_app }.not_to raise_error
94
151
  end
95
152
 
153
+ it 'verifies tap' do
154
+ driver.boot_simulator
155
+ coordinates = { x: 1, y: 1 }
156
+ expect(Logger).to receive(:info).with('Tap:', payload: JSON.pretty_generate(coordinates))
157
+ driver.tap(coordinates: coordinates)
158
+ end
159
+
160
+ it 'verifies press' do
161
+ driver.boot_simulator
162
+ duration = 0.5
163
+ coordinates = { x: 1, y: 1 }
164
+ expect(Logger).to receive(:info).with("Press (#{duration}s):", payload: JSON.pretty_generate(coordinates))
165
+ driver.press(coordinates: coordinates, duration: duration)
166
+ end
167
+
168
+ it 'verifies swipe' do
169
+ driver.boot_simulator
170
+ duration = 0.5
171
+ start_coordinates = { x: 1, y: 1 }
172
+ end_coordinates = { x: 2, y: 2 }
173
+ expect(Logger).to receive(:info).with("Swipe (#{duration}s):", payload: "#{JSON.pretty_generate(start_coordinates)} => #{JSON.pretty_generate(end_coordinates)}")
174
+ driver.swipe(start_coordinates: start_coordinates, end_coordinates: end_coordinates, duration: duration)
175
+ end
176
+
96
177
  it 'verifies that simulator was not booted' do
97
178
  driver.shutdown_simulator
98
179
  error_message = "Failed to boot #{udid}"
180
+ allow(driver).to receive(:device_info).and_return({ 'state' => 'Unknown' })
99
181
  expect(Logger).to receive(:log).with(error_message, color: :light_red, payload: nil)
100
- expect { driver.ensure_simulator_was_booted }.to raise_error(SystemExit) { |e| expect(e.status).to eq(1) }
182
+ expect { driver.boot_simulator }.to raise_error(SystemExit) { |e| expect(e.status).to eq(1) }
101
183
  end
102
184
  end
data/spec/logger_spec.rb CHANGED
@@ -33,4 +33,24 @@ describe Logger do
33
33
  expect { described_class.error(message, payload: payload) }
34
34
  .to raise_error(SystemExit) { |e| expect(e.status).to eq(1) }
35
35
  end
36
+
37
+ it 'verifies custom log without payload' do
38
+ color = :blue
39
+ time = Time.now
40
+ allow(Time).to receive(:now).and_return(time)
41
+ expected_output = "#{time.strftime('%k:%M:%S.%L')}: #{message}".colorize(color)
42
+ expect do
43
+ described_class.log(message, color: color, payload: nil)
44
+ end.to output("#{expected_output}\n\n").to_stdout
45
+ end
46
+
47
+ it 'verifies custom log with payload' do
48
+ color = :blue
49
+ time = Time.now
50
+ allow(Time).to receive(:now).and_return(time)
51
+ expected_output = "#{time.strftime('%k:%M:%S.%L')}: #{message}".colorize(color)
52
+ expect do
53
+ described_class.log(message, color: color, payload: payload)
54
+ end.to output("#{expected_output} #{payload.colorize(:light_green)}\n\n").to_stdout
55
+ end
36
56
  end
@@ -5,7 +5,10 @@ describe Xcmonkey do
5
5
 
6
6
  it 'verifies gestures' do
7
7
  gestures = described_class.new(params).gestures
8
- expect(gestures) =~ [:swipe, :precise_tap, :blind_tap]
8
+ taps = [:precise_tap, :blind_tap] * 10
9
+ swipes = [:precise_swipe, :blind_swipe] * 5
10
+ presses = [:precise_press, :blind_press]
11
+ expect(gestures) =~ presses + taps + swipes
9
12
  end
10
13
 
11
14
  it 'verifies required params' do
@@ -42,5 +45,14 @@ describe Xcmonkey do
42
45
  expect(Logger).to receive(:error).with(duration_error_msg)
43
46
  described_class.new(params)
44
47
  end
48
+
49
+ it 'verifies version' do
50
+ current_version = Gem::Version.new(Xcmonkey::VERSION)
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
45
57
  end
46
58
  end
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: 0.1.2
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - alteral
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2022-12-28 00:00:00.000000000 Z
11
+ date: 2023-01-08 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler