├── .coveralls.yml ├── .github └── workflows │ └── test.yml ├── .gitignore ├── .rubocop.yml ├── .rubocop_todo.yml ├── .travis.yml ├── CHANGELOG.md ├── Gemfile ├── Gemfile.lock ├── LICENSE ├── README.md ├── Rakefile ├── fastlane-plugin-simctl ├── .gitignore ├── .rspec ├── .rubocop.yml ├── Gemfile ├── README.md ├── Rakefile ├── fastlane-plugin-simctl.gemspec ├── fastlane │ ├── Fastfile │ └── Pluginfile ├── lib │ └── fastlane │ │ └── plugin │ │ ├── simctl.rb │ │ └── simctl │ │ ├── actions │ │ └── simctl_action.rb │ │ ├── helper │ │ └── simctl_helper.rb │ │ └── version.rb └── spec │ ├── simctl_action_spec.rb │ └── spec_helper.rb ├── lib ├── simctl.rb └── simctl │ ├── command.rb │ ├── command │ ├── boot.rb │ ├── create.rb │ ├── delete.rb │ ├── erase.rb │ ├── install.rb │ ├── io.rb │ ├── keychain.rb │ ├── kill.rb │ ├── launch.rb │ ├── list.rb │ ├── openurl.rb │ ├── privacy.rb │ ├── push.rb │ ├── rename.rb │ ├── reset.rb │ ├── shutdown.rb │ ├── spawn.rb │ ├── status_bar.rb │ ├── terminate.rb │ ├── uninstall.rb │ ├── upgrade.rb │ └── warmup.rb │ ├── device.rb │ ├── device_launchctl.rb │ ├── device_path.rb │ ├── device_settings.rb │ ├── device_type.rb │ ├── executor.rb │ ├── keychain.rb │ ├── list.rb │ ├── object.rb │ ├── runtime.rb │ ├── status_bar.rb │ ├── version.rb │ └── xcode │ ├── path.rb │ └── version.rb ├── simctl.gemspec └── spec ├── SampleApp ├── .gitignore ├── SampleApp.xcodeproj │ ├── project.pbxproj │ └── project.xcworkspace │ │ └── contents.xcworkspacedata └── SampleApp │ ├── AppDelegate.h │ ├── AppDelegate.m │ ├── Info.plist │ └── main.m ├── simctl ├── device_interaction_spec.rb ├── executor_spec.rb ├── list_spec.rb ├── readme_spec.rb ├── upgrade_spec.rb └── warmup_spec.rb └── spec_helper.rb /.coveralls.yml: -------------------------------------------------------------------------------- 1 | repo_token: kVHew1JCYhlMacKR4TheQtAbB5EfpwWkk 2 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | on: [push] 3 | jobs: 4 | build: 5 | env: 6 | LANG: en_US.UTF-8 7 | runs-on: macos-11 8 | steps: 9 | - uses: actions/checkout@v2 10 | - name: Install dependencies 11 | run: gem install bundler:1.17.3 && bundle install 12 | - name: Run tests 13 | run: bundle exec rake 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | coverage/ 2 | doc/ 3 | pkg/ 4 | .yardoc/ 5 | .idea -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | inherit_from: .rubocop_todo.yml 2 | 3 | Metrics/BlockLength: 4 | ExcludedMethods: 5 | - describe 6 | -------------------------------------------------------------------------------- /.rubocop_todo.yml: -------------------------------------------------------------------------------- 1 | # This configuration was generated by 2 | # `rubocop --auto-gen-config` 3 | # on 2017-09-08 10:56:22 +0200 using RuboCop version 0.49.1. 4 | # The point is for the user to remove these configuration records 5 | # one by one as the offenses are removed from the code base. 6 | # Note that changes in the inspected code, or installation of new 7 | # versions of RuboCop, may require this file to be generated again. 8 | 9 | # Offense count: 1 10 | Lint/HandleExceptions: 11 | Exclude: 12 | - 'spec/spec_helper.rb' 13 | 14 | # Offense count: 1 15 | Lint/NonLocalExitFromIterator: 16 | Exclude: 17 | - 'lib/simctl/executor.rb' 18 | 19 | # Offense count: 1 20 | Lint/RescueException: 21 | Exclude: 22 | - 'lib/simctl/command/reset.rb' 23 | 24 | # Offense count: 1 25 | Lint/ScriptPermission: 26 | Exclude: 27 | - 'Rakefile' 28 | 29 | # Offense count: 4 30 | Metrics/AbcSize: 31 | Max: 21 32 | 33 | # Offense count: 4 34 | # Configuration parameters: CountComments, ExcludedMethods. 35 | Metrics/BlockLength: 36 | Max: 237 37 | 38 | # Offense count: 1 39 | # Configuration parameters: CountComments. 40 | Metrics/ClassLength: 41 | Max: 200 42 | 43 | # Offense count: 58 44 | # Configuration parameters: AllowHeredoc, AllowURI, URISchemes, IgnoreCopDirectives, IgnoredPatterns. 45 | # URISchemes: http, https 46 | Metrics/LineLength: 47 | Max: 144 48 | 49 | # Offense count: 6 50 | # Configuration parameters: CountComments. 51 | Metrics/MethodLength: 52 | Max: 38 53 | 54 | # Offense count: 1 55 | Style/AccessorMethodName: 56 | Exclude: 57 | - 'lib/simctl/device_settings.rb' 58 | 59 | # Offense count: 2 60 | Style/ClassVars: 61 | Exclude: 62 | - 'lib/simctl.rb' 63 | 64 | # Offense count: 30 65 | Style/Documentation: 66 | Enabled: false 67 | 68 | # Offense count: 1 69 | Style/MethodMissing: 70 | Exclude: 71 | - 'lib/simctl/device.rb' 72 | 73 | # Offense count: 1 74 | # Cop supports --auto-correct. 75 | # Configuration parameters: AutoCorrect, EnforcedStyle, SupportedStyles. 76 | # SupportedStyles: predicate, comparison 77 | Style/NumericPredicate: 78 | Exclude: 79 | - 'spec/**/*' 80 | - 'lib/simctl/device.rb' 81 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: objective-c 2 | matrix: 3 | include: 4 | - os: osx 5 | osx_image: xcode10.3 6 | env: CUSTOM_DEVICE_SET_PATH=true 7 | - os: osx 8 | osx_image: xcode10.3 9 | env: CUSTOM_DEVICE_SET_PATH=false 10 | - os: osx 11 | osx_image: xcode11.3 12 | env: CUSTOM_DEVICE_SET_PATH=true 13 | - os: osx 14 | osx_image: xcode11.3 15 | env: CUSTOM_DEVICE_SET_PATH=false 16 | before_script: 17 | - export LANG=en_US.UTF-8 18 | install: bundle 19 | script: 20 | - bundle exec rake 21 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 1.7.0 2 | 3 | * Fix issue with Ruby >= 3.2 4 | 5 | # 1.6.9 6 | 7 | * Add `keychain_reset` command 8 | 9 | # 1.6.8 10 | 11 | * Add `upgrade` command 12 | * Add `status_bar_clear` command (requires Xcode 11.4 or newer) 13 | * Add `status_bar_override` command (requires Xcode 11.4 or newer) 14 | * Add `privacy` command (requires Xcode 11.4 or newer) 15 | * Add `push` command (requires Xcode 11.4 or newer) 16 | 17 | # 1.6.7 18 | 19 | * Turn off sliding hint on keyboard 20 | 21 | # 1.6.6 22 | 23 | * Fix `device.reset` 24 | 25 | # 1.6.4 26 | 27 | * Device locale can now be set via `device.set_locale('en_EN')` 28 | 29 | # 1.6.3 30 | 31 | * Execute `xcode-select` lazily, not during load 32 | 33 | # 1.6.2 34 | 35 | * Support Xcode 9 36 | 37 | # 1.6.1 38 | 39 | * Add `SimCtl.warmup` 40 | 41 | # 1.6.0 42 | 43 | * Breaking change: All `!` have been removed from method names 44 | * Support spawning processes 45 | * Add `device.ready?` method 46 | 47 | # 1.5.8 48 | 49 | * Support taking screenshots 50 | * Breaking change: Remove `device.disable_keyboard_helpers!` 51 | Use `device.settings.disable_keyboard_helpers!` instead 52 | 53 | # 1.5.7 54 | 55 | * Fix SimCtl::Runtime.latest 56 | 57 | # 1.5.6 58 | 59 | * Fix custom device set path with spaces 60 | 61 | # 1.5.5 62 | 63 | * Support updating the hardware keyboard setting 64 | 65 | # 1.5.4 66 | 67 | * Support uninstall command 68 | 69 | # 1.5.3 70 | 71 | * Support openurl command 72 | 73 | # 1.5.2 74 | 75 | * Support custom device set path 76 | 77 | # 1.5.1 78 | 79 | * Let `SimCtl#create_device` wait for the device to be created 80 | 81 | # 1.5.0 82 | 83 | * `SimCtl#devicetype` throws exception if device type could not be found 84 | * `SimCtl#runtime` throws exception if runtime could not be found 85 | * Support installing and launching an app 86 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gemspec 4 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: . 3 | specs: 4 | simctl (1.6.10) 5 | CFPropertyList 6 | naturally 7 | 8 | GEM 9 | remote: https://rubygems.org/ 10 | specs: 11 | CFPropertyList (3.0.6) 12 | rexml 13 | ast (2.4.2) 14 | coveralls (0.8.23) 15 | json (>= 1.8, < 3) 16 | simplecov (~> 0.16.1) 17 | term-ansicolor (~> 1.3) 18 | thor (>= 0.19.4, < 2.0) 19 | tins (~> 1.6) 20 | diff-lcs (1.5.0) 21 | docile (1.4.0) 22 | io-console (0.6.0) 23 | irb (1.6.2) 24 | reline (>= 0.3.0) 25 | json (2.6.3) 26 | naturally (2.2.1) 27 | parallel (1.22.1) 28 | parser (2.7.2.0) 29 | ast (~> 2.4.1) 30 | powerpack (0.1.3) 31 | rainbow (2.2.2) 32 | rake 33 | rake (13.0.6) 34 | reline (0.3.2) 35 | io-console (~> 0.5) 36 | rexml (3.2.5) 37 | rspec (3.12.0) 38 | rspec-core (~> 3.12.0) 39 | rspec-expectations (~> 3.12.0) 40 | rspec-mocks (~> 3.12.0) 41 | rspec-core (3.12.0) 42 | rspec-support (~> 3.12.0) 43 | rspec-expectations (3.12.2) 44 | diff-lcs (>= 1.2.0, < 2.0) 45 | rspec-support (~> 3.12.0) 46 | rspec-mocks (3.12.3) 47 | diff-lcs (>= 1.2.0, < 2.0) 48 | rspec-support (~> 3.12.0) 49 | rspec-support (3.12.0) 50 | rubocop (0.49.1) 51 | parallel (~> 1.10) 52 | parser (>= 2.3.3.1, < 3.0) 53 | powerpack (~> 0.1) 54 | rainbow (>= 1.99.1, < 3.0) 55 | ruby-progressbar (~> 1.7) 56 | unicode-display_width (~> 1.0, >= 1.0.1) 57 | ruby-progressbar (1.11.0) 58 | simplecov (0.16.1) 59 | docile (~> 1.1) 60 | json (>= 1.8, < 3) 61 | simplecov-html (~> 0.10.0) 62 | simplecov-html (0.10.2) 63 | sync (0.5.0) 64 | term-ansicolor (1.7.1) 65 | tins (~> 1.0) 66 | thor (1.2.1) 67 | tins (1.32.1) 68 | sync 69 | unicode-display_width (1.8.0) 70 | 71 | PLATFORMS 72 | arm64-darwin-21 73 | arm64-darwin-22 74 | 75 | DEPENDENCIES 76 | coveralls 77 | irb 78 | rake 79 | rspec 80 | rubocop (= 0.49.1) 81 | simctl! 82 | 83 | BUNDLED WITH 84 | 2.4.1 85 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Johannes Plunien 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | 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, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # simctl 2 | 3 | [![Build Status](https://travis-ci.org/plu/simctl.svg?branch=master)](https://travis-ci.org/plu/simctl) [![Gem Version](https://badge.fury.io/rb/simctl.svg)](https://badge.fury.io/rb/simctl) [![Coverage Status](https://coveralls.io/repos/plu/simctl/badge.svg?branch=master&service=github)](https://coveralls.io/github/plu/simctl?branch=master) 4 | 5 | Ruby interface to xcrun simctl. Manage your iOS Simulators directly from a ruby script. 6 | 7 | ## Usage 8 | 9 | ```ruby 10 | require 'simctl' 11 | 12 | # Select the iOS 14.5 runtime 13 | runtime = SimCtl.runtime(name: 'iOS 14.5') 14 | 15 | # Select the iPhone 8 device type 16 | devicetype = SimCtl.devicetype(name: 'iPhone 8') 17 | 18 | # Create a new device 19 | device = SimCtl.create_device 'Unit Tests @ iPhone 8 - 14.5', devicetype, runtime 20 | 21 | # Boot the device 22 | device.boot 23 | 24 | # Launch a new Simulator.app instance 25 | device.launch 26 | 27 | # Wait for the device to be booted 28 | device.wait {|d| d.state == :booted} 29 | 30 | # Kill the Simulator.app instance again 31 | device.shutdown 32 | device.kill 33 | 34 | # Wait until it did shutdown 35 | device.wait {|d| d.state == :shutdown} 36 | 37 | # Delete the device 38 | device.delete 39 | ``` 40 | 41 | ## License (MIT) 42 | 43 | Copyright (C) 2019 Johannes Plunien 44 | 45 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 46 | 47 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 48 | 49 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 50 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env rake 2 | require 'bundler/gem_tasks' 3 | require 'rspec/core/rake_task' 4 | require 'rubocop/rake_task' 5 | 6 | RSpec::Core::RakeTask.new(:rspec) 7 | RuboCop::RakeTask.new 8 | 9 | desc 'Default: Execute rubocop + rspec' 10 | task default: %i[rubocop rspec] 11 | -------------------------------------------------------------------------------- /fastlane-plugin-simctl/.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | Gemfile.lock 3 | 4 | ## Documentation cache and generated files: 5 | /.yardoc/ 6 | /_yardoc/ 7 | /doc/ 8 | /rdoc/ 9 | fastlane/README.md 10 | fastlane/report.xml 11 | coverage 12 | -------------------------------------------------------------------------------- /fastlane-plugin-simctl/.rspec: -------------------------------------------------------------------------------- 1 | --require spec_helper 2 | --color 3 | --format d 4 | -------------------------------------------------------------------------------- /fastlane-plugin-simctl/.rubocop.yml: -------------------------------------------------------------------------------- 1 | Style/MultipleComparison: 2 | Enabled: false 3 | 4 | Style/PercentLiteralDelimiters: 5 | Enabled: false 6 | 7 | # kind_of? is a good way to check a type 8 | Style/ClassCheck: 9 | EnforcedStyle: kind_of? 10 | 11 | Style/FrozenStringLiteralComment: 12 | Enabled: false 13 | 14 | # This doesn't work with older versions of Ruby (pre 2.4.0) 15 | Style/SafeNavigation: 16 | Enabled: false 17 | 18 | # This doesn't work with older versions of Ruby (pre 2.4.0) 19 | Performance/RegexpMatch: 20 | Enabled: false 21 | 22 | # This suggests use of `tr` instead of `gsub`. While this might be more performant, 23 | # these methods are not at all interchangable, and behave very differently. This can 24 | # lead to people making the substitution without considering the differences. 25 | Performance/StringReplacement: 26 | Enabled: false 27 | 28 | # .length == 0 is also good, we don't always want .zero? 29 | Style/NumericPredicate: 30 | Enabled: false 31 | 32 | # this would cause errors with long lanes 33 | Metrics/BlockLength: 34 | Enabled: false 35 | 36 | # this is a bit buggy 37 | Metrics/ModuleLength: 38 | Enabled: false 39 | 40 | # certificate_1 is an okay variable name 41 | Style/VariableNumber: 42 | Enabled: false 43 | 44 | # This is used a lot across the fastlane code base for config files 45 | Style/MethodMissing: 46 | Enabled: false 47 | 48 | # This rule isn't useful, lots of discussion happening around it also 49 | # e.g. https://github.com/bbatsov/rubocop/issues/2338 50 | MultilineBlockChain: 51 | Enabled: false 52 | 53 | # 54 | # File.chmod(0777, f) 55 | # 56 | # is easier to read than 57 | # 58 | # File.chmod(0o777, f) 59 | # 60 | Style/NumericLiteralPrefix: 61 | Enabled: false 62 | 63 | # 64 | # command = (!clean_expired.nil? || !clean_pattern.nil?) ? CLEANUP : LIST 65 | # 66 | # is easier to read than 67 | # 68 | # command = !clean_expired.nil? || !clean_pattern.nil? ? CLEANUP : LIST 69 | # 70 | Style/TernaryParentheses: 71 | Enabled: false 72 | 73 | # sometimes it is useful to have those empty methods 74 | Style/EmptyMethod: 75 | Enabled: false 76 | 77 | # It's better to be more explicit about the type 78 | Style/BracesAroundHashParameters: 79 | Enabled: false 80 | 81 | # specs sometimes have useless assignments, which is fine 82 | Lint/UselessAssignment: 83 | Exclude: 84 | - '**/spec/**/*' 85 | 86 | # We could potentially enable the 2 below: 87 | Layout/IndentHash: 88 | Enabled: false 89 | 90 | Layout/AlignHash: 91 | Enabled: false 92 | 93 | # HoundCI doesn't like this rule 94 | Layout/DotPosition: 95 | Enabled: false 96 | 97 | # We allow !! as it's an easy way to convert ot boolean 98 | Style/DoubleNegation: 99 | Enabled: false 100 | 101 | # Prevent to replace [] into %i 102 | Style/SymbolArray: 103 | Enabled: false 104 | 105 | # We still support Ruby 2.0.0 106 | Layout/IndentHeredoc: 107 | Enabled: false 108 | 109 | # This cop would not work fine with rspec 110 | Style/MixinGrouping: 111 | Exclude: 112 | - '**/spec/**/*' 113 | 114 | # Sometimes we allow a rescue block that doesn't contain code 115 | Lint/HandleExceptions: 116 | Enabled: false 117 | 118 | # Cop supports --auto-correct. 119 | Lint/UnusedBlockArgument: 120 | Enabled: false 121 | 122 | Lint/AmbiguousBlockAssociation: 123 | Enabled: false 124 | 125 | # Needed for $verbose 126 | Style/GlobalVars: 127 | Enabled: false 128 | 129 | # We want to allow class Fastlane::Class 130 | Style/ClassAndModuleChildren: 131 | Enabled: false 132 | 133 | # $? Exit 134 | Style/SpecialGlobalVars: 135 | Enabled: false 136 | 137 | Metrics/AbcSize: 138 | Enabled: false 139 | 140 | Metrics/MethodLength: 141 | Enabled: false 142 | 143 | Metrics/CyclomaticComplexity: 144 | Enabled: false 145 | 146 | # The %w might be confusing for new users 147 | Style/WordArray: 148 | MinSize: 19 149 | 150 | # raise and fail are both okay 151 | Style/SignalException: 152 | Enabled: false 153 | 154 | # Better too much 'return' than one missing 155 | Style/RedundantReturn: 156 | Enabled: false 157 | 158 | # Having if in the same line might not always be good 159 | Style/IfUnlessModifier: 160 | Enabled: false 161 | 162 | # and and or is okay 163 | Style/AndOr: 164 | Enabled: false 165 | 166 | # Configuration parameters: CountComments. 167 | Metrics/ClassLength: 168 | Max: 320 169 | 170 | 171 | # Configuration parameters: AllowURI, URISchemes. 172 | Metrics/LineLength: 173 | Max: 370 174 | 175 | # Configuration parameters: CountKeywordArgs. 176 | Metrics/ParameterLists: 177 | Max: 17 178 | 179 | Metrics/PerceivedComplexity: 180 | Max: 18 181 | 182 | # Sometimes it's easier to read without guards 183 | Style/GuardClause: 184 | Enabled: false 185 | 186 | # We allow both " and ' 187 | Style/StringLiterals: 188 | Enabled: false 189 | 190 | # something = if something_else 191 | # that's confusing 192 | Style/ConditionalAssignment: 193 | Enabled: false 194 | 195 | # Better to have too much self than missing a self 196 | Style/RedundantSelf: 197 | Enabled: false 198 | 199 | # e.g. 200 | # def self.is_supported?(platform) 201 | # we may never use `platform` 202 | Lint/UnusedMethodArgument: 203 | Enabled: false 204 | 205 | # the let(:key) { ... } 206 | Lint/ParenthesesAsGroupedExpression: 207 | Exclude: 208 | - '**/spec/**/*' 209 | 210 | # This would reject is_ in front of methods 211 | # We use `is_supported?` everywhere already 212 | Style/PredicateName: 213 | Enabled: false 214 | 215 | # We allow the $ 216 | Style/PerlBackrefs: 217 | Enabled: false 218 | 219 | # Disable '+ should be surrounded with a single space' for xcodebuild_spec.rb 220 | Layout/SpaceAroundOperators: 221 | Exclude: 222 | - '**/spec/actions_specs/xcodebuild_spec.rb' 223 | 224 | AllCops: 225 | TargetRubyVersion: 2.0 226 | Include: 227 | - '**/fastlane/Fastfile' 228 | Exclude: 229 | - '**/lib/assets/custom_action_template.rb' 230 | - './vendor/**/*' 231 | 232 | # They have not to be snake_case 233 | Style/FileName: 234 | Exclude: 235 | - '**/Dangerfile' 236 | - '**/Brewfile' 237 | - '**/Gemfile' 238 | - '**/Podfile' 239 | - '**/Rakefile' 240 | - '**/Fastfile' 241 | - '**/Deliverfile' 242 | - '**/Snapfile' 243 | - '**/*.gemspec' 244 | 245 | # We're not there yet 246 | Style/Documentation: 247 | Enabled: false 248 | 249 | # Added after upgrade to 0.38.0 250 | Style/MutableConstant: 251 | Enabled: false 252 | 253 | # length > 0 is good 254 | Style/ZeroLengthPredicate: 255 | Enabled: false 256 | 257 | # Adds complexity 258 | Style/IfInsideElse: 259 | Enabled: false 260 | 261 | # Sometimes we just want to 'collect' 262 | Style/CollectionMethods: 263 | Enabled: false 264 | -------------------------------------------------------------------------------- /fastlane-plugin-simctl/Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gemspec 4 | 5 | plugins_path = File.join(File.dirname(__FILE__), 'fastlane', 'Pluginfile') 6 | eval_gemfile(plugins_path) if File.exist?(plugins_path) 7 | -------------------------------------------------------------------------------- /fastlane-plugin-simctl/README.md: -------------------------------------------------------------------------------- 1 | # fastlane-plugin-simctl 2 | 3 | ## Getting Started 4 | 5 | This project is a [_fastlane_](https://github.com/fastlane/fastlane) plugin. To get started with `fastlane-plugin-simctl`, add it to your project by running: 6 | 7 | ```bash 8 | fastlane add_plugin simctl 9 | ``` 10 | 11 | ## About fastlane-plugin-simctl 12 | 13 | Fastlane plugin to interact with xcrun simctl. Manage your iOS Simulators directly from your Fastfile. 14 | 15 | ## Example 16 | 17 | Check out the [example `Fastfile`](fastlane/Fastfile) to see how to use this plugin. Try it by cloning the repo, running `fastlane install_plugins` and `bundle exec fastlane test`. 18 | 19 | ## Run tests for this plugin 20 | 21 | To run both the tests, and code style validation, run 22 | 23 | ``` 24 | rake 25 | ``` 26 | 27 | To automatically fix many of the styling issues, use 28 | ``` 29 | rubocop -a 30 | ``` 31 | 32 | ## Issues and Feedback 33 | 34 | For any other issues and feedback about this plugin, please submit it to this repository. 35 | 36 | ## Troubleshooting 37 | 38 | If you have trouble using plugins, check out the [Plugins Troubleshooting](https://docs.fastlane.tools/plugins/plugins-troubleshooting/) guide. 39 | 40 | ## Using _fastlane_ Plugins 41 | 42 | For more information about how the `fastlane` plugin system works, check out the [Plugins documentation](https://docs.fastlane.tools/plugins/create-plugin/). 43 | 44 | ## About _fastlane_ 45 | 46 | _fastlane_ is the easiest way to automate beta deployments and releases for your iOS and Android apps. To learn more, check out [fastlane.tools](https://fastlane.tools). 47 | -------------------------------------------------------------------------------- /fastlane-plugin-simctl/Rakefile: -------------------------------------------------------------------------------- 1 | require 'bundler/gem_tasks' 2 | 3 | require 'rspec/core/rake_task' 4 | RSpec::Core::RakeTask.new 5 | 6 | require 'rubocop/rake_task' 7 | RuboCop::RakeTask.new(:rubocop) 8 | 9 | task default: [:spec, :rubocop] 10 | -------------------------------------------------------------------------------- /fastlane-plugin-simctl/fastlane-plugin-simctl.gemspec: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | lib = File.expand_path("../lib", __FILE__) 4 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 5 | require 'fastlane/plugin/simctl/version' 6 | 7 | Gem::Specification.new do |spec| 8 | spec.name = 'fastlane-plugin-simctl' 9 | spec.version = Fastlane::Simctl::VERSION 10 | spec.author = 'Renzo Crisostomo' 11 | spec.email = 'renzo.crisostomo@me.com' 12 | 13 | spec.summary = 'Fastlane plugin to interact with xcrun simctl' 14 | spec.license = "MIT" 15 | 16 | spec.files = Dir["lib/**/*"] + %w(README.md) 17 | spec.test_files = spec.files.grep(%r{^(test|spec|features)/}) 18 | spec.require_paths = ['lib'] 19 | 20 | spec.add_dependency 'simctl', '~> 1.6.5' 21 | 22 | spec.add_development_dependency 'pry' 23 | spec.add_development_dependency 'bundler' 24 | spec.add_development_dependency 'rspec' 25 | spec.add_development_dependency 'rake' 26 | spec.add_development_dependency 'rubocop' 27 | spec.add_development_dependency 'simplecov' 28 | spec.add_development_dependency 'fastlane', '>= 2.53.1' 29 | end 30 | -------------------------------------------------------------------------------- /fastlane-plugin-simctl/fastlane/Fastfile: -------------------------------------------------------------------------------- 1 | lane :test do 2 | simctl(block: lambda { |other_action, device| 3 | other_action.scan(workspace: "#{Dir.pwd}/AwesomeApp.xcworkspace", 4 | scheme: 'AwesomeApp', 5 | device: device.name, 6 | derived_data_path: derived_data_path) 7 | }) 8 | trainer(output_directory: ".") 9 | end 10 | -------------------------------------------------------------------------------- /fastlane-plugin-simctl/fastlane/Pluginfile: -------------------------------------------------------------------------------- 1 | # Autogenerated by fastlane 2 | -------------------------------------------------------------------------------- /fastlane-plugin-simctl/lib/fastlane/plugin/simctl.rb: -------------------------------------------------------------------------------- 1 | require 'fastlane/plugin/simctl/version' 2 | 3 | module Fastlane 4 | module Simctl 5 | # Return all .rb files inside the "actions" and "helper" directory 6 | def self.all_classes 7 | Dir[File.expand_path('**/{actions,helper}/*.rb', File.dirname(__FILE__))] 8 | end 9 | end 10 | end 11 | 12 | # By default we want to import all available actions and helpers 13 | # A plugin can contain any number of actions and plugins 14 | Fastlane::Simctl.all_classes.each do |current| 15 | require current 16 | end 17 | -------------------------------------------------------------------------------- /fastlane-plugin-simctl/lib/fastlane/plugin/simctl/actions/simctl_action.rb: -------------------------------------------------------------------------------- 1 | require 'simctl' 2 | 3 | module Fastlane 4 | module Actions 5 | class SimctlAction < Action 6 | def self.run(params) 7 | Helper::SimctlHelper.execute_with_simulator_ready(self, params[:block], params[:runtime], params[:type], params[:name]) 8 | end 9 | 10 | def self.description 11 | "Fastlane plugin to interact with xcrun simctl." 12 | end 13 | 14 | def self.authors 15 | ["Renzo Crisostomo"] 16 | end 17 | 18 | def self.details 19 | "Fastlane plugin to interact with xcrun simctl. Manage your iOS Simulators directly from your Fastfile." 20 | end 21 | 22 | def self.available_options 23 | [ 24 | FastlaneCore::ConfigItem.new(key: :block, 25 | description: "A Ruby block given to execute in the context of a Simulator ready", 26 | optional: false, 27 | type: Proc), 28 | FastlaneCore::ConfigItem.new(key: :runtime, 29 | description: "iOS Runtime used to create the simulator", 30 | optional: true, 31 | type: String, 32 | default_value: 'latest'), 33 | FastlaneCore::ConfigItem.new(key: :type, 34 | description: "iOS device type used to create the simulator", 35 | optional: true, 36 | type: String, 37 | default_value: 'iPhone 8'), 38 | FastlaneCore::ConfigItem.new(key: :name, 39 | description: "String used to set the name to the simulator", 40 | optional: true, 41 | type: String, 42 | default_value: nil) 43 | ] 44 | end 45 | 46 | def self.is_supported?(platform) 47 | [:ios].include?(platform) 48 | end 49 | end 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /fastlane-plugin-simctl/lib/fastlane/plugin/simctl/helper/simctl_helper.rb: -------------------------------------------------------------------------------- 1 | module Fastlane 2 | module Helper 3 | class SimctlHelper 4 | def self.execute_with_simulator_ready(action, block, runtime, type, name) 5 | device = create_device(runtime, type, name) 6 | device.boot 7 | device.wait(90) do |d| 8 | Fastlane::UI.message("Waiting for simulator `#{d.name}` to be ready") 9 | d.state == :booted && d.ready? 10 | end 11 | begin 12 | block.call(action.other_action, device) 13 | rescue StandardError => error 14 | throw error 15 | ensure 16 | delete_device(device) 17 | end 18 | end 19 | 20 | def self.create_device(runtime, type, name) 21 | runtime = if runtime.eql? 'latest' 22 | SimCtl::Runtime.latest('ios') 23 | else 24 | SimCtl.runtime(name: runtime) 25 | end 26 | device_type = SimCtl.devicetype(name: type) 27 | device_name = name 28 | device_name ||= type.to_s.instance_eval do |obj| 29 | obj += "-#{ENV['JOB_NAME']}" if ENV['JOB_NAME'] 30 | obj += "@#{ENV['BUILD_NUMBER']}" if ENV['BUILD_NUMBER'] 31 | obj 32 | end 33 | Fastlane::UI.message("Starting simulator with runtime: `#{runtime.name}`, device type: `#{device_type.name}`"\ 34 | " and device name: `#{device_name}`") 35 | SimCtl.reset_device(device_name, device_type, runtime) 36 | end 37 | 38 | def self.delete_device(device) 39 | if device.state != :shutdown 40 | device.shutdown 41 | device.kill 42 | device.wait do |d| 43 | Fastlane::UI.message("Waiting for simulator `#{d.name}` to be shutdown") 44 | d.state == :shutdown 45 | end 46 | end 47 | Fastlane::UI.message("Deleting simulator `#{device.name}`") 48 | device.delete 49 | end 50 | end 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /fastlane-plugin-simctl/lib/fastlane/plugin/simctl/version.rb: -------------------------------------------------------------------------------- 1 | module Fastlane 2 | module Simctl 3 | VERSION = "0.2.2" 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /fastlane-plugin-simctl/spec/simctl_action_spec.rb: -------------------------------------------------------------------------------- 1 | describe Fastlane::Actions::SimctlAction do 2 | describe '#run' do 3 | end 4 | end 5 | -------------------------------------------------------------------------------- /fastlane-plugin-simctl/spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | $LOAD_PATH.unshift File.expand_path('../../lib', __FILE__) 2 | 3 | require 'simplecov' 4 | 5 | # SimpleCov.minimum_coverage 95 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 'fastlane' # to import the Action super class 13 | require 'fastlane/plugin/simctl' # import the actual plugin 14 | 15 | Fastlane.load_actions # load other actions (in case your plugin calls other actions or shared values) 16 | -------------------------------------------------------------------------------- /lib/simctl.rb: -------------------------------------------------------------------------------- 1 | require 'simctl/command' 2 | require 'simctl/device' 3 | require 'simctl/device_type' 4 | require 'simctl/list' 5 | require 'simctl/runtime' 6 | require 'simctl/xcode/path' 7 | require 'simctl/xcode/version' 8 | 9 | module SimCtl 10 | class UnsupportedCommandError < StandardError; end 11 | class DeviceTypeNotFound < StandardError; end 12 | class RuntimeNotFound < StandardError; end 13 | class DeviceNotFound < StandardError; end 14 | 15 | @@default_timeout = 15 16 | 17 | class << self 18 | def default_timeout 19 | @@default_timeout 20 | end 21 | 22 | def default_timeout=(timeout) 23 | @@default_timeout = timeout 24 | end 25 | 26 | def command 27 | return @command if defined?(@command) 28 | @command = SimCtl::Command.new 29 | end 30 | 31 | private 32 | 33 | def respond_to_missing?(method_name, include_private = false) 34 | command.respond_to?(method_name, include_private) 35 | end 36 | 37 | def method_missing(method_name, *args, &block) 38 | if command.respond_to?(method_name) 39 | return command.send(method_name, *args, &block) 40 | end 41 | super 42 | end 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /lib/simctl/command.rb: -------------------------------------------------------------------------------- 1 | require 'simctl/command/boot' 2 | require 'simctl/command/create' 3 | require 'simctl/command/delete' 4 | require 'simctl/command/erase' 5 | require 'simctl/command/install' 6 | require 'simctl/command/io' 7 | require 'simctl/command/keychain' 8 | require 'simctl/command/kill' 9 | require 'simctl/command/launch' 10 | require 'simctl/command/privacy' 11 | require 'simctl/command/terminate' 12 | require 'simctl/command/list' 13 | require 'simctl/command/openurl' 14 | require 'simctl/command/push' 15 | require 'simctl/command/rename' 16 | require 'simctl/command/reset' 17 | require 'simctl/command/shutdown' 18 | require 'simctl/command/spawn' 19 | require 'simctl/command/status_bar' 20 | require 'simctl/command/uninstall' 21 | require 'simctl/command/upgrade' 22 | require 'simctl/command/warmup' 23 | require 'simctl/executor' 24 | require 'shellwords' 25 | 26 | module SimCtl 27 | class Command 28 | attr_accessor :device_set_path 29 | 30 | include SimCtl::Command::Boot 31 | include SimCtl::Command::Create 32 | include SimCtl::Command::Delete 33 | include SimCtl::Command::Erase 34 | include SimCtl::Command::IO 35 | include SimCtl::Command::Install 36 | include SimCtl::Command::Keychain 37 | include SimCtl::Command::Kill 38 | include SimCtl::Command::Launch 39 | include SimCtl::Command::List 40 | include SimCtl::Command::OpenUrl 41 | include SimCtl::Command::Privacy 42 | include SimCtl::Command::Push 43 | include SimCtl::Command::Rename 44 | include SimCtl::Command::Reset 45 | include SimCtl::Command::Shutdown 46 | include SimCtl::Command::Spawn 47 | include SimCtl::Command::StatusBar 48 | include SimCtl::Command::Terminate 49 | include SimCtl::Command::Uninstall 50 | include SimCtl::Command::Upgrade 51 | include SimCtl::Command::Warmup 52 | 53 | def device_set_path=(device_set_path) 54 | @device_set_path = File.expand_path(device_set_path) 55 | end 56 | 57 | def command_for(*arguments) 58 | command = %w[xcrun simctl] 59 | command += ['--set', Shellwords.shellescape(device_set_path)] unless device_set_path.nil? 60 | command += arguments 61 | command 62 | end 63 | end 64 | end 65 | -------------------------------------------------------------------------------- /lib/simctl/command/boot.rb: -------------------------------------------------------------------------------- 1 | module SimCtl 2 | class Command 3 | module Boot 4 | # Boots a device 5 | # 6 | # @param device [SimCtl::Device] the device to boot 7 | # @return [void] 8 | def boot_device(device) 9 | Executor.execute(command_for('boot', device.udid)) 10 | end 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /lib/simctl/command/create.rb: -------------------------------------------------------------------------------- 1 | require 'shellwords' 2 | 3 | module SimCtl 4 | class Command 5 | module Create 6 | # Creates a device 7 | # 8 | # @param name [String] name of the new device 9 | # @param devicetype [SimCtl::DeviceType] device type of the new device 10 | # @param runtime [SimCtl::Runtime] runtime of the new device 11 | # @return [SimCtl::Device] the device that was created 12 | def create_device(name, devicetype, runtime) 13 | runtime = runtime(name: runtime) unless runtime.is_a?(Runtime) 14 | devicetype = devicetype(name: devicetype) unless devicetype.is_a?(DeviceType) 15 | raise "Invalid runtime: #{runtime}" unless runtime.is_a?(Runtime) 16 | raise "Invalid devicetype: #{devicetype}" unless devicetype.is_a?(DeviceType) 17 | command = command_for('create', Shellwords.shellescape(name), devicetype.identifier, runtime.identifier) 18 | device = Executor.execute(command) do |identifier| 19 | device(udid: identifier) 20 | end 21 | device.wait { |d| d.state == :shutdown && File.exist?(d.path.device_plist) } 22 | device 23 | end 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /lib/simctl/command/delete.rb: -------------------------------------------------------------------------------- 1 | module SimCtl 2 | class Command 3 | module Delete 4 | # Delete a device 5 | # 6 | # @param device [SimCtl::Device] the device to delete 7 | # @return [void] 8 | def delete_device(device) 9 | Executor.execute(command_for('delete', device.udid)) 10 | end 11 | 12 | # Delete all devices 13 | # 14 | # @return [SimCtl::List] a list of all deleted SimCtl::Device objects 15 | def delete_all_devices 16 | list_devices.each do |device| 17 | device.kill 18 | device.shutdown if device.state != :shutdown 19 | device.wait { |d| d.state == :shutdown } 20 | device.delete 21 | end 22 | end 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /lib/simctl/command/erase.rb: -------------------------------------------------------------------------------- 1 | module SimCtl 2 | class Command 3 | module Erase 4 | # Erase a device 5 | # 6 | # @param device [SimCtl::Device] the device to erase 7 | # @return [void] 8 | def erase_device(device) 9 | Executor.execute(command_for('erase', device.udid)) 10 | end 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /lib/simctl/command/install.rb: -------------------------------------------------------------------------------- 1 | require 'shellwords' 2 | 3 | module SimCtl 4 | class Command 5 | module Install 6 | # Installs an app on a device 7 | # 8 | # @param device [SimCtl::Device] the device the app should be installed on 9 | # @param path Absolute path to the app that should be installed 10 | # @return [void] 11 | def install_app(device, path) 12 | Executor.execute(command_for('install', device.udid, Shellwords.shellescape(path))) 13 | end 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /lib/simctl/command/io.rb: -------------------------------------------------------------------------------- 1 | require 'shellwords' 2 | 3 | module SimCtl 4 | class Command 5 | module IO 6 | # Saves a screenshot to a file 7 | # 8 | # @param device [SimCtl::Device] the device the screenshot should be taken from 9 | # @param file Path where the screenshot should be saved to 10 | # @param opts Optional hash that supports two keys: 11 | # * type: Can be png, tiff, bmp, gif, jpeg (default is png) 12 | # * display: Can be main or tv for iOS, tv for tvOS and main for watchOS 13 | # @return [void] 14 | def screenshot(device, file, opts = {}) 15 | unless Xcode::Version.gte? '8.2' 16 | raise UnsupportedCommandError, 'Needs at least Xcode 8.2' 17 | end 18 | optional_args = opts.map { |k, v| "--#{k}=#{Shellwords.shellescape(v)}" } 19 | Executor.execute(command_for('io', device.udid, 'screenshot', *optional_args, Shellwords.shellescape(file))) 20 | end 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /lib/simctl/command/keychain.rb: -------------------------------------------------------------------------------- 1 | require 'ostruct' 2 | 3 | module SimCtl 4 | class Command 5 | module Keychain 6 | # Reset the keychain 7 | # 8 | # @param device [SimCtl::Device] the device 9 | # @return [void] 10 | def keychain_reset(device) 11 | unless Xcode::Version.gte? '11.4' 12 | raise UnsupportedCommandError, 'Needs at least Xcode 11.4' 13 | end 14 | Executor.execute(command_for('keychain', device.udid, 'reset')) 15 | end 16 | end 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /lib/simctl/command/kill.rb: -------------------------------------------------------------------------------- 1 | module SimCtl 2 | class Command 3 | module Kill 4 | # Kills a Simulator instance with the given device 5 | # 6 | # @param device [SimCtl::Device] the device to kill 7 | # @return [void] 8 | def kill_device(device) 9 | pid = `ps xww | grep Simulator.app | grep -s #{device.udid} | grep -v grep | awk '{print $1}'`.chomp 10 | if pid.to_i > 0 11 | system 'kill', pid 12 | else 13 | false 14 | end 15 | end 16 | end 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /lib/simctl/command/launch.rb: -------------------------------------------------------------------------------- 1 | require 'shellwords' 2 | 3 | module SimCtl 4 | class Command 5 | module Launch 6 | SUPPORTED_SCALE = [1.0, 0.75, 0.5, 0.25].freeze 7 | 8 | # Launches a Simulator instance with the given device 9 | # 10 | # @param device [SimCtl::Device] the device to launch 11 | # @return [void] 12 | def launch_device(device, scale = 1.0, opts = {}) 13 | raise "unsupported scale '#{scale}' (supported: #{SUPPORTED_SCALE.join(', ')})" unless SUPPORTED_SCALE.include?(scale) 14 | # Launching the same device twice does not work. 15 | # Simulator.app would just hang. Solution: Kill first. 16 | kill_device(device) 17 | args = { 18 | '-ConnectHardwareKeyboard' => 1, 19 | '-CurrentDeviceUDID' => device.udid, 20 | "-SimulatorWindowLastScale-#{device.devicetype.identifier}" => scale 21 | } 22 | args['-DeviceSetPath'] = Shellwords.shellescape(SimCtl.device_set_path) unless SimCtl.device_set_path.nil? 23 | args = args.merge(opts).zip.flatten.join(' ') 24 | command = "open -Fgn #{Xcode::Path.home}/Applications/Simulator.app --args #{args}" 25 | system command 26 | end 27 | 28 | # Launches an app in the given device 29 | # 30 | # @param device [SimCtl::Device] the device to launch 31 | # @param opts [Hash] options hash - `{ wait_for_debugger: true/false }` 32 | # @param identifier [String] the app identifier 33 | # @param args [Array] optional launch arguments 34 | # @return [void] 35 | def launch_app(device, identifier, args = [], opts = {}) 36 | launch_args = args.map { |arg| Shellwords.shellescape arg } 37 | launch_opts = opts[:wait_for_debugger] ? '-w' : '' 38 | Executor.execute(command_for('launch', launch_opts, device.udid, identifier, launch_args)) 39 | end 40 | end 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /lib/simctl/command/list.rb: -------------------------------------------------------------------------------- 1 | module SimCtl 2 | class Command 3 | module List 4 | # Find a device 5 | # 6 | # @param filter [Hash] the filter 7 | # @return [SimCtl::Device, nil] the device matching the given filter 8 | def device(filter) 9 | list_devices.where(filter).first 10 | end 11 | 12 | # Find a device type 13 | # 14 | # @param filter [Hash] the filter 15 | # @return [SimCtl::DeviceType] the device type matching the given filter 16 | # @raise [DeviceTypeNotFound] if the device type could not be found 17 | def devicetype(filter) 18 | device_type = list_devicetypes.where(filter).first 19 | device_type || raise(DeviceTypeNotFound, "Could not find a device type matching #{filter.inspect}") 20 | end 21 | 22 | # List all devices 23 | # 24 | # @return [SimCtl::List] a list of SimCtl::Device objects 25 | def list_devices 26 | Executor.execute(command_for('list', '-j', 'devices')) do |json| 27 | devices = json['devices'].map { |os, devs| devs.map { |device| Device.new(device.merge(os: os)) } } 28 | SimCtl::List.new(devices.flatten) 29 | end 30 | end 31 | 32 | # List all device types 33 | # 34 | # @return [SimCtl::List] a list of SimCtl::DeviceType objects 35 | def list_devicetypes 36 | Executor.execute(command_for('list', '-j', 'devicetypes')) do |json| 37 | SimCtl::List.new(json['devicetypes'].map { |devicetype| DeviceType.new(devicetype) }) 38 | end 39 | end 40 | 41 | # List all runtimes 42 | # 43 | # @return [SimCtl::List] a list of SimCtl::Runtime objects 44 | def list_runtimes 45 | Executor.execute(command_for('list', '-j', 'runtimes')) do |json| 46 | SimCtl::List.new(json['runtimes'].map { |runtime| Runtime.new(runtime) }) 47 | end 48 | end 49 | 50 | # Find a runtime 51 | # 52 | # @param filter [Hash] the filter 53 | # @return [SimCtl::Runtime] the runtime matching the given filter 54 | # @raise [RuntimeNotFound] if the runtime could not be found 55 | def runtime(filter) 56 | runtime = list_runtimes.where(filter).first 57 | runtime || raise(RuntimeNotFound, "Could not find a runtime matching #{filter.inspect}") 58 | end 59 | end 60 | end 61 | end 62 | -------------------------------------------------------------------------------- /lib/simctl/command/openurl.rb: -------------------------------------------------------------------------------- 1 | require 'shellwords' 2 | 3 | module SimCtl 4 | class Command 5 | module OpenUrl 6 | # Opens a url 7 | # 8 | # @param device [SimCtl::Device] the device that should open the url 9 | # @param url The url to open on the device 10 | # @return [void] 11 | def open_url(device, url) 12 | Executor.execute(command_for('openurl', device.udid, Shellwords.shellescape(url))) 13 | end 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /lib/simctl/command/privacy.rb: -------------------------------------------------------------------------------- 1 | module SimCtl 2 | class Command 3 | module Privacy 4 | # Change privacy settings 5 | # 6 | # @param device [SimCtl::Device] the device 7 | # @param action [String] grant, revoke, reset 8 | # @param service [String] all, calendar, contacts-limited, contacts, location, 9 | # location-always, photos-add, photos, media-library, microphone, 10 | # motion, reminders, siri 11 | # @param bundle [String] bundle identifier 12 | # @return [void] 13 | def privacy(device, action, service, bundle) 14 | unless Xcode::Version.gte? '11.4' 15 | raise UnsupportedCommandError, 'Needs at least Xcode 11.4' 16 | end 17 | Executor.execute(command_for('privacy', device.udid, action, service, bundle)) 18 | end 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /lib/simctl/command/push.rb: -------------------------------------------------------------------------------- 1 | require 'fileutils' 2 | require 'json' 3 | require 'tempfile' 4 | 5 | module SimCtl 6 | class Command 7 | module Push 8 | # Send some push notification 9 | # 10 | # @param device [SimCtl::Device] the device 11 | # @param bundle [String] bundle identifier 12 | # @param payload the JSON payload. This can be a JSON [String], some [Hash] or 13 | # just a [String] path to a local file containing a JSON payload 14 | # @return [void] 15 | def push(device, bundle, payload) 16 | unless Xcode::Version.gte? '11.4' 17 | raise UnsupportedCommandError, 'Needs at least Xcode 11.4' 18 | end 19 | 20 | file = Tempfile.new('push') 21 | 22 | if payload.is_a?(Hash) 23 | JSON.dump payload, file 24 | file.close 25 | elsif payload.is_a?(String) && File.exist?(payload) 26 | file.close 27 | FileUtils.cp payload, file.path 28 | else 29 | file.write payload 30 | file.close 31 | end 32 | 33 | Executor.execute(command_for('push', device.udid, bundle, file.path)) 34 | end 35 | end 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /lib/simctl/command/rename.rb: -------------------------------------------------------------------------------- 1 | require 'shellwords' 2 | 3 | module SimCtl 4 | class Command 5 | module Rename 6 | # Boots a device 7 | # 8 | # @param device [SimCtl::Device] the device to boot 9 | # @param name [String] the new device name 10 | # @return [void] 11 | def rename_device(device, name) 12 | Executor.execute(command_for('rename', device.udid, Shellwords.shellescape(name))) 13 | end 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /lib/simctl/command/reset.rb: -------------------------------------------------------------------------------- 1 | module SimCtl 2 | class Command 3 | module Reset 4 | # Kill, shutdown, delete and create a device 5 | # 6 | # @param name [String] name of the new device 7 | # @param device_type [SimCtl::DeviceType] device type of the new device 8 | # @param runtime [SimCtl::Runtime] runtime of the new device 9 | # @return [SimCtl::Device] the device that was created 10 | # @yield [exception] an exception that might happen during shutdown/delete of the old device 11 | def reset_device(name, device_type, runtime) 12 | begin 13 | list_devices.where(name: name, os: runtime.identifier).each do |device| 14 | device.kill 15 | device.shutdown if device.state != :shutdown 16 | device.wait { |d| d.state == :shutdown } 17 | device.delete 18 | end 19 | rescue Exception => exception 20 | yield exception if block_given? 21 | end 22 | device = create_device name, device_type, runtime 23 | device.wait { |d| d.state == :shutdown } 24 | device 25 | end 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /lib/simctl/command/shutdown.rb: -------------------------------------------------------------------------------- 1 | module SimCtl 2 | class Command 3 | module Shutdown 4 | # Shutdown a device 5 | # 6 | # @param device [SimCtl::Device] the device to shutdown 7 | # @return [void] 8 | def shutdown_device(device) 9 | Executor.execute(command_for('shutdown', device.udid)) 10 | end 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /lib/simctl/command/spawn.rb: -------------------------------------------------------------------------------- 1 | require 'shellwords' 2 | 3 | module SimCtl 4 | class Command 5 | module Spawn 6 | # Spawn a process on a device 7 | # 8 | # @param device [SimCtl::Device] the device to spawn a process on 9 | # @param path [String] path to executable 10 | # @param args [Array] arguments for the executable 11 | # @return [String] standard output the spawned process generated 12 | def spawn(device, path, args = [], _opts = {}) 13 | escaped_path = Shellwords.shellescape(path) 14 | command = command_for('spawn', device.udid, escaped_path, *args.map { |a| Shellwords.shellwords(a) }) 15 | Executor.execute(command) do |output| 16 | output 17 | end 18 | end 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /lib/simctl/command/status_bar.rb: -------------------------------------------------------------------------------- 1 | require 'ostruct' 2 | 3 | module SimCtl 4 | class Command 5 | module StatusBar 6 | # Clear all status bar overrides 7 | # 8 | # @param device [SimCtl::Device] the device 9 | # @return [void] 10 | def status_bar_clear(device) 11 | unless Xcode::Version.gte? '11.4' 12 | raise UnsupportedCommandError, 'Needs at least Xcode 11.4' 13 | end 14 | Executor.execute(command_for('status_bar', device.udid, 'clear')) 15 | end 16 | 17 | # Set some status bar overrides 18 | # 19 | # Refer to `xcrun simctl status_bar` for available options. 20 | # 21 | # Example: 22 | # 23 | # SimCtl.status_bar_override device, { 24 | # time: '9:41', 25 | # dataNetwork: 'lte+', 26 | # wifiMode: 'active', 27 | # cellularMode: 'active', 28 | # batteryState: 'charging', 29 | # batteryLevel: 50 30 | # } 31 | # 32 | # @param device [SimCtl::Device] the device 33 | # @param overrides [SimCtl::StatusBarOverrides] or [Hash] the overrides to apply 34 | # @return [void] 35 | def status_bar_override(device, overrides) 36 | unless Xcode::Version.gte? '11.4' 37 | raise UnsupportedCommandError, 'Needs at least Xcode 11.4' 38 | end 39 | overrides = SimCtl::StatusBarOverrides.new overrides unless overrides.is_a?(SimCtl::StatusBarOverrides) 40 | Executor.execute(command_for('status_bar', device.udid, 'override', *overrides.to_args)) 41 | end 42 | end 43 | end 44 | end 45 | 46 | module SimCtl 47 | class StatusBarOverrides < OpenStruct 48 | def to_args 49 | to_h.map { |k, v| ["--#{k}", v] }.flatten 50 | end 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /lib/simctl/command/terminate.rb: -------------------------------------------------------------------------------- 1 | require 'shellwords' 2 | 3 | module SimCtl 4 | class Command 5 | module Terminate 6 | # Terminates an app on the given device 7 | # 8 | # @param device [SimCtl::Device] the device with the app to terminate 9 | # @param identifier [String] the app identifier 10 | # @param args [Array] optional terminate arguments 11 | # @return [void] 12 | def terminate_app(device, identifier, args = []) 13 | unless Xcode::Version.gte? '8.2' 14 | raise UnsupportedCommandError, 'Needs at least Xcode 8.2' 15 | end 16 | terminate_args = args.map { |arg| Shellwords.shellescape arg } 17 | Executor.execute(command_for('terminate', terminate_args, device.udid, identifier)) 18 | end 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /lib/simctl/command/uninstall.rb: -------------------------------------------------------------------------------- 1 | module SimCtl 2 | class Command 3 | module Uninstall 4 | # Uninstall an app on a device 5 | # 6 | # @param device [SimCtl::Device] the device the app should be uninstalled from 7 | # @param app_id App identifier of the app that should be uninstalled 8 | # @return [void] 9 | def uninstall_app(device, app_id) 10 | Executor.execute(command_for('uninstall', device.udid, app_id)) 11 | end 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /lib/simctl/command/upgrade.rb: -------------------------------------------------------------------------------- 1 | module SimCtl 2 | class Command 3 | module Upgrade 4 | # Upgrade a device to a newer runtime 5 | # 6 | # @param device [SimCtl::Device] the device the upgrade should be performed for 7 | # @param runtime [SimCtl::Runtime] the runtime the device should be upgrade to 8 | # @return [void] 9 | def upgrade(device, runtime) 10 | Executor.execute(command_for('upgrade', device.udid, runtime.identifier)) 11 | end 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /lib/simctl/command/warmup.rb: -------------------------------------------------------------------------------- 1 | module SimCtl 2 | class Command 3 | module Warmup 4 | # Warms up a device and waits for it to be ready 5 | # 6 | # @param devicetype [String] device type string 7 | # @param runtime [String] runtime string 8 | # @param timeout [Integer] timeout in seconds to wait until device is ready 9 | # @return [SimCtl::Device] 10 | def warmup(devicetype, runtime, timeout = 120) 11 | devicetype = devicetype(name: devicetype) unless devicetype.is_a?(DeviceType) 12 | runtime = runtime(name: runtime) unless runtime.is_a?(Runtime) 13 | device = device(runtime: runtime, devicetype: devicetype) 14 | raise DeviceNotFound, "Could not find device with type '#{devicetype.name}' and runtime '#{runtime.name}'" if device.nil? 15 | device.boot if device.state != :booted 16 | device.wait(timeout) { |d| d.state == :booted && d.ready? } 17 | device 18 | end 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /lib/simctl/device.rb: -------------------------------------------------------------------------------- 1 | require 'cfpropertylist' 2 | require 'ostruct' 3 | require 'simctl/device_launchctl' 4 | require 'simctl/device_path' 5 | require 'simctl/device_settings' 6 | require 'simctl/keychain' 7 | require 'simctl/object' 8 | require 'simctl/status_bar' 9 | require 'timeout' 10 | 11 | module SimCtl 12 | class Device < Object 13 | extend Gem::Deprecate 14 | 15 | attr_reader :is_available, :name, :os, :state, :udid 16 | 17 | def initialize(args) 18 | args['is_available'] = args.delete('isAvailable') 19 | super 20 | end 21 | 22 | def availability 23 | is_available 24 | end 25 | deprecate :availability, :is_available, 2020, 8 26 | 27 | # Returns true/false if the device is available 28 | # 29 | # @return [Bool] 30 | def available? 31 | case is_available 32 | when String 33 | is_available !~ /unavailable/i 34 | else 35 | is_available 36 | end 37 | end 38 | 39 | # Boots the device 40 | # 41 | # @return [void] 42 | def boot 43 | SimCtl.boot_device(self) 44 | end 45 | 46 | # Deletes the device 47 | # 48 | # @return [void] 49 | def delete 50 | SimCtl.delete_device(self) 51 | end 52 | 53 | # Returns the device type 54 | # 55 | # @return [SimCtl::DeviceType, String] 56 | def devicetype 57 | @devicetype ||= SimCtl.devicetype(identifier: plist.deviceType) 58 | rescue 59 | plist.deviceType 60 | end 61 | 62 | # Erases the device 63 | # 64 | # @return [void] 65 | def erase 66 | SimCtl.erase_device(self) 67 | end 68 | 69 | # Installs an app on a device 70 | # 71 | # @param path Absolute path to the app that should be installed 72 | # @return [void] 73 | def install(path) 74 | SimCtl.install_app(self, path) 75 | end 76 | 77 | # Uninstall an app from a device 78 | # 79 | # @param app_id App identifier of the app that should be uninstalled 80 | # @return [void] 81 | def uninstall(app_id) 82 | SimCtl.uninstall_app(self, app_id) 83 | end 84 | 85 | # Kills the device 86 | # 87 | # @return [void] 88 | def kill 89 | SimCtl.kill_device(self) 90 | end 91 | 92 | # Launches the Simulator 93 | # 94 | # @return [void] 95 | def launch(scale = 1.0, opts = {}) 96 | SimCtl.launch_device(self, scale, opts) 97 | end 98 | 99 | # Returns the launchctl object 100 | # 101 | # @ return [SimCtl::DeviceLaunchctl] 102 | def launchctl 103 | @launchctl ||= DeviceLaunchctl.new(self) 104 | end 105 | 106 | # Returns the keychain bar object 107 | # 108 | # @return [SimCtl::keychain] 109 | def keychain 110 | @keychain ||= SimCtl::Keychain.new(self) 111 | end 112 | 113 | # Launches an app in the given device 114 | # 115 | # @param opts [Hash] options hash - `{ wait_for_debugger: true/false }` 116 | # @param identifier [String] the app identifier 117 | # @param args [Array] optional launch arguments 118 | # @return [void] 119 | def launch_app(identifier, args = [], opts = {}) 120 | SimCtl.launch_app(self, identifier, args, opts) 121 | end 122 | 123 | # Terminates an app on the given device 124 | # 125 | # @param identifier [String] the app identifier 126 | # @param args [Array] optional terminate arguments 127 | # @return [void] 128 | def terminate_app(identifier, args = []) 129 | SimCtl.terminate_app(self, identifier, args) 130 | end 131 | 132 | # Opens the url on the device 133 | # 134 | # @param url [String] The url to be opened on the device 135 | # @return [void] 136 | def open_url(url) 137 | SimCtl.open_url(self, url) 138 | end 139 | 140 | def path 141 | @path ||= DevicePath.new(self) 142 | end 143 | 144 | # Change privacy settings 145 | # 146 | # @param action [String] grant, revoke, reset 147 | # @param service [String] all, calendar, contacts-limited, contacts, location, 148 | # location-always, photos-add, photos, media-library, microphone, 149 | # motion, reminders, siri 150 | # @param bundle [String] bundle identifier 151 | # @return [void] 152 | def privacy(action, service, bundle) 153 | SimCtl.privacy(self, action, service, bundle) 154 | end 155 | 156 | # Returns true/false if the device is ready 157 | # Uses [SimCtl::DeviceLaunchctl] to look for certain services being running. 158 | # 159 | # Unfortunately the 'booted' state does not mean the Simulator is ready for 160 | # installing or launching applications. 161 | # 162 | # @return [Bool] 163 | def ready? 164 | running_services = launchctl.list.reject { |service| service.pid.to_i == 0 }.map(&:name) 165 | (required_services_for_ready - running_services).empty? 166 | end 167 | 168 | # Reloads the device information 169 | # 170 | # @return [void] 171 | def reload 172 | device = SimCtl.device(udid: udid) 173 | device.instance_variables.each do |ivar| 174 | instance_variable_set(ivar, device.instance_variable_get(ivar)) 175 | end 176 | end 177 | 178 | # Renames the device 179 | # 180 | # @return [void] 181 | def rename(name) 182 | SimCtl.rename_device(self, name) 183 | @name = name 184 | end 185 | 186 | # Resets the device 187 | # 188 | # @return [void] 189 | def reset 190 | SimCtl.reset_device name, devicetype, runtime 191 | end 192 | 193 | # Returns the runtime object 194 | # 195 | # @return [SimCtl::Runtime, String] 196 | def runtime 197 | @runtime ||= SimCtl.runtime(identifier: plist.runtime) 198 | rescue 199 | plist.runtime 200 | end 201 | 202 | # Saves a screenshot to a file 203 | # 204 | # @param file Path where the screenshot should be saved to 205 | # @param opts Optional hash that supports two keys: 206 | # * type: Can be png, tiff, bmp, gif, jpeg (default is png) 207 | # * display: Can be main or tv for iOS, tv for tvOS and main for watchOS 208 | # @return [void] 209 | def screenshot(file, opts = {}) 210 | SimCtl.screenshot(self, file, opts) 211 | end 212 | 213 | # Returns the settings object 214 | # 215 | # @ return [SimCtl::DeviceSettings] 216 | def settings 217 | @settings ||= DeviceSettings.new(path) 218 | end 219 | 220 | # Shuts down the runtime 221 | # 222 | # @return [void] 223 | def shutdown 224 | SimCtl.shutdown_device(self) 225 | end 226 | 227 | # Spawn a process on a device 228 | # 229 | # @param path [String] path to executable 230 | # @param args [Array] arguments for the executable 231 | # @return [void] 232 | def spawn(path, args = [], opts = {}) 233 | SimCtl.spawn(self, path, args, opts) 234 | end 235 | 236 | # Returns the state of the device 237 | # 238 | # @return [sym] 239 | def state 240 | @state.downcase.to_sym 241 | end 242 | 243 | # Returns the status bar object 244 | # 245 | # @return [SimCtl::StatusBar] 246 | def status_bar 247 | @status_bar ||= SimCtl::StatusBar.new(self) 248 | end 249 | 250 | # Reloads the device until the given block returns true 251 | # 252 | # @return [void] 253 | def wait(timeout = SimCtl.default_timeout) 254 | Timeout.timeout(timeout) do 255 | loop do 256 | break if yield SimCtl.device(udid: udid) 257 | end 258 | end 259 | reload 260 | end 261 | 262 | def ==(other) 263 | return false if other.nil? 264 | return false unless other.is_a? Device 265 | other.udid == udid 266 | end 267 | 268 | def method_missing(method_name, *args, &block) 269 | if method_name[-1] == '!' 270 | new_method_name = method_name.to_s.chop.to_sym 271 | if respond_to?(new_method_name) 272 | warn "[#{Kernel.caller.first}] `#{method_name}` is deprecated. Please use `#{new_method_name}` instead." 273 | return send(new_method_name, &block) 274 | end 275 | end 276 | super 277 | end 278 | 279 | private 280 | 281 | def plist 282 | @plist ||= OpenStruct.new(CFPropertyList.native_types(CFPropertyList::List.new(file: path.device_plist).value)) 283 | end 284 | 285 | def required_services_for_ready 286 | case runtime.type 287 | when :tvos, :watchos 288 | if Xcode::Version.gte? '8.0' 289 | [ 290 | 'com.apple.mobileassetd', 291 | 'com.apple.nsurlsessiond' 292 | ] 293 | else 294 | [ 295 | 'com.apple.mobileassetd', 296 | 'com.apple.networkd' 297 | ] 298 | end 299 | when :ios 300 | if Xcode::Version.gte? '9.0' 301 | [ 302 | 'com.apple.backboardd', 303 | 'com.apple.mobile.installd', 304 | 'com.apple.CoreSimulator.bridge', 305 | 'com.apple.SpringBoard' 306 | ] 307 | elsif Xcode::Version.gte? '8.0' 308 | [ 309 | 'com.apple.SimulatorBridge', 310 | 'com.apple.SpringBoard', 311 | 'com.apple.backboardd', 312 | 'com.apple.mobile.installd' 313 | ] 314 | else 315 | [ 316 | 'com.apple.SimulatorBridge', 317 | 'com.apple.SpringBoard', 318 | 'com.apple.mobile.installd' 319 | ] 320 | end 321 | else 322 | [] 323 | end 324 | end 325 | end 326 | end 327 | -------------------------------------------------------------------------------- /lib/simctl/device_launchctl.rb: -------------------------------------------------------------------------------- 1 | require 'ostruct' 2 | 3 | module SimCtl 4 | class DeviceLaunchctl 5 | def initialize(device) 6 | @device = device 7 | end 8 | 9 | def list 10 | fields = %i[pid status name] 11 | device 12 | .spawn(device.path.launchctl, ['list']) 13 | .split("\n") 14 | .drop(1) 15 | .map { |item| Hash[fields.zip(item.split("\t"))] } 16 | .map { |item| OpenStruct.new(item) } 17 | end 18 | 19 | private 20 | 21 | attr_reader :device 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /lib/simctl/device_path.rb: -------------------------------------------------------------------------------- 1 | require 'cfpropertylist' 2 | 3 | module SimCtl 4 | class DevicePath 5 | def initialize(device) 6 | @device = device 7 | end 8 | 9 | def device_plist 10 | @device_plist ||= File.join(home, 'device.plist') 11 | end 12 | 13 | def global_preferences_plist 14 | @global_preferences_plist ||= File.join(home, 'data/Library/Preferences/.GlobalPreferences.plist') 15 | end 16 | 17 | def home 18 | @home ||= File.join(device_set_path, device.udid) 19 | end 20 | 21 | def launchctl 22 | @launchctl ||= File.join(runtime_root, 'bin/launchctl') 23 | end 24 | 25 | def preferences_plist 26 | @preferences_plist ||= File.join(home, 'data/Library/Preferences/com.apple.Preferences.plist') 27 | end 28 | 29 | private 30 | 31 | attr_reader :device 32 | 33 | def device_set_path 34 | return SimCtl.device_set_path unless SimCtl.device_set_path.nil? 35 | File.join(ENV['HOME'], 'Library/Developer/CoreSimulator/Devices') 36 | end 37 | 38 | def locate_runtime_root 39 | runtime_identifier = device.runtime.identifier 40 | 41 | [ 42 | Xcode::Path.runtime_profiles, 43 | '/Library/Developer/CoreSimulator/Profiles/Runtimes/' 44 | ].each do |parent_dir| 45 | Dir.glob(File.join(File.expand_path(parent_dir), '*')).each do |dir| 46 | plist_path = File.join(dir, 'Contents/Info.plist') 47 | next unless File.exist?(plist_path) 48 | info = CFPropertyList.native_types(CFPropertyList::List.new(file: plist_path).value) 49 | next unless info.is_a?(Hash) && (info['CFBundleIdentifier'] == runtime_identifier) 50 | root_path = File.join(dir, 'Contents/Resources/RuntimeRoot') 51 | return root_path if File.exist?(root_path) 52 | end 53 | end 54 | 55 | Xcode::Path.sdk_root 56 | end 57 | 58 | def runtime_root 59 | @runtime_root ||= locate_runtime_root 60 | end 61 | end 62 | end 63 | -------------------------------------------------------------------------------- /lib/simctl/device_settings.rb: -------------------------------------------------------------------------------- 1 | require 'cfpropertylist' 2 | 3 | module SimCtl 4 | class DeviceSettings 5 | attr_reader :path 6 | 7 | def initialize(path) 8 | @path = path 9 | end 10 | 11 | # Disables the keyboard helpers 12 | # 13 | # @return [void] 14 | def disable_keyboard_helpers 15 | edit_plist(path.preferences_plist) do |plist| 16 | plist['DidShowContinuousPathIntroduction'] = true 17 | %w[ 18 | KeyboardAllowPaddle 19 | KeyboardAssistant 20 | KeyboardAutocapitalization 21 | KeyboardAutocorrection 22 | KeyboardCapsLock 23 | KeyboardCheckSpelling 24 | KeyboardPeriodShortcut 25 | KeyboardPrediction 26 | KeyboardShowPredictionBar 27 | ].each do |key| 28 | plist[key] = false 29 | end 30 | end 31 | end 32 | 33 | # Updates hardware keyboard settings 34 | # 35 | # @param enabled value to replace 36 | # @return [vod] 37 | def update_hardware_keyboard(enabled) 38 | edit_plist(path.preferences_plist) do |plist| 39 | plist['AutomaticMinimizationEnabled'] = enabled 40 | end 41 | end 42 | 43 | def edit_plist(path) 44 | plist = File.exist?(path) ? CFPropertyList::List.new(file: path) : CFPropertyList::List.new 45 | content = CFPropertyList.native_types(plist.value) || {} 46 | yield content 47 | plist.value = CFPropertyList.guess(content) 48 | plist.save(path, CFPropertyList::List::FORMAT_BINARY) 49 | end 50 | 51 | # Sets the device language 52 | # 53 | # @return [void] 54 | def set_language(language) 55 | edit_plist(path.global_preferences_plist) do |plist| 56 | key = 'AppleLanguages' 57 | plist[key] = [] unless plist.key?(key) 58 | plist[key].unshift(language).uniq! 59 | end 60 | end 61 | 62 | # Sets the device locale 63 | # 64 | # @return [void] 65 | def set_locale(locale) 66 | edit_plist(path.global_preferences_plist) do |plist| 67 | plist['AppleLocale'] = locale 68 | end 69 | end 70 | end 71 | end 72 | -------------------------------------------------------------------------------- /lib/simctl/device_type.rb: -------------------------------------------------------------------------------- 1 | require 'simctl/object' 2 | 3 | module SimCtl 4 | class DeviceType < Object 5 | attr_reader :identifier, :name 6 | 7 | def ==(other) 8 | return false if other.nil? 9 | return false unless other.is_a? DeviceType 10 | other.identifier == identifier 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /lib/simctl/executor.rb: -------------------------------------------------------------------------------- 1 | require 'json' 2 | require 'open3' 3 | 4 | module SimCtl 5 | class Executor 6 | class << self 7 | def execute(command) 8 | command = command.flatten.join(' ') 9 | $stderr.puts command if ENV['SIMCTL_DEBUG'] 10 | Open3.popen3(command) do |_stdin, stdout, stderr, result| 11 | output = stdout.read 12 | if result.value.to_i > 0 13 | output = stderr.read if output.empty? 14 | raise output 15 | end 16 | return unless block_given? 17 | if looks_like_json?(output) 18 | yield JSON.parse(output) 19 | else 20 | yield output.chomp 21 | end 22 | end 23 | end 24 | 25 | private 26 | 27 | def looks_like_json?(output) 28 | output.start_with?('[', '{') 29 | end 30 | end 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /lib/simctl/keychain.rb: -------------------------------------------------------------------------------- 1 | module SimCtl 2 | class Keychain 3 | def initialize(device) 4 | @device = device 5 | end 6 | 7 | # Reset the keychain 8 | # 9 | # @return [void] 10 | def reset 11 | SimCtl.keychain_reset(device) 12 | end 13 | 14 | private 15 | 16 | attr_reader :device 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /lib/simctl/list.rb: -------------------------------------------------------------------------------- 1 | module SimCtl 2 | class List < Array 3 | # Filters an array of objects by a given hash. The keys of 4 | # the hash must be methods implemented by the objects. The 5 | # values of the hash are compared to the values the object 6 | # returns when calling the methods. 7 | # 8 | # @param filter [Hash] the filters that should be applied 9 | # @return [Array] the filtered array. 10 | def where(filter) 11 | return self if filter.nil? 12 | select do |item| 13 | matches = true 14 | filter.each do |key, value| 15 | matches &= case value 16 | when Regexp 17 | item.send(key) =~ value 18 | else 19 | item.send(key) == value 20 | end 21 | end 22 | matches 23 | end 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /lib/simctl/object.rb: -------------------------------------------------------------------------------- 1 | module SimCtl 2 | class Object 3 | def initialize(args) 4 | args.each do |k, v| 5 | instance_variable_set("@#{k}", v) unless v.nil? 6 | end 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /lib/simctl/runtime.rb: -------------------------------------------------------------------------------- 1 | require 'naturally' 2 | require 'simctl/object' 3 | 4 | module SimCtl 5 | class Runtime < Object 6 | extend Gem::Deprecate 7 | 8 | attr_reader :buildversion, :identifier, :is_available, :name, :type, :version 9 | 10 | def initialize(args) 11 | args['is_available'] = args.delete('isAvailable') 12 | super 13 | end 14 | 15 | def availability 16 | is_available 17 | end 18 | deprecate :availability, :is_available, 2020, 8 19 | 20 | def type 21 | @type ||= name.split("\s").first.downcase.to_sym 22 | end 23 | 24 | def ==(other) 25 | return false if other.nil? 26 | return false unless other.is_a? Runtime 27 | other.identifier == identifier 28 | end 29 | 30 | # Returns the latest available runtime 31 | # 32 | # @param name [String] type (ios, watchos, tvos) 33 | # @return [SimCtl::Runtime] the latest available runtime 34 | def self.latest(type) 35 | Naturally.sort_by(SimCtl.list_runtimes.where(identifier: /#{type}/i), :version).last 36 | end 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /lib/simctl/status_bar.rb: -------------------------------------------------------------------------------- 1 | module SimCtl 2 | class StatusBar 3 | def initialize(device) 4 | @device = device 5 | end 6 | 7 | # Clear all status bar overrides 8 | # 9 | # @return [void] 10 | def clear 11 | SimCtl.status_bar_clear(device) 12 | end 13 | 14 | # Set some status bar overrides 15 | # 16 | # @param overrides [SimCtl::StatusBarOverrides] or [Hash] the overrides to apply 17 | # @return [void] 18 | def override(overrides) 19 | SimCtl.status_bar_override(device, overrides) 20 | end 21 | 22 | private 23 | 24 | attr_reader :device 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /lib/simctl/version.rb: -------------------------------------------------------------------------------- 1 | module SimCtl 2 | VERSION = '1.6.10'.freeze 3 | end 4 | -------------------------------------------------------------------------------- /lib/simctl/xcode/path.rb: -------------------------------------------------------------------------------- 1 | module SimCtl 2 | module Xcode 3 | class Path 4 | class << self 5 | def home 6 | @home ||= `xcode-select -p`.chomp 7 | end 8 | 9 | def sdk_root 10 | File.join(home, 'Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk') 11 | end 12 | 13 | def runtime_profiles 14 | if Xcode::Version.gte? '11.0' 15 | File.join(home, 'Platforms/iPhoneOS.platform/Library/Developer/CoreSimulator/Profiles/Runtimes/') 16 | elsif Xcode::Version.gte? '9.0' 17 | File.join(home, 'Platforms/iPhoneOS.platform/Developer/Library/CoreSimulator/Profiles/Runtimes/') 18 | else 19 | File.join(home, 'Platforms/iPhoneSimulator.platform/Developer/Library/CoreSimulator/Profiles/Runtimes/') 20 | end 21 | end 22 | end 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /lib/simctl/xcode/version.rb: -------------------------------------------------------------------------------- 1 | module SimCtl 2 | module Xcode 3 | class Version 4 | class << self 5 | def gte?(version) 6 | @version ||= Gem::Version.new(`xcodebuild -version`.scan(/Xcode (\S+)/).flatten.first) 7 | 8 | @version >= Gem::Version.new(version) 9 | end 10 | end 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /simctl.gemspec: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | 3 | $LOAD_PATH.unshift File.expand_path('../lib', __FILE__) 4 | require 'simctl/version' 5 | 6 | Gem::Specification.new do |s| 7 | s.name = 'simctl' 8 | s.version = SimCtl::VERSION 9 | s.summary = 'Ruby interface to xcrun simctl' 10 | s.description = 'Ruby interface to xcrun simctl' 11 | 12 | s.authors = ['Johannes Plunien'] 13 | s.email = %w[plu@pqpq.de] 14 | s.homepage = 'https://github.com/plu/simctl' 15 | s.licenses = ['MIT'] 16 | 17 | s.files = `git ls-files`.split("\n") 18 | s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n") 19 | s.executables = `git ls-files -- bin/*`.split("\n").map { |f| File.basename(f) } 20 | s.require_paths = ['lib'] 21 | 22 | s.add_development_dependency 'coveralls' 23 | s.add_development_dependency 'irb' 24 | s.add_development_dependency 'rake' 25 | s.add_development_dependency 'rspec' 26 | s.add_development_dependency 'rubocop', '=0.49.1' 27 | 28 | s.add_dependency 'CFPropertyList' 29 | s.add_dependency 'naturally' 30 | end 31 | -------------------------------------------------------------------------------- /spec/SampleApp/.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## Build generated 6 | build/ 7 | DerivedData/ 8 | 9 | ## Various settings 10 | *.pbxuser 11 | !default.pbxuser 12 | *.mode1v3 13 | !default.mode1v3 14 | *.mode2v3 15 | !default.mode2v3 16 | *.perspectivev3 17 | !default.perspectivev3 18 | xcuserdata/ 19 | 20 | ## Other 21 | *.moved-aside 22 | *.xcuserstate 23 | 24 | ## Obj-C/Swift specific 25 | *.hmap 26 | *.ipa 27 | 28 | # CocoaPods 29 | # 30 | # We recommend against adding the Pods directory to your .gitignore. However 31 | # you should judge for yourself, the pros and cons are mentioned at: 32 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 33 | # 34 | # Pods/ 35 | 36 | # Carthage 37 | # 38 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 39 | # Carthage/Checkouts 40 | 41 | Carthage/Build 42 | 43 | # fastlane 44 | # 45 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 46 | # screenshots whenever they are needed. 47 | # For more information about the recommended setup visit: 48 | # https://github.com/fastlane/fastlane/blob/master/fastlane/docs/Gitignore.md 49 | 50 | fastlane/report.xml 51 | fastlane/screenshots 52 | -------------------------------------------------------------------------------- /spec/SampleApp/SampleApp.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 05185E8C1CC73EFA00987485 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 05185E8B1CC73EFA00987485 /* main.m */; }; 11 | 05185E8F1CC73EFA00987485 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 05185E8E1CC73EFA00987485 /* AppDelegate.m */; }; 12 | /* End PBXBuildFile section */ 13 | 14 | /* Begin PBXFileReference section */ 15 | 05185E871CC73EFA00987485 /* SampleApp.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = SampleApp.app; sourceTree = BUILT_PRODUCTS_DIR; }; 16 | 05185E8B1CC73EFA00987485 /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; 17 | 05185E8D1CC73EFA00987485 /* AppDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; }; 18 | 05185E8E1CC73EFA00987485 /* AppDelegate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; }; 19 | 05185E9B1CC73EFA00987485 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 20 | /* End PBXFileReference section */ 21 | 22 | /* Begin PBXFrameworksBuildPhase section */ 23 | 05185E841CC73EFA00987485 /* Frameworks */ = { 24 | isa = PBXFrameworksBuildPhase; 25 | buildActionMask = 2147483647; 26 | files = ( 27 | ); 28 | runOnlyForDeploymentPostprocessing = 0; 29 | }; 30 | /* End PBXFrameworksBuildPhase section */ 31 | 32 | /* Begin PBXGroup section */ 33 | 05185E7E1CC73EFA00987485 = { 34 | isa = PBXGroup; 35 | children = ( 36 | 05185E891CC73EFA00987485 /* SampleApp */, 37 | 05185E881CC73EFA00987485 /* Products */, 38 | ); 39 | sourceTree = ""; 40 | }; 41 | 05185E881CC73EFA00987485 /* Products */ = { 42 | isa = PBXGroup; 43 | children = ( 44 | 05185E871CC73EFA00987485 /* SampleApp.app */, 45 | ); 46 | name = Products; 47 | sourceTree = ""; 48 | }; 49 | 05185E891CC73EFA00987485 /* SampleApp */ = { 50 | isa = PBXGroup; 51 | children = ( 52 | 05185E8D1CC73EFA00987485 /* AppDelegate.h */, 53 | 05185E8E1CC73EFA00987485 /* AppDelegate.m */, 54 | 05185E9B1CC73EFA00987485 /* Info.plist */, 55 | 05185E8A1CC73EFA00987485 /* Supporting Files */, 56 | ); 57 | path = SampleApp; 58 | sourceTree = ""; 59 | }; 60 | 05185E8A1CC73EFA00987485 /* Supporting Files */ = { 61 | isa = PBXGroup; 62 | children = ( 63 | 05185E8B1CC73EFA00987485 /* main.m */, 64 | ); 65 | name = "Supporting Files"; 66 | sourceTree = ""; 67 | }; 68 | /* End PBXGroup section */ 69 | 70 | /* Begin PBXNativeTarget section */ 71 | 05185E861CC73EFA00987485 /* SampleApp */ = { 72 | isa = PBXNativeTarget; 73 | buildConfigurationList = 05185E9E1CC73EFA00987485 /* Build configuration list for PBXNativeTarget "SampleApp" */; 74 | buildPhases = ( 75 | 05185E831CC73EFA00987485 /* Sources */, 76 | 05185E841CC73EFA00987485 /* Frameworks */, 77 | 05185E851CC73EFA00987485 /* Resources */, 78 | ); 79 | buildRules = ( 80 | ); 81 | dependencies = ( 82 | ); 83 | name = SampleApp; 84 | productName = SampleApp; 85 | productReference = 05185E871CC73EFA00987485 /* SampleApp.app */; 86 | productType = "com.apple.product-type.application"; 87 | }; 88 | /* End PBXNativeTarget section */ 89 | 90 | /* Begin PBXProject section */ 91 | 05185E7F1CC73EFA00987485 /* Project object */ = { 92 | isa = PBXProject; 93 | attributes = { 94 | LastUpgradeCheck = 0730; 95 | ORGANIZATIONNAME = "Johannes Plunien"; 96 | TargetAttributes = { 97 | 05185E861CC73EFA00987485 = { 98 | CreatedOnToolsVersion = 7.3; 99 | }; 100 | }; 101 | }; 102 | buildConfigurationList = 05185E821CC73EFA00987485 /* Build configuration list for PBXProject "SampleApp" */; 103 | compatibilityVersion = "Xcode 3.2"; 104 | developmentRegion = English; 105 | hasScannedForEncodings = 0; 106 | knownRegions = ( 107 | en, 108 | Base, 109 | ); 110 | mainGroup = 05185E7E1CC73EFA00987485; 111 | productRefGroup = 05185E881CC73EFA00987485 /* Products */; 112 | projectDirPath = ""; 113 | projectRoot = ""; 114 | targets = ( 115 | 05185E861CC73EFA00987485 /* SampleApp */, 116 | ); 117 | }; 118 | /* End PBXProject section */ 119 | 120 | /* Begin PBXResourcesBuildPhase section */ 121 | 05185E851CC73EFA00987485 /* Resources */ = { 122 | isa = PBXResourcesBuildPhase; 123 | buildActionMask = 2147483647; 124 | files = ( 125 | ); 126 | runOnlyForDeploymentPostprocessing = 0; 127 | }; 128 | /* End PBXResourcesBuildPhase section */ 129 | 130 | /* Begin PBXSourcesBuildPhase section */ 131 | 05185E831CC73EFA00987485 /* Sources */ = { 132 | isa = PBXSourcesBuildPhase; 133 | buildActionMask = 2147483647; 134 | files = ( 135 | 05185E8F1CC73EFA00987485 /* AppDelegate.m in Sources */, 136 | 05185E8C1CC73EFA00987485 /* main.m in Sources */, 137 | ); 138 | runOnlyForDeploymentPostprocessing = 0; 139 | }; 140 | /* End PBXSourcesBuildPhase section */ 141 | 142 | /* Begin XCBuildConfiguration section */ 143 | 05185E9C1CC73EFA00987485 /* Debug */ = { 144 | isa = XCBuildConfiguration; 145 | buildSettings = { 146 | ALWAYS_SEARCH_USER_PATHS = NO; 147 | CLANG_ANALYZER_NONNULL = YES; 148 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 149 | CLANG_CXX_LIBRARY = "libc++"; 150 | CLANG_ENABLE_MODULES = YES; 151 | CLANG_ENABLE_OBJC_ARC = YES; 152 | CLANG_WARN_BOOL_CONVERSION = YES; 153 | CLANG_WARN_CONSTANT_CONVERSION = YES; 154 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 155 | CLANG_WARN_EMPTY_BODY = YES; 156 | CLANG_WARN_ENUM_CONVERSION = YES; 157 | CLANG_WARN_INT_CONVERSION = YES; 158 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 159 | CLANG_WARN_UNREACHABLE_CODE = YES; 160 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 161 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 162 | COPY_PHASE_STRIP = NO; 163 | DEBUG_INFORMATION_FORMAT = dwarf; 164 | ENABLE_STRICT_OBJC_MSGSEND = YES; 165 | ENABLE_TESTABILITY = YES; 166 | GCC_C_LANGUAGE_STANDARD = gnu99; 167 | GCC_DYNAMIC_NO_PIC = NO; 168 | GCC_NO_COMMON_BLOCKS = YES; 169 | GCC_OPTIMIZATION_LEVEL = 0; 170 | GCC_PREPROCESSOR_DEFINITIONS = ( 171 | "DEBUG=1", 172 | "$(inherited)", 173 | ); 174 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 175 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 176 | GCC_WARN_UNDECLARED_SELECTOR = YES; 177 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 178 | GCC_WARN_UNUSED_FUNCTION = YES; 179 | GCC_WARN_UNUSED_VARIABLE = YES; 180 | IPHONEOS_DEPLOYMENT_TARGET = 9.3; 181 | MTL_ENABLE_DEBUG_INFO = YES; 182 | ONLY_ACTIVE_ARCH = YES; 183 | SDKROOT = iphoneos; 184 | }; 185 | name = Debug; 186 | }; 187 | 05185E9D1CC73EFA00987485 /* Release */ = { 188 | isa = XCBuildConfiguration; 189 | buildSettings = { 190 | ALWAYS_SEARCH_USER_PATHS = NO; 191 | CLANG_ANALYZER_NONNULL = YES; 192 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 193 | CLANG_CXX_LIBRARY = "libc++"; 194 | CLANG_ENABLE_MODULES = YES; 195 | CLANG_ENABLE_OBJC_ARC = YES; 196 | CLANG_WARN_BOOL_CONVERSION = YES; 197 | CLANG_WARN_CONSTANT_CONVERSION = YES; 198 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 199 | CLANG_WARN_EMPTY_BODY = YES; 200 | CLANG_WARN_ENUM_CONVERSION = YES; 201 | CLANG_WARN_INT_CONVERSION = YES; 202 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 203 | CLANG_WARN_UNREACHABLE_CODE = YES; 204 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 205 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 206 | COPY_PHASE_STRIP = NO; 207 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 208 | ENABLE_NS_ASSERTIONS = NO; 209 | ENABLE_STRICT_OBJC_MSGSEND = YES; 210 | GCC_C_LANGUAGE_STANDARD = gnu99; 211 | GCC_NO_COMMON_BLOCKS = YES; 212 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 213 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 214 | GCC_WARN_UNDECLARED_SELECTOR = YES; 215 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 216 | GCC_WARN_UNUSED_FUNCTION = YES; 217 | GCC_WARN_UNUSED_VARIABLE = YES; 218 | IPHONEOS_DEPLOYMENT_TARGET = 9.3; 219 | MTL_ENABLE_DEBUG_INFO = NO; 220 | SDKROOT = iphoneos; 221 | VALIDATE_PRODUCT = YES; 222 | }; 223 | name = Release; 224 | }; 225 | 05185E9F1CC73EFA00987485 /* Debug */ = { 226 | isa = XCBuildConfiguration; 227 | buildSettings = { 228 | INFOPLIST_FILE = SampleApp/Info.plist; 229 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 230 | PRODUCT_BUNDLE_IDENTIFIER = com.github.plu.simctl.SampleApp; 231 | PRODUCT_NAME = "$(TARGET_NAME)"; 232 | }; 233 | name = Debug; 234 | }; 235 | 05185EA01CC73EFA00987485 /* Release */ = { 236 | isa = XCBuildConfiguration; 237 | buildSettings = { 238 | INFOPLIST_FILE = SampleApp/Info.plist; 239 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 240 | PRODUCT_BUNDLE_IDENTIFIER = com.github.plu.simctl.SampleApp; 241 | PRODUCT_NAME = "$(TARGET_NAME)"; 242 | }; 243 | name = Release; 244 | }; 245 | /* End XCBuildConfiguration section */ 246 | 247 | /* Begin XCConfigurationList section */ 248 | 05185E821CC73EFA00987485 /* Build configuration list for PBXProject "SampleApp" */ = { 249 | isa = XCConfigurationList; 250 | buildConfigurations = ( 251 | 05185E9C1CC73EFA00987485 /* Debug */, 252 | 05185E9D1CC73EFA00987485 /* Release */, 253 | ); 254 | defaultConfigurationIsVisible = 0; 255 | defaultConfigurationName = Release; 256 | }; 257 | 05185E9E1CC73EFA00987485 /* Build configuration list for PBXNativeTarget "SampleApp" */ = { 258 | isa = XCConfigurationList; 259 | buildConfigurations = ( 260 | 05185E9F1CC73EFA00987485 /* Debug */, 261 | 05185EA01CC73EFA00987485 /* Release */, 262 | ); 263 | defaultConfigurationIsVisible = 0; 264 | defaultConfigurationName = Release; 265 | }; 266 | /* End XCConfigurationList section */ 267 | }; 268 | rootObject = 05185E7F1CC73EFA00987485 /* Project object */; 269 | } 270 | -------------------------------------------------------------------------------- /spec/SampleApp/SampleApp.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /spec/SampleApp/SampleApp/AppDelegate.h: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.h 3 | // SampleApp 4 | // 5 | // Created by Plunien, Johannes on 20/04/16. 6 | // Copyright © 2016 Johannes Plunien. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @interface AppDelegate : UIResponder 12 | 13 | @property (strong, nonatomic) UIWindow *window; 14 | 15 | 16 | @end 17 | 18 | -------------------------------------------------------------------------------- /spec/SampleApp/SampleApp/AppDelegate.m: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.m 3 | // SampleApp 4 | // 5 | // Created by Plunien, Johannes on 20/04/16. 6 | // Copyright © 2016 Johannes Plunien. All rights reserved. 7 | // 8 | 9 | #import "AppDelegate.h" 10 | 11 | @interface AppDelegate () 12 | 13 | @end 14 | 15 | @implementation AppDelegate 16 | 17 | @end 18 | -------------------------------------------------------------------------------- /spec/SampleApp/SampleApp/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | LSRequiresIPhoneOS 24 | 25 | UIMainStoryboardFile 26 | Main 27 | UIRequiredDeviceCapabilities 28 | 29 | armv7 30 | 31 | UISupportedInterfaceOrientations 32 | 33 | UIInterfaceOrientationPortrait 34 | UIInterfaceOrientationLandscapeLeft 35 | UIInterfaceOrientationLandscapeRight 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /spec/SampleApp/SampleApp/main.m: -------------------------------------------------------------------------------- 1 | // 2 | // main.m 3 | // SampleApp 4 | // 5 | // Created by Plunien, Johannes on 20/04/16. 6 | // Copyright © 2016 Johannes Plunien. All rights reserved. 7 | // 8 | 9 | #import 10 | #import "AppDelegate.h" 11 | 12 | int main(int argc, char *argv[]) 13 | { 14 | @autoreleasepool { 15 | return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /spec/simctl/device_interaction_spec.rb: -------------------------------------------------------------------------------- 1 | require 'securerandom' 2 | require 'spec_helper' 3 | 4 | RSpec.describe SimCtl, order: :defined do 5 | before(:all) do 6 | @name = SecureRandom.hex 7 | @devicetype = SimCtl.devicetype(name: 'iPhone 8') 8 | @runtime = SimCtl::Runtime.latest(:ios) 9 | @device = SimCtl.create_device @name, @devicetype, @runtime 10 | @device.wait { |d| d.state == :shutdown } 11 | end 12 | 13 | after(:all) do 14 | with_rescue { @device.kill } 15 | with_rescue { @device.wait { |d| d.state == :shutdown } } 16 | with_rescue { @device.delete } 17 | end 18 | 19 | describe 'creating a device' do 20 | it 'raises exception if devicetype lookup failed' do 21 | expect { SimCtl.create_device @name, 'invalid devicetype', @runtime }.to raise_error SimCtl::DeviceTypeNotFound 22 | end 23 | 24 | it 'raises exception if runtime lookup failed' do 25 | expect { SimCtl.create_device @name, @devicetype, 'invalid runtime' }.to raise_error SimCtl::RuntimeNotFound 26 | end 27 | end 28 | 29 | describe 'device properties' do 30 | it 'is a device' do 31 | expect(@device).to be_kind_of SimCtl::Device 32 | end 33 | 34 | it 'has a name property' do 35 | expect(@device.name).to be == @name 36 | end 37 | 38 | it 'has a devicetype property' do 39 | expect(@device.devicetype).to be == @devicetype 40 | end 41 | 42 | it 'has a runtime property' do 43 | expect(@device.runtime).to be == @runtime 44 | end 45 | 46 | it 'has a availability property' do 47 | expect(@device.is_available).not_to be_nil 48 | end 49 | 50 | it 'has a os property' do 51 | expect(@device.os).not_to be_nil 52 | end 53 | 54 | it 'state is shutdown' do 55 | expect(@device.state).to be == :shutdown 56 | end 57 | 58 | describe '#path' do 59 | before(:all) do 60 | @device.boot 61 | @device.wait { |d| File.exist?(d.path.device_plist) && File.exist?(d.path.global_preferences_plist) } 62 | end 63 | 64 | after(:all) do 65 | @device.shutdown 66 | @device.wait { |d| d.state == :shutdown } 67 | end 68 | 69 | it 'has a device plist' do 70 | expect(File).to exist(@device.path.device_plist) 71 | end 72 | 73 | it 'has a global preferences plist' do 74 | expect(File).to exist(@device.path.global_preferences_plist) 75 | end 76 | 77 | it 'has a home' do 78 | expect(File).to exist(@device.path.home) 79 | end 80 | 81 | it 'has a launchctl' do 82 | expect(File).to exist(@device.path.launchctl) 83 | end 84 | end 85 | end 86 | 87 | describe 'device settings' do 88 | describe 'update hardware keyboard' do 89 | it 'creates the preferences plist' do 90 | File.delete(@device.path.preferences_plist) if File.exist?(@device.path.preferences_plist) 91 | @device.settings.update_hardware_keyboard(false) 92 | expect(File).to exist(@device.path.preferences_plist) 93 | end 94 | end 95 | 96 | describe 'disable keyboard helpers' do 97 | it 'creates the preferences plist' do 98 | File.delete(@device.path.preferences_plist) if File.exist?(@device.path.preferences_plist) 99 | @device.settings.disable_keyboard_helpers 100 | expect(File).to exist(@device.path.preferences_plist) 101 | end 102 | end 103 | 104 | describe 'setting the device language' do 105 | it 'sets the device language' do 106 | @device.settings.set_language('de') 107 | content = plist(@device.path.global_preferences_plist) 108 | expect(content['AppleLanguages']).to include('de') 109 | end 110 | end 111 | 112 | describe 'setting the device locale' do 113 | it 'sets the device locale' do 114 | @device.settings.set_locale('en_DE') 115 | content = plist(@device.path.global_preferences_plist) 116 | expect(content['AppleLocale']).to be == 'en_DE' 117 | end 118 | end 119 | end 120 | 121 | describe 'finding the device' do 122 | it 'finds the device by udid' do 123 | expect(SimCtl.device(udid: @device.udid)).to be == @device 124 | end 125 | 126 | it 'finds the device by name' do 127 | expect(SimCtl.device(name: @device.name)).to be == @device 128 | end 129 | 130 | unless SimCtl.device_set_path.nil? 131 | it 'finds the device by runtime' do 132 | expect(SimCtl.device(runtime: @device.runtime)).to be == @device 133 | end 134 | 135 | it 'finds the device by devicetype' do 136 | expect(SimCtl.device(devicetype: @device.devicetype)).to be == @device 137 | end 138 | 139 | it 'finds the device by all given properties' do 140 | expect(SimCtl.device(udid: @device.udid, name: @device.name, runtime: @device.runtime, devicetype: @device.devicetype)).to be == @device 141 | end 142 | end 143 | end 144 | 145 | describe 'renaming the device' do 146 | it 'renames the device' do 147 | @device.rename('new name') 148 | expect(@device.name).to be == 'new name' 149 | expect(SimCtl.device(udid: @device.udid).name).to be == 'new name' 150 | end 151 | end 152 | 153 | describe 'erasing the device' do 154 | it 'erases the device' do 155 | @device.erase 156 | end 157 | end 158 | 159 | describe 'launching the device' do 160 | it 'launches the device' do 161 | @device.boot 162 | @device.wait { |d| d.state == :booted } 163 | expect(@device.state).to be == :booted 164 | end 165 | 166 | it 'is ready' do 167 | @device.wait(&:ready?) 168 | expect(@device).to be_ready 169 | end 170 | end 171 | 172 | describe 'keychain operations' do 173 | if SimCtl::Xcode::Version.gte?('11.4') 174 | it 'resets the keychain' do 175 | @device.keychain.reset 176 | end 177 | else 178 | it 'raises exception' do 179 | expect { @device.keychain.reset }.to raise_error SimCtl::UnsupportedCommandError 180 | end 181 | end 182 | end 183 | 184 | describe 'overriding status bar values' do 185 | if SimCtl::Xcode::Version.gte?('11.4') 186 | it 'overrides the status bar values' do 187 | @device.status_bar.override SimCtl::StatusBarOverrides.new( 188 | time: '10:45', 189 | dataNetwork: 'lte+', 190 | wifiMode: 'active', 191 | cellularMode: 'active', 192 | batteryState: 'charging', 193 | batteryLevel: 50 194 | ) 195 | end 196 | 197 | it 'overrides the status bar values with a hash' do 198 | @device.status_bar.override( 199 | time: '10:45', 200 | dataNetwork: 'lte+', 201 | wifiMode: 'active', 202 | cellularMode: 'active', 203 | batteryState: 'charging', 204 | batteryLevel: 50 205 | ) 206 | end 207 | 208 | it 'clears the status bar' do 209 | @device.status_bar.clear 210 | end 211 | else 212 | it 'raises exception' do 213 | expect { @device.status_bar.clear }.to raise_error SimCtl::UnsupportedCommandError 214 | expect { @device.status_bar.override(time: '10:45') }.to raise_error SimCtl::UnsupportedCommandError 215 | end 216 | end 217 | end 218 | 219 | describe 'launching a system app' do 220 | it 'launches Safari' do 221 | @device.launch_app('com.apple.mobilesafari') 222 | end 223 | end 224 | 225 | describe 'taking a screenshot' do 226 | if SimCtl::Xcode::Version.gte? '8.2' 227 | it 'takes a screenshot' do 228 | file = File.join(Dir.mktmpdir, 'screenshot.png') 229 | @device.screenshot(file) 230 | expect(File).to exist(file) 231 | end 232 | else 233 | it 'raises exception' do 234 | expect { @device.screenshot('/tmp/foo.png') }.to raise_error SimCtl::UnsupportedCommandError 235 | end 236 | end 237 | end 238 | 239 | describe 'spawning a process' do 240 | it 'spawns launchctl list' do 241 | output = @device.spawn(@device.path.launchctl, ['list']) 242 | expect(output.length).to be > 0 243 | end 244 | end 245 | 246 | describe 'installing an app' do 247 | before(:all) do 248 | system 'cd spec/SampleApp && xcodebuild -sdk iphonesimulator >/dev/null 2>&1' 249 | end 250 | 251 | it 'installs SampleApp' do 252 | @device.install('spec/SampleApp/build/Release-iphonesimulator/SampleApp.app') 253 | end 254 | end 255 | 256 | describe 'launching an app' do 257 | it 'launches SampleApp' do 258 | @device.launch_app('com.github.plu.simctl.SampleApp') 259 | end 260 | end 261 | 262 | describe 'terminating an app' do 263 | if SimCtl::Xcode::Version.gte? '8.2' 264 | it 'terminates SampleApp' do 265 | @device.terminate_app('com.github.plu.simctl.SampleApp') 266 | end 267 | else 268 | it 'raises exception' do 269 | expect { @device.terminate_app('com.github.plu.simctl.SampleApp') }.to raise_error SimCtl::UnsupportedCommandError 270 | end 271 | end 272 | end 273 | 274 | describe 'changing privacy settings' do 275 | if SimCtl::Xcode::Version.gte?('11.4') 276 | it 'resets all privacy settings' do 277 | @device.privacy('reset', 'all', 'com.github.plu.simctl.SampleApp') 278 | end 279 | else 280 | it 'raises exception' do 281 | expect { @device.privacy('reset', 'all', 'com.github.plu.simctl.SampleApp') }.to raise_error SimCtl::UnsupportedCommandError 282 | end 283 | end 284 | end 285 | 286 | describe 'uninstall an app' do 287 | it 'uninstalls SampleApp' do 288 | @device.uninstall('com.github.plu.simctl.SampleApp') 289 | end 290 | end 291 | 292 | describe 'opening a url' do 293 | it 'opens some url' do 294 | @device.open_url('https://www.github.com') 295 | end 296 | end 297 | 298 | describe 'shutdown the device' do 299 | it 'state is booted' do 300 | expect(@device.state).to be == :booted 301 | end 302 | 303 | it 'shuts down the device' do 304 | @device.shutdown 305 | @device.wait { |d| d.state == :shutdown } 306 | end 307 | 308 | it 'state is shutdown' do 309 | expect(@device.state).to be == :shutdown 310 | end 311 | end 312 | 313 | describe 'booting the device' do 314 | it 'state is shutdown' do 315 | expect(@device.state).to be == :shutdown 316 | end 317 | 318 | it 'boots the device' do 319 | @device.boot 320 | @device.wait { |d| d.state == :booted } 321 | expect(@device.state).to be == :booted 322 | end 323 | 324 | it 'state is booted' do 325 | expect(@device.state).to be == :booted 326 | end 327 | 328 | it 'is ready' do 329 | @device.wait(&:ready?) 330 | expect(@device).to be_ready 331 | end 332 | end 333 | 334 | describe 'shutting down the device' do 335 | it 'state is booted' do 336 | expect(@device.state).to be == :booted 337 | end 338 | 339 | it 'shuts down the device' do 340 | @device.shutdown 341 | @device.wait { |d| d.state == :shutdown } 342 | end 343 | 344 | it 'state is shutdown' do 345 | expect(@device.state).to be == :shutdown 346 | end 347 | end 348 | 349 | describe 'resetting the device' do 350 | it 'deletes the old device and creates a new one' do 351 | new_device = @device.reset 352 | expect(new_device.name).to be == @device.name 353 | expect(new_device.devicetype).to be == @device.devicetype 354 | expect(new_device.runtime).to be == @device.runtime 355 | expect(new_device.udid).not_to be == @device.udid 356 | expect(SimCtl.device(udid: @device.udid)).to be_nil 357 | @device = new_device 358 | end 359 | end 360 | 361 | describe 'deleting the device' do 362 | it 'deletes the device' do 363 | device = SimCtl.create_device @name, @devicetype, @runtime 364 | device.delete 365 | device.wait { SimCtl.device(udid: device.udid).nil? } 366 | expect(SimCtl.device(udid: device.udid)).to be_nil 367 | end 368 | end 369 | end 370 | -------------------------------------------------------------------------------- /spec/simctl/executor_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | RSpec.describe SimCtl::Executor do 4 | describe '#execute' do 5 | it 'raises an exception' do 6 | expect { SimCtl::Executor.execute(['xcrun simctl asdf']) }.to raise_error RuntimeError 7 | end 8 | 9 | it 'returns json' do 10 | json = SimCtl::Executor.execute(["echo '{\"foo\":\"bar\"}'"]) do |result| 11 | result 12 | end 13 | expect(json).to eql('foo' => 'bar') 14 | end 15 | 16 | it 'returns a string' do 17 | string = SimCtl::Executor.execute(["echo 'hello world'"]) do |result| 18 | result 19 | end 20 | expect(string).to eql('hello world') 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /spec/simctl/list_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | RSpec.describe SimCtl do 4 | describe '#devicetype' do 5 | it 'find device type by name' do 6 | expect(SimCtl.devicetype(name: 'iPhone 8')).to be_kind_of SimCtl::DeviceType 7 | end 8 | 9 | it 'raise exception if device type is not found' do 10 | expect { SimCtl.devicetype(name: 'iPhone 1') }.to raise_error SimCtl::DeviceTypeNotFound 11 | end 12 | end 13 | 14 | describe '#list_devicetypes' do 15 | it 'contains some devicetypes' do 16 | expect(SimCtl.list_devicetypes.count).to be > 0 17 | end 18 | 19 | it 'is a SimCtl::DeviceType object' do 20 | expect(SimCtl.list_devicetypes.first).to be_kind_of SimCtl::DeviceType 21 | end 22 | 23 | it 'parses identifier property' do 24 | expect(SimCtl.list_devicetypes.first.identifier).not_to be_nil 25 | end 26 | 27 | it 'parses name property' do 28 | expect(SimCtl.list_devicetypes.first.name).not_to be_nil 29 | end 30 | end 31 | 32 | describe '#list_runtimes' do 33 | it 'contains some runtimes' do 34 | expect(SimCtl.list_runtimes.count).to be > 0 35 | end 36 | 37 | it 'is a SimCtl::Runtime object' do 38 | expect(SimCtl.list_runtimes.first).to be_kind_of SimCtl::Runtime 39 | end 40 | 41 | it 'parses availability property' do 42 | expect(SimCtl.list_runtimes.first.is_available).not_to be_nil 43 | end 44 | 45 | it 'parses buildversion property' do 46 | expect(SimCtl.list_runtimes.first.buildversion).not_to be_nil 47 | end 48 | 49 | it 'parses identifier property' do 50 | expect(SimCtl.list_runtimes.first.identifier).not_to be_nil 51 | end 52 | 53 | it 'parses name property' do 54 | expect(SimCtl.list_runtimes.first.name).not_to be_nil 55 | end 56 | 57 | it 'return latest ios runtime' do 58 | expect(SimCtl::Runtime.latest(:ios)).to be_kind_of SimCtl::Runtime 59 | end 60 | 61 | it 'return latest tvos runtime' do 62 | expect(SimCtl::Runtime.latest(:tvos)).to be_kind_of SimCtl::Runtime 63 | end 64 | 65 | it 'return latest watchos runtime' do 66 | expect(SimCtl::Runtime.latest(:watchos)).to be_kind_of SimCtl::Runtime 67 | end 68 | end 69 | 70 | describe '#runtime' do 71 | it 'find runtime by name' do 72 | expect(SimCtl.runtime(name: 'iOS 15.2')).to be_kind_of SimCtl::Runtime 73 | end 74 | 75 | it 'raise exception if runtime is not found' do 76 | expect { SimCtl.runtime(name: 'iOS 19.0') }.to raise_error SimCtl::RuntimeNotFound 77 | end 78 | 79 | it 'finds the latest runtime' do 80 | if SimCtl::Xcode::Version.gte?('13.2') 81 | expect(SimCtl::Runtime.latest(:ios).version).to be == '15.2' 82 | elsif SimCtl::Xcode::Version.gte?('12.5') 83 | expect(SimCtl::Runtime.latest(:ios).version).to be == '14.5' 84 | elsif SimCtl::Xcode::Version.gte?('11.4') 85 | expect(SimCtl::Runtime.latest(:ios).version).to be == '13.4' 86 | elsif SimCtl::Xcode::Version.gte?('11.3') 87 | expect(SimCtl::Runtime.latest(:ios).version).to be == '13.3' 88 | elsif SimCtl::Xcode::Version.gte?('11.2') 89 | expect(SimCtl::Runtime.latest(:ios).version).to be == '13.2.2' 90 | elsif SimCtl::Xcode::Version.gte?('10.3') 91 | expect(SimCtl::Runtime.latest(:ios).version).to be == '12.4' 92 | elsif SimCtl::Xcode::Version.gte?('9.0') 93 | expect(SimCtl::Runtime.latest(:ios).version).to be == '11.0' 94 | elsif SimCtl::Xcode::Version.gte?('8.3') 95 | expect(SimCtl::Runtime.latest(:ios).version).to be == '10.3.1' 96 | elsif SimCtl::Xcode::Version.gte?('8.2') 97 | expect(SimCtl::Runtime.latest(:ios).version).to be == '10.2' 98 | elsif SimCtl::Xcode::Version.gte?('8.1') 99 | expect(SimCtl::Runtime.latest(:ios).version).to be == '10.1' 100 | elsif SimCtl::Xcode::Version.gte?('8.0') 101 | expect(SimCtl::Runtime.latest(:ios).version).to be == '10.0' 102 | elsif SimCtl::Xcode::Version.gte?('7.3') 103 | expect(SimCtl::Runtime.latest(:ios).version).to be == '9.3' 104 | end 105 | end 106 | end 107 | 108 | describe 'unknown method' do 109 | it 'raise an exception' do 110 | expect { SimCtl.foo }.to raise_error NoMethodError 111 | end 112 | end 113 | end 114 | -------------------------------------------------------------------------------- /spec/simctl/readme_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | RSpec.describe SimCtl do 4 | it 'executes example code from readme' do 5 | # Select the iOS 15.2 runtime 6 | runtime = SimCtl.runtime(name: 'iOS 15.2') 7 | 8 | # Select the iPhone 8 device type 9 | devicetype = SimCtl.devicetype(name: 'iPhone 8') 10 | 11 | # Create a new device 12 | device = SimCtl.create_device 'Unit Tests @ iPhone 8 - 15.2', devicetype, runtime 13 | 14 | # Boot the device 15 | device.boot 16 | 17 | # Launch a new Simulator.app instance 18 | device.launch 19 | 20 | # Wait for the device to be booted 21 | device.wait { |d| d.state == :booted } 22 | 23 | # Kill the Simulator.app instance again 24 | device.shutdown 25 | device.kill 26 | 27 | # Wait until it did shutdown 28 | device.wait { |d| d.state == :shutdown } 29 | 30 | # Delete the device 31 | device.delete 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /spec/simctl/upgrade_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | RSpec.describe SimCtl do 4 | describe '#upgrade' do 5 | it 'upgrades a device to a newer runtime' do 6 | old_runtime = SimCtl.runtime(name: 'iOS 15.2') 7 | new_runtime = SimCtl.runtime(name: 'iOS 15.2') 8 | device = SimCtl.reset_device 'iPhone 8', SimCtl.devicetype(name: 'iPhone 8'), old_runtime 9 | expect(device.runtime.version).to be == old_runtime.version 10 | SimCtl.upgrade device, new_runtime 11 | device.wait { |d| d.runtime.version == new_runtime.version } 12 | device.delete 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /spec/simctl/warmup_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | RSpec.describe SimCtl do 4 | describe '#warmup' do 5 | it 'warms up and returns a device for given strings' do 6 | SimCtl.reset_device 'iPhone 8', SimCtl.devicetype(name: 'iPhone 8'), SimCtl.runtime(name: 'iOS 15.2') 7 | expect(SimCtl.warmup('iPhone 8', 'iOS 15.2')).to be_kind_of SimCtl::Device 8 | end 9 | 10 | it 'warms up and returns a device for given objects' do 11 | devicetype = SimCtl.devicetype(name: 'iPhone 8') 12 | runtime = SimCtl::Runtime.latest(:ios) 13 | SimCtl.reset_device 'iPhone 8', devicetype, runtime 14 | expect(SimCtl.warmup(devicetype, runtime)).to be_kind_of SimCtl::Device 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require 'cfpropertylist' 2 | require 'coveralls' 3 | require 'simplecov' 4 | SimpleCov.start do 5 | add_filter 'spec' 6 | end 7 | Coveralls.wear! 8 | 9 | $LOAD_PATH.push File.expand_path('../../lib', __FILE__) 10 | require File.dirname(__FILE__) + '/../lib/simctl.rb' 11 | 12 | SimCtl.default_timeout = 300 13 | SimCtl.device_set_path = Dir.mktmpdir 'foo bar' if ENV['CUSTOM_DEVICE_SET_PATH'] 14 | 15 | RSpec.configure do |config| 16 | config.tty = true 17 | 18 | config.expect_with :rspec do |c| 19 | c.syntax = :expect 20 | end 21 | 22 | config.mock_with :rspec do |c| 23 | c.syntax = :expect 24 | end 25 | 26 | def with_rescue(&block) 27 | block.class 28 | rescue 29 | end 30 | 31 | def plist(path) 32 | plist = CFPropertyList::List.new(file: path) 33 | CFPropertyList.native_types(plist.value) 34 | end 35 | end 36 | --------------------------------------------------------------------------------