├── .bundle └── config ├── .ruby-gemset ├── .ruby-version ├── lib ├── bwoken │ ├── cli │ │ ├── templates │ │ │ ├── javascript │ │ │ │ └── example_vendor.js │ │ │ └── coffeescript │ │ │ │ ├── ipad │ │ │ │ └── example.coffee │ │ │ │ └── iphone │ │ │ │ └── example.coffee │ │ ├── init.rb │ │ └── test.rb │ ├── tasks.rb │ ├── version.rb │ ├── configs │ │ └── bwoken.xcconfig │ ├── coffeescript │ │ ├── import_string.rb │ │ └── github_import_string.rb │ ├── formatters │ │ ├── passthru_formatter.rb │ │ └── colorful_formatter.rb │ ├── device.rb │ ├── simulator.rb │ ├── device_runner.rb │ ├── simulator_runner.rb │ ├── script_runner.rb │ ├── script.rb │ ├── coffeescript.rb │ ├── tasks │ │ └── bwoken.rake │ ├── formatter.rb │ ├── build.rb │ └── cli.rb └── bwoken.rb ├── Rakefile ├── bin ├── bwoken └── unix_instruments.sh ├── doc └── screenshot.png ├── .travis.yml ├── spec ├── support │ ├── stub_out.rb │ ├── be_in_matcher.rb │ ├── capture_stdout.rb │ └── stub_proj_path.rb ├── spec_helper.rb └── lib │ ├── bwoken │ ├── coffeescript │ │ ├── import_string_spec.rb │ │ └── github_import_string_spec.rb │ ├── device_spec.rb │ ├── simulator_runner_spec.rb │ ├── script_runner_spec.rb │ ├── coffeescript_spec.rb │ ├── simulator_spec.rb │ ├── script_spec.rb │ ├── build_spec.rb │ └── formatter_spec.rb │ └── bwoken_spec.rb ├── Gemfile ├── TODO.txt ├── .gitignore ├── Guardfile ├── CHANGELOG ├── Gemfile.lock ├── LICENSE ├── bwoken.gemspec └── README.md /.bundle/config: -------------------------------------------------------------------------------- 1 | --- 2 | -------------------------------------------------------------------------------- /.ruby-gemset: -------------------------------------------------------------------------------- 1 | bwoken 2 | -------------------------------------------------------------------------------- /.ruby-version: -------------------------------------------------------------------------------- 1 | 2.1.2 2 | -------------------------------------------------------------------------------- /lib/bwoken/cli/templates/javascript/example_vendor.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /lib/bwoken/tasks.rb: -------------------------------------------------------------------------------- 1 | load 'bwoken/tasks/bwoken.rake' 2 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env rake 2 | require "bundler/gem_tasks" 3 | -------------------------------------------------------------------------------- /bin/bwoken: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require 'bwoken/cli' 4 | -------------------------------------------------------------------------------- /doc/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bendyworks/bwoken/HEAD/doc/screenshot.png -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | rvm: 3 | - 2.0.0 4 | - 2.1.2 5 | script: 'bundle exec rspec spec' 6 | -------------------------------------------------------------------------------- /lib/bwoken/version.rb: -------------------------------------------------------------------------------- 1 | module Bwoken 2 | VERSION = "2.1.0.rc.2" unless defined?(::Bwoken::VERSION) 3 | end 4 | -------------------------------------------------------------------------------- /spec/support/stub_out.rb: -------------------------------------------------------------------------------- 1 | def stub_out obj, method, value 2 | obj.stub(method => value) 3 | value 4 | end 5 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | # Specify your gem's dependencies in bwoken.gemspec 4 | gemspec 5 | -------------------------------------------------------------------------------- /spec/support/be_in_matcher.rb: -------------------------------------------------------------------------------- 1 | RSpec::Matchers.define :be_in do |expected| 2 | 3 | match do |actual| 4 | expected.include?(actual) 5 | end 6 | 7 | end 8 | -------------------------------------------------------------------------------- /lib/bwoken/configs/bwoken.xcconfig: -------------------------------------------------------------------------------- 1 | GCC_PREPROCESSOR_DEFINITIONS = $(inherited) TEST_MODE=1 2 | CONFIGURATION_BUILD_DIR = $(BWOKEN_CONFIGURATION_BUILD_DIR) 3 | ONLY_ACTIVE_ARCH = NO 4 | -------------------------------------------------------------------------------- /lib/bwoken/cli/templates/coffeescript/ipad/example.coffee: -------------------------------------------------------------------------------- 1 | # iPad example test file 2 | #import ../example_vendor.js 3 | 4 | target = UIATarget.localTarget() 5 | window = target.frontMostApp().mainWindow() 6 | -------------------------------------------------------------------------------- /lib/bwoken/cli/templates/coffeescript/iphone/example.coffee: -------------------------------------------------------------------------------- 1 | # iPhone example test file 2 | #import ../example_vendor.js 3 | 4 | target = UIATarget.localTarget() 5 | window = target.frontMostApp().mainWindow() 6 | -------------------------------------------------------------------------------- /spec/support/capture_stdout.rb: -------------------------------------------------------------------------------- 1 | def capture_stdout 2 | begin 3 | result = StringIO.new 4 | $stdout = result 5 | yield 6 | ensure 7 | $stdout = STDOUT 8 | end 9 | result.string 10 | end 11 | 12 | -------------------------------------------------------------------------------- /TODO.txt: -------------------------------------------------------------------------------- 1 | * fix non-standard name for Info.plist? 2 | * set "-arch i386" 3 | * add #github to javascript files 4 | * handle when user hasn't yet installed the app on a device. error message is "unable to locate CFBundleIdentifier for path...". Watch for this, then alert the user 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | *.rbc 3 | # if bundler_bin is in here, then .bundle should not 4 | # .bundle 5 | .config 6 | .yardoc 7 | InstalledFiles 8 | _yardoc 9 | coverage 10 | lib/bundler/man 11 | pkg 12 | rdoc 13 | spec/reports 14 | tmp 15 | bundler_bin 16 | /.bundle/install.log 17 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | RSpec.configure do |c| 2 | c.treat_symbols_as_metadata_keys_with_true_values = true 3 | c.filter_run_excluding :platform => :osx unless RUBY_PLATFORM =~ /darwin/i 4 | end 5 | 6 | Dir[File.expand_path('../support', __FILE__) + '/**/*.rb'].each {|file| require file} 7 | -------------------------------------------------------------------------------- /lib/bwoken/coffeescript/import_string.rb: -------------------------------------------------------------------------------- 1 | module Bwoken 2 | class Coffeescript 3 | class ImportString 4 | 5 | def initialize string 6 | @string = string 7 | end 8 | 9 | def parse 10 | end 11 | 12 | def to_s 13 | @string 14 | end 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /Guardfile: -------------------------------------------------------------------------------- 1 | # A sample Guardfile 2 | # More info at https://github.com/guard/guard#readme 3 | 4 | guard 'rspec', :version => 2 do 5 | watch(%r{^spec/.+_spec\.rb$}) 6 | watch(%r{^lib/(.+)\.rb$}) { |m| "spec/lib/#{m[1]}_spec.rb" } 7 | watch(%r{^spec/support/(.+)\.rb$}) { "spec" } 8 | watch('spec/spec_helper.rb') { "spec" } 9 | end 10 | 11 | -------------------------------------------------------------------------------- /lib/bwoken/formatters/passthru_formatter.rb: -------------------------------------------------------------------------------- 1 | require 'bwoken/formatter' 2 | 3 | module Bwoken 4 | class PassthruFormatter < Formatter 5 | [:before_build_start, :build_line, :build_successful, :build_failed, 6 | :complete, :debug, :error, :fail, :other, :pass, :start].each do |cb| 7 | on cb do |line| 8 | puts line 9 | end 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /spec/support/stub_proj_path.rb: -------------------------------------------------------------------------------- 1 | module StubProjPath 2 | 3 | def proj_path 4 | File.expand_path('../../tmp/FakeProject', __FILE__) 5 | end 6 | 7 | def stub_proj_path 8 | Dir.stub(:pwd => proj_path) 9 | end 10 | 11 | end 12 | 13 | RSpec.configure do |c| 14 | c.include StubProjPath, :stub_proj_path 15 | 16 | c.before(:all, :stub_proj_path) { FileUtils.mkdir_p(proj_path) } 17 | end 18 | -------------------------------------------------------------------------------- /spec/lib/bwoken/coffeescript/import_string_spec.rb: -------------------------------------------------------------------------------- 1 | require 'bwoken/coffeescript/import_string' 2 | 3 | describe Bwoken::Coffeescript::ImportString do 4 | let(:string) { '#import foo.js' } 5 | subject { Bwoken::Coffeescript::ImportString.new(string) } 6 | 7 | describe '#parse' do 8 | it 'does not affect @string' do 9 | subject.parse 10 | expect(subject.instance_variable_get('@string')).to eq(string) 11 | end 12 | end 13 | 14 | its(:to_s) { should == string } 15 | end 16 | -------------------------------------------------------------------------------- /CHANGELOG: -------------------------------------------------------------------------------- 1 | Version 2.0.0 2013-12-30 Brad Grzesiak 2 | 3 | * Use `bwoken` at the command line instead of `rake`. 4 | * Allow spaces in the pwd of your project. 5 | * Allow much more configuration of the builder and test runner. 6 | * Deprecate Ruby 1.x. You must now use Ruby 2.x. (2.0.0 comes standard with Mavericks; other versions installable via RVM or rbenv) 7 | * Deprecate invocation via `rake`. 8 | 9 | Version 1.3.0 10 | 11 | Version 1.2.0 12 | 13 | Version 1.1.0 2012-7-5 Brad Grzesiak and Jaymes Waters 14 | 15 | * Add device support. 16 | 17 | Version 1.0.1 2012-7-3 Brad Grzesiak and Jaymes Waters 18 | 19 | * unix_instruments.sh: Updated to make more robust version from gist.github.com/1402258 20 | -------------------------------------------------------------------------------- /lib/bwoken/device.rb: -------------------------------------------------------------------------------- 1 | module Bwoken 2 | class Device 3 | class << self 4 | 5 | # deprecated. Remove when Rakefile support removed 6 | def should_use_simulator? 7 | want_simulator? || ! connected? 8 | end 9 | 10 | # deprecated. Remove when Rakefile support removed 11 | def want_simulator? 12 | ENV['SIMULATOR'] && ENV['SIMULATOR'].downcase == 'true' 13 | end 14 | 15 | def connected? 16 | self.uuid ? true : false 17 | end 18 | 19 | def uuid 20 | ioreg[/"USB Serial Number" = "([0-9a-z]+)"/] && $1 21 | end 22 | 23 | def device_type 24 | ioreg[/"USB Product Name" = "(.*)"/] && $1.downcase 25 | end 26 | 27 | def ioreg 28 | @ioreg ||= `ioreg -w 0 -rc IOUSBDevice -k SupportsIPhoneOS` 29 | end 30 | 31 | end 32 | 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /spec/lib/bwoken/device_spec.rb: -------------------------------------------------------------------------------- 1 | require 'bwoken/device' 2 | 3 | describe Bwoken::Device do 4 | subject { Bwoken::Device } 5 | 6 | describe '.want_simulator?' do 7 | context 'set to true from command line' do 8 | it 'is true' do 9 | ENV.stub(:[]).with('SIMULATOR').and_return('TRUE') 10 | subject.want_simulator?.should be_true 11 | end 12 | end 13 | context 'not set from command line' do 14 | it 'is false' do 15 | subject.want_simulator?.should be_false 16 | end 17 | end 18 | end 19 | 20 | describe '.connected?' do 21 | context 'device connected' do 22 | it 'is true' do 23 | subject.stub(:uuid => 'asdfasefasdfasd') 24 | subject.should be_connected 25 | end 26 | end 27 | context 'device not connected' do 28 | it 'is false' do 29 | subject.stub(:uuid => nil) 30 | subject.should_not be_connected 31 | end 32 | end 33 | end 34 | end 35 | 36 | -------------------------------------------------------------------------------- /spec/lib/bwoken/simulator_runner_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | require 'bwoken/simulator_runner' 4 | 5 | describe Bwoken::SimulatorRunner do 6 | 7 | describe '#script_filenames' do 8 | shared_examples 'for not focused' do 9 | it 'returns all scripts' do 10 | subject.should_receive(:all_test_files).and_return('y') 11 | expect(subject.script_filenames).to eq('y') 12 | end 13 | end 14 | 15 | context 'focus not defined' do 16 | include_examples 'for not focused' 17 | end 18 | 19 | context 'focus is empty array' do 20 | before { subject.focus = [] } 21 | include_examples 'for not focused' 22 | end 23 | 24 | context 'focus set' do 25 | it 'returns focused tests' do 26 | subject.focus = ['a'] 27 | subject.should_receive(:test_files_from_feature_names).and_return('y') 28 | expect(subject.script_filenames).to eq('y') 29 | end 30 | end 31 | end 32 | 33 | end 34 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: . 3 | specs: 4 | bwoken (2.1.0.rc.2) 5 | coffee-script-source 6 | colorful 7 | execjs 8 | json_pure 9 | rake 10 | slop 11 | 12 | GEM 13 | remote: https://rubygems.org/ 14 | specs: 15 | coffee-script-source (1.6.3) 16 | colorful (0.0.3) 17 | diff-lcs (1.2.1) 18 | execjs (2.0.1) 19 | ffi (1.0.11) 20 | guard (1.0.1) 21 | ffi (>= 0.5.0) 22 | thor (~> 0.14.6) 23 | guard-rspec (0.6.0) 24 | guard (>= 0.10.0) 25 | json_pure (1.8.0) 26 | rake (10.1.0) 27 | rspec (2.13.0) 28 | rspec-core (~> 2.13.0) 29 | rspec-expectations (~> 2.13.0) 30 | rspec-mocks (~> 2.13.0) 31 | rspec-core (2.13.0) 32 | rspec-expectations (2.13.0) 33 | diff-lcs (>= 1.1.3, < 2.0) 34 | rspec-mocks (2.13.0) 35 | slop (3.4.6) 36 | thor (0.14.6) 37 | 38 | PLATFORMS 39 | ruby 40 | 41 | DEPENDENCIES 42 | bwoken! 43 | guard-rspec 44 | rspec 45 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012 Bendyworks 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 | -------------------------------------------------------------------------------- /bwoken.gemspec: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | require File.expand_path('../lib/bwoken/version', __FILE__) 3 | 4 | Gem::Specification.new do |gem| 5 | gem.name = 'bwoken' 6 | gem.version = Bwoken::VERSION 7 | gem.description = %q{iOS UIAutomation Test Runner} 8 | gem.summary = %q{Runs your UIAutomation tests from the command line for both iPhone and iPad; supports coffeescript} 9 | 10 | gem.authors = ['Brad Grzesiak'] 11 | gem.email = ['brad@bendyworks.com'] 12 | gem.homepage = 'https://bendyworks.github.com/bwoken' 13 | gem.license = 'MIT' 14 | 15 | gem.files = Dir['LICENSE', 'README.md', 'bin/**/*', 'lib/**/*'] 16 | gem.require_path = 'lib' 17 | 18 | gem.bindir = 'bin' 19 | gem.executables = ['unix_instruments.sh', 'bwoken'] 20 | 21 | gem.add_dependency 'coffee-script-source' 22 | gem.add_dependency 'colorful' 23 | gem.add_dependency 'execjs' 24 | gem.add_dependency 'json_pure' 25 | gem.add_dependency 'rake' 26 | gem.add_dependency 'slop' 27 | 28 | gem.add_development_dependency 'rspec' 29 | gem.add_development_dependency 'guard-rspec' 30 | end 31 | -------------------------------------------------------------------------------- /lib/bwoken/simulator.rb: -------------------------------------------------------------------------------- 1 | module Bwoken 2 | class Simulator 3 | 4 | def self.plist_buddy; '/usr/libexec/PlistBuddy'; end 5 | def self.plist_file; "#{Bwoken::Build.app_dir(true)}/Info.plist"; end 6 | 7 | def self.device_family= device_family 8 | update_device_family_in_plist :delete_array 9 | update_device_family_in_plist :add_array 10 | update_device_family_in_plist :add_scalar, device_family 11 | end 12 | 13 | def self.update_device_family_in_plist action, args = nil 14 | system_cmd = lambda {|command| Kernel.system "#{plist_buddy} -c '#{command}' \"#{plist_file}\"" } 15 | 16 | case action 17 | when :delete_array then system_cmd['Delete :UIDeviceFamily'] 18 | when :add_array then system_cmd['Add :UIDeviceFamily array'] 19 | when :add_scalar 20 | command = lambda {|scalar| "Add :UIDeviceFamily:0 integer #{scalar == 'iphone' ? 1 : 2}"} 21 | 22 | case args 23 | when /iphone/i 24 | system_cmd[command['iphone']] 25 | when /ipad/i 26 | system_cmd[command['ipad']] 27 | when /universal/i 28 | system_cmd[command['ipad']] 29 | system_cmd[command['iphone']] 30 | end 31 | 32 | end 33 | end 34 | 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /lib/bwoken/cli/init.rb: -------------------------------------------------------------------------------- 1 | require 'fileutils' 2 | require 'slop' 3 | 4 | module Bwoken 5 | module CLI 6 | class Init 7 | 8 | class << self 9 | 10 | def help_banner 11 | < 0 39 | test_files_from_feature_names 40 | else 41 | all_test_files 42 | end 43 | end 44 | 45 | def test_files_from_feature_names 46 | feature_names.map do |feature_name| 47 | File.join(Bwoken.test_suite_path, device_family, "#{feature_name}.js") 48 | end 49 | end 50 | 51 | def all_test_files 52 | all_files_in_test_dir - helper_files 53 | end 54 | 55 | def all_files_in_test_dir 56 | Dir["#{Bwoken.test_suite_path}/#{device_family}/**/*.js"] 57 | end 58 | 59 | def helper_files 60 | Dir["#{Bwoken.test_suite_path}/#{device_family}/**/helpers/**/*.js"] 61 | end 62 | 63 | end 64 | end 65 | -------------------------------------------------------------------------------- /lib/bwoken/simulator_runner.rb: -------------------------------------------------------------------------------- 1 | require 'bwoken' 2 | require 'bwoken/script' 3 | require 'bwoken/simulator' 4 | 5 | module Bwoken 6 | class SimulatorRunner 7 | attr_accessor :focus 8 | attr_accessor :formatter 9 | attr_accessor :simulator 10 | attr_accessor :device 11 | attr_accessor :app_dir 12 | attr_accessor :device_family 13 | 14 | alias_method :feature_names, :focus 15 | 16 | def initialize 17 | yield self if block_given? 18 | end 19 | 20 | def execute 21 | Simulator.device_family = device_family 22 | scripts.each(&:run) 23 | end 24 | 25 | def scripts 26 | script_filenames.map do |filename| 27 | Script.new do |s| 28 | s.path = filename 29 | s.device_family = device_family 30 | s.formatter = formatter 31 | s.simulator = simulator 32 | s.device = device 33 | s.app_dir = app_dir 34 | end 35 | end 36 | end 37 | 38 | def script_filenames 39 | if focus.respond_to?(:length) && focus.length > 0 40 | test_files_from_feature_names 41 | else 42 | all_test_files 43 | end 44 | end 45 | 46 | def test_files_from_feature_names 47 | feature_names.map do |feature_name| 48 | File.join(Bwoken.test_suite_path, device_family, "#{feature_name}.js") 49 | end 50 | end 51 | 52 | def all_test_files 53 | all_files_in_test_dir - helper_files 54 | end 55 | 56 | def all_files_in_test_dir 57 | Dir["#{Bwoken.test_suite_path}/#{device_family}/**/*.js"] 58 | end 59 | 60 | def helper_files 61 | Dir["#{Bwoken.test_suite_path}/#{device_family}/**/helpers/**/*.js"] 62 | end 63 | 64 | end 65 | end 66 | -------------------------------------------------------------------------------- /lib/bwoken/script_runner.rb: -------------------------------------------------------------------------------- 1 | require 'bwoken/simulator_runner' 2 | require 'bwoken/device_runner' 3 | 4 | module Bwoken 5 | class ScriptRunner 6 | attr_accessor :family 7 | attr_accessor :focus 8 | attr_accessor :formatter 9 | attr_accessor :simulator 10 | attr_accessor :device 11 | attr_accessor :app_dir 12 | 13 | alias_method :feature_names, :focus 14 | 15 | def initialize 16 | yield self if block_given? 17 | end 18 | 19 | def execute 20 | if simulator 21 | execute_in_simulator 22 | else 23 | execute_on_device 24 | end 25 | end 26 | 27 | def execute_in_simulator 28 | chosen_families.each do |device_family| 29 | execute_for_family device_family 30 | end 31 | end 32 | 33 | def execute_for_family device_family 34 | runner_for_family(device_family).execute 35 | end 36 | 37 | def runner_for_family device_family 38 | SimulatorRunner.new do |sr| 39 | sr.device_family = device_family 40 | sr.focus = focus 41 | sr.formatter = formatter 42 | sr.simulator = simulator 43 | sr.device = device 44 | sr.app_dir = app_dir 45 | end 46 | end 47 | 48 | def chosen_families 49 | if family == 'all' || family == [] || family.nil? 50 | %w(iphone ipad) 51 | else 52 | Array(family) 53 | end 54 | end 55 | 56 | def execute_on_device 57 | runner_for_device.execute 58 | end 59 | 60 | def runner_for_device 61 | DeviceRunner.new do |dr| 62 | dr.focus = focus 63 | dr.formatter = formatter 64 | dr.app_dir = app_dir 65 | dr.device = device 66 | end 67 | end 68 | 69 | end 70 | end 71 | -------------------------------------------------------------------------------- /lib/bwoken/script.rb: -------------------------------------------------------------------------------- 1 | require 'fileutils' 2 | require 'open3' 3 | 4 | require 'bwoken' 5 | require 'bwoken/device' 6 | 7 | module Bwoken 8 | class ScriptFailedError < RuntimeError; end 9 | 10 | class Script 11 | 12 | def self.trace_file_path 13 | File.join(Bwoken.tmp_path, 'trace') 14 | end 15 | 16 | attr_accessor :path 17 | attr_accessor :device_family 18 | attr_accessor :formatter 19 | attr_accessor :simulator 20 | attr_accessor :device 21 | attr_accessor :app_dir 22 | 23 | def initialize 24 | yield self if block_given? 25 | end 26 | 27 | def env_variables 28 | { 29 | 'UIASCRIPT' => %Q|"#{path}"|, 30 | 'UIARESULTSPATH' => %Q|"#{Bwoken.results_path}"| 31 | } 32 | end 33 | 34 | def env_variables_for_cli 35 | env_variables.map{|key,val| "-e #{key} #{val}"}.join(' ') 36 | end 37 | 38 | def cmd 39 | #%Q|"#{File.expand_path('../../../bin', __FILE__)}/unix_instruments.sh" \ 40 | %Q|instruments \ 41 | -t "Automation" \ 42 | -D "#{self.class.trace_file_path}" \ 43 | #{device_flag} \ 44 | "#{app_dir}" \ 45 | #{env_variables_for_cli}|.tap{|x| puts "SCRIPT: #{x}"} 46 | end 47 | 48 | def device_flag 49 | if !device.nil? 50 | return "-w \"#{device}\"" 51 | end 52 | 53 | simulator ? '-w "iPhone 5s (8.1 Simulator)"' : "-w #{Bwoken::Device.uuid}" 54 | end 55 | 56 | def run 57 | formatter.before_script_run path 58 | 59 | Open3.popen3(cmd) do |stdin, stdout, stderr, wait_thr| 60 | exit_status = formatter.format stdout 61 | raise ScriptFailedError.new('Test Script Failed') unless exit_status == 0 62 | end 63 | end 64 | 65 | end 66 | end 67 | -------------------------------------------------------------------------------- /lib/bwoken.rb: -------------------------------------------------------------------------------- 1 | require 'fileutils' 2 | 3 | module Bwoken 4 | class << self 5 | DEVICE_FAMILIES = %w(iphone ipad) 6 | 7 | def path 8 | File.join(project_path, integration_path) 9 | end 10 | 11 | def integration_path 12 | @integration_path || 'integration' 13 | end 14 | 15 | def integration_path= new_integration_path 16 | @integration_path = new_integration_path 17 | end 18 | 19 | def tmp_path 20 | File.join(path, 'tmp') 21 | end 22 | 23 | def app_name 24 | @name || File.basename(File.basename(workspace_or_project, '.xcodeproj'), '.xcworkspace') 25 | end 26 | 27 | def app_name= name 28 | @name = name 29 | end 30 | 31 | def project_path 32 | Dir.pwd 33 | end 34 | 35 | def test_suite_path 36 | File.join(tmp_path, 'javascript') 37 | end 38 | 39 | def path_to_developer_dir 40 | `xcode-select -print-path`.strip 41 | end 42 | 43 | %w(xcworkspace xcodeproj).each do |xcode_root| 44 | define_method xcode_root do 45 | paths = Dir["#{project_path}/*.#{xcode_root}"] 46 | fail "Error: Found more than one #{xcode_root} file in root" if paths.count > 1 47 | paths.first 48 | end 49 | end 50 | 51 | def workspace_or_project 52 | ws = xcworkspace 53 | ws && File.exists?(ws) ? ws : xcodeproj 54 | end 55 | 56 | def workspace_or_project_flag 57 | ws = xcworkspace 58 | if ws && File.exists?(ws) 59 | "-workspace \"#{ws}\"" 60 | else 61 | "-project \"#{xcodeproj}\"" 62 | end 63 | end 64 | 65 | def results_path 66 | File.join(tmp_path, 'results').tap do |dir_name| 67 | FileUtils.mkdir_p(dir_name) unless File.directory?(dir_name) 68 | end 69 | end 70 | 71 | end 72 | end 73 | -------------------------------------------------------------------------------- /spec/lib/bwoken/script_runner_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | require 'bwoken/script_runner' 4 | require 'bwoken/simulator_runner' 5 | 6 | describe Bwoken::ScriptRunner do 7 | 8 | describe '#execute_in_simulator' do 9 | it 'executes for each family' do 10 | subject.stub(:chosen_families).and_return(%w(a b)) 11 | subject.should_receive(:execute_for_family).with('a').ordered 12 | subject.should_receive(:execute_for_family).with('b').ordered 13 | subject.execute_in_simulator 14 | end 15 | end 16 | 17 | describe '#execute_for_family' do 18 | it 'gets the appropriate device runner' do 19 | subject.should_receive(:runner_for_family).with('a').and_return(stub(:execute => lambda {})) 20 | subject.execute_for_family('a') 21 | end 22 | 23 | it 'calls execute on a device runner' do 24 | runner_stub = stub 25 | runner_stub.should_receive(:execute) 26 | subject.stub(:runner_for_family).and_return(runner_stub) 27 | subject.execute_for_family('a') 28 | end 29 | end 30 | 31 | describe '#chosen_families' do 32 | shared_examples "for wanting both families" do |family| 33 | subject do 34 | Bwoken::ScriptRunner.new {|sr| sr.family = family } 35 | end 36 | it 'returns an array of "iphone" and "ipad"' do 37 | expect(subject.chosen_families).to eq(%w(iphone ipad)) 38 | end 39 | end 40 | 41 | context 'when family is blank-ish' do 42 | [[], nil].each do |family| 43 | include_examples 'for wanting both families', family 44 | end 45 | end 46 | 47 | context 'when a device family' do 48 | let(:fam) { 'ipad' } 49 | subject do 50 | Bwoken::ScriptRunner.new {|sr| sr.family = fam } 51 | end 52 | 53 | it 'returns an array of that family' do 54 | expect(subject.chosen_families).to eq([fam]) 55 | end 56 | end 57 | end 58 | 59 | end 60 | -------------------------------------------------------------------------------- /lib/bwoken/coffeescript/github_import_string.rb: -------------------------------------------------------------------------------- 1 | require 'fileutils' 2 | require File.expand_path('../import_string', __FILE__) 3 | 4 | module Bwoken 5 | class Coffeescript 6 | class GithubImportString < ImportString 7 | attr_reader :repo_name, :file_path 8 | 9 | def initialize string 10 | @string = string 11 | parse_parts 12 | end 13 | 14 | def parse 15 | ensure_github 16 | end 17 | 18 | def to_s 19 | %Q|#import "#{repo_path}/#{file_path}"| 20 | end 21 | 22 | private 23 | 24 | # this is kinda gross. Key thing to recognize is that we're whitelisting 25 | # the account name, project name, and filename. We'll eventually be escaping 26 | # out to Kernel#`, so we shouldn't be allowing all characters (eg, ';' would be BAD) 27 | def parse_parts 28 | importing = @string.match(%r{\A\s*#github\s+['"]?\b([^'"]*)['"]?}i)[1] 29 | @repo_name, @file_path = importing.match(%r{([-a-z_0-9]+/[-a-z_0-9\.]+)/([-a-z_0-9/\.]*)}i)[1..2] 30 | end 31 | 32 | def ensure_github 33 | if !repo_exists? || ENV['FORCE_GITHUB'] == 'true' 34 | clean_repo 35 | download_repo 36 | end 37 | end 38 | 39 | def clean_repo 40 | delete_repo if repo_exists? 41 | end 42 | 43 | def delete_repo 44 | FileUtils.rm_rf(repo_path) 45 | end 46 | 47 | def repo_path 48 | File.join(Bwoken.path, 'github', repo_name) 49 | end 50 | 51 | def repo_exists? 52 | File.exist? repo_path 53 | end 54 | 55 | def download_repo 56 | prepare_repo_path 57 | clone_repo 58 | delete_dot_git 59 | end 60 | 61 | def prepare_repo_path 62 | FileUtils.mkdir_p File.dirname(repo_path) 63 | end 64 | 65 | def clone_repo 66 | `git clone --single-branch --branch master git://github.com/#{repo_name} #{repo_path}` 67 | end 68 | 69 | def delete_dot_git 70 | FileUtils.rm_rf File.join(repo_path, '.git') 71 | end 72 | 73 | end 74 | end 75 | end 76 | -------------------------------------------------------------------------------- /lib/bwoken/coffeescript.rb: -------------------------------------------------------------------------------- 1 | require 'fileutils' 2 | require 'coffee_script/source' 3 | require 'json' 4 | require 'execjs' 5 | 6 | require File.expand_path('../coffeescript/import_string', __FILE__) 7 | require File.expand_path('../coffeescript/github_import_string', __FILE__) 8 | 9 | module Bwoken 10 | class Coffeescript 11 | class << self 12 | 13 | def coffee_script_source 14 | return @coffeescript if @coffeescript 15 | 16 | @coffeescript = '' 17 | open(CoffeeScript::Source.bundled_path) do |f| 18 | @coffeescript << f.read 19 | end 20 | @coffeescript 21 | end 22 | 23 | def context 24 | @context ||= ExecJS.compile(coffee_script_source) 25 | end 26 | 27 | def precompile coffeescript 28 | coffeescript.lines.partition {|line| line =~ /^#(?:github|import) .*$/} 29 | end 30 | 31 | def compile source, target 32 | githubs_and_imports, sans_imports = precompile(IO.read source) 33 | 34 | javascript = coffeescript_to_javascript sans_imports.join 35 | import_strings = githubs_to_imports(githubs_and_imports) 36 | 37 | write import_strings, javascript, :to => target 38 | end 39 | 40 | def coffeescript_to_javascript coffee 41 | self.context.call 'CoffeeScript.compile', coffee, :bare => true 42 | end 43 | 44 | def githubs_to_imports strings 45 | strings.map do |string| 46 | obj = import_string_object(string) 47 | obj.parse 48 | obj.to_s 49 | end.join("\n") 50 | end 51 | 52 | def import_string_object string 53 | if string =~ /^#github/ 54 | GithubImportString.new(string) 55 | else 56 | ImportString.new(string) 57 | end 58 | end 59 | 60 | def write *args 61 | to_hash = args.last 62 | chunks = args[0..-2] 63 | 64 | File.open(to_hash[:to], 'w') do |io| 65 | chunks.each do |chunk| 66 | io.puts chunk unless chunk.nil? || chunk == '' 67 | end 68 | end 69 | end 70 | 71 | end 72 | end 73 | end 74 | -------------------------------------------------------------------------------- /spec/lib/bwoken/coffeescript_spec.rb: -------------------------------------------------------------------------------- 1 | require 'bwoken/coffeescript' 2 | require 'stringio' 3 | 4 | require 'spec_helper' 5 | 6 | describe Bwoken::Coffeescript do 7 | let(:subject) { Bwoken::Coffeescript } 8 | 9 | describe '.precompile' do 10 | describe '"#import"' do 11 | let(:test_coffee) {"foo = 1\n#import bazzle.js\nbar = 2"} 12 | it 'splits #import statements from other statements' do 13 | subject.precompile(test_coffee).should == [ 14 | ["#import bazzle.js\n"], 15 | ["foo = 1\n", "bar = 2"] 16 | ] 17 | end 18 | end 19 | 20 | describe '"#github"' do 21 | let(:test_coffee) {"#github alexvollmer/tuneup_js\n#import bazzle.js\nfoo = 1\nbar = 2"} 22 | it 'converts github to import' do 23 | subject.precompile(test_coffee).should == [ 24 | ["#github alexvollmer/tuneup_js\n", "#import bazzle.js\n"], 25 | ["foo = 1\n", "bar = 2"] 26 | ] 27 | end 28 | end 29 | end 30 | 31 | describe '.compile' do 32 | before do 33 | subject.stub(:precompile => [[], []]) 34 | IO.stub(:read) 35 | subject.stub(:write) 36 | subject.stub(:coffeescript_to_javascript) 37 | subject.stub(:githubs_to_imports) 38 | end 39 | 40 | after { subject.compile 'a', 'b' } 41 | 42 | it 'precompiles' do 43 | subject.should_receive(:precompile) 44 | end 45 | 46 | it 'cofffeescript-compiles' do 47 | subject.should_receive(:coffeescript_to_javascript) 48 | end 49 | 50 | it 'resolves github imports' do 51 | subject.should_receive(:githubs_to_imports) 52 | end 53 | 54 | it 'writes to disk' do 55 | subject.should_receive(:write) 56 | end 57 | end 58 | 59 | describe '.write' do 60 | it 'saves the javascript to the destination_file' do 61 | stringio = StringIO.new 62 | destination_file = 'bazzle/bar.js' 63 | 64 | File.should_receive(:open).with(destination_file, 'w').and_yield(stringio) 65 | 66 | subject.write('foo', '', 'bar', :to => destination_file) 67 | 68 | stringio.string.should == "foo\nbar\n" 69 | end 70 | end 71 | 72 | end 73 | -------------------------------------------------------------------------------- /lib/bwoken/formatters/colorful_formatter.rb: -------------------------------------------------------------------------------- 1 | require 'colorful' 2 | 3 | require 'bwoken/formatter' 4 | 5 | module Bwoken 6 | class ColorfulFormatter < Formatter 7 | 8 | on :complete do |line| 9 | tokens = line.split(' ') 10 | puts %Q( \n#{"Complete".send(@failed ? :red : :green)}\n Duration: #{tokens[5].sub(';','').underline.bold}\n ) 11 | end 12 | 13 | on :debug do |line| 14 | filtered_line = line.sub(/(target\.frontMostApp.+)\.tap\(\)/, "#{'tap'.yellow} \\1") 15 | filtered_line = filtered_line.gsub(/\[("[^\]]*")\]/, "[" + '\1'.magenta + "]") 16 | filtered_line = filtered_line.gsub('()', '') 17 | filtered_line = filtered_line.sub(/target.frontMostApp.(?:mainWindow.)?/,'') 18 | tokens = filtered_line.split(' ') 19 | puts "#{tokens[3].cyan}\t#{tokens[4..-1].join(' ')}" 20 | end 21 | 22 | on :error do |line| 23 | @failed = true 24 | tokens = line.split(' ') 25 | puts "#{tokens[3].bold.red}\t#{tokens[4..-1].join(' ').underline.bold}" 26 | end 27 | 28 | on :fail do |line| 29 | @failed = true 30 | tokens = line.split(' ') 31 | puts "#{tokens[3].bold.red}\t#{tokens[4..-1].join(' ').underline.bold}" 32 | end 33 | 34 | on :start do |line| 35 | tokens = line.split(' ') 36 | puts "#{tokens[3].cyan}\t#{tokens[4..-1].join(' ')}" 37 | end 38 | 39 | on :pass do |line| 40 | tokens = line.split(' ') 41 | puts "#{tokens[3].green}\t#{tokens[4..-1].join(' ')}" 42 | end 43 | 44 | on :before_script_run do |path| 45 | @failed = false 46 | tokens = path.split('/') 47 | puts 48 | puts "#{tokens[-2]}\t#{tokens[-1]}".cyan 49 | end 50 | 51 | on :before_build_start do 52 | print "Building".blue 53 | end 54 | 55 | on :build_line do |line| 56 | print '.'.blue 57 | end 58 | 59 | on :build_successful do |line| 60 | puts 61 | puts 'Build Successful!'.green 62 | end 63 | 64 | on :build_failed do |build_log, error_log| 65 | puts build_log 66 | puts 'Standard Error:'.yellow 67 | puts error_log 68 | puts 'Build failed!'.red 69 | end 70 | 71 | on :other do |line| 72 | nil 73 | end 74 | end 75 | end 76 | -------------------------------------------------------------------------------- /lib/bwoken/tasks/bwoken.rake: -------------------------------------------------------------------------------- 1 | require 'bwoken' 2 | require 'rake/clean' 3 | 4 | require 'slop' 5 | require 'bwoken/cli/init' 6 | require 'bwoken/cli/test' 7 | 8 | BUILD_DIR = 'build' 9 | IPHONE_DIR = 'integration/coffeescript/iphone' 10 | VENDOR_JS_DIR = 'integration/javascript' 11 | RESULTS_DIR = 'integration/tmp/results' 12 | 13 | directory IPHONE_DIR 14 | directory VENDOR_JS_DIR 15 | directory RESULTS_DIR 16 | directory BUILD_DIR 17 | 18 | task :rake_deprecated do 19 | STDERR.puts 'WARNING: Invoking bwoken with rake is deprecated. Please use the `bwoken` executable now.' 20 | STDERR.puts 'Please see https://github.com/bendyworks/bwoken/wiki/Upgrading-from-v1-to-v2' 21 | STDERR.puts '' 22 | end 23 | 24 | namespace :bwoken do 25 | desc 'Create bwoken skeleton folders' 26 | task :init => :rake_deprecated do 27 | Bwoken::CLI::Init.new({}).run 28 | end 29 | end 30 | 31 | desc 'Compile coffeescript to javascript and copy vendor javascript' 32 | task :coffeescript => :rake_deprecated do 33 | Bwoken::CLI::Test.new({}).transpile 34 | end 35 | 36 | desc 'remove any temporary products' 37 | task :clean => :rake_deprecated do 38 | Bwoken::CLI::Test.new({}).clean 39 | end 40 | 41 | desc 'remove any generated file' 42 | task :clobber => :rake_deprecated do 43 | Bwoken::CLI::Test.new({}).clobber 44 | end 45 | 46 | 47 | desc 'Compile the workspace' 48 | task :compile => :rake_deprecated do 49 | opts = {:simulator => Bwoken::Device.should_use_simulator?} 50 | Bwoken::CLI::Test.new(opts).compile 51 | end 52 | 53 | 54 | device_families = %w(iphone ipad) 55 | 56 | device_families.each do |device_family| 57 | 58 | namespace device_family do 59 | task :test => [:rake_deprecated, RESULTS_DIR, :coffeescript] do 60 | opts = { 61 | :simulator => Bwoken::Device.should_use_simulator?, 62 | :family => device_family 63 | } 64 | opts[:focus] = [ENV['RUN']] if ENV['RUN'] 65 | 66 | Bwoken::CLI::Test.new(opts).test 67 | end 68 | end 69 | 70 | desc "Run tests for #{device_family}" 71 | task device_family => [:rake_deprecated, "#{device_family}:test"] 72 | 73 | end 74 | 75 | desc 'Run all tests without compiling first' 76 | task :test => :rake_deprecated do 77 | opts = { 78 | :simulator => Bwoken::Device.should_use_simulator? 79 | } 80 | opts[:focus] = [ENV['RUN']] if ENV['RUN'] 81 | opts[:family] = ENV['FAMILY'] if ENV['FAMILY'] 82 | 83 | Bwoken::CLI::Test.new(opts).test 84 | end 85 | 86 | task :default => [:rake_deprecated, :compile, :test] 87 | -------------------------------------------------------------------------------- /spec/lib/bwoken/simulator_spec.rb: -------------------------------------------------------------------------------- 1 | require 'bwoken/simulator' 2 | 3 | describe Bwoken::Simulator do 4 | describe '.device_family=' do 5 | it 'updates the plist in the correct order' do 6 | Bwoken::Simulator.should_receive(:update_device_family_in_plist).with(:delete_array).ordered 7 | Bwoken::Simulator.should_receive(:update_device_family_in_plist).with(:add_array).ordered 8 | Bwoken::Simulator.should_receive(:update_device_family_in_plist).with(:add_scalar, 'foo').ordered 9 | Bwoken::Simulator.device_family = 'foo' 10 | end 11 | end 12 | 13 | describe '.update_device_family_in_plist' do 14 | before do 15 | Bwoken::Simulator.stub(:plist_buddy => 'plistbuddy') 16 | Bwoken::Simulator.stub(:plist_file => 'plist_file') 17 | end 18 | 19 | context 'when deleting the device family array' do 20 | it 'calls PlistBuddy with the correct args' do 21 | Kernel.should_receive(:system).with("plistbuddy -c 'Delete :UIDeviceFamily' \"plist_file\"") 22 | Bwoken::Simulator.update_device_family_in_plist :delete_array 23 | end 24 | end 25 | 26 | context 'when creating the device family array' do 27 | it 'calls PlistBuddy with the correct args' do 28 | Kernel.should_receive(:system).with("plistbuddy -c 'Add :UIDeviceFamily array' \"plist_file\"") 29 | Bwoken::Simulator.update_device_family_in_plist :add_array 30 | end 31 | end 32 | 33 | context 'when adding to the device family array' do 34 | context 'for iPhone' do 35 | it 'calls PlistBuddy with the correct args' do 36 | Kernel.should_receive(:system).with("plistbuddy -c 'Add :UIDeviceFamily:0 integer 1' \"plist_file\"") 37 | Bwoken::Simulator.update_device_family_in_plist :add_scalar, 'iphone' 38 | end 39 | end 40 | 41 | context 'for iPad' do 42 | it 'calls PlistBuddy with the correct args' do 43 | Kernel.should_receive(:system).with("plistbuddy -c 'Add :UIDeviceFamily:0 integer 2' \"plist_file\"") 44 | Bwoken::Simulator.update_device_family_in_plist :add_scalar, 'ipad' 45 | end 46 | end 47 | 48 | context 'for universal' do 49 | it 'calls PlistBuddy with the correct args' do 50 | Kernel.should_receive(:system).with("plistbuddy -c 'Add :UIDeviceFamily:0 integer 1' \"plist_file\"") 51 | Kernel.should_receive(:system).with("plistbuddy -c 'Add :UIDeviceFamily:0 integer 2' \"plist_file\"") 52 | Bwoken::Simulator.update_device_family_in_plist :add_scalar, 'universal' 53 | end 54 | end 55 | 56 | end 57 | end 58 | 59 | end 60 | 61 | -------------------------------------------------------------------------------- /lib/bwoken/formatter.rb: -------------------------------------------------------------------------------- 1 | module Bwoken 2 | class Formatter 3 | 4 | class << self 5 | def format stdout 6 | new.format stdout 7 | end 8 | 9 | def format_build stdout 10 | new.format_build stdout 11 | end 12 | 13 | def on name, &block 14 | define_method "_on_#{name}_callback" do |*line| 15 | block.call(*line) 16 | end 17 | end 18 | 19 | end 20 | 21 | def method_missing(method_name, *args, &block) 22 | callback_method_sig = "_on_#{method_name}_callback" 23 | if self.respond_to? callback_method_sig.to_sym 24 | send(callback_method_sig, *args, &block) 25 | end 26 | end 27 | 28 | def line_demuxer line, exit_status 29 | if line =~ /Instruments Trace Error/ 30 | exit_status = 1 31 | _on_fail_callback(line) 32 | elsif line =~ /^\d{4}/ 33 | tokens = line.split(' ') 34 | 35 | if tokens[3] =~ /Pass/ 36 | _on_pass_callback(line) 37 | elsif tokens[3] =~ /Start/ 38 | _on_start_callback(line) 39 | elsif tokens[3] =~ /Fail/ || line =~ /Script threw an uncaught JavaScript error/ 40 | exit_status = 1 41 | _on_fail_callback(line) 42 | elsif tokens[3] =~ /Error/ 43 | _on_error_callback(line) 44 | else 45 | _on_debug_callback(line) 46 | end 47 | elsif line =~ /Instruments Trace Complete/ 48 | _on_complete_callback(line) 49 | else 50 | _on_other_callback(line) 51 | end 52 | exit_status 53 | end 54 | 55 | %w(pass fail debug other).each do |log_level| 56 | on log_level.to_sym do |line| 57 | puts line 58 | end 59 | end 60 | 61 | def format stdout 62 | exit_status = 0 63 | 64 | stdout.each_line do |line| 65 | exit_status = line_demuxer line, exit_status 66 | end 67 | 68 | exit_status 69 | end 70 | 71 | def format_build stdout 72 | out_string = '' 73 | stdout.each_line do |line| 74 | out_string << line 75 | if line.length > 1 76 | _on_build_line_callback(line) 77 | end 78 | end 79 | out_string 80 | end 81 | 82 | on :before_build_start do 83 | puts 'Building' 84 | end 85 | 86 | on :build_line do |line| 87 | print '.' 88 | end 89 | 90 | on :build_successful do |build_log| 91 | puts 92 | puts 93 | puts "### Build Successful ###" 94 | puts 95 | end 96 | 97 | on :build_failed do |build_log, error_log| 98 | puts build_log 99 | puts "Standard Error:" 100 | puts error_log 101 | puts '## Build failed ##' 102 | end 103 | 104 | end 105 | end 106 | -------------------------------------------------------------------------------- /spec/lib/bwoken_spec.rb: -------------------------------------------------------------------------------- 1 | require 'fileutils' 2 | 3 | require 'spec_helper' 4 | require 'bwoken' 5 | 6 | describe Bwoken do 7 | 8 | describe '.app_name', :stub_proj_path do 9 | it "returns the app's name without the .app prefix" do 10 | stub_proj_path 11 | Bwoken.stub(:workspace_or_project => "#{proj_path}/FakeProject.xcworkspace") 12 | Bwoken.app_name.should == 'FakeProject' 13 | end 14 | end 15 | 16 | describe '.project_path' do 17 | it 'returns the root directory of the project' do 18 | Dir.should_receive(:pwd).and_return(:foo) 19 | Bwoken.project_path.should == :foo 20 | end 21 | end 22 | 23 | describe '.workspace_or_project_flag', :stub_proj_path do 24 | context 'xcworkspace exists' do 25 | it 'returns the workspace flag' do 26 | File.stub(:exists? => true) 27 | stub_proj_path 28 | Bwoken.stub(:xcworkspace => "#{proj_path}/FakeProject.xcworkspace") 29 | Bwoken.workspace_or_project_flag.should == "-workspace \"#{proj_path}/FakeProject.xcworkspace\"" 30 | end 31 | end 32 | 33 | context 'no xcworkspace' do 34 | it 'returns the xcodeproj project flag' do 35 | File.stub(:exists? => false) 36 | stub_proj_path 37 | Bwoken.stub(:xcodeproj => "#{proj_path}/FakeProject.xcodeproj") 38 | Bwoken.workspace_or_project_flag.should == "-project \"#{proj_path}/FakeProject.xcodeproj\"" 39 | end 40 | end 41 | end 42 | 43 | 44 | describe '.xcworkspace', :stub_proj_path do 45 | it 'returns the workspace directory' do 46 | stub_proj_path 47 | 48 | Dir.should_receive(:[]). 49 | with("#{proj_path}/*.xcworkspace"). 50 | and_return(["#{proj_path}/FakeProject.xcworkspace"]) 51 | 52 | Bwoken.xcworkspace 53 | end 54 | end 55 | 56 | describe '.path' do 57 | it 'returns bwokens working directory' do 58 | Bwoken.stub(:project_path => 'foo/bar') 59 | Bwoken.path.should == "foo/bar/integration" 60 | end 61 | end 62 | 63 | describe '.tmp_path' do 64 | it 'returns bwokens temporary directory' do 65 | Bwoken.stub(:path => 'foo/bar') 66 | Bwoken.tmp_path.should == "foo/bar/tmp" 67 | end 68 | end 69 | 70 | describe '.results_path', :stub_proj_path do 71 | context "when it doesn't yet exist" do 72 | it 'creates the results directory' do 73 | stub_proj_path 74 | File.should_receive(:directory?).with("#{proj_path}/integration/tmp/results").and_return(false) 75 | FileUtils.should_receive(:mkdir_p).with("#{proj_path}/integration/tmp/results") 76 | Bwoken.results_path 77 | end 78 | end 79 | it 'returns the results path' do 80 | stub_proj_path 81 | File.stub(:directory?).and_return(true) 82 | Bwoken.results_path.should == "#{proj_path}/integration/tmp/results" 83 | end 84 | end 85 | 86 | end 87 | -------------------------------------------------------------------------------- /lib/bwoken/build.rb: -------------------------------------------------------------------------------- 1 | require 'open3' 2 | require 'bwoken' 3 | require 'bwoken/device' 4 | 5 | module Bwoken 6 | class BuildFailedError < RuntimeError; end 7 | 8 | class Build 9 | 10 | class << self 11 | def build_path 12 | File.join(Bwoken.project_path, 'build') 13 | end 14 | 15 | def xcconfig 16 | File.join(File.dirname(__FILE__), 'configs', 'bwoken.xcconfig') 17 | end 18 | 19 | def sdk simulator 20 | simulator ? 'iphonesimulator' : 'iphoneos' 21 | end 22 | 23 | def configuration_build_dir simulator 24 | File.join(build_path, sdk(simulator)) 25 | end 26 | 27 | def app_dir simulator 28 | File.join(configuration_build_dir(simulator), "#{Bwoken.app_name}.app") 29 | end 30 | end 31 | 32 | #attr_accessor :flags #TODO: implement 33 | attr_accessor :formatter 34 | attr_accessor :scheme 35 | attr_accessor :simulator 36 | attr_accessor :configuration 37 | attr_accessor :sdk_version 38 | attr_accessor :verbose 39 | 40 | def initialize 41 | #self.flags = [] #TODO: implement 42 | self.scheme = Bwoken.app_name 43 | self.configuration = 'Debug' 44 | 45 | yield self if block_given? 46 | end 47 | 48 | def sdk 49 | self.class.sdk(simulator) 50 | end 51 | 52 | def env_variables 53 | { 54 | 'BWOKEN_CONFIGURATION_BUILD_DIR' => %Q|"#{self.class.configuration_build_dir(simulator)}"| 55 | } 56 | end 57 | 58 | def variables_for_cli 59 | env_variables.map{|key,val| "#{key}=#{val}"}.join(' ') 60 | end 61 | 62 | def scheme_string 63 | Bwoken.xcworkspace ? %Q|-scheme "#{scheme}"| : '' 64 | end 65 | 66 | def cmd 67 | "xcodebuild \ 68 | #{Bwoken.workspace_or_project_flag} \ 69 | #{scheme_string} \ 70 | -configuration #{configuration} \ 71 | -sdk #{sdk}#{sdk_version} \ 72 | -xcconfig \"#{self.class.xcconfig}\" \ 73 | #{variables_for_cli} \ 74 | clean build" 75 | end 76 | 77 | def compile 78 | formatter.before_build_start 79 | 80 | succeeded, out_string, err_string = compile_19_plus 81 | 82 | if succeeded 83 | formatter.build_successful out_string 84 | else 85 | formatter.build_failed out_string, err_string 86 | fail BuildFailedError.new 87 | end 88 | end 89 | 90 | def compile_19_plus 91 | ret = nil 92 | Open3.popen3(cmd) do |stdin, stdout, stderr, wait_thr| 93 | 94 | out_string = formatter.format_build stdout 95 | err_string = stderr.read 96 | exit_status = wait_thr.value if wait_thr 97 | 98 | ret = [exit_status == 0, out_string, err_string] 99 | end 100 | ret 101 | end 102 | end 103 | end 104 | -------------------------------------------------------------------------------- /lib/bwoken/cli.rb: -------------------------------------------------------------------------------- 1 | require 'slop' 2 | 3 | %w(version cli/init cli/test).each do |f| 4 | require File.expand_path("../#{f}", __FILE__) 5 | end 6 | 7 | ran_command = nil 8 | 9 | opts = Slop.parse :help => true do 10 | on :v, :version, 'Print the version' do 11 | puts Bwoken::VERSION 12 | exit 0 13 | end 14 | 15 | command 'init' do 16 | banner Bwoken::CLI::Init.help_banner 17 | on :'integration-path=', 'Specify a custom directory to store your test scripts in (e.g. --integration-path=uiautomation/path/dir). Default: integration. If you use the non-default value here, you will need to always run bwoken with the `--integration-path=your/integration/dir` option.', :default => 'integration' 18 | 19 | run { ran_command = 'init' } 20 | end 21 | 22 | command 'test' do 23 | banner Bwoken::CLI::Test.help_banner 24 | 25 | on :simulator, 'Use simulator, even when an iDevice is connected', :default => false 26 | on :device=, 'Use given device (name or id)', :default => nil 27 | 28 | on :family=, 'Test only one device type, either ipad or iphone. Default is to test on both', 29 | :match => /\A(?:ipad|iphone|all)\Z/i, :default => 'all' 30 | on :scheme=, 'Specify a custom scheme' 31 | on :'product-name=', 'Specify a custom product name (e.g. --product-name="My Product"). Default is the name of of the xcodeproj file' 32 | on :'integration-path=', 'Specify a custom directory to store your test scripts in (e.g. --integration-path=uiautomation/path/dir). Note that this folder still expects the same directory structure as the one create by `bwoken init`.', :default => 'integration' 33 | #on :flags=, 'Specify custom build flags (e.g., --flags="-arch=i386,foo=bar")', :as => Array, :default => [] # TODO: implement 34 | on :formatter=, 'Specify a custom formatter (e.g., --formatter=passthru)', :default => 'colorful' 35 | on :focus=, 'Specify particular tests to run', :as => Array, :default => [] 36 | on :clobber, 'Remove any generated file' 37 | on :'skip-build', 'Do not build the iOS binary' 38 | on :configuration=, 'The build configruation to use (e.g., --configuration=Release)', :default => 'Debug' 39 | on :'sdk-version=', 'The SDK version to use (e.g., --sdk-version=6.1)' 40 | on :verbose, 'Be verbose' 41 | 42 | run { ran_command = 'test' } 43 | end 44 | 45 | end 46 | 47 | if File.exists?('Rakefile') 48 | contents = open('Rakefile').read.strip 49 | if contents =~ /\Arequire ["']bwoken\/tasks["']\Z/ 50 | STDERR.puts 'You may safely delete Rakefile' 51 | elsif contents =~ /require ["']bwoken\/tasks["']/ 52 | STDERR.puts %Q|You may safely remove the "require 'bwoken/tasks'" line from Rakefile| 53 | end 54 | end 55 | 56 | case ran_command 57 | when 'init' then Bwoken::CLI::Init.new(opts.commands['init']).run 58 | when 'test' then Bwoken::CLI::Test.new(opts.commands['test']).run 59 | else puts opts 60 | end 61 | -------------------------------------------------------------------------------- /bin/unix_instruments.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # 3 | # Copyright (c) 2013 Jonathan Penn (http://cocoamanifest.net) 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. 22 | # 23 | 24 | 25 | # unix_instruments 26 | # 27 | # A wrapper around `instruments` that returns a proper unix status code 28 | # depending on whether the run failed or not. Alas, Apple's instruments tool 29 | # doesn't care about unix status codes, so I must grep for the "Fail:" string 30 | # and figure it out myself. As long as the command doesn't output that string 31 | # anywhere else inside it, then it should work. 32 | # 33 | # I use a tee pipe to capture the output and deliver it to stdout 34 | # 35 | # Author: Jonathan Penn (jonathan@cocoamanifest.net) 36 | # 37 | 38 | set -e # Bomb on any script errors 39 | 40 | run_instruments() { 41 | # Pipe to `tee` using a temporary file so everything is sent to standard out 42 | # and we have the output to check for errors. 43 | output=$(mktemp -t unix-instruments) 44 | instruments "$@" 2>&1 | tee $output 45 | 46 | # Process the instruments output looking for anything that resembles a fail 47 | # message 48 | cat $output | get_error_status 49 | } 50 | 51 | get_error_status() { 52 | # Catch "Instruments Trace Error" 53 | # Catch "Instruments Usage Error" 54 | # Catch "00-00-00 00:00:00 +000 Fail:" 55 | # Catch "00-00-00 00:00:00 +000 Error:" 56 | # Catch "00-00-00 00:00:00 +000 None: Script threw an uncaught JavaScript error" 57 | ruby -e 'exit 1 if STDIN.read =~ /Instruments Usage Error|Instruments Trace Error|^\d+-\d+-\d+ \d+:\d+:\d+ [-+]\d+ (Fail:|Error:|None: Script threw an uncaught JavaScript error)/' 58 | } 59 | 60 | # Running this file with "----test" will try to parse an error out of whatever 61 | # is handed in to it from stdin. Use this method to double check your work if 62 | # you need a custom "get_error_status" function above. 63 | if [[ $1 == "----test" ]]; then 64 | get_error_status 65 | else 66 | run_instruments "$@" 67 | fi 68 | -------------------------------------------------------------------------------- /spec/lib/bwoken/coffeescript/github_import_string_spec.rb: -------------------------------------------------------------------------------- 1 | require 'bwoken/coffeescript/github_import_string' 2 | 3 | describe Bwoken::Coffeescript::GithubImportString do 4 | let(:string) { '#github alexvollmer/tuneup_js/tuneup.js' } 5 | subject { Bwoken::Coffeescript::GithubImportString.new(string) } 6 | 7 | describe '#parse' do 8 | it 'ensures the github repo is pulled' do 9 | subject.should_receive(:ensure_github) 10 | subject.parse 11 | end 12 | end 13 | 14 | describe '#to_s' do 15 | it 'resolves to the right path' do 16 | Bwoken.stub(:path => '/foo/integration') 17 | expect(subject.to_s).to eq(%Q|#import "/foo/integration/github/alexvollmer/tuneup_js/tuneup.js"|) 18 | end 19 | end 20 | 21 | describe '#ensure_github' do 22 | before { subject.stub(:repo_exists? => repo_exists) } 23 | context 'when repo is not downloaded' do 24 | let(:repo_exists) { false } 25 | it 'cleans and downloads the repo' do 26 | subject.should_receive(:clean_repo) 27 | subject.should_receive(:download_repo) 28 | subject.send(:ensure_github) 29 | end 30 | end 31 | 32 | context 'when repo is already downloaded' do 33 | let(:repo_exists) { true } 34 | it 'does not download the repo' do 35 | subject.should_not_receive(:download_repo) 36 | subject.send(:ensure_github) 37 | end 38 | 39 | context 'when FORCE is enabled' do 40 | around do |ex| 41 | begin 42 | ENV['FORCE_GITHUB'] = 'true' 43 | ex.run 44 | ensure 45 | ENV.delete('FORCE_GITHUB') 46 | end 47 | end 48 | 49 | it 'cleans and downloads the repo' do 50 | subject.should_receive(:clean_repo) 51 | subject.should_receive(:download_repo) 52 | subject.send(:ensure_github) 53 | end 54 | end 55 | 56 | end 57 | end 58 | 59 | describe '#clean_repo' do 60 | before { subject.stub(:repo_exists? => repo_exists) } 61 | 62 | context 'when the repo exists' do 63 | let(:repo_exists) { true } 64 | it 'deletes the repo directory' do 65 | subject.should_receive(:delete_repo) 66 | subject.send(:clean_repo) 67 | end 68 | end 69 | 70 | context 'when the repo does not exist' do 71 | let(:repo_exists) { false } 72 | it 'does not try to delete the repo directory' do 73 | subject.should_not_receive(:delete_repo) 74 | subject.send(:clean_repo) 75 | end 76 | end 77 | end 78 | 79 | describe '#delete_repo' do 80 | it 'deletes the repo' do 81 | subject.stub(:repo_path => '../github/account/project') 82 | FileUtils.should_receive(:rm_rf).with('../github/account/project') 83 | subject.send(:delete_repo) 84 | end 85 | end 86 | 87 | describe '#github_repo_path' do 88 | it 'returns the right repo path' do 89 | subject.stub(:repo_name => 'account/project') 90 | Bwoken.stub(:path => 'prefix') 91 | expect(subject.send(:repo_path)).to eq('prefix/github/account/project') 92 | end 93 | end 94 | 95 | describe '#parse_parts' do 96 | shared_examples_for 'correctly parsing repo name' do 97 | it 'parses the repo name' do 98 | expect(subject.repo_name).to eq('alexvollmer/tuneup_js') 99 | end 100 | it 'parses the file path' do 101 | expect(subject.file_path).to eq('tuneup.js') 102 | end 103 | end 104 | 105 | context 'with a non-quoted project' do 106 | let(:string) { '#github alexvollmer/tuneup_js/tuneup.js' } 107 | it_should_behave_like 'correctly parsing repo name' 108 | end 109 | 110 | context 'with a single-quoted project' do 111 | let(:string) { "#github 'alexvollmer/tuneup_js/tuneup.js'" } 112 | it_should_behave_like 'correctly parsing repo name' 113 | end 114 | 115 | context 'with a double-quoted project' do 116 | let(:string) { '#github "alexvollmer/tuneup_js/tuneup.js"' } 117 | it_should_behave_like 'correctly parsing repo name' 118 | end 119 | end 120 | 121 | describe '#download_repo' do 122 | it 'clones the repo then deletes .git' do 123 | subject.should_receive(:prepare_repo_path).ordered 124 | subject.should_receive(:clone_repo).ordered 125 | subject.should_receive(:delete_dot_git).ordered 126 | subject.send(:download_repo) 127 | end 128 | end 129 | 130 | end 131 | -------------------------------------------------------------------------------- /spec/lib/bwoken/script_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | require 'bwoken' 4 | require 'bwoken/script' 5 | require 'bwoken/device' 6 | 7 | module Bwoken 8 | class Simulator; end 9 | end 10 | 11 | describe Bwoken::Script do 12 | 13 | before { subject.formatter = mock('formatter') } 14 | 15 | describe '#run' do 16 | let(:exit_status) { 0 } 17 | before do 18 | subject.formatter.stub(:format).and_return(exit_status) 19 | subject.formatter.stub(:before_script_run) 20 | end 21 | 22 | it 'outputs that a script is about to run' do 23 | subject.path = 'path' 24 | subject.formatter.should_receive(:before_script_run).with('path') 25 | Open3.stub(:popen3) 26 | subject.stub(:cmd) 27 | subject.run 28 | end 29 | 30 | it 'runs with popen3' do 31 | subject.stub(:cmd).and_return('cmd') 32 | Open3.should_receive(:popen3).with('cmd').and_yield('in', 'out', 'err', 'wait_thr') 33 | subject.run 34 | end 35 | 36 | context 'when passing' do 37 | it 'does not raise a ScriptFailedError' do 38 | Open3.stub(:popen3).and_yield(*%w(in out err thr)) 39 | subject.stub(:cmd) 40 | expect { subject.run }.not_to raise_error 41 | end 42 | end 43 | 44 | context 'when failing' do 45 | let(:exit_status) { 1 } 46 | 47 | it 'raises a ScriptFailedError' do 48 | Open3.stub(:popen3).and_yield(*%w(in out err thr)) 49 | subject.stub(:cmd) 50 | expect { subject.run }.to raise_error(Bwoken::ScriptFailedError) 51 | end 52 | end 53 | 54 | it 'formats the output with the bwoken formatter' do 55 | subject.formatter.should_receive(:format).with("a\nb\nc").and_return(0) 56 | subject.stub(:cmd) 57 | 58 | Open3.stub(:popen3).and_yield('', "a\nb\nc", '', '') 59 | 60 | subject.run 61 | end 62 | end 63 | 64 | describe '#env_variables' do 65 | it 'returns a hash with UIASCRIPT set to #path' do 66 | Bwoken.stub(:results_path => 'foo') 67 | subject.path = 'bar' 68 | subject.env_variables['UIASCRIPT'].should == '"bar"' 69 | end 70 | 71 | it 'returns a hash with UIARESULTSPATH set to Bwoken.results_path' do 72 | Bwoken.stub(:results_path => 'foo') 73 | subject.path = 'bar' 74 | subject.env_variables['UIARESULTSPATH'].should == '"foo"' 75 | end 76 | 77 | end 78 | 79 | describe '#env_variables_for_cli' do 80 | it 'preps the variables for cli use' do 81 | subject.path = 'foo' 82 | Bwoken.stub(:results_path => 'bar') 83 | 84 | allowed = ['-e UIASCRIPT "foo" -e UIARESULTSPATH "bar"', '-e UIARESULTSPATH "bar" -e UIASCRIPT "foo"'] 85 | subject.env_variables_for_cli.should be_in(allowed) 86 | end 87 | end 88 | 89 | describe '.trace_file_path' do 90 | it 'points to the trace path inside ' do 91 | tmp_path = stub_out(Bwoken, :tmp_path, 'bazzle') 92 | subject.class.trace_file_path.should == "#{tmp_path}/trace" 93 | end 94 | 95 | end 96 | 97 | describe '#cmd' do 98 | # WORKING: 99 | # instruments -w "iPhone 5s (8.1 Simulator)" -t Automation build/iphonesimulator/BwokenTestApp.app -D integration/tmp/trace -e UIASCRIPT integration/tmp/javascript/iphone/example.js -e UIARESULTSPATH integration/tmp/results 100 | 101 | let!(:trace_file_path) { stub_out(subject.class, :trace_file_path, 'trace_file_path') } 102 | let!(:env_variables_for_cli) { stub_out(subject, :env_variables_for_cli, 'baz') } 103 | 104 | let(:uuid) { 'abcdef1234567890' } 105 | let(:app_dir) { 'bar' } 106 | let(:want_simulator) { true } 107 | let(:regexp) do 108 | /\s*instruments\s+ 109 | #{expected_device_flag_regexp} 110 | -t\s"Automation"\s+ 111 | -D\s"#{trace_file_path}"\s+ 112 | "#{app_dir}"\s+ 113 | #{env_variables_for_cli}/x 114 | end 115 | 116 | before do 117 | subject.app_dir = app_dir 118 | subject.simulator = want_simulator 119 | Bwoken::Device.stub(:uuid => uuid) 120 | end 121 | 122 | context 'when a device is connected' do 123 | let(:want_simulator) { false } 124 | let(:expected_device_flag_regexp) { "-w\\s#{uuid}\\s+" } 125 | 126 | its(:cmd) { should match regexp } 127 | end 128 | 129 | context 'when a device is not connected' do 130 | let(:expected_device_flag_regexp) { '' } 131 | 132 | its(:cmd) { should match regexp } 133 | end 134 | end 135 | 136 | end 137 | -------------------------------------------------------------------------------- /spec/lib/bwoken/build_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | require 'stringio' 4 | 5 | require 'bwoken/build' 6 | 7 | describe Bwoken::Build do 8 | 9 | before { Bwoken.stub(:app_name => 'app_name') } 10 | 11 | describe '#configuration' do 12 | it 'is always Debug' do 13 | subject.configuration.should == 'Debug' 14 | end 15 | end 16 | 17 | describe '#variables_for_cli' do 18 | it 'formats variables for xcodebuild' do 19 | subject.stub(:env_variables => {'foo' => 'bar', 'baz' => 'qux'}) 20 | subject.variables_for_cli.should be_in(['foo=bar baz=qux', 'baz=qux foo=bar']) 21 | end 22 | end 23 | 24 | describe '#cmd' do 25 | it 'returns the xcodebuild command' do 26 | Bwoken.stub(:xcworkspace => 'foo') 27 | workspace = stub_out(Bwoken, :workspace_or_project_flag, '-workspace foo') 28 | workspace_regex = workspace.gsub(/ /, '\s+') 29 | scheme = stub_out(subject, :scheme_string, "-scheme bar") 30 | scheme_regex = scheme.gsub(/ /, '\s+') 31 | configuration = stub_out(subject, :configuration, :baz) 32 | sdk = stub_out(subject, :sdk, :qux) 33 | sdk_version = stub_out(subject, :sdk_version, 123) 34 | xcconfig = stub_out(subject.class, :xcconfig, :quz) 35 | variables_for_cli = stub_out(subject, :variables_for_cli, :quux) 36 | 37 | regexp = / 38 | xcodebuild\s+ 39 | #{workspace_regex}\s+ 40 | #{scheme_regex}\s+ 41 | -configuration\s+#{configuration}\s+ 42 | -sdk\s+#{sdk}#{sdk_version}\s+ 43 | -xcconfig\s+"#{xcconfig}"\s+ 44 | #{variables_for_cli}\s+ 45 | clean\s+build 46 | /x 47 | 48 | subject.cmd.should match(regexp) 49 | end 50 | end 51 | 52 | if RUBY_VERSION == '1.8.7' 53 | describe '#compile_18' do 54 | it 'works' do 55 | pending 56 | end 57 | end 58 | else 59 | describe '#compile_19_plus' do 60 | let(:compilation_output) { "foo\nbar\nbaz\nqux\nquux" } 61 | let(:stdin) { StringIO.new } 62 | let(:stdout) { StringIO.new compilation_output } 63 | let(:stderr) { StringIO.new } 64 | let(:raw_exit_code) { 0 } 65 | let(:wait_thr) { stub(:value => raw_exit_code) } 66 | let(:formatter) { double('formatter') } 67 | 68 | before { subject.stub(:cmd => 'hi') } 69 | 70 | it "executes 'cmd'" do 71 | Open3.should_receive(:popen3) 72 | subject.compile_19_plus 73 | end 74 | 75 | it 'formats the output' do 76 | formatter.should_receive(:format_build).once 77 | subject.stub(:formatter => formatter) 78 | 79 | Open3.stub(:popen3).and_yield(stdin, stdout, stderr, wait_thr) 80 | 81 | stdout = capture_stdout { subject.compile_19_plus } 82 | end 83 | end 84 | end 85 | 86 | describe '#compile' do 87 | let(:formatter) { double('formatter') } 88 | 89 | it 'calls before_build_start on formatter' do 90 | formatter.should_receive(:before_build_start).once 91 | formatter.stub(:build_successful) 92 | subject.stub(:formatter => formatter) 93 | 94 | success = [true, '', ''] 95 | subject.stub(:compile_18 => success, :compile_19_plus => success) 96 | 97 | subject.compile 98 | end 99 | 100 | context 'build succeeds' do 101 | it 'calls the build sussessful formatter' do 102 | formatter.stub(:before_build_start) 103 | formatter.should_receive(:build_successful).with('out') 104 | subject.stub(:formatter => formatter) 105 | 106 | success = [true, 'out', 'err'] 107 | subject.stub(:compile_18 => success, :compile_19_plus => success) 108 | 109 | subject.compile 110 | end 111 | 112 | it 'does not raise an error' do 113 | formatter.stub(:before_build_start) 114 | formatter.stub(:build_successful) 115 | subject.stub(:formatter => formatter) 116 | 117 | success = [true, '', ''] 118 | subject.stub(:compile_18 => success, :compile_19_plus => success) 119 | 120 | expect { subject.compile }.not_to raise_error 121 | end 122 | end 123 | 124 | context 'build fails' do 125 | it 'calls the build failed formatter' do 126 | formatter.stub(:before_build_start) 127 | formatter.should_receive(:build_failed).with('out', 'err') 128 | subject.stub(:formatter => formatter) 129 | 130 | failure = [false, 'out', 'err'] 131 | subject.stub(:compile_18 => failure, :compile_19_plus => failure) 132 | 133 | subject.compile rescue nil 134 | end 135 | 136 | it 'raises BuildFailedError' do 137 | formatter.stub(:before_build_start) 138 | formatter.stub(:build_failed) 139 | subject.stub(:formatter => formatter) 140 | 141 | failure = [false, '', ''] 142 | subject.stub(:compile_18 => failure, :compile_19_plus => failure) 143 | 144 | expect { subject.compile }.to raise_error(Bwoken::BuildFailedError) 145 | end 146 | end 147 | end 148 | 149 | end 150 | -------------------------------------------------------------------------------- /lib/bwoken/cli/test.rb: -------------------------------------------------------------------------------- 1 | require 'slop' 2 | require 'rake/file_list' 3 | require 'fileutils' 4 | 5 | require 'bwoken' 6 | require 'bwoken/build' 7 | require 'bwoken/coffeescript' 8 | require 'bwoken/device' 9 | #TODO: make formatters dynamically loadable during runtime 10 | require 'bwoken/formatter' 11 | require 'bwoken/formatters/passthru_formatter' 12 | require 'bwoken/formatters/colorful_formatter' 13 | require 'bwoken/script_runner' 14 | 15 | module Bwoken 16 | module CLI 17 | class Test 18 | 19 | def self.help_banner 20 | <<-BANNER 21 | Run your tests. If you don't specify which tests, bwoken will run them all 22 | 23 | bwoken test --simulator # runs all tests in the simulator 24 | 25 | You can specify a device family if you only want to run, say, iPad tests: 26 | 27 | bwoken test --family ipad 28 | 29 | If you only want to run a specific test, you can focus on it: 30 | 31 | bwoken test --focus login # runs iPhone and iPad tests named "login" 32 | 33 | 34 | == Options == 35 | BANNER 36 | end 37 | 38 | attr_accessor :options 39 | 40 | # opts - A slop command object (acts like super-hash) 41 | # :clobber - remove all generated files, including iOS build 42 | # :family - enum of [nil, 'iphone', 'ipad'] (case-insensitive) 43 | # :flags - custom build flag array (default: []) TODO: not yet implmented 44 | # :focus - which tests to run (default: [], meaning "all") 45 | # :formatter - custom formatter (default: 'colorful') 46 | # :scheme - custom scheme (default: nil) 47 | # :simulator - should force simulator use (default: nil) 48 | # :skip-build - do not build the iOS binary 49 | # :verbose - be verbose 50 | # :integration-path - the base directory for all the integration files 51 | # :product-name - the name of the generated .app file if it is different from the name of the project/workspace 52 | # :configuration - typically "Debug" or "Release" 53 | # :sdk-version - the version of the sdk to use when building 54 | def initialize opts 55 | opts = opts.to_hash if opts.is_a?(Slop) 56 | self.options = opts.to_hash.tap do |o| 57 | o[:formatter] = 'passthru' if o[:verbose] 58 | o[:formatter] = select_formatter(o[:formatter]) 59 | o[:simulator] = use_simulator?(o[:simulator]) 60 | o[:family] = o[:family] 61 | end 62 | 63 | Bwoken.integration_path = options[:'integration-path'] 64 | Bwoken.app_name = options[:'product-name'] 65 | end 66 | 67 | def run 68 | clobber if options[:clobber] 69 | compile unless options[:'skip-build'] 70 | clean 71 | transpile 72 | test 73 | end 74 | 75 | def compile 76 | Build.new do |b| 77 | #b.flags = opts.flags #TODO: implement 78 | b.formatter = options[:formatter] 79 | b.scheme = options[:scheme] if options[:scheme] 80 | b.simulator = options[:simulator] 81 | b.configuration = options[:configuration] 82 | b.sdk_version = options[:'sdk-version'] 83 | end.compile 84 | end 85 | 86 | def transpile 87 | integration_dir = Bwoken.integration_path 88 | coffeescripts = Rake::FileList["#{integration_dir}/coffeescript/**/*.coffee"] 89 | compiled_coffee = coffeescripts.pathmap("%{^#{integration_dir}/coffeescript,#{integration_dir}/tmp/javascript}d/%n.js") 90 | javascripts = Rake::FileList["#{integration_dir}/javascript/**/*.js"] 91 | copied_javascripts = javascripts.pathmap("%{^#{integration_dir}/javascript,#{integration_dir}/tmp/javascript}d/%f") 92 | 93 | compiled_coffee.zip(coffeescripts).each do |target, source| 94 | containing_dir = target.pathmap('%d') 95 | ensure_directory containing_dir 96 | Bwoken::Coffeescript.compile source, target 97 | end 98 | 99 | copied_javascripts.zip(javascripts).each do |target, source| 100 | containing_dir = target.pathmap('%d') 101 | ensure_directory containing_dir 102 | FileUtils.cp source, target 103 | end 104 | end 105 | 106 | def test 107 | ScriptRunner.new do |s| 108 | s.app_dir = Build.app_dir(options[:simulator]) 109 | s.family = options[:family] 110 | s.focus = options[:focus] 111 | s.formatter = options[:formatter] 112 | s.simulator = options[:simulator] 113 | s.device = options[:device] 114 | end.execute 115 | end 116 | 117 | def clobber 118 | FileUtils.rm_rf Bwoken.tmp_path 119 | FileUtils.rm_rf Bwoken::Build.build_path 120 | end 121 | 122 | def clean 123 | FileUtils.rm_rf Bwoken.test_suite_path 124 | end 125 | 126 | def select_formatter formatter_name 127 | case formatter_name 128 | when 'passthru' then Bwoken::PassthruFormatter.new 129 | else Bwoken::ColorfulFormatter.new 130 | end 131 | end 132 | 133 | def use_simulator? want_forced_simulator 134 | want_forced_simulator || ! Bwoken::Device.connected? 135 | end 136 | 137 | def ensure_directory dir 138 | FileUtils.mkdir_p dir 139 | end 140 | 141 | end 142 | 143 | end 144 | end 145 | -------------------------------------------------------------------------------- /spec/lib/bwoken/formatter_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'bwoken/formatter' 3 | 4 | describe Bwoken::Formatter do 5 | describe '.format' do 6 | it 'calls format on a new instance' do 7 | formatter_stub = double('formatter') 8 | formatter_stub.should_receive(:format).with('foo') 9 | Bwoken::Formatter.stub(:new => formatter_stub) 10 | Bwoken::Formatter.format 'foo' 11 | end 12 | end 13 | 14 | describe '.on' do 15 | let(:klass) { klass = Class.new(Bwoken::Formatter) } 16 | it 'defines an appropriately named instance method' do 17 | klass.on(:foo) {|line| ''} 18 | klass.new.should respond_to('_on_foo_callback') 19 | end 20 | 21 | it 'defines the instance method with the passed-in block' do 22 | klass.on(:bar) {|line| 42 } 23 | klass.new._on_bar_callback('').should == 42 24 | end 25 | end 26 | 27 | describe 'default log_level formatters' do 28 | %w(pass fail debug other).each do |log_level| 29 | specify "for #{log_level} outputs the passed-in line" do 30 | formatter = Bwoken::Formatter.new 31 | out = capture_stdout do 32 | formatter.send("_on_#{log_level}_callback", "- #{log_level}") 33 | end 34 | out.should == "- #{log_level}\n" 35 | end 36 | end 37 | end 38 | 39 | describe '#line_demuxer' do 40 | 41 | context 'for a passing line' do 42 | it 'calls _on_pass_callback' do 43 | subject.should_receive(:_on_pass_callback).with('1234 a a Pass') 44 | subject.line_demuxer('1234 a a Pass', 0) 45 | end 46 | it 'returns 0' do 47 | exit_status = 0 48 | capture_stdout do 49 | exit_status = subject.line_demuxer('1234 a a Pass', 0) 50 | end 51 | exit_status.should == 0 52 | end 53 | end 54 | 55 | context 'for a failing line' do 56 | context 'Fail error' do 57 | it 'calls _on_fail_callback' do 58 | subject.should_receive(:_on_fail_callback).with('1234 a a Fail') 59 | subject.line_demuxer('1234 a a Fail', 0) 60 | end 61 | end 62 | 63 | context 'Instruments Trace Error message' do 64 | it 'calls _on_fail_callback' do 65 | msg = 'Instruments Trace Error foo' 66 | subject.should_receive(:_on_fail_callback).with(msg) 67 | subject.line_demuxer(msg, 0) 68 | end 69 | end 70 | 71 | it 'returns 1' do 72 | exit_status = 0 73 | capture_stdout do 74 | exit_status = subject.line_demuxer('1234 a a Fail', 0) 75 | end 76 | exit_status.should == 1 77 | end 78 | end 79 | 80 | context 'for a debug line' do 81 | it 'calls _on_debug_callback' do 82 | subject.should_receive(:_on_debug_callback).with('1234 a a feh') 83 | subject.line_demuxer('1234 a a feh', 0) 84 | end 85 | end 86 | 87 | context 'for any other line' do 88 | it 'calls _on_other_callback' do 89 | subject.should_receive(:_on_other_callback).with('blah blah blah') 90 | subject.line_demuxer('blah blah blah', 0) 91 | end 92 | end 93 | end 94 | 95 | describe '#format' do 96 | it 'calls the demuxer for each line' do 97 | subject.should_receive(:line_demuxer).exactly(3).times 98 | subject.format("a\nb\nc") 99 | end 100 | 101 | context 'when no lines fail' do 102 | it 'returns 0' do 103 | subject.should_receive(:line_demuxer).with("a\n", 0).ordered.and_return(0) 104 | subject.should_receive(:line_demuxer).with("b\n", 0).ordered.and_return(0) 105 | subject.should_receive(:line_demuxer).with("c", 0).ordered.and_return(0) 106 | subject.format("a\nb\nc").should == 0 107 | end 108 | end 109 | 110 | context 'when any line fails' do 111 | it 'returns 1' do 112 | subject.should_receive(:line_demuxer).with("a\n", 0).ordered.and_return(0) 113 | subject.should_receive(:line_demuxer).with("b\n", 0).ordered.and_return(1) 114 | subject.should_receive(:line_demuxer).with("c", 1).ordered.and_return(1) 115 | subject.format("a\nb\nc").should == 1 116 | end 117 | end 118 | end 119 | 120 | describe '#format_build' do 121 | it 'replaces output lines with dots' do 122 | out = capture_stdout do 123 | subject.format_build("a\nb\nc\n") 124 | end 125 | out.should == '...' 126 | end 127 | 128 | it 'ignores empty lines' do 129 | out = capture_stdout do 130 | subject.format_build("\n\n\n") 131 | end 132 | out.should == '' 133 | end 134 | 135 | it 'returns the passed in build text' do 136 | build_text = '' 137 | capture_stdout do 138 | build_text = subject.format_build("a\nb\nc\n") 139 | end 140 | build_text.should == "a\nb\nc\n" 141 | end 142 | 143 | end 144 | 145 | describe '#build_successful build_log' do 146 | it 'displays build successful' do 147 | out = capture_stdout do 148 | subject.build_successful('foo') 149 | end 150 | out.should == "\n\n### Build Successful ###\n\n" 151 | end 152 | 153 | end 154 | 155 | describe '#build_failed build_log, error_log' do 156 | it 'displays the build_log' do 157 | out = capture_stdout do 158 | subject.build_failed('build', 'bar') 159 | end 160 | out.should =~ /build/ 161 | end 162 | 163 | it 'displays the error_log' do 164 | out = capture_stdout do 165 | subject.build_failed('foo', 'error') 166 | end 167 | out.should =~ /error/ 168 | end 169 | 170 | end 171 | 172 | end 173 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Bwoken ![build status](https://secure.travis-ci.org/bendyworks/bwoken.png?branch=master) 2 | 3 | Runs your UIAutomation tests from the command line for both iPhone and iPad, in the simulator or on your device. 4 | 5 | Supports coffeescript and javascript. 6 | 7 | ![screenshot](https://raw.github.com/bendyworks/bwoken/master/doc/screenshot.png) 8 | 9 | 10 | ## On the Command Line 11 | 12 | ### Running tests 13 | 14 | Make sure bwoken is properly installed. Then, build your project and run all your tests via: 15 | 16 |
# will build and run all of your tests
 17 | $ bwoken test
 18 | 
 19 | # will run one file, relative to integration/coffeescript/{iphone,ipad}/
 20 | #  (note: no file extension)
 21 | $ bwoken test --focus some_test # runs this test on (iphone and ipad) OR (connected iDevice)
 22 | $ bwoken test --focus some_test --family iphone
 23 | 
24 | 25 | ### structuring your test files 26 | To add new coffeescript test, add those file to the /integration/coffeescript/iphone or /integration/coffeescript/ipad folder. 27 | To add new javascript test, if the iphone and ipad folder don't exist in the /integration/javascript forlder, create them. 28 | Then copy your test file inside those folder depending on your target. 29 | Your file hierarchy should look something like this 30 |

 31 | | integration
 32 | 	| coffeescript
 33 | 		| iphone
 34 | 			| exemple.coffee
 35 | 		| ipad
 36 | 			| example.coffee
 37 | 	| javascript
 38 | 		| iphone
 39 | 			| myTest.js
 40 | 		| ipad
 41 | 			| myiPadtest.js
 42 | 	|tmp
 43 | 
44 | 45 | Note that your test scripts inside the coffeescript and javascript folder will be copied to their equivalent folder in the /tmp folder(ie: /tmp/javascript/iphone/myTest.js), 46 | so all your import statement should be relative to that location (ie: #import "../filetoImport.js" for file in the javascript folder) 47 | 48 | ### Simulator or Device? 49 | 50 | To run bwoken tests on your device, just plug it in! And if you want to run tests in the simulator, just unplug it! 51 | 52 |
# without a device connected, will run on the simulator:
 53 | $ bwoken test
 54 | 
 55 | # with a device connected, will run on the device:
 56 | $ bwoken test
 57 | 
 58 | # with a device connected, will run on the simulator:
 59 | $ bwoken test --simulator
 60 | 
61 | 62 | Your tests will look something like this: 63 | 64 |
$ bwoken test
 65 | Building.............................................................................
 66 | .....................................................................................
 67 | .....................................................................................
 68 | .....................................................................................
 69 | .....................................................................................
 70 | .....................................................................................
 71 | .....................................................................................
 72 | ................................................................................
 73 | Build Successful!
 74 | 
 75 | iphone  favorites.js
 76 | Start:  Favoriting a repository
 77 | Debug:  tap tableViews["Repositories"].cells["CITravis by Travis-ci"]
 78 | Debug:  tap navigationBar.rightButton
 79 | Debug:  tap actionSheet.elements["Add"]
 80 | Debug:  tap navigationBar.leftButton
 81 | Debug:  tap navigationBar.elements["Favorites"]
 82 | Debug:  navigationBar.elements["Favorites"].scrollToVisible
 83 | Debug:  tap navigationBar.elements["All"]
 84 | Pass:   Favoriting a repository
 85 | Start:  Unfavoriting a repository
 86 | Debug:  tap navigationBar.elements["Favorites"]
 87 | Debug:  tap tableViews["Repositories"].cells["CITravis by Travis-ci"]
 88 | Debug:  tap navigationBar.rightButton
 89 | Debug:  tap actionSheet.elements["Remove"]
 90 | Debug:  tap navigationBar.leftButton
 91 | Debug:  should be true null
 92 | Debug:  tap navigationBar.elements["All"]
 93 | Pass:   Unfavoriting a repository
 94 | 
 95 | Complete
 96 |  Duration: 23.419741s
 97 | 
98 | 99 | ### All the switches 100 | 101 | Here's a list of all the switches that bwoken takes for the `test` command: 102 | 103 |

104 | $ bwoken test -h
105 | [...]
106 |         --simulator             Use simulator, even when an iDevice is connected
107 |         --device                Use given device (name or id)
108 |         --family                Test only one device type, either ipad or iphone. Default is to test on both
109 |         --scheme                Specify a custom scheme
110 |         --product-name          Specify a custom product name (e.g. --product-name="My Product"). Default is the name of of the xcodeproj file
111 |         --integration-path      Specify a custom directory to store your test scripts in (e.g. --integration-path=uiautomation/path/dir). Note that this folder still expects the same directory structure as the one create by `bwoken init`.
112 |         --formatter             Specify a custom formatter (e.g., --formatter=passthru)
113 |         --focus                 Specify particular tests to run
114 |         --clobber               Remove any generated file
115 |         --skip-build            Do not build the iOS binary
116 |         --configuration         The build configruation to use (e.g., --configuration=Release)
117 |         --sdk-version           The SDK version to use (e.g., --sdk-version=6.1)
118 |         --verbose               Be verbose
119 |     -h, --help                  Display this help message.
120 | 
121 | 122 | ## In Your Code 123 | 124 | ### Like Javascript? 125 | 126 | Sometimes we'd like to have some javascript help us out. For example, what if you'd like [Underscore.js](http://underscorejs.org) in your test suite? Simple! Just put it in integration/javascript and import it in your test: 127 | 128 |
#import "../underscore.js"
129 | 
130 | 131 | ### Bring in Libraries! 132 | 133 | Wanna bring in [tuneup.js](https://github.com/alexvollmer/tuneup_js), [mechanic](https://github.com/jaykz52/mechanic), or [underscore](http://underscorejs.org) without manually downloading them first? Just use `#github` instead of `#import`: 134 | 135 |
#github "jashkenas/underscore/underscore.js"
136 | #github "alexvollmer/tuneup_js/tuneup.js"
137 | #github "jaykz52/mechanic/src/mechanic-core.js"
138 | 
139 | 140 | 141 | ## Installation 142 | 143 | ### Create an iOS project 144 | 145 | If you don't have an iOS project already, go ahead and create it. If you already have a project, no worries: you can install bwoken at any point. 146 | 147 | Ensure your project is in a workspace rather than simply a project: 148 | 149 | * In Xcode, select File -> Save as workspace... 150 | * Save the workspace in the same directory as your .xcodeproj file 151 | 152 | Note: This is done automatically if you use [CocoaPods](http://cocoapods.org/). I highly suggest you do! 153 | 154 | ### Prerequisites 155 | 156 | Ensure Xcode is up-to-date. 157 | 158 | Install rvm via the instructions. Ensure your after_cd_bundler rvm hook is enabled: 159 | 160 |
$ chmod u+x ~/.rvm/hooks/after_cd_bundler
161 | 
162 | 163 | ### Install 164 | 165 | **NOTE:** Do **NOT** use `sudo` to install or run bwoken. It will almost definitely fail you. 166 | 167 | In the terminal, inside the directory of your project (e.g., you should see a ProjectName.xcodeproj file), create .ruby-version and .ruby-gemset files and trigger their use: 168 | 169 |
$ echo '2.1.0' > .ruby-version
170 | $ echo 'my_project' > .ruby-gemset
171 | $ cd .
172 | 
173 | 174 | Install bundler (a ruby library dependency manager) and init: 175 | 176 |
$ gem install bundler # idempotent; might already be installed and that's ok
177 | $ bundle init
178 | 
179 | 180 | This will create a Gemfile. Add bwoken to it and bundle: 181 | 182 |
$ echo "gem 'bwoken', '2.0.0'" >> Gemfile
183 | $ bundle
184 | 
185 | 186 | The last installation step is to initialize the bwoken file structure: 187 | 188 |
$ bwoken init
189 | 
190 | 191 | Now, you can start using it! 192 | 193 | #### The Dirty Little Installation Method 194 | 195 | Technically, you can skip this entire Installation section and just run `sudo gem install bwoken && bwoken init`. This is listed here for completeness, but you really shouldn't install gems this way. This installation method will almost certainly not work with versions of OS X prior to Mavericks. 196 | 197 | ## Contributors 198 | 199 | Special thank you goes out to everyone who's helped with bwoken. Here's a (probably incomplete) list of those folks: 200 | 201 | * Brad Grzesiak ([listrophy](https://github.com/listrophy)) 202 | * Jaymes Waters ([jaym3s](https://github.com/jaym3s)) 203 | * Jonathan Penn ([jonathanpenn](https://github.com/jonathanpenn)) 204 | * Ryland Herrick ([rylnd](https://github.com/rylnd)) 205 | * Whitney Young ([wbyoung](https://github.com/wbyoung)) 206 | * David Gagnon ([mrdavidgagnon](https://github.com/mrdavidgagnon)) 207 | * [otusweb](https://github.com/otusweb) 208 | * Alec Gorge ([alecgorge](https://github.com/alecgorge)) 209 | 210 | ## Contributing 211 | 212 | 1. Fork it 213 | 2. Create your feature branch (`git checkout -b my-new-feature`) 214 | 3. Commit your changes (`git commit -am 'Added some feature'`) 215 | 4. Push to the branch (`git push origin my-new-feature`) 216 | 5. Create new Pull Request 217 | --------------------------------------------------------------------------------