forked from tidev/titanium-sdk
/
common.js
executable file
Β·712 lines (592 loc) Β· 21.2 KB
/
common.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
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
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
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
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
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
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
/*
* Appcelerator Titanium Mobile
* Copyright (c) 2011-2012 by Appcelerator, Inc. All Rights Reserved.
* Licensed under the terms of the Apache Public License
* Please see the LICENSE included with this distribution for details.
*
* Purpose: common file across platforms for driver tasks
*
* Description: contains common logic used by all platforms when executing driver tasks.
* Most of the logic defined here represents only partial pieces of an overall task and most
* of these functions are invoked by the platform specific task handlers. As the test format
* between driver and harness is not platform specific, the handling mechanism is defined in
* this file.
*/
var fs = require("fs"),
path = require("path"),
wrench = require("wrench"),
driverUtils = require(path.resolve(driverGlobal.driverDir, "driverUtils"));
module.exports = new function() {
var self = this;
/*
any key/value pairs stored in here will be injected into the harness config tiapp.xml as
property entries
*/
this.customTiappXmlProperties = {};
// keeps track of state within the harness set/config list
var configs;
var configSetIndex;
var configIndex;
/*
keeps track of where the specified harness set/config can be found within the harness set/
config list
*/
var selectedConfigSetIndex;
var selectedConfigIndex;
// specified suite and test args (pulled off at the start of the run so store them)
var selectedSuiteArg;
var selectedTestArg;
/*
keep track of where we are in the result set which can be different from the
set/config master list
*/
var results;
var resultConfigSetIndex;
var resultConfigIndex;
var resultSuiteIndex;
var resultTestIndex;
// actual list of suites and tests reported by the harness
var reportedSuites;
var reportedTests;
var timer; // set per test in order to catch timeouts
/*
called once per test pass no matter how many config sets, configs, suites or tests happen to be
involved. This is where general init or cleaning logic that needs to occur before a test run
kicks off should live. Inside this method, the harness config list will be built (or rebuilt),
the arguments given to the start command will be pulled off and processed (in the case of a
specified config set or config), the log file for the test run will be initialized and the
result set will be initialized
*/
this.startTestPass = function(startArgs, successCallback, errorCallback) {
configs = [];
results = [];
configSetIndex = 0;
configIndex = 0;
selectedConfigSetIndex = null;
selectedConfigIndex = null;
// build list of tiapp.xml and app.js combinations
function loadHarnessConfigs() {
function loadConfig(setIndex, configDir, configName) {
var stat = fs.statSync(path.resolve(configDir, configName));
if (stat.isDirectory()) {
configs[setIndex].setConfigs.push({
configDir: configDir,
configName: configName
});
}
}
// load standard configs
var files = fs.readdirSync(path.resolve(driverGlobal.configSetDir, "configs"));
if (files.length > 0) {
configs.push({
setDir: driverGlobal.configSetDir,
setName: "standard",
setConfigs: []
});
for (var i = 0; i < files.length; i++) {
loadConfig((configs.length - 1), path.resolve(driverGlobal.configSetDir, "configs"), files[i]);
}
}
// load custom configs
if ((typeof driverGlobal.config.customHarnessConfigDirs) !== "undefined") {
if (Object.prototype.toString.call(driverGlobal.config.customHarnessConfigDirs) === "[object Array]") {
for (var i = 0; i < driverGlobal.config.customHarnessConfigDirs.length; i++) {
var configSetDir = driverGlobal.config.customHarnessConfigDirs[i];
var configSetName;
// load the config set name
if (path.existsSync(path.resolve(configSetDir, "name.txt"))) {
configSetName = driverUtils.trimStringRight(fs.readFileSync(path.resolve(configSetDir, "name.txt"), "ascii"));
} else {
driverUtils.log("the custom harness config set at <" + configSetDir + "/name.txt" +
"> does not contain a name.txt file that provides a harness config set name, ignoring",
driverGlobal.logLevels.quiet);
continue;
}
var files = fs.readdirSync(path.resolve(configSetDir, "configs"));
if (files.length > 0) {
configs.push({
setDir: configSetDir,
setName: configSetName,
setConfigs: []
});
for (var j = 0; j < files.length; j++) {
loadConfig((configs.length - 1), path.resolve(configSetDir, "configs"), files[j]);
}
}
}
} else {
driverUtils.log("the customHarnessConfigDirs property in the config module is set but is not an array",
driverGlobal.logLevels.quiet);
}
}
if (configs.length < 1) {
driverUtils.log("there must be at least one harness config, exiting");
process.exit(1);
}
}
/*
process the arguments passed to the "start" command (not the arguments that were passed to
the driver itself)
*/
var processStartArgsCallback = function() {
var errorState = false;
var configSetArg = driverUtils.getArgument(startArgs, "--config-set");
if ((typeof configSetArg) !== "undefined") {
var numConfigSets = configs.length;
for (var i = 0; i < numConfigSets; i++) {
if (configSetArg === configs[i].setName) {
configSetIndex = i;
selectedConfigSetIndex = i;
break;
}
}
if (selectedConfigSetIndex === null) {
driverUtils.log("specified config set is not recognized");
errorState = true;
}
}
// config set must have also been specified if there is more than a single config set
var configArg = driverUtils.getArgument(startArgs, "--config");
if ((selectedConfigSetIndex !== null) || (configs.length === 1 )) {
if ((typeof configArg) !== "undefined") {
var numConfigs = configs[configSetIndex].setConfigs.length;
for (var i = 0; i < numConfigs; i++) {
if (configArg === configs[configSetIndex].setConfigs[i].configName) {
configIndex = i;
selectedConfigIndex = i;
break;
}
}
if (selectedConfigIndex === null) {
driverUtils.log("specified config is not recognized");
errorState = true;
}
}
} else if ((selectedConfigSetIndex === null) && ((typeof configArg) !== "undefined")) {
driverUtils.log("valid --config-set argument must be provided when --config is specified");
errorState = true;
}
if (!errorState) {
selectedSuiteArg = driverUtils.getArgument(startArgs, "--suite");
selectedTestArg = driverUtils.getArgument(startArgs, "--test");
successCallback();
} else {
errorCallback();
}
};
loadHarnessConfigs();
driverUtils.openLog(processStartArgsCallback);
};
/*
* invoked each time a run of a config is started. the commandElements argument is only
* defined the first time this function is called at the start of a test pass. the overall
* purpose of this function is to cycle configs during a test pass and reset other state
* variables
*/
this.startConfig = function(callback) {
/*
this is the only safe place to reset the suite and test states - if done in the message
processing there is a chance that the config has just been restarted to deal with a timeout
and the states would be reset prematurely
*/
resultSuiteIndex = 0;
resultTestIndex = 0;
// add the config set to the results if it does not exist
var foundSet = false;
for (var i = 0; i < results.length; i++) {
if (results[i].setName === configs[configSetIndex].setName) {
foundSet = true;
}
}
if (!foundSet) {
results.push({
setName: configs[configSetIndex].setName,
setConfigs: []
});
resultConfigSetIndex = results.length - 1;
}
// add the config to the config set entry in the results (no risk of duplicate entry)
results[resultConfigSetIndex].setConfigs.push({
configName: configs[configSetIndex].setConfigs[configIndex].configName,
configSuites: []
});
resultConfigIndex = results[resultConfigSetIndex].setConfigs.length - 1;
callback();
};
/*
* basically this function handles picking between starting a new config pass and finishing
* the test pass
*/
this.finishConfig = function(passFinishedCallback) {
var finishPass = false;
// roll to the next config and roll to the next set if need be
configIndex++;
if ((configIndex + 1) > configs[configSetIndex].setConfigs.length) {
configSetIndex++;
if ((configSetIndex + 1) > configs.length) {
finishPass = true; // this was the last set, finish the whole pass
} else {
// this was the last config in the set, reset for the start of the new set
configIndex = 0;
}
}
if (selectedConfigIndex !== null) {
/*
if we are here then that means that the specified config has finished and we don't
support running a specified config across multiple sets so finish the pass
*/
finishPass = true;
}
if (finishPass) {
driverUtils.closeLog();
passFinishedCallback(results);
} else {
driverGlobal.platform.startConfig();
}
};
this.createHarness = function(platform, command, successCallback, errorCallback) {
var harnessPlatformDir = path.resolve(driverGlobal.harnessDir, platform);
var createCallback = function() {
try {
fs.mkdirSync(harnessPlatformDir, 0777);
} catch(e) {
driverUtils.log("temp " + platform + " harness dir already exist");
}
driverUtils.runCommand(command, driverUtils.logStdout, function(error) {
if (error !== null) {
driverUtils.log("error encountered when creating harness: " + error);
if (errorCallback) {
errorCallback();
}
} else {
driverUtils.log("harness created");
updateHarness(platform, successCallback, errorCallback);
}
});
};
if (path.existsSync(path.resolve(harnessPlatformDir, "harness", "tiapp.xml"))) {
this.deleteHarness(platform, createCallback);
} else {
createCallback();
}
};
/*
* makes sure that the newly created harness contains the correct tiapp.xml and resources
* based on the harness template
*/
var updateHarness = function(platform, successCallback, errorCallback) {
var config = configs[configSetIndex].setConfigs[configIndex];
var configDir = path.resolve(config.configDir, config.configName);
var harnessPlatformDir = path.resolve(driverGlobal.harnessDir, platform);
var updateSuitesCallback = function() {
try {
wrench.copyDirSyncRecursive(path.resolve(configs[configSetIndex].setDir, "Resources"), path.resolve(harnessPlatformDir, "harness", "Resources"), {preserve: true});
driverUtils.log("harness suites updated");
updateTiappCallback();
} catch(exception) {
driverUtils.log("unable to update the harness suites: " + exception);
if (errorCallback) {
errorCallback();
}
}
};
var updateTiappCallback = function() {
function injectCustomTiappXmlProperties() {
if (self.customTiappXmlProperties.length < 1) {
// nothing custom to add so leave the copy alone
return;
}
var tiappXmlPath = path.resolve(harnessPlatformDir, "harness", "tiapp.xml");
var tiappXmlContents;
// load the config set name
if (path.existsSync(tiappXmlPath)) {
try {
tiappXmlContents = fs.readFileSync(tiappXmlPath, "utf8");
} catch(e) {
self.log("exception <" + e + "> occurred when reading tiapp.xml at: " + tiappXmlPath);
}
} else {
/*
this should not happen since the path to the tiapp.xml is passed in and it is assumed
that the file should exist in the harness before we start injecting values. Die hard!
*/
driverUtils.log("no tiapp.xml file found at: " + tiappXmlPath, driverGlobal.logLevels.quiet);
process.exit(1);
}
var splitPos = tiappXmlContents.indexOf("</ti:app>");
if (splitPos === -1) {
/*
this could only happen if the tiapp.xml in the config is messed up so die
hard if it happens
*/
driverUtils.log("no closing ti:app tag found in the tiapp.xml file");
process.exit(1);
}
var preSplit = tiappXmlContents.substring(0, splitPos);
var postSplit = tiappXmlContents.substring(splitPos, tiappXmlContents.length - 1);
var newTiappXmlContents = preSplit;
for (var key in self.customTiappXmlProperties) {
if (self.customTiappXmlProperties.hasOwnProperty(key)) {
var foundPos = tiappXmlContents.indexOf("<property name=\"" + key + "\"");
if (foundPos === -1) {
/*
make sure to only inject if the value doesn't already exist in the
config tiapp.xml so we avoid duplicates
*/
newTiappXmlContents += "\t<property name=\"" + key + "\" type=\"" + self.customTiappXmlProperties[key].type + "\">" + self.customTiappXmlProperties[key].value + "</property>\n";
}
}
}
newTiappXmlContents += postSplit;
fs.writeFileSync(tiappXmlPath, newTiappXmlContents);
}
driverUtils.copyFile(path.resolve(configDir, "tiapp.xml"), path.resolve(harnessPlatformDir, "harness", "tiapp.xml"), function(error) {
if (error !== null) {
driverUtils.log("unable to update the harness tiapp.xml: " + error);
if (errorCallback) {
errorCallback();
}
} else {
injectCustomTiappXmlProperties();
driverUtils.log("harness tiapp.xml updated");
updateAppjsCallback();
}
});
};
var updateAppjsCallback = function() {
if (path.existsSync(path.resolve(configDir, "app.js"))) {
driverUtils.copyFile(path.resolve(configDir, "app.js"), path.resolve(harnessPlatformDir, "harness", "Resources", "app.js"), function(error) {
if (error !== null) {
driverUtils.log("unable to update app.js for harness: " + error);
if (errorCallback) {
errorCallback();
}
} else {
driverUtils.log("app.js updated for harness");
if (successCallback) {
successCallback();
}
}
});
} else {
successCallback();
}
};
// update the harness based on the harness template packaged with the driver
try {
wrench.copyDirSyncRecursive(driverGlobal.harnessTemplateDir, path.resolve(harnessPlatformDir, "harness", "Resources"), {preserve: true});
driverUtils.log("harness updated with template");
updateSuitesCallback();
} catch(exception) {
driverUtils.log("unable to update harness with template: " + exception);
if (errorCallback) {
errorCallback();
}
}
};
this.deleteHarness = function(platform, callback) {
var harnessDir = path.resolve(driverGlobal.harnessDir, platform, "harness");
if (path.existsSync(harnessDir)) {
wrench.rmdirSyncRecursive(harnessDir, true);
driverUtils.log("harness deleted");
}
callback();
};
/*
this function handles messages from the driver and implements the communication protocol
outlined in the driver.js comment section
*/
this.processHarnessMessage = function(rawMessage) {
var message;
try {
message = JSON.parse(rawMessage);
} catch(e) {
// this means something has gone waaaaaay wrong
console.log("exception <" + e + "> occured when trying to convert JSON message <" +
rawMessage + "> from Driver");
process.exit(1);
}
var responseData = "";
if ((typeof message) !== "object") {
driverUtils.log("invalid message, expecting object");
return responseData;
}
if (message.type === "ready") {
responseData = "getSuites";
} else if (message.type === "suites") {
reportedSuites = message.suites;
if (selectedSuiteArg) {
var found = false;
// does the specified suite even exist in the current config?
for (var i = 0; i < reportedSuites.length; i++) {
if (selectedSuiteArg === reportedSuites[i].name) {
/*
usually we let incrementTest() handle rolling this value but in this case
we need to jump to the specified index
*/
resultSuiteIndex = i;
responseData = startSuite();
found = true;
}
}
if (!found) {
driverUtils.log("specified suite not found");
// maybe the next config has the suite we are looking for?
driverGlobal.platform.finishConfig();
}
} else {
if (reportedSuites.length === 0) {
driverUtils.log("no suites found for configuration");
driverGlobal.platform.finishConfig();
} else {
responseData = startSuite();
}
}
} else if (message.type === "tests") {
var found = false;
reportedTests = message.tests;
/*
does the current suite already exist in the results? Since we restart a suite when a
timeout occurs, it is possible to already have the suite in the results
*/
var configSuites = results[resultConfigSetIndex].setConfigs[resultConfigIndex].configSuites;
var numSuites = configSuites.length;
for (var i = 0; i < numSuites; i++) {
if (reportedSuites[resultSuiteIndex].name === configSuites[i].suiteName) {
found = true;
}
}
// only add the suite to the results if the suite doesn't already exist in the results
if (!found) {
configSuites.push({
suiteName: reportedSuites[resultSuiteIndex].name,
suiteTests: []
});
}
// in order to run a specific test, the suite must also be specified. Ignore otherwise
if (selectedSuiteArg && selectedTestArg) {
var found = false;
for (var i = 0; i < reportedTests.length; i++) {
if (selectedTestArg === reportedTests[i].name) {
resultTestIndex = i;
found = true;
break;
}
}
if (found) {
responseData = startTest();
} else {
/*
we can finish the entire config here since we know that a suite had to be
specified and therefore this suite is the only one being run for the config.
In the case of the test not being found, we could just ignore the arg and run
the entire suite but I would prefer to log an error and just finish the config
rather than running tests that the user did not specify
*/
driverUtils.log("specified test not found");
driverGlobal.platform.finishConfig();
}
} else {
responseData = startTest();
}
} else if (message.type === "result") {
/*
now that we have a result, make sure we cancel the timer first so we don't risk
triggering a exception while we process the result
*/
clearTimeout(timer);
addResult(message.result, message.description, message.duration);
driverUtils.log("suite<" + message.suite + "> test<" + message.test + "> result<" + message.result + ">");
if (selectedSuiteArg && selectedTestArg) {
/*
since this was the only test in the config that we needed to run just move onto the
next config
*/
driverGlobal.platform.finishConfig();
} else {
var next = incrementTest();
if (next === "suite") {
responseData = startSuite();
} else if (next === "test") {
responseData = startTest();
}
}
}
return responseData;
};
var startSuite = function() {
return "getTests|" + reportedSuites[resultSuiteIndex].name;
};
var startTest = function() {
var timeout = driverGlobal.config.defaultTestTimeout;
// if there is a custom timeout set for the test, override the default
var testTimeout = reportedTests[resultTestIndex].timeout;
if ((typeof testTimeout) !== "undefined") {
timeout = parseInt(testTimeout);
}
/*
add this so that we give a little overhead for network latency. The goal here is that is
that general network overhead doesn't eat up time out of a specific test timeout
*/
timeout += 500;
var suiteName = reportedSuites[resultSuiteIndex].name;
var testName = reportedTests[resultTestIndex].name;
/*
we need a fall back in the case of a test timing out which might occur for a variety of
reason such as poorly written test, legitimate test failure of device issue
*/
timer = setTimeout(function() {
addResult("timeout", "", timeout);
driverUtils.log("suite<" + suiteName + "> test<" + testName + "> result<timeout>");
if (selectedSuiteArg && selectedTestArg) {
driverGlobal.platform.finishConfig();
} else {
/*
make sure we skip to the next test in the event of failure otherwise this will
loop forever (assuming that the timeout is consistent with each run of the test)
*/
incrementTest();
driverGlobal.platform.resumeConfig();
}
}, timeout);
console.log("running suite<" + suiteName + "> test<" + testName + ">...");
return "run|" + suiteName + "|" + testName;
};
function addResult(result, description, duration) {
var configSuites = results[resultConfigSetIndex].setConfigs[resultConfigIndex].configSuites;
var numSuites = configSuites.length;
configSuites[numSuites - 1].suiteTests.push({
testName: reportedTests[resultTestIndex].name,
testResult: {
result: result,
description: description,
duration: duration
}
});
}
/*
* when a test is finished, update the current test and if need be, roll over into the
* next suite. returns value indicating what the next test position is
*/
var incrementTest = function() {
if (resultTestIndex < (reportedTests.length - 1)) {
resultTestIndex++;
return "test";
} else {
driverUtils.log("test run finished for suite<" + reportedSuites[resultSuiteIndex].name + ">");
if (resultSuiteIndex < (reportedSuites.length - 1)) {
if (selectedSuiteArg) {
driverGlobal.platform.finishConfig();
} else {
resultTestIndex = 0;
resultSuiteIndex++;
return "suite";
}
} else {
driverUtils.log("all suites completed");
driverGlobal.platform.finishConfig();
}
}
return "";
};
};