├── .gitignore ├── Gemfile ├── lib ├── cocoapods-xcautotest.rb ├── cocoapods-xcautotest │ ├── command.rb │ ├── gem_version.rb │ ├── command │ │ └── xcautotest.rb │ ├── pod │ │ ├── XCAutoTest.h │ │ ├── XCASocketCommunicator.h │ │ ├── XCAutoTest.podspec.json │ │ ├── xcautotest_server.js │ │ ├── fishhook │ │ │ ├── LICENSE │ │ │ ├── fishhook.h │ │ │ └── fishhook.c │ │ ├── XCASocketCommunicator.m │ │ └── XCAutoTest.m │ └── installer │ │ └── installer.rb └── cocoapods_plugin.rb ├── Rakefile ├── spec ├── command │ └── xcautotest_spec.rb └── spec_helper.rb ├── cocoapods-xcautotest.gemspec ├── VISION.md ├── LICENSE.txt └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | pkg 3 | .idea/ 4 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gemspec 4 | -------------------------------------------------------------------------------- /lib/cocoapods-xcautotest.rb: -------------------------------------------------------------------------------- 1 | require 'cocoapods-xcautotest/gem_version' 2 | -------------------------------------------------------------------------------- /lib/cocoapods-xcautotest/command.rb: -------------------------------------------------------------------------------- 1 | require 'cocoapods-xcautotest/command/xcautotest' 2 | -------------------------------------------------------------------------------- /lib/cocoapods-xcautotest/gem_version.rb: -------------------------------------------------------------------------------- 1 | module CocoapodsXcautotest 2 | VERSION = "0.0.1" 3 | end 4 | -------------------------------------------------------------------------------- /lib/cocoapods_plugin.rb: -------------------------------------------------------------------------------- 1 | require 'cocoapods-xcautotest/command' 2 | require 'cocoapods-xcautotest/installer/installer' 3 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'bundler/gem_tasks' 2 | 3 | def specs(dir) 4 | FileList["spec/#{dir}/*_spec.rb"].shuffle.join(' ') 5 | end 6 | 7 | desc 'Runs all the specs' 8 | task :specs do 9 | sh "bundle exec bacon #{specs('**')}" 10 | end 11 | 12 | task :default => :specs 13 | 14 | -------------------------------------------------------------------------------- /spec/command/xcautotest_spec.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path('../../spec_helper', __FILE__) 2 | 3 | module Pod 4 | describe Command::Xcautotest do 5 | describe 'CLAide' do 6 | it 'registers it self' do 7 | Command.parse(%w{ xcautotest }).should.be.instance_of Command::Xcautotest 8 | end 9 | end 10 | end 11 | end 12 | 13 | -------------------------------------------------------------------------------- /lib/cocoapods-xcautotest/command/xcautotest.rb: -------------------------------------------------------------------------------- 1 | module Pod 2 | class Command 3 | class XCAutotest < Command 4 | self.summary = 'Short description of cocoapods-xcautotest.' 5 | 6 | self.description = <<-DESC 7 | Longer description of cocoapods-xcautotest. 8 | DESC 9 | 10 | self.command = 'autotest' 11 | 12 | def run 13 | # start the websocket server 14 | 15 | end 16 | end 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /lib/cocoapods-xcautotest/pod/XCAutoTest.h: -------------------------------------------------------------------------------- 1 | /// The class that represents an XCAutoTest client. 2 | 3 | @interface XCAutoTest: NSObject 4 | 5 | /// As this needs to talk to global c-functions, this is the 6 | /// best way to pass along a single instance. 7 | + (XCAutoTest *)sharedDriver; 8 | 9 | /// Starts a runloop to continue execution until the server has closed, 10 | /// or a developer does a `cmd + r` or `cmd + u` 11 | - (void)startRunloop; 12 | 13 | /// Starts up the Web Socket between the Auto Test server, and 14 | /// this client. 15 | - (void)connect; 16 | 17 | @end 18 | -------------------------------------------------------------------------------- /lib/cocoapods-xcautotest/pod/XCASocketCommunicator.h: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | @protocol XCASocketDelegate 4 | 5 | - (void)socketHasChangedConnected:(BOOL)connected; 6 | 7 | @end 8 | 9 | @interface XCASocketCommunicator : NSObject 10 | 11 | /// Is the socket connected to a local Auto Test server? 12 | @property BOOL connected; 13 | 14 | /// Is the socket still connecting to the Auto Test server? 15 | @property BOOL connecting; 16 | 17 | 18 | @property (weak) id delegate; 19 | 20 | /// Starts the socket connection 21 | - (void)connect; 22 | 23 | @end 24 | 25 | -------------------------------------------------------------------------------- /lib/cocoapods-xcautotest/pod/XCAutoTest.podspec.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "XCAutoTest", 3 | "version": "1.0.0", 4 | "summary": "Injected podspec used by XCAutoTest plugin.", 5 | "description": "This is intended to be used as an injected podspec template", 6 | "homepage": "https://github.com/orta/cocoapods-xcautotest", 7 | "license": { 8 | "type": "MIT", 9 | "text": "MIT LICENSE Found in the repo" 10 | }, 11 | "authors": { 12 | "Orta Therox": "orta.therox@gmail.com" 13 | }, 14 | "source": { 15 | "git": "https://github.com/orta/cocoapods-xcautotest.git", 16 | "tag": "23" 17 | }, 18 | "source_files": ["*.{h,m}", "fishhook/*.{h,c}"], 19 | "resources": "*.js", 20 | "frameworks": "Foundation", 21 | "requires_arc": true 22 | } -------------------------------------------------------------------------------- /lib/cocoapods-xcautotest/pod/xcautotest_server.js: -------------------------------------------------------------------------------- 1 | function setupServer () { 2 | 3 | 4 | try { 5 | socket = new WebSocket("ws://0.0.0.0:1101") 6 | 7 | socket.onopen = function(event){ 8 | var message = { "connected": true } 9 | window.webkit.messageHandlers.host.postMessage(message) 10 | } 11 | 12 | socket.onerror = function(msg){ 13 | var message = { "connected": false } 14 | window.webkit.messageHandlers.host.postMessage(message) 15 | } 16 | 17 | socket.onclose = function (event) { 18 | var message = { "connected": false } 19 | window.webkit.messageHandlers.host.postMessage(message) 20 | } 21 | 22 | } catch (e) { 23 | var message = { "connected": false } 24 | window.webkit.messageHandlers.host.postMessage(message) 25 | } 26 | 27 | } 28 | 29 | // Moves this call to the end of the setup call-stack in JS 30 | setTimeout(setupServer, 1); 31 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require 'pathname' 2 | ROOT = Pathname.new(File.expand_path('../../', __FILE__)) 3 | $:.unshift((ROOT + 'lib').to_s) 4 | $:.unshift((ROOT + 'spec').to_s) 5 | 6 | require 'bundler/setup' 7 | require 'bacon' 8 | require 'mocha-on-bacon' 9 | require 'pretty_bacon' 10 | require 'pathname' 11 | require 'cocoapods' 12 | 13 | Mocha::Configuration.prevent(:stubbing_non_existent_method) 14 | 15 | require 'cocoapods_plugin' 16 | 17 | #-----------------------------------------------------------------------------# 18 | 19 | module Pod 20 | 21 | # Disable the wrapping so the output is deterministic in the tests. 22 | # 23 | UI.disable_wrap = true 24 | 25 | # Redirects the messages to an internal store. 26 | # 27 | module UI 28 | @output = '' 29 | @warnings = '' 30 | 31 | class << self 32 | attr_accessor :output 33 | attr_accessor :warnings 34 | 35 | def puts(message = '') 36 | @output << "#{message}\n" 37 | end 38 | 39 | def warn(message = '', actions = []) 40 | @warnings << "#{message}\n" 41 | end 42 | 43 | def print(message) 44 | @output << message 45 | end 46 | end 47 | end 48 | end 49 | 50 | #-----------------------------------------------------------------------------# 51 | -------------------------------------------------------------------------------- /lib/cocoapods-xcautotest/installer/installer.rb: -------------------------------------------------------------------------------- 1 | require 'fileutils' 2 | 3 | module CocoaPodsXCAutoTest 4 | class Installer 5 | class << self 6 | 7 | # Register for the pre-install hooks to setup & run Keys 8 | Pod::HooksManager.register('cocoapods-xcautotest', :pre_install) do |context, options| 9 | Installer.setup(context.podfile, options) 10 | end 11 | 12 | def setup(podfile, options) 13 | auto_target_names = options['target'] || options['targets'] 14 | auto_target_names = ([] << auto_target_names).flatten 15 | 16 | raise "Need to add 'targets', or 'target' to the plugin options" unless auto_target_names 17 | 18 | installation_root = Pod::Config.instance.installation_root 19 | pod_path = installation_root.+('Pods/XCAutoTest/') 20 | 21 | # move our Pod metadata in to the Pods dir 22 | FileUtils.cp_r Pathname(__dir__) + "../pod", pod_path 23 | 24 | auto_target_names.each do |auto_target_name| 25 | pod_target = podfile.target_definition_list.find do |target| 26 | target.label == "Pods-#{auto_target_name}" 27 | end 28 | 29 | pod_target.store_pod('XCAutoTest', path: pod_path.to_path) if pod_target 30 | end 31 | end 32 | end 33 | end 34 | end -------------------------------------------------------------------------------- /cocoapods-xcautotest.gemspec: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | lib = File.expand_path('../lib', __FILE__) 3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 4 | require 'cocoapods-xcautotest/gem_version.rb' 5 | 6 | Gem::Specification.new do |spec| 7 | spec.name = 'cocoapods-xcautotest' 8 | spec.version = CocoapodsXcautotest::VERSION 9 | spec.authors = ['Orta Therox'] 10 | spec.email = ['orta.therox@gmail.com'] 11 | spec.description = %q{A short description of cocoapods-xcautotest.} 12 | spec.summary = %q{A longer description of cocoapods-xcautotest.} 13 | spec.homepage = 'https://github.com/orta/cocoapods-xcautotest' 14 | spec.license = 'MIT' 15 | 16 | spec.files = `git ls-files`.split($/) 17 | spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) } 18 | spec.test_files = spec.files.grep(%r{^(test|spec|features)/}) 19 | spec.require_paths = ['lib'] 20 | 21 | spec.add_dependency 'faye-websocket', '~> 0.10.4' 22 | 23 | spec.add_development_dependency 'cocoapods', '~> 1.0', '>= 1.0.1' 24 | spec.add_development_dependency 'bundler', '~> 1.3' 25 | spec.add_development_dependency 'rake' 26 | spec.add_development_dependency 'mocha', '~> 1.1' 27 | spec.add_development_dependency 'bacon', '~> 1.2' 28 | spec.add_development_dependency 'mocha-on-bacon', '~> 0.2.2' 29 | spec.add_development_dependency 'prettybacon', '~> 0.0.2' 30 | end 31 | -------------------------------------------------------------------------------- /lib/cocoapods-xcautotest/pod/fishhook/LICENSE: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2013, Facebook, Inc. 2 | // All rights reserved. 3 | // Redistribution and use in source and binary forms, with or without 4 | // modification, are permitted provided that the following conditions are met: 5 | // * Redistributions of source code must retain the above copyright notice, 6 | // this list of conditions and the following disclaimer. 7 | // * Redistributions in binary form must reproduce the above copyright notice, 8 | // this list of conditions and the following disclaimer in the documentation 9 | // and/or other materials provided with the distribution. 10 | // * Neither the name Facebook nor the names of its contributors may be used to 11 | // endorse or promote products derived from this software without specific 12 | // prior written permission. 13 | // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 14 | // AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 15 | // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 16 | // DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 17 | // FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 18 | // DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 19 | // SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 20 | // CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 21 | // OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 22 | // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /VISION.md: -------------------------------------------------------------------------------- 1 | ### Testing can get harder to do as you grow. 2 | 3 | This project aims to address that flaw, by using the Objective-C runtime in your favour. It's inspired by the idea that code can be injected at runtime, and you can iterate significantly faster inside your application. 4 | 5 | ## v1.0.0 6 | 7 | Version one should be able to have a command line app, that would compile new tests as they are saved and run them into the long-running `XCTest` runner. 8 | 9 | ## v2.0.0 10 | 11 | Version two should build off the infrastructure that Facebook has been building [around multi-sim](https://github.com/facebook/FBSimulatorControl#fbsimulatorcontrol). This means that the server can run it's own headless simulator. So you can constantly have tests running when they're saved. 12 | 13 | ## v3.0.0 14 | 15 | Version three should ideally look at being able to inject application changes as well as testing changes. 16 | 17 | Until now, there is an assumption that you are only changing your test suite, but real-world usage is that you would also want to trigger related tests when you press save on any source file. 18 | 19 | This would effectively mean replicating all of Injection for Xcode, and is not something I'd take lightly. 20 | 21 | ## Notes 22 | 23 | Ideally at some point, we can move this off the CocoaPods Plugin infrastructure. This infrastructure is in place so that we can execute code on both the host OS, and the sim. 24 | 25 | Injection for Xcode works around this problem by being an Xcode plugin. I'm not sure that's tenable on the long term. So this project will always be looking for ways in which we can work around this. [Smuggler](https://github.com/johnno1962/Smuggler) looks the most feasible alternative. 26 | -------------------------------------------------------------------------------- /lib/cocoapods-xcautotest/pod/XCASocketCommunicator.m: -------------------------------------------------------------------------------- 1 | #import "XCASocketCommunicator.h" 2 | @import WebKit; 3 | 4 | @interface XCASocketCommunicator() 5 | @property (strong) WKWebView *headlessWebView; 6 | @end 7 | 8 | @implementation XCASocketCommunicator 9 | 10 | - (void)connect 11 | { 12 | self.connecting = true; 13 | 14 | /// So, we don't want to be adding a lot of dependencies, but a lot of the work 15 | /// involved in writing Auto Test is about the communication between client and server. 16 | NSString *path = [[NSBundle bundleForClass:XCASocketCommunicator.class] pathForResource:@"xcautotest_server" ofType:@"js"]; 17 | NSString *javascript = [NSString stringWithContentsOfFile:path encoding:NSUTF8StringEncoding error:nil]; 18 | 19 | /// By using a WKWebView, we can use their web-socket to sanely connect to a web socket 20 | /// generated by the command-line tool. This reduces overhead, but also can avoid 21 | /// issues with: 22 | /// XPC's private iOS API usage, 23 | /// Raw Socket's hard to follow code, 24 | /// and REST's ability to bypass ATS. 25 | /// Credit goes to Mike Lazer-Walker for this one, was not an idea I would have come to. 26 | WKWebViewConfiguration *configuration = [[WKWebViewConfiguration alloc] init]; 27 | WKUserContentController *controller = [[WKUserContentController alloc] init]; 28 | 29 | [controller addScriptMessageHandler:self name:@"host"]; 30 | configuration.userContentController = controller; 31 | 32 | self.headlessWebView = [[WKWebView alloc] initWithFrame:CGRectMake(0, 0, 10, 10) configuration:configuration]; 33 | [self.headlessWebView evaluateJavaScript:javascript completionHandler:^(id _Nullable response, NSError * _Nullable error) { 34 | if (error) { NSLog(@"Error in parsing script:\n%@", error.localizedDescription); } 35 | }]; 36 | } 37 | 38 | - (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message 39 | { 40 | self.connecting = false; 41 | 42 | BOOL connected = [message.body[@"connected"] boolValue]; 43 | if (self.connected != connected) { 44 | [self.delegate socketHasChangedConnected:connected]; 45 | } 46 | 47 | self.connected = connected; 48 | } 49 | 50 | @end 51 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016 Orta Therox 2 | 3 | MIT License 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | This library also vends Facebook's Fishhook - this is it's license, it is also included 25 | in `lib/pod/fishhook`. 26 | 27 | From commit https://github.com/facebook/fishhook/commit/3958ea38abe787a7dba9f3ed17099f4bfa02cf5a 28 | 29 | // Copyright (c) 2013, Facebook, Inc. 30 | // All rights reserved. 31 | // Redistribution and use in source and binary forms, with or without 32 | // modification, are permitted provided that the following conditions are met: 33 | // * Redistributions of source code must retain the above copyright notice, 34 | // this list of conditions and the following disclaimer. 35 | // * Redistributions in binary form must reproduce the above copyright notice, 36 | // this list of conditions and the following disclaimer in the documentation 37 | // and/or other materials provided with the distribution. 38 | // * Neither the name Facebook nor the names of its contributors may be used to 39 | // endorse or promote products derived from this software without specific 40 | // prior written permission. 41 | // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 42 | // AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 43 | // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 44 | // DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 45 | // FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 46 | // DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 47 | // SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 48 | // CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 49 | // OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 50 | // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /lib/cocoapods-xcautotest/pod/fishhook/fishhook.h: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2013, Facebook, Inc. 2 | // All rights reserved. 3 | // Redistribution and use in source and binary forms, with or without 4 | // modification, are permitted provided that the following conditions are met: 5 | // * Redistributions of source code must retain the above copyright notice, 6 | // this list of conditions and the following disclaimer. 7 | // * Redistributions in binary form must reproduce the above copyright notice, 8 | // this list of conditions and the following disclaimer in the documentation 9 | // and/or other materials provided with the distribution. 10 | // * Neither the name Facebook nor the names of its contributors may be used to 11 | // endorse or promote products derived from this software without specific 12 | // prior written permission. 13 | // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 14 | // AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 15 | // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 16 | // DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 17 | // FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 18 | // DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 19 | // SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 20 | // CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 21 | // OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 22 | // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 23 | 24 | #ifndef fishhook_h 25 | #define fishhook_h 26 | 27 | #include 28 | #include 29 | 30 | #ifdef __cplusplus 31 | extern "C" { 32 | #endif //__cplusplus 33 | 34 | /* 35 | * A structure representing a particular intended rebinding from a symbol 36 | * name to its replacement 37 | */ 38 | struct rebinding { 39 | const char *name; 40 | void *replacement; 41 | void **replaced; 42 | }; 43 | 44 | /* 45 | * For each rebinding in rebindings, rebinds references to external, indirect 46 | * symbols with the specified name to instead point at replacement for each 47 | * image in the calling process as well as for all future images that are loaded 48 | * by the process. If rebind_functions is called more than once, the symbols to 49 | * rebind are added to the existing list of rebindings, and if a given symbol 50 | * is rebound more than once, the later rebinding will take precedence. 51 | */ 52 | int rebind_symbols(struct rebinding rebindings[], size_t rebindings_nel); 53 | 54 | /* 55 | * Rebinds as above, but only in the specified image. The header should point 56 | * to the mach-o header, the slide should be the slide offset. Others as above. 57 | */ 58 | int rebind_symbols_image(void *header, 59 | intptr_t slide, 60 | struct rebinding rebindings[], 61 | size_t rebindings_nel); 62 | 63 | #ifdef __cplusplus 64 | } 65 | #endif //__cplusplus 66 | 67 | #endif //fishhook_h 68 | 69 | -------------------------------------------------------------------------------- /lib/cocoapods-xcautotest/pod/XCAutoTest.m: -------------------------------------------------------------------------------- 1 | #import "XCAutoTest.h" 2 | #import "XCASocketCommunicator.h" 3 | 4 | #import "fishhook.h" 5 | #import 6 | 7 | @interface XCAutoTest() 8 | @property (strong) XCASocketCommunicator *socket; 9 | @end 10 | 11 | 12 | // Keep a reference to the origin exit function 13 | static int (*orig_exit)(int); 14 | 15 | static BOOL connectToAutoServer = YES; 16 | 17 | // Our replacement Exit which starts looking to see if the server is active. 18 | int xc_auto_exit(int code) { 19 | #ifdef DEBUG 20 | 21 | /// Start up the server 22 | XCAutoTest *autoTest = [XCAutoTest sharedDriver]; 23 | if (autoTest.socket.connecting || autoTest.socket.connected) { 24 | /// Keep this thread alive, until `connectToAutoServer` becomes `NO` 25 | [autoTest startRunloop]; 26 | } else { 27 | /// Pass straight on by 28 | orig_exit(code); 29 | } 30 | #endif 31 | } 32 | 33 | @implementation XCAutoTest 34 | #ifdef DEBUG 35 | 36 | /// As the class heirarchy is being set up, in DEBUG mode, hook in our testing infrastructure 37 | 38 | + (void)load 39 | { 40 | static dispatch_once_t pred; 41 | dispatch_once(&pred, ^{ 42 | // Re-bind the `exit` function to a custom c-function we control. 43 | // 44 | // XCTest calls `exit` when it has finished working, as otherwise the app would continue 45 | // to run indefinitely. Thus in re-binding we can halt the closing of the test runner. 46 | rebind_symbols((struct rebinding[1]){{"exit", xc_auto_exit, (void *)&orig_exit}}, 1); 47 | 48 | /// Setup the shared driver. 49 | [self sharedDriver]; 50 | }); 51 | } 52 | 53 | + (XCAutoTest *)sharedDriver 54 | { 55 | static XCAutoTest *_sharedController = nil; 56 | static dispatch_once_t oncePredicate; 57 | dispatch_once(&oncePredicate, ^{ 58 | _sharedController = [[XCAutoTest alloc] init]; 59 | 60 | /// Wait until the app is ready to go before starting to work with 61 | /// any of the web socket work, just feels better than doing it in here. 62 | [[NSNotificationCenter defaultCenter] addObserver:_sharedController 63 | selector:@selector(connect) 64 | name:UIApplicationDidBecomeActiveNotification object:nil]; 65 | 66 | }); 67 | 68 | return _sharedController; 69 | } 70 | 71 | 72 | - (void)startRunloop 73 | { 74 | NSRunLoop *runLoop = [NSRunLoop currentRunLoop]; 75 | [runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode]; 76 | while (connectToAutoServer && [runLoop runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]]); 77 | 78 | // Pass through to the original exit function 79 | orig_exit(1); 80 | } 81 | 82 | - (void)connect 83 | { 84 | [[NSNotificationCenter defaultCenter] removeObserver:self]; 85 | self.socket = [[XCASocketCommunicator alloc] init]; 86 | self.socket.delegate = self; 87 | [self.socket connect]; 88 | } 89 | 90 | - (void)socketHasChangedConnected:(BOOL)connected 91 | { 92 | connectToAutoServer = connected; 93 | } 94 | 95 | #endif 96 | @end 97 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # cocoapods-xcautotest 2 | 3 | _note_ - this is a README driven project, this is where I want it to be. A lot of the infrastructure is in place. 4 | 5 | **OK, so, here's the problem.** 6 | 7 | You write tests, because you want to ensure a baseline quality. Good on 'ya. You write a lot of tests over time. 8 | 9 | Eventually this becomes it's own problem. Tests take time to compile, they start to really rack up the minutes to run. 10 | 11 | So you think, OK, I can work around this. You use [Xcode Schemes to run only a few tests](http://artsy.github.io/blog/2016/04/06/Testing-Schemes/) each time. It's a bit of work, but you can deal with it. 12 | 13 | Then you do [some work in another language](http://danger.systems/guides/creating_your_first_plugin.html#tests) - with real TDD. You end up being pretty frustrated at waiting tens of seconds for your sim to be running your tests. This isn't how it should be. 14 | 15 | --- 16 | 17 | XCAutoTest is a CocoaPods plugin. It is both a server that runs inside your terminal, and a library that runs inside your project. 18 | 19 | ### Terminal 20 | 21 | The job of this part of the system is to listen for file system changes from your tests. When a file is saved, it will compile those tests into a bundle, then pass that over to the app to run as a testcase. 22 | 23 | ### Library 24 | 25 | The library's job is to see if the server is running at the end of an XCTest run. If the server is running, then it will stop the suite from closing, and listen for new test bundles. This will trigger a test-run for just the compiled tests. 26 | 27 | ## Installation 28 | 29 | $ gem install cocoapods-xcautotest 30 | 31 | ## Usage 32 | 33 | I'm still thinking a bit about how this can be done independent of your entire team. For now, I will be using the CocoaPods plugin infrastructure. If this is a blocker, I'm interested in ways to improve it. 34 | 35 | Add the gem to your `Gemfile`, and add `plugin 'cocoapods-xcautotest'` to your `Podfile`. 36 | 37 | Start up the server: 38 | 39 | $ bundle exec pod xcautotest 40 | 41 | 42 | ## Hacking on the project 43 | 44 | To understand the principals of this project - you should be familiar with how [Injection for Xcode works](http://artsy.github.io/blog/2016/06/29/code-spelunking-injection/). If you want the history on the project, look at [this issue](https://github.com/artsy/mobile/issues/26). 45 | 46 | Then it'd be a good idea to have a brief look over the [VISION.md](VISION.md) to grok the overall plan. 47 | 48 | To get started, clone the repo, install the deps, and run the tests. 49 | 50 | ```ruby 51 | git clone https://github.com/orta/cocoapods-xcautotest.git 52 | cd cocoapods-xcautotest 53 | bundle 54 | bundle exec rake 55 | ``` 56 | 57 | This gets you fully set up, however, you're going to want to have this working inside an iOS project to true make changes. So you'll need to use [a Gemfile](https://guides.cocoapods.org/using/a-gemfile.html) on that project. Your Gemfile should look somewhat like: 58 | 59 | ``` ruby 60 | gem "cocoapods", "~> 1.0" 61 | gem "cocoapods-xcautotest", path: "/path/to/where/this/is/cloned" 62 | ``` 63 | 64 | Then include the plugin reference in your `Podfile`: 65 | 66 | ``` ruby 67 | plugin "cocoapods-xcautotest" 68 | ``` 69 | 70 | Then whenever you `bundle exec pod install` it will use your development version of xcautotest. 71 | -------------------------------------------------------------------------------- /lib/cocoapods-xcautotest/pod/fishhook/fishhook.c: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2013, Facebook, Inc. 2 | // All rights reserved. 3 | // Redistribution and use in source and binary forms, with or without 4 | // modification, are permitted provided that the following conditions are met: 5 | // * Redistributions of source code must retain the above copyright notice, 6 | // this list of conditions and the following disclaimer. 7 | // * Redistributions in binary form must reproduce the above copyright notice, 8 | // this list of conditions and the following disclaimer in the documentation 9 | // and/or other materials provided with the distribution. 10 | // * Neither the name Facebook nor the names of its contributors may be used to 11 | // endorse or promote products derived from this software without specific 12 | // prior written permission. 13 | // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 14 | // AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 15 | // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 16 | // DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 17 | // FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 18 | // DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 19 | // SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 20 | // CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 21 | // OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 22 | // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 23 | 24 | #import "fishhook.h" 25 | 26 | #import 27 | #import 28 | #import 29 | #import 30 | #import 31 | #import 32 | #import 33 | 34 | #ifdef __LP64__ 35 | typedef struct mach_header_64 mach_header_t; 36 | typedef struct segment_command_64 segment_command_t; 37 | typedef struct section_64 section_t; 38 | typedef struct nlist_64 nlist_t; 39 | #define LC_SEGMENT_ARCH_DEPENDENT LC_SEGMENT_64 40 | #else 41 | typedef struct mach_header mach_header_t; 42 | typedef struct segment_command segment_command_t; 43 | typedef struct section section_t; 44 | typedef struct nlist nlist_t; 45 | #define LC_SEGMENT_ARCH_DEPENDENT LC_SEGMENT 46 | #endif 47 | 48 | #ifndef SEG_DATA_CONST 49 | #define SEG_DATA_CONST "__DATA_CONST" 50 | #endif 51 | 52 | struct rebindings_entry { 53 | struct rebinding *rebindings; 54 | size_t rebindings_nel; 55 | struct rebindings_entry *next; 56 | }; 57 | 58 | static struct rebindings_entry *_rebindings_head; 59 | 60 | static int prepend_rebindings(struct rebindings_entry **rebindings_head, 61 | struct rebinding rebindings[], 62 | size_t nel) { 63 | struct rebindings_entry *new_entry = malloc(sizeof(struct rebindings_entry)); 64 | if (!new_entry) { 65 | return -1; 66 | } 67 | new_entry->rebindings = malloc(sizeof(struct rebinding) * nel); 68 | if (!new_entry->rebindings) { 69 | free(new_entry); 70 | return -1; 71 | } 72 | memcpy(new_entry->rebindings, rebindings, sizeof(struct rebinding) * nel); 73 | new_entry->rebindings_nel = nel; 74 | new_entry->next = *rebindings_head; 75 | *rebindings_head = new_entry; 76 | return 0; 77 | } 78 | 79 | static void perform_rebinding_with_section(struct rebindings_entry *rebindings, 80 | section_t *section, 81 | intptr_t slide, 82 | nlist_t *symtab, 83 | char *strtab, 84 | uint32_t *indirect_symtab) { 85 | uint32_t *indirect_symbol_indices = indirect_symtab + section->reserved1; 86 | void **indirect_symbol_bindings = (void **)((uintptr_t)slide + section->addr); 87 | for (uint i = 0; i < section->size / sizeof(void *); i++) { 88 | uint32_t symtab_index = indirect_symbol_indices[i]; 89 | if (symtab_index == INDIRECT_SYMBOL_ABS || symtab_index == INDIRECT_SYMBOL_LOCAL || 90 | symtab_index == (INDIRECT_SYMBOL_LOCAL | INDIRECT_SYMBOL_ABS)) { 91 | continue; 92 | } 93 | uint32_t strtab_offset = symtab[symtab_index].n_un.n_strx; 94 | char *symbol_name = strtab + strtab_offset; 95 | if (strnlen(symbol_name, 2) < 2) { 96 | continue; 97 | } 98 | struct rebindings_entry *cur = rebindings; 99 | while (cur) { 100 | for (uint j = 0; j < cur->rebindings_nel; j++) { 101 | if (strcmp(&symbol_name[1], cur->rebindings[j].name) == 0) { 102 | if (cur->rebindings[j].replaced != NULL && 103 | indirect_symbol_bindings[i] != cur->rebindings[j].replacement) { 104 | *(cur->rebindings[j].replaced) = indirect_symbol_bindings[i]; 105 | } 106 | indirect_symbol_bindings[i] = cur->rebindings[j].replacement; 107 | goto symbol_loop; 108 | } 109 | } 110 | cur = cur->next; 111 | } 112 | symbol_loop:; 113 | } 114 | } 115 | 116 | static void rebind_symbols_for_image(struct rebindings_entry *rebindings, 117 | const struct mach_header *header, 118 | intptr_t slide) { 119 | Dl_info info; 120 | if (dladdr(header, &info) == 0) { 121 | return; 122 | } 123 | 124 | segment_command_t *cur_seg_cmd; 125 | segment_command_t *linkedit_segment = NULL; 126 | struct symtab_command* symtab_cmd = NULL; 127 | struct dysymtab_command* dysymtab_cmd = NULL; 128 | 129 | uintptr_t cur = (uintptr_t)header + sizeof(mach_header_t); 130 | for (uint i = 0; i < header->ncmds; i++, cur += cur_seg_cmd->cmdsize) { 131 | cur_seg_cmd = (segment_command_t *)cur; 132 | if (cur_seg_cmd->cmd == LC_SEGMENT_ARCH_DEPENDENT) { 133 | if (strcmp(cur_seg_cmd->segname, SEG_LINKEDIT) == 0) { 134 | linkedit_segment = cur_seg_cmd; 135 | } 136 | } else if (cur_seg_cmd->cmd == LC_SYMTAB) { 137 | symtab_cmd = (struct symtab_command*)cur_seg_cmd; 138 | } else if (cur_seg_cmd->cmd == LC_DYSYMTAB) { 139 | dysymtab_cmd = (struct dysymtab_command*)cur_seg_cmd; 140 | } 141 | } 142 | 143 | if (!symtab_cmd || !dysymtab_cmd || !linkedit_segment || 144 | !dysymtab_cmd->nindirectsyms) { 145 | return; 146 | } 147 | 148 | // Find base symbol/string table addresses 149 | uintptr_t linkedit_base = (uintptr_t)slide + linkedit_segment->vmaddr - linkedit_segment->fileoff; 150 | nlist_t *symtab = (nlist_t *)(linkedit_base + symtab_cmd->symoff); 151 | char *strtab = (char *)(linkedit_base + symtab_cmd->stroff); 152 | 153 | // Get indirect symbol table (array of uint32_t indices into symbol table) 154 | uint32_t *indirect_symtab = (uint32_t *)(linkedit_base + dysymtab_cmd->indirectsymoff); 155 | 156 | cur = (uintptr_t)header + sizeof(mach_header_t); 157 | for (uint i = 0; i < header->ncmds; i++, cur += cur_seg_cmd->cmdsize) { 158 | cur_seg_cmd = (segment_command_t *)cur; 159 | if (cur_seg_cmd->cmd == LC_SEGMENT_ARCH_DEPENDENT) { 160 | if (strcmp(cur_seg_cmd->segname, SEG_DATA) != 0 && 161 | strcmp(cur_seg_cmd->segname, SEG_DATA_CONST) != 0) { 162 | continue; 163 | } 164 | for (uint j = 0; j < cur_seg_cmd->nsects; j++) { 165 | section_t *sect = 166 | (section_t *)(cur + sizeof(segment_command_t)) + j; 167 | if ((sect->flags & SECTION_TYPE) == S_LAZY_SYMBOL_POINTERS) { 168 | perform_rebinding_with_section(rebindings, sect, slide, symtab, strtab, indirect_symtab); 169 | } 170 | if ((sect->flags & SECTION_TYPE) == S_NON_LAZY_SYMBOL_POINTERS) { 171 | perform_rebinding_with_section(rebindings, sect, slide, symtab, strtab, indirect_symtab); 172 | } 173 | } 174 | } 175 | } 176 | } 177 | 178 | static void _rebind_symbols_for_image(const struct mach_header *header, 179 | intptr_t slide) { 180 | rebind_symbols_for_image(_rebindings_head, header, slide); 181 | } 182 | 183 | int rebind_symbols_image(void *header, 184 | intptr_t slide, 185 | struct rebinding rebindings[], 186 | size_t rebindings_nel) { 187 | struct rebindings_entry *rebindings_head = NULL; 188 | int retval = prepend_rebindings(&rebindings_head, rebindings, rebindings_nel); 189 | rebind_symbols_for_image(rebindings_head, header, slide); 190 | free(rebindings_head); 191 | return retval; 192 | } 193 | 194 | int rebind_symbols(struct rebinding rebindings[], size_t rebindings_nel) { 195 | int retval = prepend_rebindings(&_rebindings_head, rebindings, rebindings_nel); 196 | if (retval < 0) { 197 | return retval; 198 | } 199 | // If this was the first call, register callback for image additions (which is also invoked for 200 | // existing images, otherwise, just run on existing images 201 | if (!_rebindings_head->next) { 202 | _dyld_register_func_for_add_image(_rebind_symbols_for_image); 203 | } else { 204 | uint32_t c = _dyld_image_count(); 205 | for (uint32_t i = 0; i < c; i++) { 206 | _rebind_symbols_for_image(_dyld_get_image_header(i), _dyld_get_image_vmaddr_slide(i)); 207 | } 208 | } 209 | return retval; 210 | } 211 | --------------------------------------------------------------------------------