allow loading of app by path or url from desired capabilities

also a lot of stability improvements and error handling
This commit is contained in:
Jonathan Lipps
2013-01-25 09:06:54 -08:00
committed by Sebastian Tiedtke
parent 79f971670e
commit a9d58a6ce7
9 changed files with 222 additions and 20 deletions

View File

@@ -3,6 +3,9 @@
"use strict";
var routing = require('./routing')
, logger = require('../logger').get('appium')
, helpers = require('./helpers')
, downloadFile = helpers.downloadFile
, unzipApp = helpers.unzipApp
, UUID = require('uuid-js')
, _ = require('underscore')
, ios = require('./ios');
@@ -21,6 +24,7 @@ var Appium = function(args) {
this.sessions = [];
this.counter = -1;
this.progress = -1;
this.tempFiles = [];
};
Appium.prototype.attachTo = function(rest, cb) {
@@ -35,9 +39,76 @@ Appium.prototype.attachTo = function(rest, cb) {
};
Appium.prototype.start = function(desiredCaps, cb) {
this.desiredCapabilities = desiredCaps;
this.sessions[++this.counter] = { sessionId: '', callback: cb };
this.invoke();
this.configure(desiredCaps, _.bind(function(err) {
this.desiredCapabilities = desiredCaps;
if (err) {
cb(err, null);
} else {
this.sessions[++this.counter] = { sessionId: '', callback: cb };
this.invoke();
}
}, this));
};
Appium.prototype.configure = function(desiredCaps, cb) {
if (typeof desiredCaps.app !== "undefined") {
if (desiredCaps.app[0] === "/") {
this.args.app = desiredCaps.app;
logger.info("Using local app from desiredCaps: " + desiredCaps.app);
cb(null);
} else if (desiredCaps.app.substring(0, 4) === "http") {
var appUrl = desiredCaps.app;
if (appUrl.substring(appUrl.length - 4) === ".zip") {
try {
this.downloadAndUnzipApp(appUrl, _.bind(function(zipErr, appPath) {
if (zipErr) {
cb(zipErr);
} else {
this.args.app = appPath;
logger.info("Using extracted app: " + this.args.app);
cb(null);
}
}, this));
logger.info("Using downloadable app from desiredCaps: " + appUrl);
} catch (e) {
var err = e.toString();
logger.error("Failed downloading app from appUrl " + appUrl);
cb(err);
}
} else {
cb("App URL (" + appUrl + ") didn't seem to end in .zip");
}
} else if (!this.args.app) {
cb("Bad app passed in through desiredCaps: " + desiredCaps.app +
". Apps need to be absolute local path or URL to zip file");
} else {
logger.warn("Got bad app through desiredCaps: " + desiredCaps.app);
logger.warn("Sticking with default app: " + this.args.app);
this.desiredCapabilities.app = this.args.app;
cb(null);
}
} else if (!this.args.app) {
cb("No app set; either start appium with --app or pass in an 'app' " +
"value in desired capabilities");
} else {
logger.info("Using app from command line: " + this.args.app);
cb(null);
}
};
Appium.prototype.downloadAndUnzipApp = function(appUrl, cb) {
var me = this;
downloadFile(appUrl, function(zipPath) {
me.tempFiles.push(zipPath);
unzipApp(zipPath, function(err, appPath) {
if (err) {
cb(err, null);
} else {
me.tempFiles.push(appPath);
cb(null, appPath);
}
});
});
};
Appium.prototype.invoke = function() {

View File

@@ -44,8 +44,7 @@ exports.createSession = function(req, res) {
var desired = req.body.desiredCapabilities;
req.appium.start(req.body.desiredCapabilities, function(err, instance) {
if (err) {
// of course we need to deal with err according to the WDJP spec.
throw err;
return res.send({status: status.codes.NoSuchDriver, value: err});
}
if (desired && desired.newCommandTimeout) {

95
app/helpers.js Normal file
View File

@@ -0,0 +1,95 @@
"use strict";
var logger = require('../logger').get('appium')
, url = require('url')
, fs = require('fs')
, path = require('path')
, exec = require('child_process').exec
, http = require('http')
, temp = require('temp');
exports.downloadFile = function(fileUrl, cb) {
// We will be downloading the files to a directory, so make sure it's there
// This step is not required if you have manually created the directory
temp.open({prefix: 'appium-app', suffix: '.zip'}, function(err, info) {
var options = {
host: url.parse(fileUrl).host,
port: 80,
path: url.parse(fileUrl).pathname
};
var file = fs.createWriteStream(info.path);
http.get(options, function(res) {
res.on('data', function(data) {
file.write(data);
}).on('end', function() {
file.end();
logger.info(fileUrl + ' downloaded to ' + info.path);
cb(info.path);
});
});
});
};
exports.unzipFile = function(zipPath, cb) {
logger.info("Unzipping " + zipPath);
var execOpts = {cwd: path.dirname(zipPath)};
exports.testZipArchive(zipPath, function(err, valid) {
if (valid) {
exec('unzip -o ' + zipPath, execOpts, function(err, stderr, stdout) {
if (!err) {
logger.info("Unzip successful");
cb(null, stderr);
} else {
logger.error("Unzip threw error " + err);
logger.error("Stderr: " + stderr);
logger.error("Stdout: " + stdout);
cb("Archive could not be unzipped, check appium logs.", null);
}
});
} else {
cb(err, null);
}
});
};
exports.testZipArchive = function(zipPath, cb) {
logger.info("Testing zip archive: " + zipPath);
var execOpts = {cwd: path.dirname(zipPath)};
exec("unzip -t " + zipPath, execOpts, function(err, stderr, stdout) {
if (!err) {
if(/No errors detected/.exec(stderr)) {
logger.info("Zip archive tested clean");
cb(null, true);
} else {
logger.error("Zip file " + zipPath + " was not valid");
logger.error("Stderr: " + stderr);
logger.error("Stdout: " + stdout);
cb("Zip archive did not test successfully, check appium server logs " +
"for output", false);
}
} else {
logger.error("Test zip archive threw error " + err);
logger.error("Stderr: " + stderr);
logger.error("Stdout: " + stdout);
cb("Error testing zip archive, are you sure this is a zip file?", null);
}
});
};
exports.unzipApp = function(zipPath, cb) {
exports.unzipFile(zipPath, function(err, output) {
if (!err) {
var match = /inflating: ([^\/]+\.app)\//.exec(output);
if (match) {
var appPath = path.resolve(path.dirname(zipPath), match[1]);
cb(null, appPath);
} else {
cb("App zip unzipped OK, but we couldn't find a .app bundle in it. " +
"Make sure your archive contains the .app package and nothing else",
null);
}
} else {
cb(err, null);
}
});
};

View File

@@ -43,17 +43,24 @@ var IOS = function(rest, app, udid, verbose, removeTraceDir) {
IOS.prototype.start = function(cb, onDie) {
var me = this;
var didLaunch = false;
if (typeof onDie === "function") {
this.onStop = onDie;
}
var onLaunch = function() {
didLaunch = true;
logger.info('Instruments launched. Starting poll loop for new commands.');
me.instruments.setDebug(true);
cb(null);
};
var onExit = function(code, traceDir) {
if (!didLaunch) {
logger.error("Instruments did not launch successfully, failing session");
cb("Instruments did not launch successfully--please check your app " +
"paths or bundle IDs and try again");
}
this.instruments = null;
if (me.removeTraceDir && traceDir) {
rimraf(traceDir, function() {

View File

@@ -10,7 +10,7 @@ module.exports = function() {
});
parser.addArgument([ '--app' ]
, { required: true, help: 'path to simulators .app file or the bundle_id of the desired target on device'
, { required: false, help: 'path to simulators .app file or the bundle_id of the desired target on device'
});
parser.addArgument([ '-V', '--verbose' ], { required: false, help: 'verbose mode' });

View File

@@ -53,10 +53,19 @@ Instruments.prototype.startSocketServer = function(sock) {
fs.unlinkSync(sock);
} catch (Exception) {}
var onSocketNeverConnect = function() {
logger.error("Instruments socket client never checked in; timing out".red);
this.proc.kill('SIGTERM');
this.exitHandler(1);
};
var socketConnectTimeout = setTimeout(_.bind(onSocketNeverConnect, this), 30000);
var server = net.createServer({allowHalfOpen: true}, _.bind(function(conn) {
if (!this.hasConnected) {
this.hasConnected = true;
this.debug("Instruments is ready to receive commands");
clearTimeout(socketConnectTimeout);
this.readyHandler(this);
}
conn.setEncoding('utf8'); // get strings from sockets rather than buffers
@@ -109,19 +118,19 @@ Instruments.prototype.launch = function() {
var tmpDir = '/tmp/' + this.guid;
fs.mkdir(tmpDir, function(e) {
if (!e || (e && e.code === 'EEXIST')) {
self.proc = self.spawnInstruments(tmpDir);
self.proc.stdout.on('data', function(data) {
self.outputStreamHandler(data);
});
self.proc.stderr.on('data', function(data) {
self.errorStreamHandler(data);
});
self.proc = self.spawnInstruments(tmpDir);
self.proc.stdout.on('data', function(data) {
self.outputStreamHandler(data);
});
self.proc.stderr.on('data', function(data) {
self.errorStreamHandler(data);
});
self.proc.on('exit', function(code) {
self.debug("Instruments exited with code " + code);
self.exitCode = code;
self.exitHandler(self.exitCode, self.traceDir);
});
self.proc.on('exit', function(code) {
self.debug("Instruments exited with code " + code);
self.exitCode = code;
self.exitHandler(self.exitCode, self.traceDir);
});
} else {
throw e;
}

View File

@@ -32,7 +32,9 @@
"path": "~0.4.9",
"rimraf": "~2.1.1",
"uuid-js": "~0.7.4",
"winston": "~0.6.2"
"temp": "~0.5.0",
"winston": "~0.6.2",
"unzip": "~0.1.1"
},
"scripts": {
"test": "grunt lint unit"
@@ -46,7 +48,6 @@
"grunt-mocha-test": "0.0.1",
"request": "~2.12.0",
"winston": "~0.6.2",
"temp": "~0.5.0",
"difflib": "~0.2.4",
"prompt": "~0.2.9"
}

View File

@@ -1,4 +1,5 @@
/*global it:true */
"use strict";
var describeWd = require('../helpers/driverblock.js').describe

View File

@@ -3,6 +3,8 @@
var wd = require('wd')
, _ = require("underscore")
, path = require("path")
, should = require("should")
, defaultHost = '127.0.0.1'
, defaultPort = 4723
, defaultCaps = {
@@ -22,6 +24,7 @@ var driverBlock = function(tests, host, port, caps, extraCaps) {
beforeEach(function(done) {
driverHolder.driver = wd.remote(host, port);
driverHolder.driver.init(caps, function(err, sessionId) {
should.not.exist(err);
driverHolder.sessionId = sessionId;
done();
});
@@ -45,5 +48,21 @@ var describeWithDriver = function(desc, tests, host, port, caps, extraCaps) {
});
};
var describeForApp = function(app, isBundle) {
var appPath;
if (typeof isBundle === "undefined") {
isBundle = false;
}
if (/\//.exec(app) || isBundle) {
appPath = app;
} else {
appPath = path.resolve(__dirname, "../../sample-code/apps/" + app + "/build/Release-iphonesimulator/" + app + ".app");
}
defaultCaps = _.extend(defaultCaps, {app: appPath});
return describeWithDriver;
};
module.exports.block = driverBlock;
module.exports.describe = describeWithDriver;
module.exports.describeForApp = describeForApp;