xcmonkey 0.1.2 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  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