Skip to content

Commit

Permalink
FIRST
Browse files Browse the repository at this point in the history
  • Loading branch information
enriquez committed Aug 16, 2013
0 parents commit 3a8b164
Show file tree
Hide file tree
Showing 13 changed files with 459 additions and 0 deletions.
17 changes: 17 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
*.gem
*.rbc
.bundle
.config
.yardoc
Gemfile.lock
InstalledFiles
_yardoc
coverage
doc/
lib/bundler/man
pkg
rdoc
spec/reports
test/tmp
test/version_tmp
tmp
4 changes: 4 additions & 0 deletions Gemfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
source 'https://rubygems.org'

# Specify your gem's dependencies in uiauto.gemspec
gemspec
22 changes: 22 additions & 0 deletions LICENSE.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
Copyright (c) 2013 Mike Enriquez

MIT License

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:

The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.

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.
125 changes: 125 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
# UIAuto

UIAuto is a command line tool for running UI Automation scripts. It improves Apple's `instruments` command by assuming reasonable defaults specific to UI Automation.

UIAuto also facilitates the setup of simulator data for running scripts in a repeatable and known state.

Note: UIAuto only works with the simulator for now.

## Prerequisites

* Xcode command line tools

## Installation

$ gem install uiauto

## Usage

First, you need to build your app to run on the simulator. By default, `uiauto` will look for the most recently built app bundle in derived data based on your current working directory. UIAuto also provides commands and can read speical comment headers to setup simulator data. More details below.

### Build your project

UIAuto does not build your app. You can use Xcode or `xcodebuild`.

#### With Xcode

The easiest way to build your app for UIAuto is to use Xcode. Open your app in Xcode, select the simulator, then build your project (command+b). This builds your app and places the resulting bundle in derived data.

#### With xcodebuild

You can build from the command line using `xcodebuild`. The following examples show how to build your iOS 6.1 app for the simulator.

# Example using xcodebuild to build a workspace
$ xcodebuild -workspace MyApp.xcworkspace -scheme MyApp -sdk iphonesimulator6.1

# Example using xcodebuild to build a project
$ xcodebuild -project MyApp.xcodeproj -sdk iphonesimulator6.1

By building with the commands above, the resulting bundle is placed in derived data.

### Run UI Automation scripts

The command to run automation scripts is simplified if the built app is in derived data and the defaults are used. For special use cases, these defaults may be overridden.

#### Default options

In the same directory as your project's .xcworkspace or .xcodeproj run the following.

$ uiauto exec path_to_your_script.js

This will run `path_to_your_script.js` using the app bundle located in derivated data that you just built. The results and trace file are placed in `./uiauto/results` and `./uiauto/results/trace`.

#### Advanced options

Running `uiauto help exec` prints the following message.

Usage:
uiauto exec [FILE_OR_DIRECTORY]

Options:
[--results=RESULTS]

[--trace=TRACE]

[--app=APP]

If you build your app outside of derived data, then you can specify the `--app` flag to tell uiauto where to find the `*.app`. You can also override the default locations for the trace file and results. For example, if your build your app in a build directory you can run the following

uiauto exec uiauto/scripts/script_to_run.js --app=build/MyApp.app

### Simulator data

UIAuto's `simulator` subcommand allows you to setup the simulator's applications, settings, and data. This is done by taking a "snapshot" of the simulator's current data by saving the `~/Library/Application Support/iPhone Simulator/(SDK VERSION)/` directory somewhere, then loading it back in when needed.

This is useful for getting the simulator into a known state before running automation scripts.

#### uiauto simulator command

Running `uiauto simulator` prints the following message

Commands:
uiauto simulator close # Closes the simulator
uiauto simulator help [COMMAND] # Describe subcommands or one specific subcommand
uiauto simulator load DATA # Loads previously saved simulator data
uiauto simulator open # Opens the simulator
uiauto simulator reset # Deletes all applications and settings
uiauto simulator save DATA # Saves simulator data

#### Script comment headers

By placing a special comment header at the top of your script, `uiauto exec` will automatically load in a previously saved simulator data dump before running the script.

Let's say you have a TODO list application. You want to script the delete feature, but the problem is that you need tasks to delete first. With UIAuto, you can add a comment header that loads the simulator with your app with its tasks already there. Therefore, your script can safely make this assumption every time that it is ran.

The steps below explain how to do this.

##### 1. Manually add tasks to your app in the simulator

Get data into your app up to the point where you want your script to start. In this case, you want to add some tasks so that your script can delete them later.

##### 2. Save the simulator's current state

$ uiauto simulator save uiauto/simulator_data/with_tasks

This command will save ALL of the data in the simulator including your application's data (and therefore, it'll save the tasks you just created). It is stored in the directory you specified.

##### 3. Add the comment header

Add the following to the top of `uiauto/scripts/delete_tasks.js`

// setup_simulator_data "../simulator_data/with_tasks"

Inside the quotes is where you specify the location of the simulator data you want loaded in before running the script. It may be a path relative to the script.

##### 4. Run the script

Build if needed, then run:

$ uiauto exec uiauto/scripts/delete_tasks.js

This will load in the simulator data that was saved in step 2 before running the script.

## License

MIT License. See LICENSE.txt
1 change: 1 addition & 0 deletions Rakefile
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
require "bundler/gem_tasks"
4 changes: 4 additions & 0 deletions bin/uiauto
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
#!/usr/bin/env ruby

require 'uiauto/cli'
UIAuto::CLI.start
4 changes: 4 additions & 0 deletions lib/uiauto.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
require "uiauto/version"

module UIAuto
end
53 changes: 53 additions & 0 deletions lib/uiauto/cli.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
require 'thor'
require 'uiauto/simulator'
require 'uiauto/runner'

module UIAuto
class SimulatorCLI < Thor
namespace :simulator

desc "reset", "Deletes all applications and settings"
method_option :sdk, :default => Simulator::CURRENT_IOS_SDK_VERSION
def reset
simulator = Simulator.new(options[:sdk])
simulator.reset
end

desc "load DATA", "Loads previously saved simulator data"
method_option :sdk, :default => Simulator::CURRENT_IOS_SDK_VERSION
def load(data_path)
simulator = Simulator.new(options[:sdk])
simulator.load(data_path)
end

desc "save DATA", "Saves simulator data"
method_option :sdk, :default => Simulator::CURRENT_IOS_SDK_VERSION
def save(data_path)
simulator = Simulator.new(options[:sdk])
simulator.save(data_path)
end

desc "open", "Opens the simulator"
def open
Simulator.open
end

desc "close", "Closes the simulator"
def close
Simulator.close
end
end

class CLI < Thor
desc "exec FILE_OR_DIRECTORY", "Runs the given script or directory of scripts through UI Automation"
method_option :results, :default => File.expand_path("./uiauto/results")
method_option :trace, :default => File.expand_path("./uiauto/results/trace")
method_option :app
def exec(file_or_dir = "./uiauto/scripts/")
Runner.run(file_or_dir, options)
end

desc "simulator SUBCOMMAND ...ARGS", "manage simulator data"
subcommand "simulator", SimulatorCLI
end
end
91 changes: 91 additions & 0 deletions lib/uiauto/instruments.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
require 'pty'
require 'fileutils'
require 'cfpropertylist'

module UIAuto
class Instruments
attr_accessor :trace, :app, :results, :script

def initialize(script, opts = {})
@script = script
@trace = opts[:trace]
@results = opts[:results]
@app = opts[:app] || default_application

FileUtils.mkdir_p(@results) unless File.exists?(@results)
end

def command
command = ["xcrun instruments"]
command << "-D #{@trace}"
command << "-t #{automation_template_location}"
command << @app
command << "-e UIASCRIPT #{@script}"
command << "-e UIARESULTSPATH #{@results}"

command.join(" ")
end

def execute
exit_status = 0
read, write = PTY.open
pid = spawn(command, :in => STDIN, :out => write, :err => write)
write.close

begin
loop do
buffer = read.readpartial(8192)
lines = buffer.split("\n")
lines.each do |line|
puts line
exit_status = 1 if line =~ /Fail:/ && exit_status != 2
exit_status = 2 if line =~ /Instruments Usage Error|Instruments Trace Error|^\d+-\d+-\d+ \d+:\d+:\d+ [-+]\d+ (Error:|None: Script threw an uncaught JavaScript error)/
end
end
rescue EOFError
ensure
read.close
end

exit_status
end

protected

def default_application
current_dir = Dir.pwd
product_directories = Dir.glob(File.join(derived_data_location, "*"))

matching_directories = product_directories.select do |product_dir|
info_plist_file = File.join(product_dir, "info.plist")
if File.exists?(info_plist_file)
info_plist = CFPropertyList::List.new(:file => info_plist_file)
data = CFPropertyList.native_types(info_plist.value)
current_dir == File.dirname(data["WorkspacePath"])
else
false
end
end

# TODO: Add support for running on a device
sorted_matches = matching_directories.sort_by { |dir| File.mtime(dir) }
Dir.glob(File.join(sorted_matches.last, "Build/Products/*-iphonesimulator/*.app")).sort_by { |dir| File.mtime(dir) }.last
end

def automation_template_location
template = nil
`xcrun instruments -s 2>&1 | grep Automation.tracetemplate`.split("\n").each do |path|
path = path.gsub(/^\s*"|",\s*$/, "")
template = path if File.exists?(path)
break if template
end
template
end

def derived_data_location
# TODO: Parse ~/Library/Preferences/com.apple.dt.Xcode.plist to find customized location
File.expand_path("~/Library/Developer/Xcode/DerivedData/")
end

end
end
58 changes: 58 additions & 0 deletions lib/uiauto/runner.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
require 'uiauto/instruments'
require 'uiauto/simulator'

module UIAuto
class Runner
def self.run(file_or_dir, options = {})
exit_status = 0
if file_or_dir.nil?
exit_status = self.run_one options
elsif File.directory?(file_or_dir)
exit_status = self.run_all(file_or_dir, options)
else
exit_status = self.run_one(file_or_dir, options)
end

exit exit_status
end

private

def self.run_one(script, options)
self.process_comment_header script
instruments = Instruments.new(script, options)
exit_status = instruments.execute

exit_status
end

def self.run_all(dir, options)
exit_status = 0
scripts = Dir.glob(File.join(dir, "*.js"))
scripts.each do |script|
script_exit_status = self.run_one(script, options)
if script_exit_status > exit_status
exit_status = script_exit_status
end
end

exit_status
end

def self.process_comment_header(script)
File.open(script) do |file|
if file.readline =~ /\s*\/\/ setup_simulator_data "(.+)"/
path = $1
full_path = path
if !path.start_with?("/")
full_path = File.expand_path(File.join(File.dirname(script), path))
end

simulator = Simulator.new
simulator.load full_path
end
end
end

end
end
Loading

0 comments on commit 3a8b164

Please sign in to comment.