Class: Calabash::Cucumber::Launcher

Inherits:
Object
  • Object
show all
Includes:
Logging, SimulatorAccessibility
Defined in:
lib/calabash-cucumber/launcher.rb

Overview

Used to launch apps for testing in iOS Simulator or on iOS Devices. By default it uses Apple’s ‘instruments` process to launch your app, but has legacy support for using `sim_launcher`.

### Accessing the current launcher from ruby.

If you need a reference to the current launcher in your ruby code. This is usually not required, but might be useful in ‘support/01_launch.rb`.

‘Calabash::Cucumber::Launcher.launcher`

### Attaching to the current launcher in a console

If Calabash already running and you want to attach to the current launcher, use ‘console_attach`. This is useful when a cucumber Scenario has failed and you want to query the current state of the app.

  • **Pro Tip:** set the ‘NO_STOP` environmental variable to 1 so calabash does

not exit the simulator when a Scenario fails.

Constant Summary collapse

KNOWN_PRIVACY_SETTINGS =

A hash of known privacy settings that calabash can control.

{:photos => 'kTCCServicePhotos', :calendar => 'kTCCServiceCalendar', :address_book => 'kTCCServiceAddressBook'}

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Methods included from SimulatorAccessibility

#launch_simulator, #quit_simulator, #reset_simulator_content_and_settings

Methods included from Logging

#calabash_info, #calabash_warn

Instance Attribute Details

#actions=(value) ⇒ Object

Sets the attribute actions

Parameters:

  • value

    the value to set the attribute actions to.



55
56
57
# File 'lib/calabash-cucumber/launcher.rb', line 55

def actions=(value)
  @actions = value
end

#deviceObject

Returns the value of attribute device.



54
55
56
# File 'lib/calabash-cucumber/launcher.rb', line 54

def device
  @device
end

#launch_argsObject

Returns the value of attribute launch_args.



56
57
58
# File 'lib/calabash-cucumber/launcher.rb', line 56

def launch_args
  @launch_args
end

#run_loopObject

Returns the value of attribute run_loop.



53
54
55
# File 'lib/calabash-cucumber/launcher.rb', line 53

def run_loop
  @run_loop
end

#simulator_launcherObject

Returns the value of attribute simulator_launcher.



57
58
59
# File 'lib/calabash-cucumber/launcher.rb', line 57

def simulator_launcher
  @simulator_launcher
end

Class Method Details

.attachObject



92
93
94
95
96
# File 'lib/calabash-cucumber/launcher.rb', line 92

def self.attach
  l = launcher
  return l if l && l.active?
  l.attach
end

.instruments?Boolean

Are we running using instruments?

Returns:

  • (Boolean)

    true if we’re using instruments to launch



145
146
147
148
149
# File 'lib/calabash-cucumber/launcher.rb', line 145

def self.instruments?
  l = launcher_if_used
  return false unless l
  l.instruments?
end

.launcherCalabash::Cucumber::Launcher

Get a reference to the current launcher (instantiates a new one if needed). Usually we use a singleton launcher throughout a test run.

Returns:



153
154
155
# File 'lib/calabash-cucumber/launcher.rb', line 153

def self.launcher
  @@launcher ||= Calabash::Cucumber::Launcher.new
end

.launcher_if_usedCalabash::Cucumber::Launcher

Get a reference to the current launcher (does not instantiate a new one if unset). Usually we use a singleton launcher throughout a test run.

Returns:



160
161
162
# File 'lib/calabash-cucumber/launcher.rb', line 160

def self.launcher_if_used
  @@launcher
end

Instance Method Details

#attach(max_retry = 1, timeout = 10) ⇒ Object



99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
# File 'lib/calabash-cucumber/launcher.rb', line 99

def attach(max_retry=1, timeout=10)
  if calabash_no_launch?
    self.actions= Calabash::Cucumber::PlaybackActions.new
    return
  end

  pids_str = `ps x -o pid,command | grep -v grep | grep "instruments" | awk '{printf "%s,", $1}'`
  pids = pids_str.split(',').map { |pid| pid.to_i }
  pid = pids.first
  run_loop = {}
  if pid
    run_loop[:pid] = pid
    self.actions= Calabash::Cucumber::InstrumentsActions.new
  else
    self.actions= Calabash::Cucumber::PlaybackActions.new
  end

  # Sets the device attribute.
  ensure_connectivity(max_retry, timeout)

  if self.device.simulator?
    run_loop[:uia_strategy] = :preferences
  else
    if self.device.ios_major_version < '8'
      run_loop[:uia_strategy] = :preferences
    else
      run_loop[:uia_strategy] = :host
    end
  end

  self.run_loop = run_loop
  major = self.device.ios_major_version
  if major.to_i >= 7 && self.actions.is_a?(Calabash::Cucumber::PlaybackActions)
    puts "\n\n WARNING \n\n"
    puts 'Warning Trying to connect to simulator that was not launched by Calabash/instruments.'
    puts 'To fix this you must let Calabash or instruments launch the app'
    puts 'Continuing... query et al will work.'
    puts "\n\n WARNING \n\n"
    puts 'Please read: https://github.com/calabash/calabash-ios/wiki/A0-UIAutomation---instruments-problems'
  end
  self
end

#ios_major_versionString

“Major” component of the current iOS version of the device

Returns:

  • (String)

    the “major” component, e.g., “7” for “7.1.1”



166
167
168
169
170
171
172
# File 'lib/calabash-cucumber/launcher.rb', line 166

def ios_major_version
  # pinging the app will set self.device
  ping_app if self.device.nil?
  # guard against Runtime errors
  return nil if device.nil? or device.ios_version.nil?
  device.ios_major_version
end

#ios_versionString

the current iOS version of the device

Returns:

  • (String)

    the current iOS version of the device



176
177
178
179
# File 'lib/calabash-cucumber/launcher.rb', line 176

def ios_version
  return nil if device.nil?
  device.ios_version
end

#relaunch(args = {}) ⇒ Object

Note:

an important part of relaunch behavior is controlled by environment variables, specified below

Launches your app on the connected device or simulator. Stops the app if it is already running. ‘relaunch` does a lot of error detection and handling to reliably start the app and test. Instruments (particularly the cli) has stability issues which we workaround by restarting the simulator process and checking that UIAutomation is correctly attaching.

Takes optional args to specify details of the launch (e.g. device or simulator, sdk version, target device, launch method…). The two most important environment variables are ‘DEVICE_TARGET` and `APP_BUNDLE_PATH`.

  • ‘DEVICE_TARGET` controls which device you’re running on. To see the options run: ‘instruments -s devices`. In addition you can specify `DEVICE_TARGET=device` to run on a (unique) usb-connected device.

  • ‘APP_BUNDLE_PATH` controls which `.app` bundle to launch in simulator (don’t use for on-device testing, instead use ‘BUNDLE_ID`).

  • ‘BUNDLE_ID` used with `DEVICE_TARGET=device` to specify which app to launch on device

  • ‘DEBUG` - set to “1” to obtain debug info (typically used to debug launching, UIAutomation and other issues)

  • ‘DEBUG_HTTP` - set to “1” to show raw HTTP traffic

Examples:

Launching on iPad simulator with DEBUG settings

DEBUG_HTTP=1 DEVICE_TARGET="iPad - Simulator - iOS 7.1" DEBUG=1 APP_BUNDLE_PATH=FieldServiceiOS.app bundle exec calabash-ios console

Parameters:

  • args (Hash) (defaults to: {})

    optional args to specify details of the launch (e.g. device or simulator, sdk version, target device, launch method…).

Options Hash (args):

  • :app (String) — default: detect the location of the bundle from project settings

    app bundle path

  • :bundle_id (String)

    if launching on device, specify this or env ‘BUNDLE_ID` to be the bundle identifier of the application to launch

  • :privacy_settings (Hash)

    preset privacy settings for the, e.g., ‘=> {:allow => true}`. See KNOWN_PRIVACY_SETTINGS



514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
# File 'lib/calabash-cucumber/launcher.rb', line 514

def relaunch(args={})
  #TODO stopping is currently broken, but this works anyway because instruments stop the process before relaunching
  RunLoop.stop(run_loop) if run_loop

  # @todo Don't overwrite the _args_ parameter!
  args = default_launch_args.merge(args)

  # RunLoop::Core.run_with_options can reuse the SimControl instance.  Many
  # of the Xcode tool calls, like instruments -s templates, take a long time
  # to execute.  The SimControl instance has XCTool attribute which caches
  # the results of many of these time-consuming calls so they only need to
  # be called 1 time per launch.
  # @todo Use SimControl in Launcher in place of methods like simulator_target?
  args[:sim_control] = RunLoop::SimControl.new

  args[:app] = args[:app] || args[:bundle_id] || app_path || detect_app_bundle_from_args(args)


  if args[:app]
    if File.directory?(args[:app])
      args[:app] = File.expand_path(args[:app])
    else
      # args[:app] is not a directory so must be a bundle id
      if simulator_target?(args) ## bundle id set, but simulator target
        args[:app] = app_path || detect_app_bundle_from_args(args)
      end
    end
  end

  unless args[:app]
    if simulator_target?(args)
      device_xamarin_build_dir = 'iPhoneSimulator'
    else
      device_xamarin_build_dir = 'iPhone'
    end
    args[:app] = self.simulator_launcher.app_bundle_or_raise(app_path, device_xamarin_build_dir)
  end

  args[:bundle_id] ||= detect_bundle_id_from_app_bundle(args)

  args[:device] ||= detect_device_from_args(args)

  if simulator_target?(args) and args[:reset]
    # attempt to find the sdk version from the :device_target
    sdk = sdk_version_for_simulator_target(args)

    # *** LEGACY SUPPORT ***
    # If DEVICE_TARGET has not been set and is not a device UDID, then
    # :device_target will be 'simulator'.  In that case, we cannot know what
    # SDK version of the app sandbox we should reset.  The user _might_ give
    # us a hint with SDK_VERSION, but we want to deprecate that variable ASAP.
    #
    # If passed a nil SDK arg, reset_app_sandbox will reset the _latest_ SDK.
    # This is not good, because this is probably _not_ the SDK that should be
    # reset.  Our only option is to reset every sandbox for all SDKs by
    # passing :sdk => :all to reset_app_sandbox.
    if sdk.nil? and args[:device_target] == 'simulator'
      sdk = :all
    end
    reset_app_sandbox({:sdk => sdk,
                       :path => args[:app],
                       :udid => args[:udid],
                       :sim_control => args[:sim_control]})
  end

  if args[:privacy_settings]
    if simulator_target?(args)
      update_privacy_settings(args[:bundle_id], args[:privacy_settings])
    else
      #Not supported on device
      puts 'Warning: :privacy_settings not supported on device'
    end
  end

  # The public API is true/false, but we need to pass a path to a dylib to
  # run-loop.
  if args.fetch(:inject_dylib, false)
    if simulator_target?(args)
      args[:inject_dylib] = Calabash::Dylibs.path_to_sim_dylib
    else
      args[:inject_dylib] = Cucumber::Dylibs.path_to_device_dylib
    end
  end

  if run_with_instruments?(args)
    # Patch for bug in Xcode 6 GM + iOS 8 device testing.
    # http://openradar.appspot.com/radar?id=5891145586442240
    uia_strategy = default_uia_strategy(args, args[:sim_control])
    args[:uia_strategy] ||= uia_strategy
    calabash_info "Using uia strategy: '#{args[:uia_strategy]}'" if debug_logging?

    self.run_loop = new_run_loop(args)
    self.actions= Calabash::Cucumber::InstrumentsActions.new
  else
    # run with sim launcher
    self.actions= Calabash::Cucumber::PlaybackActions.new
    # why not just pass args - AFAICT args[:app] == app_path?
    self.simulator_launcher.relaunch(app_path, sdk_version(), args)
  end
  self.launch_args = args

  unless args[:calabash_lite]
    ensure_connectivity
    # skip compatibility check if injecting dylib
    unless args.fetch(:inject_dylib, false)
      check_server_gem_compatibility
    end
  end
end

#reset_app_jail(sdk = nil, path = nil) ⇒ Object

Deprecated.

0.10.0 Replaced with #reset_app_sandbox.

Reset the app sandbox for a device.



183
184
185
186
187
# File 'lib/calabash-cucumber/launcher.rb', line 183

def reset_app_jail(sdk=nil, path=nil)
  # will be deprecated in a future version
  #_deprecated('0.10.0', 'use reset_app_sandbox instead', :warn)
  reset_app_sandbox({:sdk => sdk, :path => path})
end

#reset_app_sandbox(opts = {}) ⇒ Object

Note:

It is not recommended that you call this method directly. See the examples below for how use the ‘RESET_BETWEEN_SCENARIOS` environmental variable to reset the app sandbox.

Note:

This method is only available for the iOS Simulator.

Note:

Generates a warning if called when targeting a physical device and otherwise has no effect.

Note:

When testing against the Xamarin Test Cloud, this method is never called. Use the ‘RESET_BETWEEN_SCENARIOS` environmental variable. See the examples.

Resets the app’s content and settings by deleting the following directories from application sandbox:

  • Library

  • Documents

  • tmp

Examples:

Use ‘RESET_BETWEEN_SCENARIOS` to reset the app sandbox before every Scenario.

When testing devices outside the Xamarin Test Cloud this has no effect.

On the Xamarin Test Cloud, the app sandbox will be reset, but this method
will not be called; the resetting is done via an alternative mechanism.

When testing simulators, this method will be called.

Launch cucumber with RESET_BETWEEN_SCENARIOS=1

$ RESET_BETWEEN_SCENARIOS=1 bundle exec cucumber

Use tags and a Before hook to reset the app sandbox before specific Scenarios.

# in your .feature file

@reset_app_before_hook
Scenario:  some scenario that requires the app be reset

# in your support/01_launch.rb file
#
# 1. add a Before hook
Before('@reset_app_before_hook') do
  ENV['RESET_BETWEEN_SCENARIOS'] = '1'
end

# 2. after launching, revert the env var value
Before do |scenario|
  # launch the app
  launcher = Calabash::Cucumber::Launcher.new
  unless launcher.calabash_no_launch?
    launcher.relaunch
    launcher.calabash_notify(self)
  end
  # disable resetting between Scenarios
  ENV['RESET_BETWEEN_SCENARIOS'] = ''
end

Parameters:

  • opts (Hash) (defaults to: {})

    can pass the target sdk or the path to the application bundle

Options Hash (opts):

  • :sdk (String, Symbol) — default: nil

    The target sdk. If nil is passed, then only app sandbox for the latest sdk will be deleted. If ‘:all` is passed, then the sandboxes for all sdks will be deleted.

  • :path (String) — default: nil

    path to the application bundle



251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
# File 'lib/calabash-cucumber/launcher.rb', line 251

def reset_app_sandbox(opts={})

  if device_target?
    calabash_warn("calling 'reset_app_sandbox' when targeting a device.")
    return
  end

  default_opts = {:sdk => nil, :path => nil}
  merged_opts = default_opts.merge opts

  sim_control = opts.fetch(:sim_control, RunLoop::SimControl.new)
  if sim_control.xcode_version_gte_6?
    default_sim = RunLoop::Core.default_simulator(sim_control.xctools)
    name_or_udid = merged_opts[:udid] || ENV['DEVICE_TARGET'] || default_sim

    target_simulator = nil
    sim_control.simulators.each do |device|
      instruments_launch_name = "#{device.name} (#{device.version.to_s} Simulator)"
      if instruments_launch_name == name_or_udid or device.udid == name_or_udid
        target_simulator = device
      end
    end

    if target_simulator.nil?
      raise "Could not find a simulator that matches '#{name_or_udid}'"
    end

    sim_control.reset_sim_content_and_settings({:sim_udid => target_simulator.udid})
  else
    sdk ||= merged_opts[:sdk] || sdk_version || self.simulator_launcher.sdk_detector.latest_sdk_version
    path ||= merged_opts[:path] || self.simulator_launcher.app_bundle_or_raise(app_path)

    app = File.basename(path)

    directories_for_sdk_prefix(sdk).each do |sdk_dir|
      app_dir = File.expand_path("#{sdk_dir}/Applications")
      next unless File.exists?(app_dir)

      bundle = `find "#{app_dir}" -type d -depth 2 -name "#{app}" | head -n 1`

      next if bundle.empty? # Assuming we're already clean

      if debug_logging?
        puts "Reset app state for #{bundle}"
      end
      sandbox = File.dirname(bundle)
      ['Library', 'Documents', 'tmp'].each do |content_dir|
        FileUtils.rm_rf(File.join(sandbox, content_dir))
      end
    end
  end
end

#reset_simulatorObject

Note:

WARNING This is a destructive operation. You have been warned.

Erases the contents and setting for every available simulator.

For Xcode 6, this is equivalent to calling: ‘$ xcrun simctl erase` on every available simulator. For Xcode < 6, it is equivalent to touching the ’Reset Content & Settings’ menu item.

Raises:

  • RuntimeError if called when targeting a physical device



314
315
316
317
318
319
# File 'lib/calabash-cucumber/launcher.rb', line 314

def reset_simulator
  if device_target?
    raise "Calling 'reset_simulator' when targeting a device is not allowed"
  end
  RunLoop::SimControl.new.reset_sim_content_and_settings
end

#server_version_from_serverString

queries the server for its version.

SPECIAL: sets the @@server_version class variable to cache the server version because the server version will never change during runtime.

Returns:

  • (String)

    the server version

Raises:

  • (RuntimeError)

    if the server cannot be reached



996
997
998
999
1000
# File 'lib/calabash-cucumber/launcher.rb', line 996

def server_version_from_server
  return @@server_version unless @@server_version.nil?
  ensure_connectivity if self.device == nil
  @@server_version = self.device.server_version
end