illuminator 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (47) hide show
  1. checksums.yaml +7 -0
  2. data/gem/README.md +37 -0
  3. data/gem/bin/illuminatorTestRunner.rb +22 -0
  4. data/gem/lib/illuminator.rb +171 -0
  5. data/gem/lib/illuminator/argument-parsing.rb +299 -0
  6. data/gem/lib/illuminator/automation-builder.rb +39 -0
  7. data/gem/lib/illuminator/automation-runner.rb +589 -0
  8. data/gem/lib/illuminator/build-artifacts.rb +118 -0
  9. data/gem/lib/illuminator/device-installer.rb +45 -0
  10. data/gem/lib/illuminator/host-utils.rb +42 -0
  11. data/gem/lib/illuminator/instruments-runner.rb +301 -0
  12. data/gem/lib/illuminator/javascript-runner.rb +98 -0
  13. data/gem/lib/illuminator/listeners/console-logger.rb +32 -0
  14. data/gem/lib/illuminator/listeners/full-output.rb +13 -0
  15. data/gem/lib/illuminator/listeners/instruments-listener.rb +22 -0
  16. data/gem/lib/illuminator/listeners/intermittent-failure-detector.rb +49 -0
  17. data/gem/lib/illuminator/listeners/pretty-output.rb +26 -0
  18. data/gem/lib/illuminator/listeners/saltinel-agent.rb +66 -0
  19. data/gem/lib/illuminator/listeners/saltinel-listener.rb +26 -0
  20. data/gem/lib/illuminator/listeners/start-detector.rb +52 -0
  21. data/gem/lib/illuminator/listeners/stop-detector.rb +46 -0
  22. data/gem/lib/illuminator/listeners/test-listener.rb +58 -0
  23. data/gem/lib/illuminator/listeners/trace-error-detector.rb +38 -0
  24. data/gem/lib/illuminator/options.rb +96 -0
  25. data/gem/lib/illuminator/resources/IlluminatorGeneratedEnvironment.erb +13 -0
  26. data/gem/lib/illuminator/resources/IlluminatorGeneratedRunnerForInstruments.erb +19 -0
  27. data/gem/lib/illuminator/test-definitions.rb +23 -0
  28. data/gem/lib/illuminator/test-suite.rb +155 -0
  29. data/gem/lib/illuminator/version.rb +3 -0
  30. data/gem/lib/illuminator/xcode-builder.rb +144 -0
  31. data/gem/lib/illuminator/xcode-utils.rb +219 -0
  32. data/gem/resources/BuildConfiguration.xcconfig +10 -0
  33. data/gem/resources/js/AppMap.js +767 -0
  34. data/gem/resources/js/Automator.js +1132 -0
  35. data/gem/resources/js/Base64.js +142 -0
  36. data/gem/resources/js/Bridge.js +102 -0
  37. data/gem/resources/js/Config.js +92 -0
  38. data/gem/resources/js/Extensions.js +2025 -0
  39. data/gem/resources/js/Illuminator.js +228 -0
  40. data/gem/resources/js/Preferences.js +24 -0
  41. data/gem/resources/scripts/UIAutomationBridge.rb +248 -0
  42. data/gem/resources/scripts/common.applescript +25 -0
  43. data/gem/resources/scripts/diff_png.sh +61 -0
  44. data/gem/resources/scripts/kill_all_sim_processes.sh +17 -0
  45. data/gem/resources/scripts/plist_to_json.sh +40 -0
  46. data/gem/resources/scripts/set_hardware_keyboard.applescript +0 -0
  47. metadata +225 -0
@@ -0,0 +1,228 @@
1
+ /**
2
+ * Main entry point
3
+ *
4
+ * There are no arguments to this function, because it will be controlled by Config arguments
5
+ * and invoked by testAutomatically.js (a generated file). See the documentation on how to
6
+ * set up and run Illuminator.
7
+ */
8
+ function IlluminatorIlluminate() {
9
+ // initial sanity checks
10
+ assertDesiredSimVersion();
11
+
12
+ // send test definitions back to framework
13
+ if (!host().writeToFile(config.buildArtifacts.automatorScenarioJSON,
14
+ JSON.stringify(automator.toScenarioObject(false), null, " ")))
15
+ {
16
+ throw new IlluminatorSetupException("Could not save necessary build artifact to " +
17
+ config.buildArtifacts.automatorScenarioJSON);
18
+ }
19
+ notifyIlluminatorFramework("Saved scenario definitions to: " + config.buildArtifacts.automatorScenarioJSON);
20
+
21
+ // run app-specific callback
22
+ if (!automator._executeCallback("onInit", {entryPoint: config.entryPoint}, false, false)) return;
23
+
24
+ // choose entry point and go
25
+ switch (config.entryPoint) {
26
+
27
+ case "runTestsByName":
28
+ automator.runNamedScenarios(config.automatorScenarioNames, config.automatorSequenceRandomSeed);
29
+ break;
30
+
31
+ case "runTestsByTag":
32
+ if (0 == (config.automatorTagsAny.length + config.automatorTagsAll.length + config.automatorTagsNone.length)) {
33
+ UIALogger.logMessage("No tag sets (any / all / none) were specified, so printing some information about defined scenarios");
34
+ automator.logInfo();
35
+ notifyIlluminatorFramework("Successful launch");
36
+ } else {
37
+ automator.runTaggedScenarios(config.automatorTagsAny,
38
+ config.automatorTagsAll,
39
+ config.automatorTagsNone,
40
+ config.automatorSequenceRandomSeed);
41
+ }
42
+ break;
43
+
44
+ case "describe":
45
+ notifyIlluminatorFramework("Successful launch");
46
+ var appMapMarkdownPath = config.buildArtifacts.appMapMarkdown;
47
+ var automatorMarkdownPath = config.buildArtifacts.automatorMarkdown;
48
+ var automatorJSONPath = config.buildArtifacts.automatorJSON;
49
+ host().writeToFile(appMapMarkdownPath, appmap.toMarkdown());
50
+ UIALogger.logMessage("Wrote AppMap definitions to " + appMapMarkdownPath);
51
+ host().writeToFile(automatorMarkdownPath, automator.toMarkdown());
52
+ UIALogger.logMessage("Wrote automator definitions to " + automatorMarkdownPath);
53
+ host().writeToFile(automatorJSONPath, JSON.stringify(automator.toScenarioObject(true), null, " "));
54
+ UIALogger.logMessage("Wrote automator definition data to " + automatorJSONPath);
55
+ break;
56
+
57
+ default:
58
+ notifyIlluminatorFramework("Successful launch");
59
+ throw new IlluminatorSetupException("Unknown Illuminator entry point specified: " + config.entryPoint);
60
+ }
61
+ }
62
+
63
+
64
+ /**
65
+ * Send a message back to the log analyzers (ruby)
66
+ *
67
+ * @param message string
68
+ */
69
+ function notifyIlluminatorFramework(message) {
70
+ UIALogger.logDebug(config.saltinel + " " + message + " " + config.saltinel);
71
+ }
72
+
73
+ function isMatchingVersion(input, prefix, major, minor, rev) {
74
+ var findStr = prefix + major;
75
+
76
+ if (undefined !== minor) {
77
+ findStr += "." + minor;
78
+ if (undefined !== rev) {
79
+ findStr += "." + rev;
80
+ }
81
+ }
82
+
83
+ return input.indexOf(findStr) > -1;
84
+ }
85
+
86
+ function isSimVersion(major, minor, rev) {
87
+ return isMatchingVersion(target().systemVersion(), "", major, minor, rev);
88
+ }
89
+
90
+ function assertDesiredSimVersion() {
91
+ var ver = target().systemVersion();
92
+ if (("iOS " + ver).indexOf(config.automatorDesiredSimVersion) == -1) {
93
+ throw new IlluminatorSetupException("Simulator version " + ver + " is running, but generated-config.js " +
94
+ "specifies " + config.automatorDesiredSimVersion);
95
+ }
96
+ }
97
+
98
+ function actionCompareScreenshotToMaster(parm) {
99
+ var masterPath = parm.masterPath;
100
+ var maskPath = parm.maskPath;
101
+ var captureTitle = parm.captureTitle;
102
+ var delayCapture = parm.delay === undefined ? 0.4 : parm.delay;
103
+
104
+ delay(delayCapture); // wait for any animations to settle
105
+
106
+ var diff_pngPath = IlluminatorScriptsDirectory + "/diff_png.sh";
107
+ UIATarget.localTarget().captureScreenWithName(captureTitle);
108
+
109
+ var screenshotFile = captureTitle + ".png";
110
+ var screenshotPath = config.screenshotDir + "/" + screenshotFile;
111
+ var compareFileBase = config.screenshotDir + "/compared_" + captureTitle;
112
+
113
+ var output = target().host().performTaskWithPathArgumentsTimeout("/bin/sh",
114
+ [diff_pngPath,
115
+ masterPath,
116
+ screenshotPath,
117
+ maskPath,
118
+ compareFileBase],
119
+ 20);
120
+
121
+ // turn the output into key/value pairs separated by ":"
122
+ var outputArr = output.stdout.split("\n");
123
+ var outputObj = {};
124
+ for (var i = 0; i < outputArr.length; ++i) {
125
+ var sp = outputArr[i].split(": ", 2)
126
+ if (sp.length == 2) {
127
+ outputObj[sp[0]] = sp[1];
128
+ }
129
+ }
130
+
131
+ // sanity check
132
+ if (!outputObj["pixels changed"]) {
133
+ throw new IlluminatorRuntimeVerificationException("actionCompareScreenshotToMaster: diff_png.sh failed to produce 'pixels changed' output");
134
+ }
135
+
136
+ // if differences are outside tolerances, throw errors
137
+ var allPixels = parseInt(outputObj["pixels (total)"]);
138
+ var wrongPixels = parseInt(outputObj["pixels changed"]);
139
+
140
+ var allowedPixels = parm.allowedPixels === undefined ? 0 : parm.allowedPixels;
141
+ var errmsg = "";
142
+ if (allowedPixels < wrongPixels) {
143
+ errmsg = ["Screenshot differed from", masterPath,
144
+ "by", wrongPixels, "pixels. ",
145
+ "Comparison image saved to:", compareFileBase + ".png",
146
+ " and comparison animation saved to:", compareFileBase + ".gif"].join(" ");
147
+
148
+ if (parm.deferFailure === true) {
149
+ automator.deferFailure(errmsg);
150
+ } else {
151
+ throw new IlluminatorRuntimeVerificationException(errmsg);
152
+ }
153
+ }
154
+
155
+ if (parm.allowedPercent !== undefined) {
156
+ var wrongPct = 100.0 * wrongPixels / allPixels;
157
+ if (wrongPct > parm.allowedPercent) {
158
+ errmsg = ["Screenshot differed from", masterPath,
159
+ "by", wrongPct, "%. ",
160
+ "Comparison image saved to:", compareFileBase + ".png",
161
+ " and comparison animation saved to:", compareFileBase + ".gif"].join(" ");
162
+
163
+ if (parm.deferFailure === true) {
164
+ } else {
165
+ throw new IlluminatorRuntimeVerificationException(errmsg);
166
+ }
167
+ }
168
+ }
169
+ }
170
+
171
+ function actionLogAccessors(parm) {
172
+ if (parm !== undefined && parm.delay !== undefined) {
173
+ delay(parm.delay);
174
+ }
175
+ var visibleOnly = parm !== undefined && parm.visibleOnly === true;
176
+ UIALogger.logDebug(target().elementReferenceDump("target", visibleOnly));
177
+ }
178
+
179
+ function actionCaptureElementTree(parm) {
180
+ target().captureImageTree(parm.imageBaseName);
181
+ }
182
+
183
+
184
+ ////////////////////////////////////////////////////////////////////////////////////////////////////
185
+ // Appmap additions - common capabilities
186
+ ////////////////////////////////////////////////////////////////////////////////////////////////////
187
+ appmap.createOrAugmentApp("Illuminator").withScreen("do")
188
+ .onTarget(config.implementation, function() { return true; })
189
+ // onTarget(config.implementation...) is a HACK.
190
+ // Illuminator doesn't define implementations, so we use what we're given.
191
+ // (i.e. you shouldn't copy/paste this example into your project code)
192
+
193
+ .withAction("delay", "Delay a given amount of time")
194
+ .withParam("seconds", "Number of seconds to delay", true, true)
195
+ .withImplementation(function(parm) {delay(parm.seconds);})
196
+
197
+ .withAction("debug", "Print the results of a debug function")
198
+ .withParam("debug_fn", "Function returning a string", true)
199
+ .withImplementation(function(parm) { UIALogger.logMessage(parm.debug_fn()); })
200
+
201
+ .withAction("logTree", "Log the UI element tree")
202
+ .withImplementation(function() { UIATarget.localTarget().logElementTree(); })
203
+
204
+ .withAction("logAccessors", "Log the list of valid element accessors")
205
+ .withParam("visibleOnly", "Whether to log only the visible elements", false, true)
206
+ .withParam("delay", "Number of seconds to delay before logging", false, true)
207
+ .withImplementation(actionLogAccessors)
208
+
209
+ .withAction("captureElementTree", "Take individual screenshots of all screen elements")
210
+ .withParam("imageBaseName", "The base name for the image files", true, true)
211
+ .withImplementation(actionCaptureElementTree)
212
+
213
+ .withAction("fail", "Unconditionally fail the current test for debugging purposes")
214
+ .withImplementation(function() { throw new IlluminatorRuntimeVerificationException("purposely-thrown exception to halt the test scenario"); })
215
+
216
+ .withAction("verifyScreenshot", "Validate a screenshot against a png template of the expected view")
217
+ .withParam("masterPath", "The path to the file that is considered the 'expected' view", true, true)
218
+ .withParam("maskPath", "The path to the file that masks variable portions of the 'expected' view", true, true)
219
+ .withParam("captureTitle", "The title of the screenshot to capture", true, true)
220
+ .withParam("delay", "The amount of time to delay before taking the screenshot", false, true)
221
+ .withParam("allowedPixels", "The maximum number of pixels that are allowed to differ (default 0)", false, true)
222
+ .withParam("allowedPercent", "The maximum percentage of pixels that are allowed to differ (default 0)", false, true)
223
+ .withParam("deferFailure", "Whether to defer a failure until the end of the test", false, true)
224
+ .withImplementation(actionCompareScreenshotToMaster)
225
+
226
+ .withAction("testAsAction", "Execute an entire test as one action")
227
+ .withParam("test", "The function that performs the entire test")
228
+ .withImplementation(function (parm) { parm.test(); });
@@ -0,0 +1,24 @@
1
+ // Preferences.js
2
+ //
3
+ // Provides the set of known user preferences for Illuminator
4
+
5
+
6
+ (function() {
7
+
8
+ var root = this,
9
+ preferences = null;
10
+
11
+ // put preferences in namespace of importing code
12
+ if (typeof exports !== 'undefined') {
13
+ preferences = exports;
14
+ } else {
15
+ preferences = root.preferences = {};
16
+ }
17
+
18
+ preferences.extensions = {};
19
+ preferences.extensions.reduceTimeout = 10; // seconds
20
+
21
+ preferences.automator = {};
22
+ preferences.automator.onError = {};
23
+
24
+ }).call(this);
@@ -0,0 +1,248 @@
1
+ require 'bundler/setup'
2
+ require 'rubygems'
3
+ require 'json'
4
+ require 'base64'
5
+ require 'dnssd'
6
+ require 'socket'
7
+ require 'timeout'
8
+ require 'optparse'
9
+
10
+ # all parsing code goes here
11
+ def parse_arguments(args)
12
+ ret = {}
13
+ ret["timeout"] = 15
14
+ op = OptionParser.new do |opts|
15
+ opts.banner = "Usage: #{__FILE__} [options]"
16
+ opts.separator ""
17
+ opts.separator "Specific options:"
18
+
19
+ opts.on("-a", "--argument=[JSON]",
20
+ "Pass the supplied JSON data over the bridge") do |v|
21
+ ret["argument"] = v
22
+ end
23
+
24
+ opts.on("-b", "--b64argument=[B64-JSON]",
25
+ "Pass the base64-encoded JSON data over the bridge") do |v|
26
+ ret["b64argument"] = v
27
+ end
28
+
29
+ opts.on("-s", "--selector=SELECTOR",
30
+ "Call the given function (selector) via the bridge") do |v|
31
+ ret["selector"] = v
32
+ end
33
+
34
+ opts.on("-c", "--callUID=UID",
35
+ "Use the given UID to properly identify the return value of this call") do |v|
36
+ ret["callUID"] = v
37
+ end
38
+
39
+ opts.on("-r", "--hardwareID=[HARDWAREID]",
40
+ "If provided, connect to the physical iOS device with this hardware ID instead of a simulator") do |v|
41
+ ret["hardwareID"] = v
42
+ end
43
+
44
+ opts.on("-t", "--timeout=[TIMEOUT]",
45
+ "The timeout in seconds for reading a response from the bridge (default 15)") do |v|
46
+ ret["timeout"] = v.to_i
47
+ end
48
+
49
+ opts.on_tail("-h", "--help", "Show this message") do
50
+ puts opts
51
+ exit
52
+ end
53
+
54
+ end
55
+ op.parse!(args)
56
+ return ret
57
+ end
58
+
59
+
60
+ # communicate the result to the console
61
+ # success: boolean whether it all went well
62
+ # failmsg: what to say went wrong
63
+ # checkpoints: some stuff to print out, debug info
64
+ # output_data: the actual results
65
+ def print_result_and_exit(success, failmsg, checkpoints, response=nil)
66
+ output_data = {}
67
+ output_data["checkpoints"] = checkpoints
68
+ output_data["response"] = response unless response.nil?
69
+ output_data["success"] = success
70
+ output_data["message"] = failmsg
71
+ puts JSON.pretty_generate(output_data)
72
+ exit(success ? 0 : 1)
73
+ end
74
+
75
+
76
+ def get_host_port_of_hardware_id(hardware_id, timeout_seconds)
77
+ service = DNSSD::Service.new
78
+ host, port = nil, nil
79
+ begin
80
+ Timeout::timeout(timeout_seconds) do
81
+ service.browse '_bridge._tcp' do |result|
82
+ if result.name.eql? "UIAutomationBridge_#{hardware_id}"
83
+ resolver = DNSSD::Service.new
84
+ resolver.resolve result do |r|
85
+ host = r.target
86
+ port = r.port
87
+ break unless r.flags.more_coming?
88
+ end
89
+ break
90
+ end
91
+ end
92
+ return host, port
93
+ end
94
+ rescue Timeout::Error
95
+ end
96
+ return host, port
97
+ end
98
+
99
+
100
+ def connect(host, port, timeout=5)
101
+ addr = Socket.getaddrinfo(host, nil)
102
+ sock = Socket.new(Socket.const_get(addr[0][0]), Socket::SOCK_STREAM, 0)
103
+
104
+ if timeout
105
+ secs = Integer(timeout)
106
+ usecs = Integer((timeout - secs) * 1_000_000)
107
+ optval = [secs, usecs].pack('l_2')
108
+ sock.setsockopt Socket::SOL_SOCKET, Socket::SO_RCVTIMEO, optval
109
+ sock.setsockopt Socket::SOL_SOCKET, Socket::SO_SNDTIMEO, optval
110
+ end
111
+ begin
112
+ sock.connect(Socket.pack_sockaddr_in(port, addr[0][3]))
113
+ return sock, ""
114
+ rescue Exception => e
115
+ return nil, "Failed to connect to #{host}:#{port} - #{e.class.name} - #{e.message}"
116
+ end
117
+ end
118
+
119
+
120
+ # these describe what we expect to have happen, and we will check them off as they do
121
+ checkpoints = {}
122
+ checkpoints["ruby"] = true
123
+ checkpoints["argument"] = nil
124
+ checkpoints["hardwareID"] = nil
125
+ checkpoints["connection"] = nil
126
+ checkpoints["request"] = nil
127
+ checkpoints["response"] = nil
128
+ checkpoints["callUIDMatch"] = nil
129
+
130
+
131
+ # process the input
132
+ options = parse_arguments(ARGV)
133
+
134
+ # verify that the input selector was provided
135
+ if options["selector"].nil?
136
+ print_result_and_exit(false, "selector not provided", checkpoints)
137
+ end
138
+
139
+ # decode b64 argument if provided
140
+ unless options["b64argument"].nil?
141
+ begin
142
+ decoded_arg = Base64.strict_decode64(options["b64argument"])
143
+ options['argument'] = decoded_arg # the next if block will find & process this
144
+ JSON.parse(decoded_arg)
145
+ rescue ArgumentError => e
146
+ print_result_and_exit(false, "Error decoding b64argument: #{e.message}", checkpoints)
147
+ rescue JSON::ParserError => e
148
+ print_result_and_exit(false, "Decoded b64argument does not appear to contain (valid) JSON", checkpoints)
149
+ end
150
+ end
151
+
152
+ # parse JSON in argument if provided (or b64 provided)
153
+ unless options["argument"].nil?
154
+ checkpoints["argument"] = false
155
+ begin
156
+ parsed_arg = JSON.parse(options["argument"])
157
+ options["jsonArgument"] = parsed_arg
158
+ checkpoints["argument"] = true
159
+ rescue JSON::ParserError => e
160
+ print_result_and_exit(false, "Error parsing JSON argument: #{e.message}", checkpoints)
161
+ end
162
+ end
163
+
164
+ # build the request that will go the server
165
+ request_hash = {}
166
+ request_hash['argument'] = options["jsonArgument"] unless options["jsonArgument"].nil?
167
+ request_hash["selector"] = options["selector"]
168
+ request_hash["callUID"] = options["callUID"]
169
+ request = request_hash.to_json
170
+ checkpoints["actual_request"] = request_hash
171
+
172
+ # get the host/port according to whether we are using hardware
173
+ host, port = '127.0.0.1', 4200
174
+ unless options["hardwareID"].nil?
175
+ checkpoints["hardwareID"] = false
176
+ host, port = get_host_port_of_hardware_id(options["hardwareID"], 3)
177
+ if host.nil? or port.nil?
178
+ print_result_and_exit(false, "Failed to get host/port for hardware ID", checkpoints)
179
+ end
180
+ checkpoints["hardwareID"] = true
181
+ end
182
+
183
+ # connect
184
+ checkpoints["host"] = host
185
+ checkpoints["port"] = port
186
+ checkpoints["connection"] = false
187
+ socket_stream, err_message = connect host, port
188
+ if socket_stream.nil?
189
+ print_result_and_exit(false, err_message, checkpoints)
190
+ end
191
+ checkpoints["connection"] = true
192
+
193
+ begin
194
+ # send request
195
+ checkpoints["request"] = false
196
+ socket_stream.write(request)
197
+
198
+ checkpoints["request"] = true
199
+
200
+ # read response
201
+ checkpoints["response"] = false
202
+ response = ''
203
+
204
+ timeout(options["timeout"]) do
205
+ while true
206
+ new_data = nil
207
+ begin
208
+ timeout(0.1) do
209
+ new_data = socket_stream.gets("}") # read up to closing brace at a time
210
+ end
211
+ rescue Timeout::Error
212
+ # nothing
213
+ end
214
+
215
+ unless new_data.nil?
216
+ response = response + new_data
217
+ end
218
+
219
+ begin
220
+ # successfully parse, or go back and try again
221
+ JSON.parse(response)
222
+ break
223
+ rescue
224
+ # nothing
225
+ end
226
+ end
227
+ end
228
+ checkpoints["response"] = true
229
+ checkpoints["response_length"] = response.length
230
+
231
+ rescue Timeout::Error
232
+ print_result_and_exit(false, "Timed out waiting for response", checkpoints)
233
+ rescue Exception => e
234
+ print_result_and_exit(false, "Error while waiting for response: #{e.inspect()}", checkpoints)
235
+ ensure
236
+ socket_stream.close
237
+ end
238
+
239
+ resp = JSON.parse(response)
240
+
241
+ # check callUID
242
+ checkpoints["callUIDMatch"] = false
243
+ if options["callUID"] != resp["callUID"]
244
+ print_result_and_exit(false, "Expected callUID=#{options["callUID"]} but got callUID=#{resp["callUID"]}", checkpoints, resp)
245
+ end
246
+ checkpoints["callUIDMatch"] = true
247
+
248
+ print_result_and_exit(true, "all bridge options completed successfully", checkpoints, resp)