blob: fac13b24b2ae267d659ea8cf246c751a36e265e1 [file] [log] [blame]
#!/usr/bin/env node
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
/* jshint node: true */
"use strict";
var fs = require("fs");
var path = require("path");
var shelljs = require("shelljs");
var optimist = require("optimist");
var request = require("request");
var util = require("../lib/util");
var testwait = require("../lib/testwait");
// constants
var CORDOVA_MEDIC_DIR = "cordova-medic";
var DEFAULT_APP_PATH = "mobilespec";
var CORDOVA_ERROR_PATTERN = /^ERROR/m;
var NO_DEVICE_PATTERN = /(^.*no .* was detected)|(^.*no devices found)/m;
var DEFAULT_APP_ENTRY = "index.html";
var ANDROID_PAGE_LOAD_TIMEOUT = 120000; // in milliseconds
var MEDIC_BUILD_PREFIX = "medic-cli-build";
var DEFAULT_WINDOWS_VERSION = "store";
var WINDOWS_VERSION_CHOICES = ["store", "store80", "phone"];
var DEFAULT_TIMEOUT = 600; // in seconds
var SERVER_RESPONSE_TIMEOUT = 15000; // in milliseconds
var MAX_NUMBER_OF_TRIES = 3;
var WAIT_TIME_TO_RETRY_CONNECTION = 15000; // in milliseconds
// helpers
function currentMillisecond() {
return new Date().valueOf();
}
function generateBuildID() {
var components = [MEDIC_BUILD_PREFIX, currentMillisecond()];
return components.join("-");
}
function getConfigPath(appPath) {
return path.join(appPath, "config.xml");
}
function getCSPPath(appPath) {
return path.join(appPath, "www", "csp-incl.js");
}
function createMedicJson(appPath, buildId, couchdbURI) {
util.medicLog("Writing medic.json to " + appPath);
util.medicLog(" sha: " + buildId);
util.medicLog(" couchdb: " + couchdbURI);
// NOTE:
// the "sha" name is a misnomer, but is kept
// to be compatible with plugin-test-framework
var medicConfig = {
sha: buildId,
couchdb: couchdbURI
};
var medicConfigContents = JSON.stringify(medicConfig) + "\n";
var medicConfigPath = path.join(appPath, "www", "medic.json");
fs.writeFileSync(medicConfigPath, medicConfigContents, util.DEFAULT_ENCODING);
}
function addURIToWhitelist(appPath, uri) {
var configFile = getConfigPath(appPath);
var cspFile = getCSPPath(appPath);
var configContent = fs.readFileSync(configFile, util.DEFAULT_ENCODING);
var cspContent = fs.readFileSync(cspFile, util.DEFAULT_ENCODING);
// add whitelisting rule allow access to couch server
util.medicLog("Adding whitelist rule for CouchDB host: " + uri);
var accessOriginTag = "<access origin=\"" + uri + "\" />";
if (!util.contains(configContent, accessOriginTag)) {
configContent = configContent.split("</widget>").join("");
configContent += " " + accessOriginTag + "\n</widget>\n";
fs.writeFileSync(configFile, configContent, util.DEFAULT_ENCODING);
}
// add couchdb address to csp rules
util.medicLog("Adding CSP rule for CouchDB host: " + uri);
var cspRule = "connect-src " + uri;
if (!util.contains(cspContent, cspRule)) {
cspContent = cspContent.replace("connect-src", cspRule);
fs.writeFileSync(cspFile, cspContent, util.DEFAULT_ENCODING);
}
}
function setEntryPoint(appPath, entryPoint) {
var configFile = getConfigPath(appPath);
var configContent = fs.readFileSync(configFile, util.DEFAULT_ENCODING);
// replace/add start page preference
// check if config.xml already contains <content /> element
util.medicLog("Setting entry point to " + entryPoint + " in config.xml");
if (configContent.match(/<content\s*src=".*"\s*\/>/gi)) {
configContent = configContent.replace(
/<content\s*src=".*"\s*\/>/gi,
"<content src=\"" + entryPoint + "\" />"
);
} else {
// add entry point to config
configContent = configContent.split("</widget>").join("") +
" <content src=\"" + entryPoint + "\" />\n</widget>";
}
// write the changes
fs.writeFileSync(configFile, configContent, util.DEFAULT_ENCODING);
}
function changeAndroidLoadTimeout(appPath, timeout) {
util.medicLog("Increasing url loading timeout for android to " + timeout);
var timeoutRegex = /<preference\s*name\s *= \s*"?loadUrlTimeoutValue"?.*?((\/>)|(>.*?<\/\s*preference>))/i;
var timeoutTag = "<preference name=\"loadUrlTimeoutValue\" value=\"" + timeout + "\" />";
var timeoutTagWithPlatform = " <platform name=\"android\">\n <preference name=\"loadUrlTimeoutValue\" value=\"120000\" />\n </platform>\n";
var platformRegex = /<platform\s*name\s *= \s*"android"\s*>/i;
var widgetRegex = /<\/s*widget\s*>/i;
var configFile = getConfigPath(appPath);
var configContent = fs.readFileSync(configFile, util.DEFAULT_ENCODING);
if (timeoutRegex.test(configContent)) {
configContent = configContent.replace(timeoutRegex, timeoutTag);
util.medicLog("Found \"loadUrlTimeoutValue\" preference, replacing with desired value");
} else if (platformRegex.test(configContent)) {
var oldPlatformTag = platformRegex.exec(configContent)[0];
configContent = configContent.replace(platformRegex, oldPlatformTag + "\n " + timeoutTag);
util.medicLog("Found platform tag, appending \"loadUrlTimeoutValue\" preference");
} else if (widgetRegex.test(configContent)) {
var oldWidgetTag = widgetRegex.exec(configContent)[0];
configContent = configContent.replace(widgetRegex, timeoutTagWithPlatform + oldWidgetTag);
util.medicLog("Did not find platform tag, adding preference with platform tag");
} else {
util.medicLog("Warning: could not modify config.xml for android: no <widget> tag found!");
}
// write the changes
fs.writeFileSync(configFile, configContent, util.DEFAULT_ENCODING);
}
function setWindowsTargetStoreVersion(appPath, version) {
util.medicLog("setting target store version to " + version);
var configFile = getConfigPath(appPath);
var configContent = fs.readFileSync(configFile, util.DEFAULT_ENCODING);
var versionPreference = " <preference name=\"windows-target-version\" value=\"" + version + "\" />";
configContent = configContent.replace("</widget>", versionPreference + "\r\n</widget>");
fs.writeFileSync(configFile, configContent, "utf8");
}
function androidSpecificPreparation(argv) {
var appPath = argv.app;
var extraArgs = "--gradle";
changeAndroidLoadTimeout(appPath, ANDROID_PAGE_LOAD_TIMEOUT);
return extraArgs;
}
function windowsSpecificPreparation(argv) {
var appPath = argv.app;
var winVersion = argv.winvers;
var extraArgs = "";
if (!util.contains(WINDOWS_VERSION_CHOICES, winVersion)) {
util.fatal("invalid windows version: " + winVersion);
}
// set windows target store version
if (winVersion === "store80") {
setWindowsTargetStoreVersion(appPath, "8.0");
extraArgs = "--win";
} else if (winVersion === "store") {
setWindowsTargetStoreVersion(appPath, "8.1");
extraArgs = "--win";
} else if (winVersion === "phone") {
setWindowsTargetStoreVersion(appPath, "8.1");
extraArgs = "--phone";
}
// patch WindowsStoreAppUtils script to allow app run w/out active desktop/remote session
if (winVersion === "store80" || winVersion === "store") {
util.medicLog("Patching WindowsStoreAppUtils to allow app to be run in automated mode");
var platformPath = path.join(appPath, "platforms", "windows");
var libPath = path.join(platformPath, "cordova", "lib");
var appUtilsPath = path.join(libPath, "WindowsStoreAppUtils.ps1");
var srcScriptPath = path.join(CORDOVA_MEDIC_DIR, "lib", "patches", "EnableDebuggingForPackage.ps1");
var destScriptPath = path.join(libPath, "EnableDebuggingForPackage.ps1");
// copy over the patch
shelljs.cp("-f", srcScriptPath, libPath);
// add extra code to patch
shelljs.sed(
"-i",
/^\s*\$appActivator .*$/gim,
"$&\n" +
" powershell " + path.join(process.cwd(), destScriptPath) + " $$ID\n" +
" $Ole32 = Add-Type -MemberDefinition '[DllImport(\"Ole32.dll\")]public static extern int CoAllowSetForegroundWindow(IntPtr pUnk, IntPtr lpvReserved);' -Name 'Ole32' -Namespace 'Win32' -PassThru\n" +
" $Ole32::CoAllowSetForegroundWindow([System.Runtime.InteropServices.Marshal]::GetIUnknownForObject($appActivator), [System.IntPtr]::Zero)",
appUtilsPath
);
}
return extraArgs;
}
function getLocalCLI() {
if (util.isWindows()) {
return "cordova.bat";
} else {
return "./cordova";
}
}
function cordovaReturnedError(returnCode, output) {
if (returnCode !== 0 || CORDOVA_ERROR_PATTERN.test(output)) {
return true;
}
return false;
}
function failedBecauseNoDevice(output) {
return NO_DEVICE_PATTERN.test(output);
}
function tryConnect(couchdbURI, pendingNumberOfTries, callback) {
util.medicLog("checking if " + couchdbURI + " is up.");
// check if results server is up
request({
uri: couchdbURI,
method: "GET",
timeout: SERVER_RESPONSE_TIMEOUT
}).on('response', function (response){
callback();
}).on('error', function (error){
if(pendingNumberOfTries > 1) {
util.medicLog("it's not up. Going to retry after " + WAIT_TIME_TO_RETRY_CONNECTION + " milliseconds");
setTimeout(function (){
tryConnect(couchdbURI, pendingNumberOfTries-1 , callback);
}, WAIT_TIME_TO_RETRY_CONNECTION);
} else {
util.fatal("it's not up even after " + MAX_NUMBER_OF_TRIES + " attempts to connect, so test run can't be monitored");
process.exit(1);
}
});
}
// main
function main() {
// shell config
shelljs.config.fatal = false;
shelljs.config.silent = false;
// get args
var argv = optimist
.usage("Usage: $0 {options}")
.demand("platform")
.demand("couchdb")
.default("entry", DEFAULT_APP_ENTRY)
.default("id", generateBuildID())
.default("app", DEFAULT_APP_PATH)
.default("timeout", DEFAULT_TIMEOUT).describe("timeout", "timeout in seconds")
.default("winvers", DEFAULT_WINDOWS_VERSION).describe("winvers", "[" + WINDOWS_VERSION_CHOICES.join("|") + "]")
.argv;
var platform = argv.platform;
var buildId = argv.id;
var appPath = argv.app;
var couchdbURI = argv.couchdb;
var entryPoint = argv.entry;
var timeout = argv.timeout;
var cli = getLocalCLI();
// check that the app exists
if (!fs.existsSync(appPath)) {
util.fatal("app " + appPath + " does not exist");
}
tryConnect(couchdbURI, MAX_NUMBER_OF_TRIES, function (){
util.medicLog("it's up");
// modify the app to run autonomously
createMedicJson(appPath, buildId, couchdbURI);
setEntryPoint(appPath, entryPoint);
addURIToWhitelist(appPath, couchdbURI);
// do platform-specific preparations
var platformArgs = "";
if (platform === util.ANDROID) {
platformArgs = androidSpecificPreparation(argv);
} else if (platform === util.WINDOWS) {
platformArgs = windowsSpecificPreparation(argv);
}
// start waiting for test results
// NOTE:
// timeout needs to be in milliseconds, but it's
// given in seconds, so we multiply by 1000
testwait.init(couchdbURI);
testwait.waitTestsCompleted(buildId, timeout * 1000).then(
function onFulfilled(value) {
util.medicLog("got test results");
process.exit(0);
},
function onRejected(error) {
console.error("didn't get test results: " + error);
process.exit(1);
}
);
util.medicLog("started waiting for test results");
// enter the app directory
util.medicLog("moving into " + appPath);
shelljs.pushd(appPath);
// compose commands
var buildCommand = cli + " build " + platform + " -- " + platformArgs;
var runCommandEmulator = cli + " run --emulator " + platform + " -- " + platformArgs;
var runCommandDevice = cli + " run --device " + platform + " -- " + platformArgs;
// build the code
// NOTE:
// this is SYNCHRONOUS
util.medicLog("running:");
util.medicLog(" " + buildCommand);
var result = shelljs.exec(buildCommand, {silent: false, async: false});
if (cordovaReturnedError(result.code, result.output)) {
util.fatal("build failed");
}
// run the code
// NOTE:
// this is ASYNCHRONOUS
util.medicLog("running:");
util.medicLog(" " + runCommandDevice);
shelljs.exec(runCommandDevice, {silent: false, async: true}, function (returnCode, output) {
if (failedBecauseNoDevice(output)) {
util.medicLog("no device found, so switching to emulator");
util.medicLog("running:");
util.medicLog(" " + runCommandEmulator);
shelljs.exec(runCommandEmulator, {silent: false, async: true}, function (returnCode, output) {
if (cordovaReturnedError(returnCode, output)) {
util.fatal("running on emulator failed");
}
});
} else {
if (cordovaReturnedError(returnCode, output)) {
util.fatal("running on device failed");
}
}
});
});
}
main();