diff --git a/.eslintrc b/.eslintrc index ab0a673..6189871 100644 --- a/.eslintrc +++ b/.eslintrc @@ -2,8 +2,8 @@ "env": { "node": true }, - "globals": { - "Mobile": true + "parserOptions": { + "ecmaVersion": 6 }, "rules": { "no-cond-assign": 2, diff --git a/CI.js b/CI.js index d117cf3..3c62db6 100644 --- a/CI.js +++ b/CI.js @@ -1,8 +1,17 @@ -var http = require('http'); -var greq = require('./hook/git_request'); +// Copyright (C) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE.txt file in the project root +// for full license information. +// + +'use strict'; + var db = require('./db_actions'); var git = require('./hook/git_actions'); -require('./logger'); +var greq = require('./hook/git_request'); +var http = require('http'); + +var Logger = require('./logger'); +var logger = new Logger(); // VM builder require('./builder/virtual.js'); @@ -11,14 +20,14 @@ require('./builder/virtual.js'); require('./tasker/taskman'); function getPost(request, response, callback) { - var queryData = ""; + var queryData = ''; if (typeof callback !== 'function') return null; if (request.method == 'POST') { request.on('data', function (data) { queryData += data; if (queryData.length > 1e6) { - queryData = ""; + queryData = ''; response.writeHead(413, {'Content-Type': 'text/plain'}).end(); request.connection.destroy(); } @@ -30,7 +39,7 @@ function getPost(request, response, callback) { callback(); } catch (e) { // TODO log bad request - response.writeHead(200, "OK", {'Content-Type': 'text/plain'}); + response.writeHead(200, 'OK', {'Content-Type': 'text/plain'}); response.end(); } }); @@ -43,7 +52,7 @@ function getPost(request, response, callback) { db.getGithubUser(function (data) { if (!data || !data.username) { - throw new Error("Github user data or data.username is undefined or null"); + throw new Error('Github user data or data.username is undefined or null'); } git.setLogin(data.username, data.password); @@ -55,18 +64,20 @@ db.getGithubUser(function (data) { greq.OnRequest(request, response); } catch (e) { // TODO log the things went bad - logme("Error On Request: ", e, "red"); + logger.error('Error On Request: ', e); } - response.writeHead(200, "OK", {'Content-Type': 'text/plain'}); + response.writeHead(200, 'OK', {'Content-Type': 'text/plain'}); response.end(); }); } else { // TODO locate webUI here - response.writeHead(200, "OK", {'Content-Type': 'text/plain'}); + response.writeHead(200, 'OK', {'Content-Type': 'text/plain'}); response.end(); } - }).listen(8080); - logme("Github WebHook Server Started on 8080", "green"); -}); \ No newline at end of file + }) + .listen(8080); + + logger.info('Github WebHook Server Started on 8080'); +}); diff --git a/builder/virtual.js b/builder/virtual.js index 5798aa4..a817d47 100644 --- a/builder/virtual.js +++ b/builder/virtual.js @@ -1,12 +1,21 @@ +// Copyright (C) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE.txt file in the project root +// for full license information. +// + +'use strict'; -var git = require('./../hook/git_actions'); var db = require('./../db_actions'); +var exec = require('child_process').exec; +var execSync = jxcore.utils.cmdSync; var fs = require('fs'); +var git = require('./../hook/git_actions'); var path = require('path'); -var exec = require('child_process').exec; -var sync = jxcore.utils.cmdSync; var tester = require('../internal/tester'); +var Logger = require('../logger'); +var logger = new Logger(); + var eopts = { encoding: 'utf8', timeout: 1000 * 60 * 35, // single command timeout - 35 min. @@ -27,17 +36,17 @@ var stopVM = function (cb) { var vm = '/Applications/VMware\\ Fusion.app/Contents/Library/vmrun'; exec(vm + ' stop ~/Desktop/Virtual\\ Machines/OSXDEV.vmwarevm/OSXDEV.vmx', eopts, function (err, out, stderr) { if (err) - logme('Error stopping VM', err, out, stderr, 'red'); + logger.error('Error stopping VM', err, out, stderr); cb(err, out, stderr); }); }; var resetVM = function (cb) { - var vm = "/Applications/VMware\\ Fusion.app/Contents/Library/vmrun"; - vmChild = exec(vm + " revertToSnapshot ~/Desktop/Virtual\\ Machines/OSXDEV.vmwarevm/OSXDEV.vmx snapshot1", eopts, function (err, stdout, stderr) { + var vm = '/Applications/VMware\\ Fusion.app/Contents/Library/vmrun'; + vmChild = exec(vm + ' revertToSnapshot ~/Desktop/Virtual\\ Machines/OSXDEV.vmwarevm/OSXDEV.vmx snapshot1', eopts, function (err, stdout, stderr) { vmChild = null; if (err) { - logme("Error: Something went terribly bad.. ", err + "\n" + stdout + "\n" + stderr, "red"); + logger.error('Error: Something went terribly bad.. ', err + '\n' + stdout + '\n' + stderr); stopVM(function () { setTimeout(function () { resetVM(cb); @@ -47,13 +56,14 @@ var resetVM = function (cb) { return; } - logme("VM: Revert snapshot", "green"); + logger.info('VM: Revert snapshot'); // check queue if there is something start vmChild = exec(vm + " start ~/Desktop/Virtual\\ Machines/OSXDEV.vmwarevm/OSXDEV.vmx", eopts, function (err, stdout, stderr) { vmChild = null; if (err) { - logme("Error: Something went terribly bad... ", err + "\n" + stdout + "\n" + stderr, "red"); + logger.error("Error: Something went terribly bad... ", err + "\n" + stdout + "\n" + stderr); + stopVM(function () { setTimeout(function () { resetVM(cb); @@ -61,7 +71,7 @@ var resetVM = function (cb) { }); return; } - logme("VM: Start OS", "green"); + logger.info('VM: Start OS'); builderReset = false; cb(); @@ -117,7 +127,7 @@ var updateScripts = function (job, cmd) { var jobErrorReportAndRemove = function (job, err, stdout, stderr) { var msg = err + "\n\n" + stdout + "\n\n" + stderr; - logme("Error: ", msg, "(JOB ID:" + job.uqID + ")", "red"); + logger.error("Error: ", msg, "(JOB ID:" + job.uqID + ")"); if (msg.length > 12 * 1024) { var left = msg.substr(0, 6 * 1024); @@ -158,7 +168,7 @@ var runBuild = function (cmds, job, index, cb) { if (!cmd.sync) updateScripts(job, cmd); - currentBuildCommand = exec("cd " + __dirname + ";" + cmd.cmd, eopts, function (err, stdout, stderr) { + currentBuildCommand = exec('cd ' + __dirname + ';' + cmd.cmd, eopts, function (err, stdout, stderr) { currentBuildCommand = null; if (cancelJobId == job.prId) return; @@ -166,7 +176,7 @@ var runBuild = function (cmds, job, index, cb) { // cleanup the script file if (!cmd.sync && cmd.to) { for (var i = 0; i < cmd.to.length; i++) { - sync("rm " + __dirname + "/" + cmd.to[i]) + execSync('rm ' + __dirname + '/' + cmd.to[i]); } } @@ -296,12 +306,12 @@ var buildJob = function (job) { tester.logIssue(job, 'Test ' + job.prId + ' (' + job.commitIndex + ') build started.', ''); - logme("Running builds for job:", job.uqID); + logger.info('Running builds for job:', job.uqID); runBuild(cmds, job, 0, function (err) { if (err || cancelJobId == job.prId) return; - logme("Build finished", "green"); + logger.info('Build finished'); activeJobId = 0; cancelJobId = 0; @@ -309,7 +319,7 @@ var buildJob = function (job) { var prPath = "builds/" + job.uqID; exec("cd " + __dirname + "; rm -rf " + prPath + "; mkdir -p " + prPath + "; mv build_ios/ " + prPath + "; mv build_android/ " + prPath, eopts, function (err, stdout, stderr) { if (err) { - logme("something happened and couldn't move the builds?", err, stdout, stderr, "red"); + logger.error("something happened and couldn't move the builds?", err, stdout, stderr); jobErrorReportAndRemove(job, err, stdout, stderr); } else { // save job @@ -317,11 +327,12 @@ var buildJob = function (job) { db.updateJob(job); } - if (job.target != "all") { - if (job.target == 'ios') - sync("cd " + __dirname + "; rm -rf " + prPath + "/build_android"); - else - sync("cd " + __dirname + "; rm -rf " + prPath + "/build_ios"); + if (job.target != 'all') { + if (job.target == 'ios') { + execSync('cd ' + __dirname + '; rm -rf ' + prPath + '/build_android'); + } else { + execSync('cd ' + __dirname + '; rm -rf ' + prPath + '/build_ios'); + } } builderReset = true; @@ -341,9 +352,9 @@ exports.IsActive = function (prId) { }; exports.cancelIfActive = function (prId) { - logme("checking for cancel ", prId, activeJobId, "yellow"); + logger.info("checking for cancel ", prId, activeJobId); if (activeJobId == prId) { - logme("Cancelling job ", prId, "yellow"); + logger.info("Cancelling job ", prId); cancelJobId = prId; var vm = "/Applications/VMware\\ Fusion.app/Contents/Library/vmrun"; builderReset = true; diff --git a/db_actions.js b/db_actions.js index 4fdfab4..eecb830 100644 --- a/db_actions.js +++ b/db_actions.js @@ -1,15 +1,24 @@ +// Copyright (C) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE.txt file in the project root +// for full license information. +// + +'use strict'; + +var fs = require('fs'); var loki = require('lokijs'); -var server = new loki('server.json'); var local = new loki('config.json'); -var fs = require('fs'); -var virtual = require('./builder/virtual'); +var server = new loki('server.json'); + +var Logger = require('./logger'); +var logger = new Logger(); exports.nodeCount = 2; var config, hook, test; if (!fs.existsSync('config.json')) { - console.log("Creating config.json") + console.log('Creating config.json'); local.addCollection('config'); // local.getCollection('config').insert({name: "GithubUser", username: "obastemur", password: ""}) local.saveDatabase(); @@ -17,7 +26,7 @@ if (!fs.existsSync('config.json')) { if (!fs.existsSync('server.json')) { - console.log("Creating server.json") + console.log('Creating server.json'); server.addCollection('hooks'); server.addCollection('test'); server.saveDatabase(); @@ -26,21 +35,22 @@ if (!fs.existsSync('server.json')) { exports.getGithubUser = function (cb) { local.loadDatabase({}, function () { config = local.getCollection('config'); - var arr = config.find({name: "GithubUser"}) + var arr = config.find({name: 'GithubUser'}); + server.loadDatabase({}, function () { hook = server.getCollection('hooks'); if (!hook) { - console.error("server.json is empty!"); + console.error('server.json is empty!'); process.exit(1); } test = server.getCollection('test'); cb(arr.length ? arr[0] : null); - }) + }); }); }; -exports.getHookInfo = function (repo_name) { - return hook.findObject({repository: repo_name}); +exports.getHookInfo = function (repoName) { + return hook.findObject({repository: repoName}); }; exports.saveHook = function (opts) { @@ -53,32 +63,6 @@ exports.updateHook = function (obj) { server.saveDatabase(); }; -function find(arr, props, vals) { - for (var i = 0; i < arr.length; i++) { - var marker = 0; - for (var j = 0; j < props.length; j++) { - if (arr[i][j] == vals[j]) { - marker++; - } - } - - if (marker == props.length) - return i; - } - - return -1; -} - -function remove(arr, index) { - var tmp = []; - for (var i = 0; i < arr.length; i++) { - if (i != index) - tmp.push(arr[i]) - } - - return tmp; -} - exports.updateJob = function (job) { // grab job list var obj = test.findObject({pt_zero: 0}); @@ -92,7 +76,7 @@ exports.updateJob = function (job) { var q = obj.jobsQueue; for (var i = 0; i < q.length; i++) { - if (q[i].uqID == job.uqID) { + if (q[i].uqID === job.uqID) { q[i] = job; break; } @@ -121,7 +105,7 @@ exports.removeJob = function (job) { var q = obj.jobsQueue; var arr = []; for (var i = 0; i < q.length; i++) { - if (q[i].uqID != job.uqID) { + if (q[i].uqID !== job.uqID) { arr.push(q[i]); } } @@ -140,7 +124,9 @@ exports.removeJob = function (job) { // result can be null (in case the box is looking for a new job) exports.getJob = function (isBuilder) { // give a job to an empty node - if (!test) return {noJob: true}; + if (!test) { + return {noJob: true}; + } // grab job list var obj = test.findObject({pt_zero: 0}); @@ -162,11 +148,15 @@ exports.getJob = function (isBuilder) { var q = obj.jobsQueue; for (var i = 0; i < q.length; i++) { if (!isBuilder) { - if (!q[i].compiled) continue; + if (!q[i].compiled) { + continue; + } return q[i]; } else { - if (q[i].compiled) continue; + if (q[i].compiled) { + continue; + } return q[i]; } @@ -189,7 +179,7 @@ exports.hasJob = function(prId, commitIndex) { var uid = prId + commitIndex; var q = obj.jobsQueue; for (var i = 0; i < q.length; i++) { - if (q[i].uqID == uid) { + if (q[i].uqID === uid) { return true; } } @@ -213,7 +203,7 @@ exports.addJob = function (user, repo, branch, opts, json) { prId: opts.prId, // prId or hookId prNumber: opts.prNumber, // null or prNumber sender: opts.sender, // sender user - title: opts.title.replace(/[:\)\(#;+*]/g, "_"), // repo or pr title + title: opts.title.replace(/[:\)\(#;+*]/g, '_'), // repo or pr title target: json.target, // all, ios, android priority: json.priority, // normal, asap, now compiled: false, // whether osx VM compiled the application file or not @@ -223,13 +213,13 @@ exports.addJob = function (user, repo, branch, opts, json) { }; // locate based on priority - if (json.priority == "now" || json.priority == "asap") { + if (json.priority === 'now' || json.priority === 'asap') { obj.jobsQueue.unshift(job); } else { obj.jobsQueue.push(job); } - logme("New Test", job.user, job.repo, job.prId, "green"); + logger.info('New Test', job.user, job.repo, job.prId); // update collection test.update(obj); @@ -237,8 +227,9 @@ exports.addJob = function (user, repo, branch, opts, json) { // save to file system server.saveDatabase(); - if (json.priority == "now" || json.priority == "asap") + if (json.priority === 'now' || json.priority === 'asap') { return 1; + } return obj.jobsQueue.length; }; @@ -246,7 +237,7 @@ exports.addJob = function (user, repo, branch, opts, json) { exports.addGithubUser = function (uname, pass) { local.loadDatabase({}, function () { config = local.getCollection('config'); - config.insert({name: "GithubUser", username: uname, password: pass}); + config.insert({name: 'GithubUser', username: uname, password: pass}); local.saveDatabase(); }); -}; \ No newline at end of file +}; diff --git a/hook/git_actions.js b/hook/git_actions.js index f4602d2..e1735fe 100644 --- a/hook/git_actions.js +++ b/hook/git_actions.js @@ -1,8 +1,18 @@ -var GitHubApi = require("github"); +// Copyright (C) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE.txt file in the project root +// for full license information. +// + +'use strict'; + +var GitHubApi = require('github'); var path = require('path'); +var reporting = require('../reporting/report'); var tester = require('./../internal/tester'); var virtual = require('./../builder/virtual') -var reporting = require('../reporting/report'); + +var Logger = require('../logger'); +var logger = new Logger(); var github = new GitHubApi({ // required @@ -18,7 +28,7 @@ var github = new GitHubApi({ }); var createIssue = function (user, repo, title, body, cb) { - logme("Creating Github Issue", "red"); + logger.info('Creating Github Issue'); if (!cb) cb = function () { @@ -35,11 +45,11 @@ var createIssue = function (user, repo, title, body, cb) { exports.createIssue = createIssue; var createGist = function (title, body, cb) { - logme("Creating Github Gist", "red"); + logger.info('Creating Github Gist'); var opts = { description: title, - public: "true", + public: 'true', files: {} }; @@ -140,7 +150,7 @@ var addBranchToQueue = function (user, repo, branch, opts) { getContent(user, repo, "/mobile_test.json", branch, function (err, res) { if (err) { createIssue(user, repo, "mobile_test.json is missing", "Skipped testing branch `" + branch + "`. There was no mobile_test.json file"); - logme("skipped testing branch", branch, "on", user + "/" + repo, "(no mobile_test.json found)", "red"); + logger.info("skipped testing branch", branch, "on", user + "/" + repo, "(no mobile_test.json found)"); return; } else { if (res instanceof Buffer) { @@ -169,13 +179,13 @@ var addBranchToQueue = function (user, repo, branch, opts) { delete json.body; } - logme("Job Index", index); + logger.info("Job Index", index); return; } } catch (e) { Error.captureStackTrace(e); - logme("Error at addBranchQueue", e, e.stack, "red"); + logger.error("Error at addBranchQueue", e, e.stack); err = e; } } @@ -184,7 +194,7 @@ var addBranchToQueue = function (user, repo, branch, opts) { + branch + ". mobile_test.json file must be broken. \n\n> " + (err ? err : "") + "\n\n```\n" + res + "\n```"); - logme("skipped testing branch", branch, "on", user + "/" + repo, "(mobile_test.json file must be corrupted)", "red"); + logger.error('skipped testing branch', branch, 'on', user + '/' + repo, '(mobile_test.json file must be corrupted)'); } }); }; @@ -212,7 +222,7 @@ var newTest = function (prId, prNumber, user, title, repo_name, branch, commits, // check the updated file types to see if this PR deserves testing getFiles(opts, function (err, res) { if (err) { - logme("Error newTest (getFiles) : " + err, "red"); + logger.error("Error newTest (getFiles) : " + err); } else { for (var i = 0; i < res.length; i++) { var ext = path.extname(res[i].filename).toLowerCase(); @@ -230,7 +240,7 @@ var newTest = function (prId, prNumber, user, title, repo_name, branch, commits, opts.body = "Skipping PR - No APP related changes"; createComment(opts, function () { }); - logme("Skipping PR (no app changes):", prNumber, repo_name, "green"); + logger.info("Skipping PR (no app changes):", prNumber, repo_name); } }); }; diff --git a/hook/git_request.js b/hook/git_request.js index 729b820..3e58f13 100644 --- a/hook/git_request.js +++ b/hook/git_request.js @@ -1,12 +1,23 @@ +// Copyright (C) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE.txt file in the project root +// for full license information. +// + +'use strict'; + var db = require('./../db_actions'); +var fs = require('fs'); var git = require('./git_actions'); +var Logger = require('../logger'); +var logger = new Logger(); + var jobs_done = {}; exports.OnRequest = function (req, res) { var json = req.post; if (!json || typeof json !== 'object') { - logme("Error: corrupted request?", req.post, "red"); + logger.error('Error: corrupted request?', req.post); return; } @@ -14,14 +25,14 @@ exports.OnRequest = function (req, res) { if (json.action != 'opened' && json.action != 'reopened' && json.action != 'synchronize') { - console.log('Skipping PR > ', json.action, json.number, json.pull_request.title); + logger.info('Skipping PR > ', json.action, json.number, json.pull_request.title); return; } try { - require('fs').writeFileSync('last_request.json', JSON.stringify(json, null, 2)); + fs.writeFileSync('last_request.json', JSON.stringify(json, null, 2)); } catch (e_) { - console.error('Couldn\'t write the JSON', e_); + logger.error('Couldn\'t write the JSON', e_); } var prNumber = json.number; @@ -51,7 +62,7 @@ exports.OnRequest = function (req, res) { if (pr && pr.state && pr.state != 'open') { // nothing to do } else { - logme('BAD REQUEST from repo', repo_name, 'red'); + logger.error('BAD REQUEST from repo', repo_name); } } else { if (db.hasJob(prNumber, commits)) return; @@ -59,7 +70,7 @@ exports.OnRequest = function (req, res) { if (jobs_done[prNumber + commits]) return; jobs_done[prNumber + commits] = 1; - logme('PR >', prId, commits, prNumber, user, title, repo_name, branch, 'yellow'); + logger.info('PR >', prId, commits, prNumber, user, title, repo_name, branch); git.newTest(prId, prNumber, user, title, repo_name, branch, commits, target_branch); } } else if (json.hook && (typeof json.hook_id !== 'undefined')) { // new web hook @@ -70,7 +81,7 @@ exports.OnRequest = function (req, res) { if (repo_name.indexOf('thaliproject/') != 0 && repo_name.indexOf('obastemur/') != 0) { - logme("BAD REQUEST from repo", repo_name, "red"); + logger.error('BAD REQUEST from repo', repo_name); } else { // check if we have the repo on DB var obj = db.getHookInfo(repo_name); @@ -80,13 +91,13 @@ exports.OnRequest = function (req, res) { sender: json.sender.login, repository: repo_name }); - logme("New Hook Received", hook_id, "sender:" + json.sender.login, "repo:" + repo_name, "yellow"); + logger.info('New Hook Received', hook_id, 'sender:' + json.sender.login, 'repo:' + repo_name); } else { obj.hook_id = hook_id; obj.sender = json.sender.login; db.updateHook(obj); - logme("Received webhook for a repository that is already in database", repo_name, "yellow"); + logger.info('Received webhook for a repository that is already in database', repo_name); } git.newTest(hook_id, null, user, json.repository.description, repo_name, branch, /*hook commit index is 99999, meaning @@ -94,6 +105,6 @@ exports.OnRequest = function (req, res) { the hook registered into db*/ 99999); } } else { - logme('Unkown Request Received', 'red'); + logger.warn('Unkown Request Received'); } }; diff --git a/internal/tester.js b/internal/tester.js index 3407a05..fb630cc 100644 --- a/internal/tester.js +++ b/internal/tester.js @@ -1,12 +1,21 @@ -require('../logger'); -var git = require('./../hook/git_actions'); -var db = require('./../db_actions'); -var path = require('path'); +// Copyright (C) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE.txt file in the project root +// for full license information. +// + +'use strict'; + +var db = require('../db_actions'); var fs = require('fs'); +var git = require('../hook/git_actions'); +var path = require('path'); + +var Logger = require('../logger'); +var logger = new Logger(); exports.validateConfig = function (user, repo, json) { if (!json) { - logme("Error: json is null!", "red"); + logger.error("Error: json is null!"); return false; } @@ -143,11 +152,11 @@ exports.validateConfig = function (user, repo, json) { exports.createJob = function (user, repo, branch, json, opts) { if (user != "thaliproject" && user != "obastemur") { - logme("Unkown repo:", user + "/" + repo, "(discarding job)", "red"); + logger.error("Unkown repo:", user + "/" + repo, "(discarding job)"); } else { // see if we have the hook for the user/repo if (!db.getHookInfo(user + "/" + repo)) { - logme("Unkown repo:", user + "/" + repo, "(discarding job)", "red"); + logger.error("Unkown repo:", user + "/" + repo, "(discarding job)"); if (opts.prNumber) { opts.body = "Discarding the PR Testing JOB. Looks like Repository records are changed. Please re-define the WebHook for testing"; @@ -173,7 +182,7 @@ var logIssue = function (job, title, body) { }; git.createComment(opts, function (err, res) { if (err) { - logme("Error: PR commit failed", err, opts); + logger.error("Error: PR commit failed", err, opts); } }); } else { @@ -218,7 +227,7 @@ var grabLogs = function (job, target) { var fname = git.commitFile(job, dirs[i] + "_" + o, "Test " + job.uqID + "_" +dirs[i] + "_" + o + " Logs", "\n```\n" + str + "\n```\n", function (err, res, url) { if (err) { - logme("Failed to create a device log gist. (" + dirs[i] + "_" + o + ")", err + "\n" + res, "red"); + logger.error("Failed to create a device log gist. (" + dirs[i] + "_" + o + ")", err + "\n" + res); } }, true); @@ -251,7 +260,7 @@ var grabLogs = function (job, target) { + o + " Logs", "\n```\n" + str + "\n```\n", function (err, res, url) { if (err) { if ((err+"").indexOf("Already on 'master'")<0) { - logme("Failed to create a device log gist. (IOS_" + o + ")\n", err + "\n" + res, "red"); + logger.error("Failed to create a device log gist. (IOS_" + o + ")\n", err + "\n" + res); } } @@ -332,7 +341,7 @@ exports.commitLog = function (uqID) { git.commitFile(log.job, "test_log", "Test " + uqID + " Logs", str, function (err, res, url) { if (err) { - logme("Failed to create a fail gist.", err + "\n" + res, "red"); + logger.error("Failed to create a fail gist.", err + "\n" + res); } else { if (failed) logIssue(log.job, "Test " + uqID + "("+log.job.commitIndex+") has failed", "See " + url + " for the fail logs"); @@ -344,4 +353,4 @@ exports.commitLog = function (uqID) { log.failed = null; log.success = null; }); -}; \ No newline at end of file +}; diff --git a/logger.js b/logger.js index 7ec12bf..85c00e7 100644 --- a/logger.js +++ b/logger.js @@ -1,29 +1,53 @@ -var util = require('util'); -var fs = require('fs'); -var fileName = false; +// Copyright (C) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE.txt file in the project root +// for full license information. +// -exports.toFile = function (name) { - fileName = name; - fs.writeFileSync(fileName, ""); -}; +'use strict'; + +const chalk = require('chalk'); +const fs = require('fs'); + +const { format } = require('util'); + +function Logger(options) { + if (options) { + this._filePath = options.filePath; + } +} + +Logger.prototype._log = function (logger, style, messages) { + const filePath = this._filePath; + if (filePath) { + const message = format(...messages) + '\n'; + fs.appendFileSync(filePath, message); + } -global.logme = function () { - if (fileName) { - var msg = util.format.apply(this, arguments) + "\n"; - fs.appendFileSync(fileName, msg); - return; + var newMessages = messages; + if (style) { + newMessages = newMessages + .map((e) => style(e)); } - var currentDate = new Date(); - var dateTime = currentDate.getDate() + "/" - + (currentDate.getMonth() + 1) + "/" - + currentDate.getFullYear() + "@" - + (currentDate.getHours() < 10 ? "0" : "") - + currentDate.getHours() + ":" - + (currentDate.getMinutes() < 10 ? "0" : "") - + currentDate.getMinutes() + ":" - + (currentDate.getSeconds() < 10 ? "0" : "") - + currentDate.getSeconds() + " "; - util.print(dateTime); - jxcore.utils.console.log.apply(null, arguments); -}; \ No newline at end of file + const now = new Date() + .toISOString() + .replace(/TZ/, ' ') + .trim(); + newMessages.unshift(now); + + logger(...newMessages); +}; + +Logger.prototype.error = function() { + this._log(console.error, chalk.red, Array.from(arguments)); +}; + +Logger.prototype.info = function() { + this._log(console.log, null, Array.from(arguments)); +}; + +Logger.prototype.warn = function() { + this._log(console.log, chalk.yellow, Array.from(arguments)); +}; + +module.exports = Logger; diff --git a/package.json b/package.json index 700d5a1..d2e7764 100644 --- a/package.json +++ b/package.json @@ -8,8 +8,12 @@ "node": ">=0.10.0" }, "dependencies": { + "bluebird": "3.4.6", + "child-process-promise" : "2.2.0", + "chalk": "1.1.1", "express": "4.13.3", "express-handlebars": "2.0.1", + "fs-extra-promise" : "0.4.1", "github": "^0.2.4", "handlebars": "^3.0.3", "lokijs": "^1.3.7", diff --git a/reporting/report.js b/reporting/report.js index 5ff5a6e..7f24f8b 100644 --- a/reporting/report.js +++ b/reporting/report.js @@ -1,8 +1,18 @@ +// Copyright (C) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE.txt file in the project root +// for full license information. +// + +'use strict'; + var db = require('./../db_actions'); -var sync = jxcore.utils.cmdSync; var exec = require('child_process').exec; -var path = require('path'); +var execSync = jxcore.utils.cmdSync; var fs = require('fs'); +var path = require('path'); + +var Logger = require('../logger'); +var logger = new Logger(); var eopts = { encoding: 'utf8', @@ -25,7 +35,7 @@ var push_logs = function() { var task = log_queue.shift(); - logme("Creating Github Branch for " + task.bn, "red"); + logger.warn("Creating Github Branch for " + task.bn); createBranch(task.bn, function (err, res) { if (err) { @@ -34,33 +44,37 @@ var push_logs = function() { return; } - if (task.sk !== -1) + if (task.sk !== -1) { fs.writeFileSync(process.cwd() + '/TestResults/' + task.fn, task.lg); + } - if(fs.existsSync(process.cwd() + "/TMP/" + task.bn + "/")) { - sync("mv " + process.cwd() + "/TMP/" + task.bn + "/* " + process.cwd() + '/TestResults/'); - sync("rm -rf " + process.cwd() + "/TMP/" + task.bn + "/"); + var tmpDir = process.cwd() + '/TMP/' + task.bn; + if (fs.existsSync(tmpDir + '/')) { + execSync('mv ' + tmpDir + '/* ' + process.cwd() + '/TestResults/'); + execSync('rm -rf ' + tmpDir + '/'); } exec("cd " + process.cwd() + "/reporting;chmod +x ./commit_logs.sh;./commit_logs.sh " + task.bn, eopts, function (err, stdout, stderr) { - logme("Creating Github Branch for " + task.bn + " was " + (err ? "failed" : "successful"), "blue"); + logger.info("Creating Github Branch for " + task.bn + " was " + (err ? "failed" : "successful")); + if (err) { task.cb(err, stdout + "\n" + stderr); } else { task.cb(null); } + push_logs(); }); }); }; -exports.logIntoBranch = function (branch_name, filename, log, cb, skip) { +exports.logIntoBranch = function (branch_name, filename, log, callback, skip) { if(skip && skip !== -1) { - sync("mkdir -p " + process.cwd() + "/TMP/" + branch_name + "/"); - fs.writeFileSync(process.cwd() + '/TMP/' + branch_name + "/" + filename, log); - cb(null); + execSync('mkdir -p ' + process.cwd() + '/TMP/' + branch_name + '/'); + fs.writeFileSync(process.cwd() + '/TMP/' + branch_name + '/' + filename, log); + callback(null); return; } @@ -68,4 +82,4 @@ exports.logIntoBranch = function (branch_name, filename, log, cb, skip) { if (log_queue.length == 1) { push_logs(); } -}; \ No newline at end of file +}; diff --git a/tasker/android.js b/tasker/android.js index fcb3fdf..5d89d35 100644 --- a/tasker/android.js +++ b/tasker/android.js @@ -1,448 +1,284 @@ -require('../logger').toFile("../../console.json"); -var fs = require('fs'); -var path = require('path'); -var exec = require('child_process').exec; -var sync = jxcore.utils.cmdSync; -var spawn = require('child_process').spawn; -var eopts = { - encoding: 'utf8', - timeout: 0, - maxBuffer: 1e8, - killSignal: 'SIGTERM' -}; +// Copyright (C) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE.txt file in the project root +// for full license information. +// -if (!process.argv[2]) { - logme("Needs argument!"); - process.exit(1); - return; -} +'use strict'; -var job = JSON.parse(new Buffer(process.argv[2], "base64") + ""); -var nodeId = 0; +const exec = require('child-process-promise').exec; +const os = require('os'); +const fs = require('fs-extra-promise'); +const path = require('path'); -// out: [ [ '8a09fc3c', 'device' ] ] -var getAndroidDevices = function () { - var res = sync("adb devices"); - var i; +const Buffer = require('buffer').Buffer; +const Logger = require('../logger'); +const logger = new Logger({ filePath: '../../console.json' }); - if (res.exitCode != 0) { - logme("Error: getAndroidDevices failed", res.out); - process.exit(1); - return; - } +const Logcat = require('../tools/android/logcat'); +const Promise = require('bluebird'); - var devs = []; - res = res.out.split('\n'); - if (res[0].indexOf("List of devices") == 0) { - for (i = 1; i < res.length; i++) { - if (res[i].trim().length == 0) continue; - if (res[i].indexOf('offline') > 0 || - res[i].indexOf('unauthorized') > 0 || - res[i].indexOf('no permissions') > 0) { - logme("Warning: Phone " + res[i] + " - CANNOT BE USED"); - continue; // phone offline/unauthorized/no debug permissions - } - var dev = res[i].split('\t'); - devs.push(dev); - } - } - - if (devs.length == 0) { - logme("Error: No Android device found.", ""); - process.exit(1); - return; - } - - var manufacturer, model, sdkVersion, - devices = []; - for (i = 0; i < devs.length; i++) { - manufacturer = sync("adb -s " + devs[i][0] + " shell getprop ro.product.manufacturer"); - model = sync("adb -s " + devs[i][0] + " shell getprop ro.product.model"); - sdkVersion = sync("adb -s " + devs[i][0] + " shell getprop ro.build.version.sdk"); - - devices.push({ - deviceId: devs[i][0], - deviceName: manufacturer.out.replace("\n", "").trim() + "-" + model.out.replace("\n", "").trim(), - sdkVersion: sdkVersion.out.replace("\n", "").trim() - }) - } +const AndroidDebugBridge = require('../tools/android/adb'); +const adb = new AndroidDebugBridge(logger); - return devices; +const config = { + bootRetries: 15, + bootDelay: 10000 }; -var arrDevices = getAndroidDevices(); -var builds = path.join(__dirname, "../builder/builds/" + job.uqID + "/build_android"); -var appCounter = 0; -var testFailed = false; +function installApp(apkPath, appId, device) { + const deviceId = device.deviceId; + const sdkVersion = device.sdkVersion; -var deployAndroid = function (apk_path, device_name, class_name, isMarshmallow) { - var grantPermission = ''; - if (isMarshmallow) { - grantPermission = '&& adb -s ' + device_name + ' shell pm grant com.test.thalitest android.permission.ACCESS_COARSE_LOCATION'; - logme("Marshmallow device. Granting ACCESS_COARSE_LOCATION permission."); - } + return adb.installApp(apkPath, appId, deviceId) + .then(() => adb.listPackages(deviceId)) + .then(() => { + if (sdkVersion > 22) { + logger.warn('SDK > 22. Granting ACCESS_COARSE_LOCATION permission.'); - var cmd = 'adb -s ' + device_name + ' install -r ' + apk_path + - '&& adb -s ' + device_name + ' shell pm list packages' + grantPermission; - - var res = null; - var failureReasonIndex = -1; - var failureReason = ''; - exec(cmd, eopts, function (err, stdout, stderr) { - if (err || - stdout.indexOf(class_name) === -1 || - stdout.indexOf('Success') === -1) { - res = ('Error: problem deploying Android apk(' + apk_path + ') to device ' + device_name + (err ? ('\n' + err) : '')); - failureReasonIndex = stdout.indexOf('Failure'); - if (failureReasonIndex > -1) { - failureReason = stdout.substring(failureReasonIndex); - failureReason = failureReason.substring(0, failureReason.indexOf('\n')); - res += '\n' + failureReason; - } - } else { - logme('App was succesfully deployed to ' + device_name + '\n'); - } - - jxcore.utils.continue(); - }); - - jxcore.utils.jump(); - - return res; -}; - -var logArray = {}; -var grabLogcat = function (class_name, deviceId, deviceName, cb) { - this.deviceId = deviceId; - this.class_name = class_name; - this.deviceName = deviceName; - this.cb = cb; - var _this = this; - - this.run = function () { - var child = spawn('adb', ['-s', _this.deviceId, 'logcat', '-v', 'threadtime'], eopts); - for (var i = 0; i < arrDevices.length; i++) { - if (arrDevices[i].deviceId == _this.deviceId) { - arrDevices[i].child = child; - break; - } - } - - _this.child = child; - _this.child.deviceId = _this.deviceId; - logArray[_this.deviceName] = []; - - var firstLog = true; - child.stdout.on('data', function (data) { - if (firstLog) { - firstLog = false; - // logcat in place run app - _this.cb(null, _this); - } - data = data + ""; - - if (data.indexOf("****TEST_LOGGER:[PROCESS_ON_EXIT") >= 0) { - if (data.indexOf("****TEST_LOGGER:[PROCESS_ON_EXIT_FAILED]****") >= 0) - _this.child.failed = true; - - if (_this.child.failed) { - logme("STOP log received from " + _this.deviceId + "\nTest has FAILED\n"); - } else { - logme("STOP log received from " + _this.deviceId + "\nTest has SUCCEED\n"); - } - - _this.killing = true; - stopAndroidApp(_this.class_name, _this.deviceId, function () { - _this.child.kill(); - }); + return adb.grantPermission( + appId, deviceId, 'android.permission.ACCESS_COARSE_LOCATION'); } + }) + .catch((error) => { + const message = `Failed deploying '${appId}' Android apk ${apkPath} + to device ${deviceId}\nerror: ` + error ? `\n${error}` : ''; - logArray[_this.deviceName].push(data); + return Promise.reject(new Error(message)); }); +} - child.stderr.on('data', function (data) { - if (firstLog) { - firstLog = false; - // logcat in place run app - _this.cb(null, _this); - } - - logArray[_this.deviceName].push(data + ""); +function runApp(appId, device, timeout) { + const deviceId = device.deviceId; + const deviceName = device.deviceName; + + const logcat = new Logcat(logger); + + const stopWithError = function (error) { + logger.info(error); + + logcat.stop(); + cleanupDevice(device, appId); + }; + + return new Promise((resolve) => { + logcat.emmiter.on('start', () => { + exec(`adb -s "${deviceId}" shell am start -n ${appId}/${appId}.MainActivity`) + .catch(function (error) { + const newMessage = + `Running Android app failed. apk: ${appId}, device: ${deviceName}, error: ${error}`; + const newError = new Error(newMessage); + stopWithError(newError); + + return; + }) + .then((result) => { + const exitCode = result.childProcess.exitCode; + const stdout = result.stdout; + + if (exitCode !== 0 || + stdout.indexOf('Error') !== -1) { + var message = stdout.toString(); + if (message > 512) { + message = message.substr(0, 512); + } + + const error = new Error(message); + stopWithError(error); + } else { + logger.info(`Running Android app success. device: ${deviceId}`); + } + }); }); - - child.on('exit', function (code) { - if (_this.killing) { - logme("Device test finished on", _this.deviceId, ""); - } else { - _this.child.failed = true; - _this.cb("Unexpected exit on device " + _this.deviceId + " app:" + _this.class_name + " code:" + code); - - logme("Child process exited with code " + code, "on device", _this.deviceId); - } - process.emit('mobile_ready', _this.deviceId, _this.child.failed); + logcat.emmiter.on('complete', (result) => { + cleanupDevice(device, appId); + resolve(result); }); - } -}; - -var logcatIndex = 0; -var runAndroidApp = function (class_name, deviceId, deviceName, cb) { - // clear logcat cache - sync('adb -s "' + deviceId + '" logcat -c'); - // !! this may not work on some devices so don't check the failure - // CI restarts the devices on each run - - //listen logcat on parallel - var lg = new grabLogcat(class_name, deviceId, deviceName, function (err, _this) { - logcatIndex++; - if (!err) { - var cmd = 'adb -s "' + deviceId + '" shell am start -n ' + - class_name + '/' + class_name + '.MainActivity'; - var res = sync(cmd); - if (res.exitCode !== 0 || - res.out.indexOf('Error') !== -1) { - var str = '\n' + res.out; - if (str.length > 512) { - str = str.substr(0, 512); - } - - logme('Error: problem running Android apk(' + - class_name + ') on device ' + deviceName, str, ''); - - if (_this) { - _this.child.kill(); - } - cb(true, null); - return false; - } - - logme('App was succesfully started on ' + deviceId + '\n'); - cb(null); - } else { - cb(err); - } + logcat.start(appId, deviceId, deviceName, timeout); }); - lg.run(); -}; - -var runAndroidInstrumentationTests = function (class_name, runner, deviceIndex) { - var cmd = 'adb -s "' + arrDevices[deviceIndex].deviceId + '" shell am instrument -w ' + class_name + "/" + runner; - exec(cmd, eopts, function (err, stdout, stderr) { - if (err || stdout.indexOf("FAILURES!!!") > -1 || stdout.indexOf("INSTRUMENTATION_CODE: 0") > -1) { - testFailed = true; - arrDevices[deviceIndex].failed = true; - logme("Error: problem running Android instrumentation tests (" + class_name + ") on device " + arrDevices[deviceIndex].deviceName, ""); - } - logArray[arrDevices[deviceIndex].deviceName] = [stdout, stderr]; - arrDevices[deviceIndex].finished = true; - appCounter++; - - if (appCounter === arrDevices.length) { - process.logsOnDisk = true; - try { - fs.writeFileSync(path.join(__dirname, "../../result_.json"), JSON.stringify(logArray)); - } catch(e) { - logme("Could not write logs. Error:", e + ""); - } - - logme("Android instrumentation tests task is completed.", testFailed ? "[FAILED]" : "[SUCCESS]"); - for (var i = 0; i < arrDevices.length; i++) { - stopAndroidApp(job.config.csname.android, arrDevices[i].deviceId); - } - process.exit(testFailed ? 1 : 0); - } - }); - logcatIndex++; -}; - -var uninstallApp = function (class_name, device_name) { - var cmd = 'sleep 1;adb -s "' + device_name + '" uninstall ' + class_name; - var res = sync(cmd); - if (res.exitCode != 0) { - logme("Error: problem stopping Android apk(" + class_name + ") to device " + device_name, res.out, ""); - return false; - } - - return true; -}; +} -var stopAndroidApp = function (class_name, device_name, cb) { - var cmd = 'adb -s "'+device_name+'" shell pm uninstall '+class_name - + ';adb -s "' + device_name + '" reboot'; +function runInstrumentationTests(device, appId, runner) { + const deviceId = device.deviceId; + const deviceName = device.deviceName; - if (cb) { - exec(cmd, eopts, cb); - } else { - sync(cmd); + return exec( + `adb -s "${deviceId}" shell am instrument -w ${appId}/${runner}`) + .then((result) => { - return true; - } -}; + if (result.stdout.indexOf('FAILURES!!!') > -1 || + result.stdout.indexOf('INSTRUMENTATION_CODE: 0') > -1) { + const error = new Error( + `Failed running Android instrumentation tests (${appId}) on device ${deviceName}`); -process.on('SIGTERM', function(){ - if(process.logsOnDisk) return; - try { - fs.writeFileSync(path.join(__dirname, "../../result_.json"), JSON.stringify(logArray)); - } catch(e) { - logme("Could not write logs. Error:", e + ""); - } - process.exit(1); -}); - -process.on('mobile_ready', function (deviceId, failed) { - for (var i = 0; i < arrDevices.length; i++) { - if (arrDevices[i].deviceId == deviceId) { - arrDevices[i].finished = true; - arrDevices[i].failed = failed; - if (failed) - testFailed = true; - break; - } - } - appCounter++; - if (appCounter < arrDevices.length) return; - appCounter = 0; - - process.logsOnDisk = true; - try { - fs.writeFileSync(path.join(__dirname, "../../result_.json"), JSON.stringify(logArray)); - } catch(e) { - logme("Could not write logs. Error:", e + ""); - } - - logme("Android task is completed.", testFailed ? "[FAILED]" : "[SUCCESS]"); - - for (var i = 0; i < arrDevices.length; i++) { - stopAndroidApp(job.config.csname.android, arrDevices[i].deviceId); - } - process.exit(testFailed ? 1 : 0); -}); + return Promise.reject(error); + } + return result; + }); +} -// remove apps -for (var i = 0; i < arrDevices.length; i++) { - logme("Stopping app on ", arrDevices[i].deviceId); - stopAndroidApp(job.config.csname.android, arrDevices[i].deviceId); +function cleanupDevice(device, appId) { + const deviceId = device.deviceId; - logme("Uninstalling app on ", arrDevices[i].deviceId); - uninstallApp(job.config.csname.android, arrDevices[i].deviceId); + return adb.stopApp(appId, deviceId) + .then(() => adb.uninstallApp(appId, deviceId)) + .then(() => adb.reboot(deviceId)); } -var isDeviceBooted = function (device_name, timeout) { - var result = false; - setTimeout(function () { - var cmd = 'adb -s ' + device_name + ' shell getprop sys.boot_completed'; - var res = sync(cmd); - result = res.exitCode === 0 && res.out.indexOf('1') === 0; - jxcore.utils.continue(); - }, timeout); - jxcore.utils.pause(); - return result; -}; - -//ensure all devices are up and running -var devicesReady = true; -for (var i = 0; i < arrDevices.length; i++) { - var bootCheckCount = 0; - var bootMaxCheckCount = 10; - var bootCheckTimeout = 0; - while (bootCheckCount < bootMaxCheckCount && !isDeviceBooted(arrDevices[i].deviceId, bootCheckTimeout)) { - bootCheckCount += 1; - bootCheckTimeout = 10000; // wait 10 seconds before next try - } - if (bootCheckCount === bootMaxCheckCount) { - devicesReady = false; - break; - } +function cleanupDevices(devices, appId) { + return Promise.reduce(devices, (cleanDevices, device) => { + return cleanupDevice(device, appId) + .then(() => cleanDevices.concat([device])); + }, []); } -if (!devicesReady) { - logme("\n\nDevices on this node are not ready.\n", - "Cancelling the test result on this node.\n"); - if (job.config.serverScript && job.config.serverScript.length) { - jxcore.utils.cmdSync("curl 192.168.1.150:8060/cancel=1"); + +function runTests() { + if (!process.argv[2]) { + logger.error('Needs argument!'); + process.exit(1); + return; } - process.exit(0); -} else { - logme("\nAll devices are ready!\n"); -} -var retry_count=0; -// deploy apps -for (var i = 0; i < arrDevices.length; i++) { - var isMarshmallow = arrDevices[i].sdkVersion > 22; + const bootRetries = config.bootRetries; + const bootDelay = config.bootDelay; + + const job = JSON.parse(new Buffer(process.argv[2], 'base64') + ''); + const nodeId = 0; + + const appId = job.config.csname.android; + const className = job.config.csname.android; + const serverDir = job.config.serverScript; + const instrumentationTestRunner = job.config.instrumentationTestRunner; + const timeout = job.config.timeout ? job.config.timeout * 1000 : 300000; + const jobUID = job.uqID; + const buildsDir = path.join( + __dirname, '..', 'builder', 'builds', jobUID, 'build_android'); + const appPath = path.join( + buildsDir, `android_${nodeId}_${jobUID}.apk`); + const logsPath = path.join( + __dirname, '..', '..', 'result_.json'); + + return adb.devices() + .then((devices) => { + return cleanupDevices(devices, appId) + .then((cleanDevices) => [devices, cleanDevices]); + }) + .then(([devices, cleanDevices]) => { + if (cleanDevices.length !== devices.length) { + const error = new Error( + `${cleanDevices.length} of ${devices.length} cleaned.`); + return Promise.reject(error); + } - logme("Deploying to " + arrDevices[i].deviceId); + return [devices, cleanDevices]; + }) + .then(([devices, cleanDevices]) => { + return Promise.reduce(cleanDevices, (readyDevices, device) => { + return adb.isDeviceReady(device.deviceId, bootDelay, bootRetries) + .then((ready) => { + if (!ready) { + const error = new Error(`${device} isn't ready.`); + return Promise.reject(error); + } + + return readyDevices.concat([device]); + }) + .then((readyDevices) => { + if (readyDevices.length !== devices.length) { + const error = new Error( + `${readyDevices.length} of ${devices.length} ready.`); + return Promise.reject(error); + } + + return cleanDevices; + }); + }, []); + }) + .then((devices) => { + return Promise.reduce(devices, (readyDevices, device) => { + return installApp(appPath, appId, device) + .then(() => readyDevices.concat([device])); + }, []) + .then((readyDevices) => { + if (readyDevices.length !== devices.length) { + const error = new Error( + `Failed install app for ${readyDevices.length} of + ${devices.length}.`); + return Promise.reject(error); + } - var res = deployAndroid(builds + "/android_" + nodeId + "_" + job.uqID + ".apk", arrDevices[i].deviceId, job.config.csname.android, isMarshmallow); - if (res && retry_count < 2) { - retry_count++; - i--; - continue; - } - if (res) { - logme("\n\nTest on this node has failed but the reason wasn't the test application itself.\n", - "Cancelling the test result on this node.\n", res); + return readyDevices; + }); + }) + .then((devices) => { + if (serverDir && serverDir.length) { + return exec(`curl 192.168.1.150:8060/droid=${devices.length}`) + .then(() => devices); + } - if (job.config.serverScript && job.config.serverScript.length) - jxcore.utils.cmdSync("curl 192.168.1.150:8060/cancel=1"); + return devices; + }) + .then((devices) => { + if (instrumentationTestRunner) { + const devicesWithLogs = devices + .map((device) => { + return runInstrumentationTests( + device, appId, instrumentationTestRunner) + .then(() => device); + }); + + return Promise.all(devicesWithLogs); + } else { + const devicesWithLogs = devices + .map((device) => { + return runApp(appId, device, timeout) + .then((result) => { + device.failed = result.failed; + device.logs = result.logs; + + return device; + }); + }); + + return Promise.all(devicesWithLogs); + } + }) + .then((devices) => { + const result = devices + .reduce((accumulator, device) => { + accumulator.logs[device.deviceName] = device.logs; + accumulator.failed = accumulator.failed || device.failed; + + return accumulator; + }, { logs: {}, failed: false }); + + cleanupDevices(devices, appId) + .catch((error) => { + logger.error(`Failed cleaning devices, error: ${error}`); + }); - process.exit(0); - } - retry_count = 0; + return fs.writeFileAsync(logsPath, JSON.stringify(result.logs)) + .then(() => result.failed); + }); } -var callback = function (err) { - if (err) { - logme("Error!", err, ""); +if (require.main === module) { + if (os.platform() !== 'darwin') { + console.log('WARNING: WE ONLY SUPPORT MACOS AS A BUILD PLATFORM, USING ANY' + + 'OTHER PLATFORM IS NOT OFFICIALLY SUPPORTED. WE STILL CHECK A FEW ' + + 'THINGS BUT YOU ARE REALLY ON YOUR OWN'); } -}; -if (job.config.serverScript && job.config.serverScript.length) - jxcore.utils.cmdSync("curl 192.168.1.150:8060/droid=" + arrDevices.length); + runTests() + .then((failed) => { + logger.info('Finish running Android tests'); -for (var i = 0; i < arrDevices.length; i++) { - if (job.config.instrumentationTestRunner) { - runAndroidInstrumentationTests(job.config.csname.android, job.config.instrumentationTestRunner, i); - } else { - logme("Starting application ThaliTest on " + arrDevices[i].deviceId + "\n"); - runAndroidApp(job.config.csname.android, arrDevices[i].deviceId, arrDevices[i].deviceName, callback); - } -} + process.exit(failed ? 1 : 0); + }) + .catch((error) => { + logger.error(`Failed running Android tests with error: ${error}`); -function timeoutKill() { - // shut down the test; - logme("TIMEOUT REACHED. KILLING the APPS"); - for (var i = 0; i < arrDevices.length; i++) { - var dev = arrDevices[i]; - if (dev.finished) continue; - - if (!logArray[dev.deviceName]) { - logArray[dev.deviceName] = []; - } - logArray[dev.deviceName].push("TIME-OUT KILL (timeout was " + timeout + "ms)"); - stopAndroidApp(job.config.csname.android, dev.deviceId); - if (dev.child) { - dev.child.kill(); - } - } - setTimeout(function(){ - process.emit("SIGTERM"); - }, 1000); + process.exit(-1); + }); } - -// set timeout -var timeout = job.config.timeout ? job.config.timeout * 1000 : 300000; -var logcatCounter = 0; -var interTimer = setInterval(function(){ - if (logcatIndex == arrDevices.length) { - clearInterval(interTimer); - setTimeout(function () { - timeoutKill(); - }, timeout + 10000); - } else { - if (logcatCounter > 90) { - timeoutKill(); - } - } - logcatCounter++; -}, 1000); diff --git a/tasker/deploy.js b/tasker/deploy.js index 19141a8..9996c23 100644 --- a/tasker/deploy.js +++ b/tasker/deploy.js @@ -1,9 +1,20 @@ -require('../logger'); +// Copyright (C) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE.txt file in the project root +// for full license information. +// + +'use strict'; + var fs = require('fs'); -var path = require('path'); var exec = require('child_process').exec; -var sync = jxcore.utils.cmdSync; +var execSync = jxcore.utils.cmdSync; +var path = require('path'); var spawn = require('child_process').spawn; +var tester = require('../internal/tester'); + +var Logger = require('../logger'); +var logger = new Logger(); + var eopts = { encoding: 'utf8', timeout: 0, @@ -11,9 +22,7 @@ var eopts = { killSignal: 'SIGTERM' }; -var tester = require('../internal/tester'); - -var node_config = fs.readFileSync(__dirname + "/nodes.json") + ""; +var node_config = fs.readFileSync(__dirname + '/nodes.json') + ''; function counterExec(index, cmd, cb) { this.cmd = cmd; @@ -35,7 +44,7 @@ function busyCheck(nodes, callback) { return; } - logme("Identifying Available Nodes", nodes, "yellow"); + logger.info("Identifying Available Nodes", nodes); var counter = nodes.length; var arr = []; var cb = function (err, stdout, stderr, index) { @@ -57,7 +66,7 @@ function busyCheck(nodes, callback) { } function getAvailableNodes(callback) { - logme("Ping Testing Nodes", "yellow"); + logger.info("Ping Testing Nodes"); // read nodes var config = JSON.parse(node_config); var nodes = []; @@ -98,7 +107,7 @@ exports.test = function (job, trying, callback_) { trying = 0; getAvailableNodes(function (nodes) { if (nodes.length == 0) { - logme("Error: No active node at the moment", "red"); + logger.error('Error: No active node at the moment'); if (trying < 4) { tryInter = setTimeout(function () { exports.test(job, trying++, callback); @@ -110,13 +119,13 @@ exports.test = function (job, trying, callback_) { } } else { if (job.config.serverScript && job.config.serverScript.length) - jxcore.utils.cmdSync("curl 192.168.1.150:8060/nodes=" + nodes.length); + execSync("curl 192.168.1.150:8060/nodes=" + nodes.length); trying = 0; - logme("Deploying on", nodes, "green"); + logger.info("Deploying on", nodes); deploy(job, nodes, function (err, stdout, stderr) { if (err) { - logme(err, stdout, stderr, "red"); + logger.error(err, stdout, stderr); if (err.retry && trying < 2) { tryInter = setTimeout(function () { @@ -145,7 +154,7 @@ exports.test = function (job, trying, callback_) { exports.leave = function () { leaveRecevied = true; - sync("cd "+__dirname+";./stop_nodes.sh"); + execSync("cd "+__dirname+";./stop_nodes.sh"); if (deployerChild) { deployerChild.kill(); } diff --git a/tasker/deployer.js b/tasker/deployer.js index 1f9e2ec..bf26b29 100644 --- a/tasker/deployer.js +++ b/tasker/deployer.js @@ -1,9 +1,16 @@ -require('../logger'); +// Copyright (C) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE.txt file in the project root +// for full license information. +// + +'use strict'; + var fs = require('fs'); var path = require('path'); var exec = require('child_process').exec; -var sync = jxcore.utils.cmdSync; +var execSync = jxcore.utils.cmdSync; var spawn = require('child_process').spawn; + var eopts = { encoding: 'utf8', timeout: 0, @@ -24,10 +31,10 @@ var apk_name = "android_0_" + job.uqID + ".apk"; function CLEANUP() { //cleanup target for (var i = 0; i < job.nodes.length; i++) { - sync("cd " + __dirname + ";ssh pi@" + job.nodes[i].ip + " 'bash -s' < cleanup.sh"); + execSync("cd " + __dirname + ";ssh pi@" + job.nodes[i].ip + " 'bash -s' < cleanup.sh"); } - sync("cd " + __dirname + ";rm reset.sh;rm run.sh"); + execSync("cd " + __dirname + ";rm reset.sh;rm run.sh"); } var logs_copied = false; @@ -42,8 +49,8 @@ function COPY_LOGS() { if (logs_copied) return; logs_copied = true; for (var i = 0; i < job.nodes.length; i++) { - sync("mkdir -p " + __dirname + "/results/" + job.uqID + "/" + job.nodes[i].name + "/"); - var res = sync("scp pi@" + job.nodes[i].ip + ":~/*.json " + __dirname + "/results/" + job.uqID + "/" + job.nodes[i].name + "/"); + execSync("mkdir -p " + __dirname + "/results/" + job.uqID + "/" + job.nodes[i].name + "/"); + var res = execSync("scp pi@" + job.nodes[i].ip + ":~/*.json " + __dirname + "/results/" + job.uqID + "/" + job.nodes[i].name + "/"); if (res.exitCode) console.error("CopyLog Failed ("+job.nodes[i].name+"):", res.out); } @@ -56,14 +63,14 @@ fs.writeFileSync(__dirname + "/reset.sh", reset) //cleanup target var retry = []; for (var i = 0; i < job.nodes.length; i++) { - var res = sync("cd " + __dirname + ";ssh pi@" + job.nodes[i].ip + " 'bash -s' < reset.sh"); + var res = execSync("cd " + __dirname + ";ssh pi@" + job.nodes[i].ip + " 'bash -s' < reset.sh"); if (res.exitCode != 0) { retry.push(job.nodes[i]); } } for (var i = 0; i < retry.length; i++) { - var res = sync("cd " + __dirname + ";ssh pi@" + retry[i].ip + " 'bash -s' < reset.sh"); + var res = execSync("cd " + __dirname + ";ssh pi@" + retry[i].ip + " 'bash -s' < reset.sh"); if (res.exitCode != 0) { console.error("error while trying to reset node on ", retry[i]," details:", res.out, "\n"); process.exit(1); @@ -74,7 +81,7 @@ for (var i = 0; i < retry.length; i++) { var apk_path = "../builder/builds/" + job.uqID + "/build_android/" + apk_name; //copy apk for (var i = 0; i < job.nodes.length; i++) { - var res = sync("cd " + __dirname + ";scp " + apk_path + " pi@" + job.nodes[i].ip + var res = execSync("cd " + __dirname + ";scp " + apk_path + " pi@" + job.nodes[i].ip + ":~/test/builder/builds/" + job.uqID + "/build_android/" + apk_name); if (res.exitCode != 0) { console.error("Error while transferring APK:", res.out); @@ -85,7 +92,7 @@ for (var i = 0; i < job.nodes.length; i++) { //copy android.js script for (var i = 0; i < job.nodes.length; i++) { - var res = sync("cd " + __dirname + ";scp android.js pi@" + job.nodes[i].ip + ":~/test/tasker/"); + var res = execSync("cd " + __dirname + ";scp android.js pi@" + job.nodes[i].ip + ":~/test/tasker/"); if (res.exitCode != 0) { console.error("copy android.js:", res.out); process.exit(1); @@ -95,9 +102,9 @@ for (var i = 0; i < job.nodes.length; i++) { //copy logger.js script for (var i = 0; i < job.nodes.length; i++) { - var res = sync("cd " + __dirname + ";scp ../logger.js pi@" + job.nodes[i].ip + ":~/test/"); + var res = execSync('cd ' + __dirname + ';scp ../logger.js pi@' + job.nodes[i].ip + ':~/test/'); if (res.exitCode != 0) { - console.error("copy logger.js:", res.out); + console.error('copy logger.js:', res.out); process.exit(1); return; } @@ -141,4 +148,4 @@ function callback(err, stdout, stderr, index) { for (var i = 0; i < job.nodes.length; i++) { var task = new counterExec(i, "cd " + __dirname + ";ssh pi@" + job.nodes[i].ip + " 'bash -s' < run.sh", callback); task.run(); -} \ No newline at end of file +} diff --git a/tasker/ios.js b/tasker/ios.js index f476f0e..18d1366 100644 --- a/tasker/ios.js +++ b/tasker/ios.js @@ -1,9 +1,19 @@ -require('../logger'); +// Copyright (C) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE.txt file in the project root +// for full license information. +// + +'use strict'; + +var exec = require('child_process').exec; +var execSync = jxcore.utils.cmdSync; var fs = require('fs'); var path = require('path'); -var exec = require('child_process').exec; -var sync = jxcore.utils.cmdSync; var spawn = require('child_process').spawn; + +var Logger = require('../logger'); +var logger = new Logger(); + var eopts = { encoding: 'utf8', timeout: 0, @@ -12,11 +22,12 @@ var eopts = { }; if (!process.argv[2]) { - logme("Needs argument!"); + logger.error('Needs argument!'); process.exit(1); return; } -var job = JSON.parse(new Buffer(process.argv[2], "base64") + ""); + +var job = JSON.parse(new Buffer(process.argv[2], 'base64') + ''); var builds = path.join(__dirname, "../builder/builds"); var arrDevices = []; @@ -24,7 +35,7 @@ var arrDevices = []; var getIOSDevices = function (cb) { exec("ios-deploy --detect --timeout 1", eopts, function (err, stdout, stderr) { if (err) { - logme("Error: ios-deploy", err, stdout, stderr, ""); + logger.error("Error: ios-deploy", err, stdout, stderr); cb(err); return; } @@ -32,11 +43,11 @@ var getIOSDevices = function (cb) { var arr = stdout.split('\n'); for (var i = 0; i < arr.length; i++) { var str = arr[i]; - str = str.replace("[....] ", ""); - str = str.replace(" connected through USB.", ""); + str = str.replace('[....] ', ''); + str = str.replace(' connected through USB.', ''); - if (str.indexOf("Found ") >= 0) { - str = str.replace("Found ", ""); + if (str.indexOf('Found ') >= 0) { + str = str.replace('Found ', ''); var n1 = str.indexOf("'"); if (n1) n1 = str.indexOf("'", n1 + 1); @@ -48,11 +59,15 @@ var getIOSDevices = function (cb) { if (index >= 0) { name = name.substr(index + 1).replace("'", "").trim(); } - var deviceId = str.substr(n1 + 1, str.length - (n1 + 1)).trim().replace("(", "").replace(")", ""); + var deviceId = str + .substr(n1 + 1, str.length - (n1 + 1)) + .trim() + .replace('(', '') + .replace(')', ''); - logme('ios: device name: ' + name, ', device identifier: ', deviceId); + logger.info('ios: device name: ' + name, ', device identifier: ', deviceId); if (deviceId.indexOf('\'') !== -1) { - logme('ios: unexpected device identifier ', deviceId); + logger.error('ios: unexpected device identifier ', deviceId); } arrDevices.push({name: name, deviceId: deviceId}); @@ -73,7 +88,7 @@ var uninstallApp = function (job, cb) { return; if (err && err.code != 253) { counter = -1; - logme("Error: uninstalling app from device.", err, stdout, stderr, ""); + logger.error('Error: uninstalling app from device.', err, stdout, stderr); cb({"server" : [err + "\n\n" + stdout + stderr + "\n\n**Call Log** \n" + call_log.join("\n") + "\n"]}); return; } @@ -147,7 +162,7 @@ var grabLLDB = function (index, loc, deviceId, cb) { if (!_this.killing) { _this.child.failed = true; _this.cb("Unexpected exit on device " + _this.deviceId + " app:" + _this.deviceName + " code:" + code); - logme('ios: child process exited with code ' + code, "on device", _this.deviceId, ""); + logger.error('ios: child process exited with code ' + code, "on device", _this.deviceId); } process.emit('mobile_ready', _this.deviceId, _this.child.failed); }); @@ -171,7 +186,7 @@ var installApp = function (job, cb) { var loc = path.join(builds, job.uqID + "/build_ios/", job.config.binary_path.ios); if (job.config.serverScript && job.config.serverScript.length) - jxcore.utils.cmdSync("curl 192.168.1.150:8060/ios=" + arrDevices.length); + execSync("curl 192.168.1.150:8060/ios=" + arrDevices.length); for (var i = 0; i < arrDevices.length; i++) { var ll = new grabLLDB(i, loc, arrDevices[i].deviceId, function (err) { @@ -186,15 +201,16 @@ var installApp = function (job, cb) { }; var deployIOS = function (job, cb) { - logme("uninstalling the application", ""); + logger.info('uninstalling the application'); uninstallApp(job, function (err) { if (err) { cb(err, true); return; } - sync("killall ios-deploy;killall lldb"); - logme("installing the application", ""); + execSync('killall ios-deploy;killall lldb'); + logger.info('installing the application'); + installApp(job, function (err, result) { cb(err, result); }) @@ -206,14 +222,14 @@ var test_ = function (job, cb) { appCounter = 0; IsFailed = false; // get devices - logme("Getting the list of iOS devices", ""); + logger.info('Getting the list of iOS devices'); getIOSDevices(function (err) { if (err) { cb({"server" : [err.message]}, true); return; } - logme("Deploying iOS test app", "") + logger.info('Deploying iOS test app') // devices are available under arrDevices -> Array of {name, deviceId} deployIOS(job, function (err, failed) { cb(err, failed) @@ -222,10 +238,10 @@ var test_ = function (job, cb) { }; exports.test = function (job, cb) { - sync("killall ios-deploy;killall lldb"); + execSync("killall ios-deploy;killall lldb"); test_(job, function (arr, isFailed) { activeJob = null; - var res = sync("cd " + __dirname + ";mkdir -p results/" + job.uqID + "/ios/"); + var res = execSync("cd " + __dirname + ";mkdir -p results/" + job.uqID + "/ios/"); if (res.exitCode) { isFailed = res.out; } else { @@ -246,14 +262,14 @@ exports.test = function (job, cb) { }; process.on('exit', function () { - sync("killall ios-deploy;killall lldb"); + execSync("killall ios-deploy;killall lldb"); }); var timeout = job.config.timeout ? parseInt(job.config.timeout) * 1000 : 300000; var inter = setTimeout(function () { if (!logArray['server']) logArray['server'] = []; - logArray.server.push("iOS application timeout\n"); + logArray.server.push('iOS application timeout\n'); for (var i = 0; i < llChildren.length; i++) { var child = llChildren[i].child; @@ -265,7 +281,7 @@ var inter = setTimeout(function () { } } setTimeout(function () { - logme("TIMEOUT REACHED"); + logger.info('TIMEOUT REACHED'); process.exit(1) }, 500); }, timeout); diff --git a/tasker/run_.sh b/tasker/run_.sh index 42a6d74..36efe72 100644 --- a/tasker/run_.sh +++ b/tasker/run_.sh @@ -6,23 +6,15 @@ NORMAL_COLOR='\033[0m' RED_COLOR='' -GREEN_COLOR='\033[0;32m' -GRAY_COLOR='\033[0;37m' -LOG() { - COLOR="$1" - TEXT="$2" - echo -e "${COLOR}$TEXT ${NORMAL_COLOR}" +OUTPUT() { + echo -e "${RED_COLOR}$BASH_COMMAND CI FAILED - run.sh failure${NORMAL_COLOR}" } +set -euo pipefail +trap OUTPUT ERR -ERROR_ABORT() { - if [[ $? != 0 ]] - then - LOG "Android testing process has failed\n" - exit -1 - fi -} ### END - JXcore Test Server -------- -cd test/tasker;ERROR_ABORT -sudo jx android.js {{JOB64}};ERROR_ABORT + +cd test/tasker +sudo node android.js {{JOB64}} diff --git a/tasker/taskman.js b/tasker/taskman.js index 21839e3..83fff82 100644 --- a/tasker/taskman.js +++ b/tasker/taskman.js @@ -1,10 +1,20 @@ -var db = require('./../db_actions'); +// Copyright (C) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE.txt file in the project root +// for full license information. +// + +'use strict'; + var android = require('./deploy'); -var sync = jxcore.utils.cmdSync; -var tester = require('../internal/tester'); +var db = require('./../db_actions'); +var fs = require('fs'); var exec = require('child_process').exec; +var execSync = jxcore.utils.cmdSync; var path = require('path'); -var fs = require('fs'); +var tester = require('../internal/tester'); + +var Logger = require('../logger'); +var logger = new Logger(); var eopts = { encoding: 'utf8', @@ -22,7 +32,7 @@ var taskCounter = 0; var iosChild; function runIos(job, cb) { - logme("iOS test task is running", 'yellow') + logger.info('iOS test task is running') var job64 = new Buffer(JSON.stringify(job), "").toString("base64"); return exec("cd " + __dirname + "; jx ios.js " + job64, eopts, function (err, stdout, stderr) { @@ -31,7 +41,7 @@ function runIos(job, cb) { } else { tester.report(job, "ios", false, true); } - logme("iOS test task FINISHED"); + logger.info("iOS test task FINISHED"); cb(1); }); } @@ -49,24 +59,24 @@ var runTask = function (job) { if (taskCounter <= 0 || itemTotal == 0) { taskCounter = 0; if (!activeJob) return; - logme("Test " + activeJob.uqID + " has finished", "green"); + logger.info("Test " + activeJob.uqID + " has finished"); taskerReset = true; // rebooting nodes give some time if (serverChild) { serverChild.killing = true; - logme("Killing IS child"); - sync("cd " + process.cwd() + "/tasker;ssh pi@192.168.1.150 'bash -s' < pkill.sh"); + logger.info("Killing IS child"); + execSync("cd " + process.cwd() + "/tasker;ssh pi@192.168.1.150 'bash -s' < pkill.sh"); } setTimeout(function () { - logme("Cleaning up"); + logger.info("Cleaning up"); // test is finished taskerBusy = false; taskerReset = false; db.removeJob(activeJob); - sync("cd " + __dirname + ";rm -rf results/" + activeJob.uqID + "/; rm -rf ../builder/builds/" + activeJob.uqID); + execSync("cd " + __dirname + ";rm -rf results/" + activeJob.uqID + "/; rm -rf ../builder/builds/" + activeJob.uqID); tester.commitLog(activeJob.uqID); activeJob = null; @@ -74,9 +84,9 @@ var runTask = function (job) { } }; - var res = sync("cd " + __dirname + ";rm -rf results/" + job.uqID + "/;mkdir -p results/" + job.uqID); + var res = execSync("cd " + __dirname + ";rm -rf results/" + job.uqID + "/;mkdir -p results/" + job.uqID); if (res.exitCode) { - logme("Error while creating the logs folder: ", err, stdout, stderr, "red"); + logger.error("Error while creating the logs folder: ", err, stdout, stderr); process.exit(1); } @@ -112,7 +122,7 @@ var runTask = function (job) { fs.writeFileSync(rs_final, src); - logme("IS Args:", p); + logger.info('IS Args:', p); serverChild = exec("cd " + process.cwd() + "/tasker;chmod +x ./runServer.sh;./runServer.sh " + p, eopts, function (err, stdout, stderr) { if (err && !serverChild.killing) { @@ -134,11 +144,11 @@ var runTask = function (job) { } serverChild = null; - logme("IS exiting"); + logger.info("IS exiting"); - sync("rm " + rs_final); - sync("cd " + process.cwd() + "/tasker; ssh pi@192.168.1.150 'bash -s' < cleanupServer.sh"); - sync("rm -rf " + p); + execSync("rm " + rs_final); + execSync("cd " + process.cwd() + "/tasker; ssh pi@192.168.1.150 'bash -s' < cleanupServer.sh"); + execSync("rm -rf " + p); }); } @@ -146,7 +156,7 @@ var runTask = function (job) { itemTotal = 0; if (delay > 1 && serverChild == null) { // serverScript task has failed. do not run tests - logme("serverChild execution has failed", "red"); + logger.error("serverChild execution has failed"); cb(); } else { if (job.target == "all" || job.target == "ios") { @@ -175,7 +185,7 @@ var testTask = function () { taskerReset = true; if (serverChild) { serverChild.killing = true; - sync("cd " + process.cwd() + "/tasker;ssh pi@192.168.1.150 'bash -s' < pkill.sh"); + execSync("cd " + process.cwd() + "/tasker;ssh pi@192.168.1.150 'bash -s' < pkill.sh"); } android.leave(); if (iosChild) @@ -195,7 +205,7 @@ var testTask = function () { var delay = 45000; // phones were rebooting // randomly also restart the raspberries - sync("cd " + __dirname + ";./clean_nodes.sh"); + execSync("cd " + __dirname + ";./clean_nodes.sh"); lastStartTime = Date.now() + delay; setTimeout(function() { @@ -204,4 +214,4 @@ var testTask = function () { }, delay); }; -setInterval(testTask, 3000); \ No newline at end of file +setInterval(testTask, 3000); diff --git a/tools/android/adb.js b/tools/android/adb.js new file mode 100644 index 0000000..77edf47 --- /dev/null +++ b/tools/android/adb.js @@ -0,0 +1,156 @@ +// Copyright (C) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE.txt file in the project root +// for full license information. +// + +'use strict'; + +const exec = require('child-process-promise').exec; +const Promise = require('bluebird'); + +function AndroidDebugBridge(logger) { + if (logger) { + this._logger = logger; + } +}; + +AndroidDebugBridge.prototype.devices = function() { + return exec('adb devices') + .then((result) => { + if (result.stderr) { + return Promise.reject(new Error(result.stderr)); + } + + var devices = []; + var stdoutByLines = result.stdout.split('\n'); + var stdoutTitle = stdoutByLines.shift(); + if (stdoutTitle.indexOf('List of devices') === 0) { + for (let deviceInfo of stdoutByLines) { + + if (deviceInfo.trim().length === 0) { + continue; + } + + if (deviceInfo.indexOf('offline') > 0 || + deviceInfo.indexOf('unauthorized') > 0 || + deviceInfo.indexOf('no permissions') > 0) { + this._logger.warn(`Warning: Phone ${deviceInfo} - CANNOT BE USED`); + continue; + } + + const device = deviceInfo.split('\t'); + devices.push(device); + } + } + + if (devices.length === 0) { + return Promise.reject(new Error('Android devices weren\'t found.')); + } else { + return Promise.resolve(devices); + } + }) + .then((dirtyDevices) => { + const devicesPromise = dirtyDevices + .map((dirtyDevice) => { + const deviceId = dirtyDevice[0]; + + return Promise.all([ + exec(`adb -s ${deviceId} shell getprop ro.product.manufacturer`), + exec(`adb -s ${deviceId} shell getprop ro.product.model`), + exec(`adb -s ${deviceId} shell getprop ro.build.version.sdk`) + ]) + .then((deviceData) => { + const manufacturer = deviceData[0].stdout.replace('\n', '').trim(); + const model = deviceData[1].stdout.replace('\n', '').trim(); + const sdkVersion = deviceData[2].stdout.replace('\n', '').trim(); + + return { + deviceId: deviceId, + manufacturer: manufacturer, + model: model, + deviceName: `${manufacturer}-${model}`, + sdkVersion: sdkVersion + }; + }); + }); + + return Promise.all(devicesPromise); + }); +}; + +AndroidDebugBridge.prototype.isBootCompleted = function(deviceId) { + return exec(`adb -s ${deviceId} shell getprop sys.boot_completed`) + .then((result) => { + const exitCode = result.childProcess.exitCode; + const stdout = result.stdout; + + return exitCode === 0 && stdout.startsWith('1'); + }) + .catch (() => false); +}; + +AndroidDebugBridge.prototype.isDeviceReady = function(deviceId, delay, retries) { + return Promise.delay(delay) + .then(() => this.isBootCompleted(deviceId)) + .then((completed) => { + if (!completed) { + this._logger.info( + `waiting device '${deviceId}' boot completed, retry: ${retries}`); + + if (retries && retries !== 0) { + return this.isDeviceReady(deviceId, delay, retries - 1); + } + + return false; + } + + return true; + }); +}; + +AndroidDebugBridge.prototype.installApp = function(apkPath, appId, deviceId) { + return exec(`adb -s ${deviceId} install -r ${apkPath}`) + .then((result) => { + const exitCode = result.childProcess.exitCode; + const stdout = result.stdout; + const stderr = result.stderr; + + if (exitCode !== 0 || + stdout.indexOf('Success') === -1) { + const error = new Error({ + stdout: stdout, + stderr: stderr + }); + + return Promise.reject(error); + } + + this._logger.info(`'${appId}' was succesfully deployed to ${deviceId}\n`); + + return; + }); +}; + +AndroidDebugBridge.prototype.uninstallApp = function(appId, deviceId) { + return exec(`adb -s "${deviceId}" shell pm uninstall ${appId}`) + .then(() => exec('sleep 1')) + .then(() => exec(`adb -s "${deviceId}" uninstall ${appId}`)); +}; + +AndroidDebugBridge.prototype.stopApp = function(appId, deviceId) { + return exec(`adb -s ${deviceId} shell am force-stop ${appId}`); +}; + +AndroidDebugBridge.prototype.reboot = function(deviceId) { + return exec(`adb -s ${deviceId} reboot`); +}; + +AndroidDebugBridge.prototype.listPackages = function(deviceId) { + return exec(`adb -s ${deviceId} shell pm list packages`); +}; + +AndroidDebugBridge.prototype.grantPermission = function(appId, deviceId, permission) { + return exec(`adb -s ${deviceId} shell pm grant ${appId} ${permission}`); +}; + +module.exports = AndroidDebugBridge; diff --git a/tools/android/logcat.js b/tools/android/logcat.js new file mode 100644 index 0000000..29cd784 --- /dev/null +++ b/tools/android/logcat.js @@ -0,0 +1,141 @@ +// Copyright (C) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE.txt file in the project root +// for full license information. +// + +'use strict'; + +const spawn = require('child_process').spawn; +const spawnSync = require('child_process').spawnSync; + +const EventEmitter = require('events'); + +const eopts = { + encoding: 'utf8', + timeout: 0, + maxBuffer: 1e8, + killSignal: 'SIGTERM' +}; + +function Logcat(logger) { + this.emmiter = new EventEmitter(); + + this._logger = logger; + this._failed = null; + this._logs = null; + this._child = null; + this._completed = false; + this._timeout = null; +} + +Logcat.prototype.start = function (appId, deviceId, deviceName, timeout) { + // clear logcat cache + // !! this may not work on some devices so don't check the failure + // CI restarts the devices on each run + spawnSync('adb', ['-s', deviceId, 'logcat', '-c']); + + const child = spawn('adb', ['-s', deviceId, 'logcat', '-v', 'threadtime'], eopts); + const self = this; + + this._child = child; + this._logs = []; + + this._startTimeout(timeout); + + var starting = true; + + child.stdout.on('data', (data) => { + if (starting) { + starting = false; + // logcat in place run app + self.emmiter.emit('start'); + } + + data = data.toString(); + + if (data.indexOf('****TEST_LOGGER:[PROCESS_ON_EXIT') >= 0) { + if (data.indexOf('****TEST_LOGGER:[PROCESS_ON_EXIT_FAILED]****') >= 0) { + self._failed = true; + } + + if (self._failed) { + self._logger.info( + `logcat: STOP logging received from ${deviceId}\nTest has FAILED\n`); + } else { + self._logger.info( + `logcat: STOP loggin received from ${deviceId}\nTest has SUCCEED\n`); + } + + self.stop(); + } + + self._logs.push(data); + }); + + child.stderr.on('data', (data) => { + if (starting) { + starting = false; + // logcat in place run app + self.emmiter.emit('start'); + } + + self._logs.push(data.toString()); + }); + + child.on('exit', (code) => { + self._completed = true; + self._stopTimeout(); + + var error = null; + + if (self._child) { + self._failed = true; + + const message = `logcat: Unexpected exit. + code: ${code}, device: ${deviceId}, app: ${appId}`; + error = new Error(message); + + self._logger.info(message); + } else { + self._failed = false; + + self._logger.info(`logcat: Completed. device: ${deviceId}`); + } + + self.emmiter.emit('complete', + { failed: self._failed, + logs: self._logs, + error: error + }); + + self.stop(); + }); +}; + +Logcat.prototype.stop = function () { + if (this._child) { + this._child.kill(); + + this._child = null; + } +}; + +Logcat.prototype._startTimeout = function (timeout) { + this._stopTimeout(); + + this._timeout = setTimeout(() => { + if (this._child) { + this._failed = true; + this._logs.push(`TIME-OUT KILL (timeout was ${timeout}ms)`); + this.stop(); + } + }, timeout); +}; + +Logcat.prototype._stopTimeout = function () { + if (this._timeout) { + clearTimeout(this._timeout); + } +}; + +module.exports = Logcat;