lib/run_loop/xcuitest.rb in run_loop-2.1.2 vs lib/run_loop/xcuitest.rb in run_loop-2.1.3

- old
+ new

@@ -1,10 +1,13 @@ module RunLoop # @!visibility private class XCUITest + require "run_loop/shell" + include RunLoop::Shell + class HTTPError < RuntimeError; end # @!visibility private DEFAULTS = { :port => 27753, @@ -36,63 +39,68 @@ simctl.ensure_software_keyboard(device) core_sim.install end - xcuitest = RunLoop::XCUITest.new(bundle_id, device) + cbx_launcher = XCUITest.detect_cbx_launcher(options, device) + + xcuitest = RunLoop::XCUITest.new(bundle_id, device, cbx_launcher) xcuitest.launch xcuitest end # @!visibility private - def self.xcodebuild_log_file - path = File.join(XCUITest.dot_dir, "xcodebuild.log") - FileUtils.touch(path) if !File.exist?(path) - path + # + # @param [RunLoop::Device] device the device under test + def self.default_cbx_launcher(device) + RunLoop::DeviceAgent::XCTestctl.new(device) end # @!visibility private + # @param [Hash] options the options passed by the user + # @param [RunLoop::Device] device the device under test + def self.detect_cbx_launcher(options, device) + value = options[:cbx_launcher] + if value + if value == :xcodebuild + RunLoop::DeviceAgent::Xcodebuild.new(device) + elsif value == :xctestctl + RunLoop::DeviceAgent::XCTestctl.new(device) + else + raise(ArgumentError, + "Expected :cbx_launcher => #{value} to be :xcodebuild or :xctestctl") + end + else + XCUITest.default_cbx_launcher(device) + end + end + + attr_reader :bundle_id, :device, :cbx_launcher + + # @!visibility private # # The app with `bundle_id` needs to be installed. # # @param [String] bundle_id The identifier of the app under test. # @param [RunLoop::Device] device The device device. - def initialize(bundle_id, device) + def initialize(bundle_id, device, cbx_launcher) @bundle_id = bundle_id @device = device + @cbx_launcher = cbx_launcher end + # @!visibility private def to_s - "#<XCUITest #{url} : #{bundle_id} : #{device}>" + "#<XCUITest #{url} : #{bundle_id} : #{device} : #{cbx_launcher}>" end + # @!visibility private def inspect to_s end # @!visibility private - def bundle_id - @bundle_id - end - - # @!visibility private - def device - @device - end - - # @!visibility private - def workspace - @workspace ||= lambda do - path = RunLoop::Environment.send(:cbxws) - if path - path - else - raise "TODO: figure out how to distribute the CBX-Runner" - end - end.call - end - def launch start = Time.now launch_cbx_runner launch_aut elapsed = Time.now - start @@ -122,37 +130,73 @@ def launch_other_app(bundle_id) launch_aut(bundle_id) end # @!visibility private + def runtime + options = http_options + request = request("device") + client = client(options) + response = client.get(request) + expect_200_response(response) + end + + # @!visibility private def query(mark) options = http_options parameters = { :id => mark } request = request("query", parameters) client = client(options) response = client.post(request) expect_200_response(response) end # @!visibility private - def tap_mark(mark) + def query_for_coordinate(mark) body = query(mark) - tap_query_result(body) + coordinate_from_query_result(body) end # @!visibility private - def tap_coordinate(x, y) - options = http_options - parameters = { - :gesture => "touch", + def tap_mark(mark) + coordinate = query_for_coordinate(mark) + tap_coordinate(coordinate[:x], coordinate[:y]) + end + + # @!visibility private + def tap_coordinate(x, y, options) + make_coordinate_gesture_request("touch", x, y) + end + + # @!visibility private + def double_tap(x, y) + make_coordinate_gesture_request("double_tap", x, y) + end + + # @!visibiity private + def coordinate_gesture_parameters(name, x, y, options={}) + { + :gesture => name, :specifiers => { - :coordinate => {x: x, y: y} + :coordinate => [x, y] }, - :options => {} + :options => options } - request = request("gesture", parameters) - client(options) + end + + # @!visibility private + def coordinate_gesture_request(name, x, y, gesture_options={}) + parameters = coordinate_gesture_parameters(name, x, y, gesture_options) + request("gesture", parameters) + end + + # @!visibility private + def make_coordinate_gesture_request(name, x, y, options={}) + gesture_options = options.fetch(:gesture_options, {}) + http_options = options.fetch(:http_options, http_options()) + request = coordinate_gesture_request(name, x, y, gesture_options) + client = client(http_options) response = client.post(request) expect_200_response(response) end # @!visibility private @@ -160,11 +204,11 @@ orientation = orientation_for_position(position) parameters = { :orientation => orientation } request = request("rotate_home_button_to", parameters) - client(http_options) + client = client(http_options) response = client.post(request) json = expect_200_response(response) sleep(sleep_for) json end @@ -177,27 +221,50 @@ :coordinate => {x: x, y: y} }, :options => options } + RunLoop.log_debug(%Q[ +Sending request to perform '#{gesture}' with: + +#{JSON.pretty_generate(parameters)} + +]) request = request("gesture", parameters) - client(http_options) + client = client(http_options) response = client.post(request) expect_200_response(response) end # @!visibility private - def tap_query_result(hash) - rect = hash["rect"] + def coordinate_from_query_result(hash) + matches = hash["result"] + + if matches.nil? || matches.empty? + raise "Expected #{hash} to contain some results" + end + + rect = matches.first["rect"] h = rect["height"] w = rect["width"] x = rect["x"] y = rect["y"] - touchx = x + (h/2) - touchy = y + (w/2) - tap_coordinate(touchx, touchy) + touchx = x + (w/2.0) + touchy = y + (h/2.0) + + new_rect = rect.dup + new_rect[:center_x] = touchx + new_rect[:center_y] = touchy + + RunLoop.log_debug(%Q[Rect from query: + +#{JSON.pretty_generate(new_rect)} + +]) + {:x => touchx, + :y => touchy} end private # @!visibility private @@ -221,11 +288,11 @@ encoding_options = { :invalid => :replace, # Replace invalid byte sequences :undef => :replace, # Replace anything not defined in ASCII :replace => "" # Use a blank for those replacements } - encoded = device_name.encode(Encoding.find("ASCII"), encoding_options) + encoded = device_name.encode(::Encoding.find("ASCII"), encoding_options) "http://#{encoded}.local:27753/" end end end.call end @@ -294,18 +361,10 @@ client = client(options) begin response = client.post(request) body = response.body RunLoop.log_debug("CBX-Runner says, \"#{body}\"") - 5.times do - begin - health - sleep(1.0) - rescue => _ - break - end - end body rescue => e RunLoop.log_debug("CBX-Runner shutdown error: #{e}") nil end @@ -322,68 +381,25 @@ RunLoop.log_debug("CBX-Runner driver says, \"#{body}\"") body end # @!visibility private - def xcodebuild - env = { - "COMMAND_LINE_BUILD" => "1" - } - - args = [ - "xcrun", - "xcodebuild", - "-scheme", "CBXAppStub", - "-workspace", workspace, - "-config", "Debug", - "-destination", - "id=#{device.udid}", - "clean", - "test" - ] - - log_file = XCUITest.xcodebuild_log_file - - options = { - :out => log_file, - :err => log_file - } - - command = "#{env.map.each { |k, v| "#{k}=#{v}" }.join(" ")} #{args.join(" ")}" - RunLoop.log_unix_cmd("#{command} >& #{log_file}") - - pid = Process.spawn(env, *args, options) - Process.detach(pid) - pid - end - - # @!visibility private def launch_cbx_runner - # Fail fast if CBXWS is not defined. - # WIP - we will distribute the workspace somehow. - workspace - shutdown - # Temp measure; we need to manage the xcodebuild pids. - system("pkill xcodebuild") + options = {:log_cmd => true} + exec(["pkill", "xctestctl"], options) + exec(["pkill", "testmanagerd"], options) + exec(["pkill", "xcodebuild"], options) - if device.simulator? - # quits the simulator - sim = CoreSimulator.new(device, "") - sim.launch_simulator - else - # anything special about physical devices? - end - start = Time.now - pid = xcodebuild - RunLoop.log_debug("Waiting for CBX-Runner to build...") + RunLoop.log_debug("Waiting for CBX-Runner to launch...") + pid = cbx_launcher.launch health + RunLoop.log_debug("Took #{Time.now - start} launch and respond to /health") - RunLoop.log_debug("Took #{Time.now - start} seconds to build and launch") - pid.to_i + pid end # @!visibility private def launch_aut(bundle_id = @bundle_id) client = client(http_options) @@ -421,29 +437,31 @@ end # @!visibility private def expect_200_response(response) body = response_body_to_hash(response) - return body if response.status_code < 300 + if response.status_code < 300 && !body["error"] + return body + end - raise RunLoop::XCUITest::HTTPError, - %Q[Expected status code < 200, found #{response.status_code}. + if response.status_code > 300 + raise RunLoop::XCUITest::HTTPError, + %Q[Expected status code < 300, found #{response.status_code}. Server replied with: #{body} + ] - end + else + raise RunLoop::XCUITest::HTTPError, + %Q[Expected JSON response with no error, but found - # @!visibility private - def self.dot_dir - path = File.join(RunLoop::DotDir.directory, "xcuitest") +#{body["error"]} - if !File.exist?(path) - FileUtils.mkdir_p(path) - end +] - path + end end # @!visibility private def orientation_for_position(position) symbol = position.to_sym