illuminator 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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)