├── bin └── SimctlCLI ├── docs ├── Overview.png ├── Overview.sketch ├── SimctlExample.gif └── XcodeSwiftPackage.png ├── .github ├── FUNDING.yml ├── workflows │ ├── markdown-link-check.yml │ └── ci-macos.yml ├── ISSUE_TEMPLATE │ ├── FEATURE_REQUEST.md │ └── BUG_REPORT.md └── pull_request_template.md ├── renovate.json ├── Sources ├── SimctlCLI │ ├── main.swift │ ├── Swifter+Extensions.swift │ ├── ListDevices.swift │ ├── StartServer.swift │ ├── Commands.swift │ └── SimctlServer.swift ├── SimctlShared │ └── SimctlShared.swift └── Simctl │ └── SimctlClient.swift ├── CODEOWNERS ├── .gitignore ├── Package.resolved ├── Package.swift ├── LICENSE ├── Makefile ├── .swiftlint.yml ├── CONTRIBUTING.md ├── CODE_OF_CONDUCT.md └── README.md /bin/SimctlCLI: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ctreffs/SwiftSimctl/HEAD/bin/SimctlCLI -------------------------------------------------------------------------------- /docs/Overview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ctreffs/SwiftSimctl/HEAD/docs/Overview.png -------------------------------------------------------------------------------- /docs/Overview.sketch: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ctreffs/SwiftSimctl/HEAD/docs/Overview.sketch -------------------------------------------------------------------------------- /docs/SimctlExample.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ctreffs/SwiftSimctl/HEAD/docs/SimctlExample.gif -------------------------------------------------------------------------------- /docs/XcodeSwiftPackage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ctreffs/SwiftSimctl/HEAD/docs/XcodeSwiftPackage.png -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [ctreffs] 2 | custom: ['https://www.paypal.com/donate?hosted_button_id=GCG3K54SKRALQ'] 3 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | "config:base" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /Sources/SimctlCLI/main.swift: -------------------------------------------------------------------------------- 1 | import ArgumentParser 2 | 3 | struct SimctlCLI: ParsableCommand { 4 | static var configuration = CommandConfiguration( 5 | commandName: "SimctlCLI", 6 | abstract: "Swift client-server tool to call xcrun simctl from your test code.", 7 | subcommands: [ 8 | StartServer.self, 9 | ListDevices.self 10 | ] 11 | ) 12 | } 13 | 14 | SimctlCLI.main() 15 | -------------------------------------------------------------------------------- /.github/workflows/markdown-link-check.yml: -------------------------------------------------------------------------------- 1 | name: Check markdown links 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | workflow_dispatch: 9 | 10 | jobs: 11 | markdown-link-check: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Checkout 15 | uses: actions/checkout@master 16 | - name: markdown-link-check 17 | uses: gaurav-nelson/github-action-markdown-link-check@master 18 | 19 | -------------------------------------------------------------------------------- /CODEOWNERS: -------------------------------------------------------------------------------- 1 | # This file is a list of the people responsible for ensuring that contributions 2 | # to this projected are reviewed, either by themselves or by someone else. 3 | # They are also the gatekeepers for their part of this project, with the final 4 | # word on what goes in or not. 5 | # The code owners file uses a .gitignore-like syntax to specify which parts of 6 | # the codebase is associated with an owner. See 7 | # 8 | # for details. 9 | # The following lines are used by GitHub to automatically recommend reviewers. 10 | # Each line is a file pattern followed by one or more owners. 11 | 12 | * @ctreffs 13 | -------------------------------------------------------------------------------- /Sources/SimctlCLI/Swifter+Extensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Christian Treffs on 18.03.20. 6 | // 7 | 8 | import SimctlShared 9 | import Swifter 10 | 11 | extension HttpRequest { 12 | func headerValue(for key: HeaderFieldKey) -> String? { 13 | self.headers[key.rawValue] 14 | } 15 | 16 | func headerValue(for key: HeaderFieldKey, _ initWith: (String) -> V?) -> V? { 17 | guard let string = headerValue(for: key) else { 18 | return nil 19 | } 20 | 21 | return initWith(string) 22 | } 23 | 24 | func headerValue(for key: HeaderFieldKey) -> V? where V: RawRepresentable, V.RawValue == String { 25 | headerValue(for: key) { V(rawValue: $0) } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | **/xcshareddata/WorkspaceSettings.xcsettings 2 | *.dSYM 3 | *.dSYM.zip 4 | *.hmap 5 | *.ipa 6 | *.mode1v3 7 | *.mode2v3 8 | *.moved-aside 9 | *.pbxuser 10 | *.perspectivev3 11 | *.xccheckout 12 | *.xcodeproj/* 13 | *.xcscmblueprint 14 | *.xcworkspace 15 | ._* 16 | .accio/ 17 | .apdisk 18 | .AppleDB 19 | .AppleDesktop 20 | .AppleDouble 21 | .build/ 22 | .com.apple.timemachine.donotpresent 23 | .DocumentRevisions-V100 24 | .DS_Store 25 | .fseventsd 26 | .LSOverride 27 | .Spotlight-V100 28 | .swiftpm/xcode 29 | .TemporaryItems 30 | .Trashes 31 | .VolumeIcon.icns 32 | /*.gcno 33 | build/ 34 | Carthage/Build 35 | Carthage/Checkouts 36 | Dependencies/ 37 | DerivedData/ 38 | fastlane/Preview.html 39 | fastlane/report.xml 40 | fastlane/screenshots/**/*.png 41 | fastlane/test_output 42 | Icon 43 | iOSInjectionProject/ 44 | Network Trash Folder 45 | Packages/ 46 | playground.xcworkspace 47 | Temporary Items 48 | timeline.xctimeline 49 | xcuserdata/ -------------------------------------------------------------------------------- /Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "object": { 3 | "pins": [ 4 | { 5 | "package": "ShellOut", 6 | "repositoryURL": "https://github.com/JohnSundell/ShellOut.git", 7 | "state": { 8 | "branch": null, 9 | "revision": "e1577acf2b6e90086d01a6d5e2b8efdaae033568", 10 | "version": "2.3.0" 11 | } 12 | }, 13 | { 14 | "package": "swift-argument-parser", 15 | "repositoryURL": "https://github.com/apple/swift-argument-parser", 16 | "state": { 17 | "branch": null, 18 | "revision": "6b2aa2748a7881eebb9f84fb10c01293e15b52ca", 19 | "version": "0.5.0" 20 | } 21 | }, 22 | { 23 | "package": "Swifter", 24 | "repositoryURL": "https://github.com/httpswift/swifter.git", 25 | "state": { 26 | "branch": null, 27 | "revision": "9483a5d459b45c3ffd059f7b55f9638e268632fd", 28 | "version": "1.5.0" 29 | } 30 | } 31 | ] 32 | }, 33 | "version": 1 34 | } 35 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.2 2 | import PackageDescription 3 | 4 | let package = Package( 5 | name: "SwiftSimctl", 6 | platforms: [ 7 | .iOS(.v11), 8 | .tvOS(.v11), 9 | .macOS(.v10_12) 10 | ], 11 | products: [ 12 | .executable(name: "SimctlCLI", targets: ["SimctlCLI"]), 13 | .library(name: "Simctl", targets: ["Simctl"]) 14 | ], 15 | dependencies: [ 16 | .package(name: "swift-argument-parser", url: "https://github.com/apple/swift-argument-parser", from: "1.2.2"), 17 | .package(name: "ShellOut", url: "https://github.com/JohnSundell/ShellOut.git", from: "2.3.0"), 18 | .package(name: "Swifter", url: "https://github.com/httpswift/swifter.git", from: "1.5.0") 19 | ], 20 | targets: [ 21 | .target(name: "SimctlShared"), 22 | .target(name: "SimctlCLI", dependencies: ["SimctlShared", "ShellOut", "Swifter", .product(name: "ArgumentParser", package: "swift-argument-parser")]), 23 | .target(name: "Simctl", dependencies: ["SimctlShared"]) 24 | ], 25 | swiftLanguageVersions: [.v5] 26 | ) 27 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/FEATURE_REQUEST.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: 💡 Feature Request 3 | about: A suggestion for a new feature or idea for this project 4 | labels: enhancement 5 | --- 6 | 7 | 13 | 14 | ### Feature request 15 | 16 | *Replace this paragraph with a description of your proposed feature. 17 | A clear and concise description of what the idea or problem is you want to solve. 18 | Please be sure to describe some concrete use cases for the new feature -- be as specific as possible. 19 | Provide links to existing issues or external references/discussions, if appropriate.* 20 | 21 | ### Describe the solution you'd like 22 | 23 | *A clear and concise description of what you want to happen.* 24 | 25 | ### Describe alternatives you've considered 26 | 27 | *A clear and concise description of any alternative solutions or features you've considered.* 28 | 29 | ### Additional context 30 | 31 | *Add any other context or screenshots about the feature request here.* 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020-2021 Christian Treffs 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /Sources/SimctlCLI/ListDevices.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ListDevices.swift 3 | // 4 | // 5 | // Created by Christian Treffs on 18.03.20. 6 | // 7 | 8 | import ArgumentParser 9 | import Foundation 10 | import ShellOut 11 | import SimctlShared 12 | import Swifter 13 | 14 | public enum ListDevicesError: Swift.Error { 15 | case dataConversionFailed 16 | } 17 | 18 | struct ListDevices: ParsableCommand { 19 | static var configuration = CommandConfiguration(abstract: "List the simulator devices") 20 | 21 | mutating func run() throws { 22 | print("\(listDevices().map { $0.description }.sorted().joined(separator: "\n"))") 23 | } 24 | } 25 | 26 | private func listDevices() -> [SimulatorDevice] { 27 | do { 28 | let devicesJSONString = try shellOut(to: .simctlList(.devices, true)) 29 | guard let devicesData: Data = devicesJSONString.data(using: .utf8) else { 30 | throw ListDevicesError.dataConversionFailed 31 | } 32 | let decoder = JSONDecoder() 33 | let listing = try decoder.decode(SimulatorDeviceListing.self, from: devicesData) 34 | return listing.devices 35 | } catch { 36 | return [] 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /.github/workflows/ci-macos.yml: -------------------------------------------------------------------------------- 1 | name: macOS 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | workflow_dispatch: 9 | 10 | jobs: 11 | macos-build-release: 12 | runs-on: macOS-latest 13 | strategy: 14 | matrix: 15 | xcode: ["14.2", "13.4.1"] 16 | steps: 17 | - name: Checkout 18 | uses: actions/checkout@master 19 | 20 | - name: Select Xcode ${{ matrix.xcode }} 21 | run: sudo xcode-select -s /Applications/Xcode_${{ matrix.xcode }}.app 22 | 23 | - name: Build Release 24 | run: make buildRelease 25 | env: 26 | DEVELOPER_DIR: /Applications/Xcode_${{ matrix.xcode }}.app/Contents/Developer 27 | 28 | - name: Build SimctlCLI Release 29 | run: make buildSimctlCLI 30 | env: 31 | DEVELOPER_DIR: /Applications/Xcode_${{ matrix.xcode }}.app/Contents/Developer 32 | 33 | - name: Upload build artifacts on failure 34 | if: failure() 35 | uses: actions/upload-artifact@v3.1.2 36 | with: 37 | name: build-artifacts-${{ matrix.xcode }}-${{ github.run_id }} 38 | path: | 39 | *.lcov 40 | .build/*.yaml 41 | .build/**/*.a 42 | .build/**/*.so 43 | .build/**/*.dylib 44 | .build/**/*.dSYM 45 | .build/**/*.json 46 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | BINDIR_PREFIX?=/usr/local 2 | SIMCTLCLI_NAME = SimctlCLI 3 | 4 | .PHONY: lint-fix 5 | lint-fix: 6 | swiftlint --fix --format 7 | swiftlint lint --quiet 8 | 9 | .PHONY: buildRelease 10 | buildRelease: 11 | swift build -c release 12 | 13 | .PHONY: buildSimctlCLI 14 | buildSimctlCLI: 15 | @printf "Building SimctlCLI..." 16 | @swift build -Xswiftc -Osize -Xswiftc -whole-module-optimization -c release --product $(SIMCTLCLI_NAME) 17 | @cp "`swift build -c release --product $(SIMCTLCLI_NAME) --show-bin-path`/$(SIMCTLCLI_NAME)" ./bin 18 | @echo "Done" 19 | 20 | .PHONY: cleanBuildSimctlCLI 21 | cleanBuildSimctlCLI: cleanArtifacts buildSimctlCLI 22 | 23 | .PHONY: installSimctlCLI 24 | installSimctlCLI: buildSimctlCLI 25 | @mkdir -p $(BINDIR_PREFIX)/bin 26 | @install `swift build -c release --product $(SIMCTLCLI_NAME) --show-bin-path`/$(SIMCTLCLI_NAME) $(BINDIR_PREFIX)/bin 27 | @echo "Installed $(SIMCTLCLI_NAME) to $(BINDIR_PREFIX)/bin/$(SIMCTLCLI_NAME)" 28 | 29 | .PHONY: uninstallSimctlCLI 30 | uninstallSimctlCLI: 31 | @rm -f $(BINDIR_PREFIX)/bin/$(SIMCTLCLI_NAME) 32 | @echo "Removed $(BINDIR_PREFIX)/bin/$(SIMCTLCLI_NAME)" 33 | 34 | .PHONY: precommit 35 | precommit: lint-fix 36 | 37 | .PHONY: genLinuxTests 38 | genLinuxTests: 39 | swift test --generate-linuxmain 40 | swiftlint --fix --format --path Tests/ 41 | 42 | .PHONY: test 43 | test: genLinuxTests 44 | swift test 45 | 46 | .PHONY: genXcode 47 | genXcode: 48 | swift package generate-xcodeproj --enable-code-coverage --skip-extra-files 49 | 50 | .PHONY: clean 51 | clean: 52 | swift package reset 53 | rm -rdf .swiftpm/xcode 54 | rm -rdf .build/ 55 | rm Package.resolved 56 | rm .DS_Store 57 | 58 | .PHONY: cleanArtifacts 59 | cleanArtifacts: 60 | swift package clean 61 | 62 | # Test links in README 63 | # requires 64 | .PHONY: testReadme 65 | testReadme: 66 | markdown-link-check -p -v ./README.md 67 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | 11 | 12 | ### Description 13 | 14 | *Replace this paragraph with a description of your changes and rationale. 15 | Provide links to an existing issue or external references/discussions, if appropriate.* 16 | 17 | ### Detailed Design 18 | 19 | *Include any additional information about the design here. At minimum, describe a synopsis of any public API additions.* 20 | 21 | ```swift 22 | /// The new feature implemented by this pull request. 23 | public struct Example: Collection { 24 | } 25 | ``` 26 | 27 | ### Documentation 28 | 29 | *How has the new feature been documented? 30 | Have the relevant portions of the guides in the Documentation folder been updated in addition to symbol-level documentation?* 31 | 32 | ### Testing 33 | 34 | *How is the new feature tested? 35 | Please ensure CI is not broken* 36 | 37 | ### Performance 38 | 39 | *How did you verify the new feature performs as expected?* 40 | 41 | ### Source Impact 42 | 43 | *What is the impact of this change on existing users of this package? Does it deprecate or remove any existing API?* 44 | 45 | ### Checklist 46 | 47 | - [ ] I've read the [Contribution Guidelines](https://github.com/ctreffs/SwiftSimctl/blob/master/CONTRIBUTING.md) 48 | - [ ] I've followed the coding style of the rest of the project. 49 | - [ ] I've added tests covering all new code paths my change adds to the project (to the extent possible). 50 | - [ ] I've added benchmarks covering new functionality (if appropriate). 51 | - [ ] I've verified that my change does not break any existing tests or introduce unexpected benchmark regressions. 52 | - [ ] I've updated the documentation (if appropriate). 53 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/BUG_REPORT.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: 🐛 Bug Report 3 | about: Something isn't working as expected, create a report to help us improve 4 | labels: bug 5 | --- 6 | 7 | 17 | 18 | ### Bug Description 19 | 20 | *A clear and concise description of what the bug is. 21 | Replace this paragraph with a short description of the incorrect behavior. 22 | (If this is a regression, please note the last version of the package that exhibited the correct behavior in addition to your current version.)* 23 | 24 | ### Information 25 | 26 | - **Package version:** What tag or branch of this package are you using? e.g. tag `1.2.3` or branch `main` 27 | - **Platform version:** Please tell us the version number of your operating system. e.g. `macOS 11.2.3` or `Ubuntu 20.04` 28 | - **Swift version:** Paste the output of `swift --version` here. 29 | 30 | ### Checklist 31 | 32 | - [ ] If possible, I've reproduced the issue using the `main`/`master` branch of this package. 33 | - [ ] I've searched for existing issues under the issues tab. 34 | - [ ] The bug is reproducible 35 | 36 | ### Steps to Reproduce 37 | 38 | *Steps to reproduce the behavior:* 39 | 40 | 1. Go to '...' 41 | 2. '....' 42 | 43 | *Replace this paragraph with an explanation of how to reproduce the incorrect behavior. 44 | Include a simple code example, if possible.* 45 | 46 | ### Expected behavior 47 | 48 | *A clear and concise description of what you expected to happen. 49 | Describe what you expect to happen.* 50 | 51 | ### Actual behavior 52 | 53 | *Describe or copy/paste the behavior you observe.* 54 | 55 | ### Screenshots 56 | 57 | If applicable, add screenshots to help explain your problem. 58 | 59 | ### Additional context 60 | 61 | *Add any other context about the problem here.* 62 | -------------------------------------------------------------------------------- /.swiftlint.yml: -------------------------------------------------------------------------------- 1 | included: 2 | - Sources 3 | - Tests 4 | excluded: 5 | - docs 6 | - build 7 | - .build 8 | - Tests/*/XCTestManifests.swift 9 | - Tests/*/*/XCTestManifests.swift 10 | - Tests/LinuxMain.swift 11 | - Sources/Demos/*/main.swift 12 | identifier_name: 13 | excluded: 14 | - as 15 | - dt 16 | - dx 17 | - dy 18 | - dz 19 | - i 20 | - id 21 | - j 22 | - s 23 | - t 24 | - to 25 | - u 26 | - up 27 | - v 28 | - w 29 | - x 30 | - y 31 | - z 32 | line_length: 220 33 | number_separator: 34 | minimum_length: 5 35 | opt_in_rules: 36 | #- anyobject_protocol 37 | #- explicit_acl 38 | #- explicit_enum_raw_value 39 | #- explicit_type_interface 40 | #- extension_access_modifier 41 | #- file_header 42 | #- file_name 43 | #- missing_docs 44 | #- multiline_arguments_brackets 45 | #- no_grouping_extension 46 | #- multiline_literal_brackets 47 | - array_init 48 | - attributes 49 | - closure_body_length 50 | - closure_end_indentation 51 | - closure_spacing 52 | - collection_alignment 53 | - conditional_returns_on_newline 54 | - contains_over_first_not_nil 55 | - convenience_type 56 | - custom_rules 57 | - discouraged_object_literal 58 | - discouraged_optional_boolean 59 | - discouraged_optional_collection 60 | - empty_count 61 | - empty_string 62 | - empty_xctest_method 63 | - explicit_init 64 | - explicit_self 65 | - explicit_top_level_acl 66 | - fallthrough 67 | - fatal_error_message 68 | - first_where 69 | - force_unwrapping 70 | - function_default_parameter_at_end 71 | - identical_operands 72 | - implicit_return 73 | - implicitly_unwrapped_optional 74 | - joined_default_parameter 75 | - legacy_random 76 | - let_var_whitespace 77 | - literal_expression_end_indentation 78 | - lower_acl_than_parent 79 | - modifier_order 80 | - multiline_arguments 81 | - multiline_function_chains 82 | - multiline_parameters 83 | #- multiline_parameters_brackets 84 | - nimble_operator 85 | - no_extension_access_modifier 86 | - number_separator 87 | - object_literal 88 | - operator_usage_whitespace 89 | - overridden_super_call 90 | - override_in_extension 91 | - pattern_matching_keywords 92 | - prefixed_toplevel_constant 93 | - private_action 94 | - private_outlet 95 | - prohibited_interface_builder 96 | - prohibited_super_call 97 | - quick_discouraged_call 98 | - quick_discouraged_focused_test 99 | - quick_discouraged_pending_test 100 | - redundant_nil_coalescing 101 | - redundant_type_annotation 102 | - required_enum_case 103 | - single_test_class 104 | - sorted_first_last 105 | - sorted_imports 106 | - static_operator 107 | - strict_fileprivate 108 | - switch_case_on_newline 109 | - toggle_bool 110 | - trailing_closure 111 | #- unavailable_function 112 | - unneeded_parentheses_in_closure_argument 113 | - untyped_error_in_catch 114 | - unused_import 115 | - vertical_parameter_alignment_on_call 116 | - vertical_whitespace_between_cases 117 | - vertical_whitespace_closing_braces 118 | - vertical_whitespace_opening_braces 119 | - yoda_condition -------------------------------------------------------------------------------- /Sources/SimctlCLI/StartServer.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StartServer.swift 3 | // 4 | // 5 | // Created by Christian Treffs on 18.03.20. 6 | // 7 | 8 | import ArgumentParser 9 | import Foundation 10 | import ShellOut 11 | import SimctlShared 12 | import Swifter 13 | 14 | struct StartServer: ParsableCommand { 15 | static var configuration = CommandConfiguration(abstract: "Start the server that will be called by your test code to run the commands") 16 | 17 | @Option(name: .shortAndLong, help: "The port to listen on") 18 | var port: SimctlShared.Port = Port(8080) 19 | 20 | @Flag(name: .shortAndLong, help: "Show the commands received and the responses sent") 21 | var verbose = false 22 | 23 | mutating func run() throws { 24 | let server = SimctlServer() 25 | let v = verbose 26 | 27 | server.onPushNotification { deviceId, bundleId, pushContent -> Result in 28 | runCommand(.simctlPush(to: deviceId, pushContent: pushContent, bundleIdentifier: bundleId), verbose: v) 29 | } 30 | 31 | server.onPrivacy { deviceId, bundleId, action, service -> Result in 32 | runCommand(.simctlPrivacy(action, permissionsFor: service, on: deviceId, bundleIdentifier: bundleId), verbose: v) 33 | } 34 | 35 | server.onRename { deviceId, _, newName -> Result in 36 | runCommand(.simctlRename(device: deviceId, to: newName), verbose: v) 37 | } 38 | 39 | server.onTerminateApp { deviceId, _, appBundleId -> Result in 40 | runCommand( .simctlTerminateApp(device: deviceId, appBundleIdentifier: appBundleId), verbose: v) 41 | } 42 | 43 | server.onErase { deviceId -> Result in 44 | runCommand( .simctlErase(device: deviceId), verbose: v) 45 | } 46 | 47 | server.onSetDeviceAppearance { deviceId, _, appearance -> Result in 48 | runCommand(.simctlSetUI(appearance: appearance, on: deviceId), verbose: v) 49 | } 50 | 51 | server.onTriggerICloudSync { deviceId, _ -> Result in 52 | runCommand(.simctlTriggerICloudSync(device: deviceId), verbose: v) 53 | } 54 | 55 | server.onUninstallApp { deviceId, _, appBundleId -> Result in 56 | runCommand(.simctlUninstallApp(device: deviceId, appBundleIdentifier: appBundleId), verbose: v) 57 | } 58 | 59 | server.onSetStatusBarOverride { deviceId, _, overrides -> Result in 60 | runCommand(.simctlSetStatusBarOverrides(device: deviceId, overrides: overrides), verbose: v) 61 | } 62 | 63 | server.onClearStatusBarOverrides { deviceId, _ -> Result in 64 | runCommand(.simctlClearStatusBarOverrides(device: deviceId), verbose: v) 65 | } 66 | 67 | server.onOpenUrl { deviceId, _, url -> Result in 68 | runCommand(.simctlOpen(url: url, on: deviceId)) 69 | } 70 | 71 | server.onGetAppContainer { deviceId, appBundleId, container -> Result in 72 | runCommand(.simctlGetAppContainer(device: deviceId, appBundleIdentifier: appBundleId, container: container)) 73 | } 74 | 75 | server.startServer(on: port) 76 | } 77 | } 78 | 79 | private func runCommand(_ cmd: ShellOutCommand, verbose: Bool = false) -> Result { 80 | if verbose { 81 | print("Command: \(cmd.string)") 82 | } 83 | do { 84 | let output: String = try shellOut(to: cmd) 85 | if verbose { 86 | print("Success: \(output)") 87 | } 88 | return .success(output) 89 | } catch { 90 | if verbose { 91 | print("Failure: \(error)") 92 | } 93 | return .failure(error) 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # 💁 Contributing to this project 2 | 3 | 4 | > First off, thank you for considering contributing to this project. 5 | > It’s [people like you][ref-contributors] that keep this project alive and make it great! 6 | > Thank you! 🙏💜🎉👍 7 | 8 | The following is a set of **guidelines for contributing** to this project. 9 | Use your best judgment and feel free to propose changes to this document in a pull request. 10 | 11 | **Working on your first Pull Request?** You can learn how from this *free* series [How to Contribute to an Open Source Project on GitHub](https://egghead.io/courses/how-to-contribute-to-an-open-source-project-on-github) 12 | 13 | ### 💡 Your contribution - the sky is the limit 🌈 14 | 15 | This is an open source project and we love to receive contributions from our community — [**you**][ref-contributors]! 16 | 17 | There are many ways to contribute, from writing __tutorials__ or __blog posts__, improving the [__documentation__][ref-documentation], submitting [__bug reports__][ref-issues-new] and [__enhancement__][ref-pull-request-new] or 18 | [__writing code__][ref-pull-request-new] which can be incorporated into the repository itself. 19 | 20 | When contributing to this project, please feel free to discuss the changes and ideas you wish to contribute with the repository owners before making a change by opening a [new issue][ref-issues-new] and add the **feature request** tag to that issue. 21 | 22 | Note that we have a [code of conduct][ref-code-of-conduct], please follow it in all your interactions with the project. 23 | 24 | ### 🐞 You want to report a bug or file an issue? 25 | 26 | 1. Ensure that it was **not already reported** and is being worked on by checking [open issues][ref-issues]. 27 | 2. Create a [new issue][ref-issues-new] with a **clear and descriptive title** 28 | 3. Write a **detailed comment** with as much relevant information as possible including 29 | - *how to reproduce* the bug 30 | - a *code sample* or an *executable test case* demonstrating the expected behavior that is not occurring 31 | - any *files that could help* trace it down (i.e. logs) 32 | 33 | ### 🩹 You wrote a patch that fixes an issue? 34 | 35 | 1. Open a [new pull request (PR)][ref-pull-request-new] with the patch. 36 | 2. Ensure the PR description clearly describes the problem and solution. 37 | 3. Link the relevant **issue** if applicable ([how to link issues in PRs][ref-pull-request-how-to]). 38 | 4. Ensure that [**no tests are failing**][ref-gh-actions] and **coding conventions** are met 39 | 5. Submit the patch and await review. 40 | 41 | ### 🎁 You want to suggest or contribute a new feature? 42 | 43 | That's great, thank you! You rock 🤘 44 | 45 | If you want to dive deep and help out with development on this project, then first get the project [installed locally][ref-readme]. 46 | After that is done we suggest you have a look at tickets in our [issue tracker][ref-issues]. 47 | You can start by looking through the beginner or help-wanted issues: 48 | - [__Good first issues__][ref-issues-first] are issues which should only require a few lines of code, and a test or two. 49 | - [__Help wanted issues__][ref-issues-help] are issues which should be a bit more involved than beginner issues. 50 | These are meant to be a great way to get a smooth start and won't put you in front of the most complex parts of the system. 51 | 52 | If you are up to more challenging tasks with a bigger scope, then there are a set of tickets with a __feature__, __enhancement__ or __improvement__ tag. 53 | These tickets have a general overview and description of the work required to finish. 54 | If you want to start somewhere, this would be a good place to start. 55 | That said, these aren't necessarily the easiest tickets. 56 | 57 | For any new contributions please consider these guidelines: 58 | 59 | 1. Open a [new pull request (PR)][ref-pull-request-new] with a **clear and descriptive title** 60 | 2. Write a **detailed comment** with as much relevant information as possible including: 61 | - What your feature is intended to do? 62 | - How it can be used? 63 | - What alternatives where considered, if any? 64 | - Has this feature impact on performance or stability of the project? 65 | 66 | #### Your contribution responsibilities 67 | 68 | Don't be intimidated by these responsibilities, they are easy to meet if you take your time to develop your feature 😌 69 | 70 | - [x] Create issues for any major changes and enhancements that you wish to make. Discuss things transparently and get community feedback. 71 | - [x] Ensure (cross-)platform compatibility for every change that's accepted. An addition should not reduce the number of platforms that the project supports. 72 | - [x] Ensure **coding conventions** are met. Lint your code with the project's default tools. Project wide commands are available through the [Makefile][ref-makefile] in the repository root. 73 | - [x] Add tests for your feature that prove it's working as expected. Code coverage should not drop below its previous value. 74 | - [x] Ensure none of the existing tests are failing after adding your changes. 75 | - [x] Document your public API code and ensure to add code comments where necessary. 76 | 77 | 78 | ### ⚙️ How to set up the environment 79 | 80 | Please consult the [README][ref-readme] for installation instructions. 81 | 82 | 83 | 84 | [ref-code-of-conduct]: https://github.com/ctreffs/SwiftSimctl/blob/master/CODE_OF_CONDUCT.md 85 | [ref-contributors]: https://github.com/ctreffs/SwiftSimctl/graphs/contributors 86 | [ref-documentation]: https://github.com/ctreffs/SwiftSimctl/wiki 87 | [ref-gh-actions]: https://github.com/ctreffs/SwiftSimctl/actions 88 | [ref-issues-first]: https://github.com/ctreffs/SwiftSimctl/issues?q=is%3Aopen+is%3Aissue+label%3A"good+first+issue" 89 | [ref-issues-help]: https://github.com/ctreffs/SwiftSimctl/issues?q=is%3Aopen+is%3Aissue+label%3A"help+wanted" 90 | [ref-issues-new]: https://github.com/ctreffs/SwiftSimctl/issues/new/choose 91 | [ref-issues]: https://github.com/ctreffs/SwiftSimctl/issues 92 | [ref-pull-request-how-to]: https://docs.github.com/en/github/writing-on-github/autolinked-references-and-urls 93 | [ref-pull-request-new]: https://github.com/ctreffs/SwiftSimctl/compare 94 | [ref-readme]: https://github.com/ctreffs/SwiftSimctl/blob/master/README.md 95 | [ref-makefile]: https://github.com/ctreffs/SwiftSimctl/blob/master/Makefile 96 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement 63 | e.g. via [content abuse report][ref-report-abuse]. 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series 86 | of actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or 93 | permanent ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within 113 | the community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][ref-homepage-cc], 118 | version 2.0, available at 119 | . 120 | 121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 122 | enforcement ladder](https://github.com/mozilla/diversity). 123 | 124 | For answers to common questions about this code of conduct, see the FAQ at 125 | . 126 | Translations are available at 127 | . 128 | 129 | 130 | 131 | [ref-homepage-cc]: https://www.contributor-covenant.org 132 | [ref-report-abuse]: https://docs.github.com/communities/maintaining-your-safety-on-github/reporting-abuse-or-spam#reporting-an-issue-or-pull-request 133 | [ref-gh-coc]: https://docs.github.com/en/communities/setting-up-your-project-for-healthy-contributions/adding-a-code-of-conduct-to-your-project 134 | [ref-gh-abuse]: https://docs.github.com/en/communities/moderating-comments-and-conversations/managing-how-contributors-report-abuse-in-your-organizations-repository 135 | [ref-coc-guide]: https://opensource.guide/code-of-conduct/ 136 | 137 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Swift Simctl 2 | 3 | [![macOS](https://github.com/ctreffs/SwiftSimctl/actions/workflows/ci-macos.yml/badge.svg)](https://github.com/ctreffs/SwiftSimctl/actions/workflows/ci-macos.yml) 4 | [![license](https://img.shields.io/badge/license-MIT-brightgreen.svg)](https://github.com/ctreffs/SwiftSimctl/blob/master/LICENSE) 5 | [![swift-version-compatibility](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2Fctreffs%2FSwiftSimctl%2Fbadge%3Ftype%3Dswift-versions)](https://swiftpackageindex.com/ctreffs/SwiftSimctl) 6 | [![platform-compatibility](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2Fctreffs%2FSwiftSimctl%2Fbadge%3Ftype%3Dplatforms)](https://swiftpackageindex.com/ctreffs/SwiftSimctl) 7 | 8 |

9 | simctl-example-gif 10 |

11 | 12 | 13 | This is a small tool (SimctlCLI) and library (Simctl), written in Swift, to automate [`xcrun simctl`](https://developer.apple.com/library/archive/documentation/IDEs/Conceptual/iOS_Simulator_Guide/InteractingwiththeiOSSimulator/InteractingwiththeiOSSimulator.html#//apple_ref/doc/uid/TP40012848-CH3-SW4) commands for Simulator in unit and UI tests. 14 | 15 | It enables, among other things, reliable **fully automated** testing of Push Notifications with dynamic content, driven by a UI Test you control. 16 | 17 | ### 🚧 Architecture 18 | 19 |

20 | 21 |

22 | 23 | Swift Simctl is made of two parts. `SimctlCLI` and `Simctl`. 24 | 25 | `Simctl` is a Swift library that can be added to your project's test bundles. 26 | It provides an interface to commands that are otherwise only available via `xcrun simctl` from within your test code. 27 | To enable calling these commands `Simctl` communicates over a local network connection to `SimctlCLI`. 28 | 29 | `SimctlCLI` is a small command line tool that starts a local server, listens to requests from `Simctl` (the client library) and executes `xcrun simctl` commands. 30 | 31 | ### ⌨ Available Commands 32 | 33 | The following commands will be available in code in your (test) targets: 34 | 35 | - Send push notifications with custom payload 36 | - Grant or revoke privacy permissions (i.e. camera, photos ...) 37 | - Set the device UI appearance to light or dark mode 38 | - Set status bar overrides (i.e. data network, time ...) 39 | - Uninstall app by bundle id 40 | - Terminate app by bundle id 41 | - Rename device 42 | - Trigger iCloud Sync 43 | - Open URLs including registered URL schemes 44 | - Erase the contents and settings of the simulator 45 | - Get app container 46 | 47 | ## ❔ Why would you (not) use this 48 | 49 | #### ➕ Pro 50 | 51 | - Closed system (Mac with Xcode + Simulator) 52 | - No external dependencies on systems like [APNS](https://developer.apple.com/library/archive/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/APNSOverview.html) 53 | - No custom test code bloating your code base (AppDelegate) unnecessarily 54 | - Push notifications can be simulated properly and the normal app cycle is preserved 55 | - Runs on CI machines 56 | - Your app stays a black box and does not need to be modified 57 | 58 | #### ➖ Contra 59 | 60 | - Needs a little configuration in your Xcode project 61 | - Only available for Xcode 11.4+ 62 | 63 | For specific usage please refer to the example projects **[Swift Simctl Package Example](https://github.com/ctreffs/SwiftSimctlExample)** 64 | 65 | ## 🚀 Getting Started 66 | 67 | These instructions will get your copy of the project up and running on your machine. 68 | 69 | ### 📋 Prerequisites 70 | 71 | - [Xcode 11.4](https://developer.apple.com/documentation/xcode_release_notes/) and higher. 72 | - [Swift Package Manager (SPM)](https://github.com/apple/swift-package-manager) 73 | 74 | ### 💻 Usage 75 | 76 | ### 📦 Swift Package 77 | 78 | To use Swift Simctl in your Xcode project add the package: 79 | 80 | 1. Xcode > File > Swift Packages > Add Package Dependency... 81 | 2. Choose Package Repository > Search: `SwiftSimctl` or find `https://github.com/ctreffs/SwiftSimctl.git` 82 | 3. Select `SwiftSimctl` package > `Next` ![xcode-swift-package](docs/XcodeSwiftPackage.png) 83 | 4. Do not forget to add the dependency to your (test) target 84 | 5. Use `import Simctl` to access the library in your (test) target. 85 | 86 | #### Running the server alongside your tests 87 | 88 | Make sure that for the duration of your test run `SimctlCLI` runs on your host machine. 89 | To automate that with Xcode itself use the following snippets as pre and post action of your test target. 90 | 91 | ###### `Your Scheme` > Test > Pre-Actions > Run Script 92 | 93 | ```sh 94 | #!/bin/bash 95 | killall SimctlCLI # cleaning up hanging servers 96 | set -e # fail fast 97 | # start the server non-blocking from the checked out package 98 | ${BUILD_ROOT}/../../SourcePackages/checkouts/SwiftSimctl/bin/SimctlCLI start-server > /dev/null 2>&1 & 99 | ``` 100 | 101 | ###### `Your Scheme` > Test > Post-Actions > Run Script 102 | 103 | ```sh 104 | #!/bin/bash 105 | set -e 106 | killall SimctlCLI 107 | 108 | ``` 109 | 110 | ###### 📝 Code Example Swift Package 111 | 112 | Please refer to the example project for an in depth code example **** 113 | 114 | ##### 💭 Port and settings 115 | 116 | The default port used by the server is `8080`. 117 | If you need to use another port you need to provide it via the `--port` flag when calling `SimctlCLI` and adjust 118 | the client port accordingly when setting up your test in code. 119 | Use `SimctlCLI --help` to get help regarding this and other server configuration settings. 120 | 121 | 122 | ## 🙏 Kudos 123 | 124 | Swift Simctl would not be possible without these awesome libraries: 125 | 126 | - [ShellOut](https://github.com/JohnSundell/ShellOut) - easy command line invocations 127 | - [Swifter](https://github.com/httpswift/swifter) - a tiny http server 128 | 129 | ## 💁 How to contribute 130 | 131 | If you want to contribute please see the [CONTRIBUTION GUIDE](CONTRIBUTING.md) first. 132 | 133 | Before commiting code please ensure to run: 134 | 135 | - `make precommit` 136 | 137 | This project is currently maintained by [@ctreffs](https://github.com/ctreffs). 138 | See also the list of [contributors](https://github.com/ctreffs/SwiftSimctl/contributors) who participated in this project. 139 | 140 | ## 🔏 Licenses 141 | 142 | This project is licensed under the MIT License - see the [LICENSE](https://github.com/ctreffs/SwiftSimctl/blob/master/LICENSE) file for details. 143 | -------------------------------------------------------------------------------- /Sources/SimctlCLI/Commands.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Commands.swift 3 | // 4 | // 5 | // Created by Christian Treffs on 17.03.20. 6 | // 7 | 8 | import Foundation 9 | import ShellOut 10 | import SimctlShared 11 | 12 | extension ShellOutCommand { 13 | static func openSimulator() -> ShellOutCommand { 14 | .init(string: "open -b com.apple.iphonesimulator") 15 | } 16 | 17 | static func killAllSimulators() -> ShellOutCommand { 18 | .init(string: "killall Simulator") 19 | } 20 | 21 | private static func simctl(_ cmd: String) -> String { 22 | "xcrun simctl \(cmd)" 23 | } 24 | 25 | /// Usage: simctl list [-j | --json] [-v] [devices|devicetypes|runtimes|pairs] [|available] 26 | static func simctlList(_ filter: ListFilterType = .noFilter, _ asJson: Bool = false, _ verbose: Bool = false) -> ShellOutCommand { 27 | let cmd: String = [ 28 | "list", 29 | "\(asJson ? "--json" : "")", 30 | "\(verbose ? "-v" : "")", 31 | filter.rawValue 32 | ].joined(separator: " ") 33 | 34 | return .init(string: simctl(cmd)) 35 | } 36 | 37 | static func simctlBoot(device: UUID) -> ShellOutCommand { 38 | .init(string: simctl("boot \(device.uuidString)")) 39 | } 40 | 41 | static func simctlShutdown(device: UUID) -> ShellOutCommand { 42 | .init(string: simctl("shutdown \(device.uuidString)")) 43 | } 44 | 45 | static func simctlShutdownAllDevices() -> ShellOutCommand { 46 | .init(string: simctl("shutdown all")) 47 | } 48 | 49 | static func simctlOpen(url: URL, on device: UUID) -> ShellOutCommand { 50 | .init(string: simctl("openurl \(device.uuidString) \(url.absoluteString)")) 51 | } 52 | 53 | /// Usage: simctl ui