Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 41 additions & 25 deletions test-app/runtests.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,32 @@ def isWinOs = System.properties['os.name'].toLowerCase().contains('windows')
def runOnDevice = project.hasProperty("runOnDevice")
def runOnDeviceOrEmulator = runOnDevice ? "-d" : "-e"

// One device for the whole run. Without ANDROID_SERIAL, :app:installDebug installs on every
// connected device, overwriting whatever build of the test app the others are running.
def targetSerial = null
def resolveTargetSerial = { ->
if (targetSerial != null) {
return targetSerial
}
def fromEnv = System.getenv("ANDROID_SERIAL")
if (fromEnv) {
targetSerial = fromEnv
return targetSerial
}
def proc = ["adb", runOnDeviceOrEmulator, "get-serialno"].execute()
def stdout = new StringBuilder()
def stderr = new StringBuilder()
proc.waitForProcessOutput(stdout, stderr)
def serial = stdout.toString().trim()
if (proc.exitValue() != 0 || !serial) {
throw new GradleException("Could not pick a single ${runOnDevice ? 'device' : 'emulator'} " +
"(adb ${runOnDeviceOrEmulator}: ${stderr.toString().trim()}). Set ANDROID_SERIAL to choose one.")
}
targetSerial = serial
return targetSerial
}
def adbTarget = { -> ["-s", resolveTargetSerial()] }

def onlyX86 = project.hasProperty("onlyX86")
def useCCache = project.hasProperty("useCCache")
def selectedAbis = project.hasProperty("abis") ? project.property("abis") : null
Expand All @@ -16,10 +42,12 @@ task waitForEmulatorToStart(type: Exec) {
doFirst {
println "Waiting for emulator to start..."

def envSerial = System.getenv("ANDROID_SERIAL")
def selector = envSerial ? ["-s", envSerial] : [runOnDeviceOrEmulator]
if (isWinOs) {
commandLine "cmd", "/c", "node", "$rootDir\\tools\\make_sure_emulator_is_ready.js", runOnDeviceOrEmulator
commandLine(["cmd", "/c", "node", "$rootDir\\tools\\make_sure_emulator_is_ready.js"] + selector)
} else {
commandLine "node", "$rootDir/tools/make_sure_emulator_is_ready.js", runOnDeviceOrEmulator
commandLine(["node", "$rootDir/tools/make_sure_emulator_is_ready.js"] + selector)
}
}
}
Expand Down Expand Up @@ -53,32 +81,20 @@ def getBuildArguments = { ->
task installApk(type: Exec) {
doFirst {
println "Attempting to install buit apk"
environment "ANDROID_SERIAL", resolveTargetSerial()
commandLine getBuildArguments()
}
}

task runAdbAsRoot(type: Exec) {
doFirst {
println "Running adb as root"

if (isWinOs) {
commandLine "cmd", "/c", "adb", "-e", "root"
} else {
commandLine "adb", "-e", "root"
}
sleep(10 * 1000) // wait 10s to make sure adb properly restarts, otherwise error: closed might happen on next command
}
}

task deletePreviousResultXml(type: Exec) {
ignoreExitValue = true
doFirst {
println "Removing previous android_unit_test_results.xml"

if (isWinOs) {
commandLine "cmd", "/c", "adb", runOnDeviceOrEmulator, "-e", "shell", "run-as", "com.tns.testapplication", "rm", "-f", "android_unit_test_results.xml"
commandLine(["cmd", "/c", "adb"] + adbTarget() + ["shell", "run-as", "com.tns.testapplication", "rm", "-f", "android_unit_test_results.xml"])
} else {
commandLine "adb", runOnDeviceOrEmulator, "-e", "shell", "run-as", "com.tns.testapplication", "rm", "-f", "android_unit_test_results.xml"
commandLine(["adb"] + adbTarget() + ["shell", "run-as", "com.tns.testapplication", "rm", "-f", "android_unit_test_results.xml"])
}
}
}
Expand All @@ -88,9 +104,9 @@ task startInstalledApk(type: Exec) {
println "Starting test application"

if (isWinOs) {
commandLine "cmd", "/c", "adb", runOnDeviceOrEmulator, "-e", "shell", "am", "start", "-n", "com.tns.testapplication/com.tns.NativeScriptActivity", "-a", "android.intent.action.MAIN", "-c", "android.intent.category.LAUNCHER"
commandLine(["cmd", "/c", "adb"] + adbTarget() + ["shell", "am", "start", "-n", "com.tns.testapplication/com.tns.NativeScriptActivity", "-a", "android.intent.action.MAIN", "-c", "android.intent.category.LAUNCHER"])
} else {
commandLine "adb", runOnDeviceOrEmulator, "-e", "shell", "am", "start", "-n", "com.tns.testapplication/com.tns.NativeScriptActivity", "-a", "android.intent.action.MAIN", "-c", "android.intent.category.LAUNCHER"
commandLine(["adb"] + adbTarget() + ["shell", "am", "start", "-n", "com.tns.testapplication/com.tns.NativeScriptActivity", "-a", "android.intent.action.MAIN", "-c", "android.intent.category.LAUNCHER"])
}
}
}
Expand All @@ -107,9 +123,9 @@ task waitForUnitTestResultFile(type: Exec) {
println "Waiting for tests to finish..."

if (isWinOs) {
commandLine "cmd", "/c", "node", "$rootDir\\tools\\try_to_find_test_result_file.js", runOnDeviceOrEmulator
commandLine(["cmd", "/c", "node", "$rootDir\\tools\\try_to_find_test_result_file.js"] + adbTarget())
} else {
commandLine "node", "$rootDir/tools/try_to_find_test_result_file.js", runOnDeviceOrEmulator
commandLine(["node", "$rootDir/tools/try_to_find_test_result_file.js"] + adbTarget())
}
}
}
Expand Down Expand Up @@ -138,10 +154,10 @@ task runtests {
}

// waitForEmulatorToStart.dependsOn(deleteDist)
runAdbAsRoot.dependsOn(waitForEmulatorToStart)
deletePreviousResultXml.dependsOn(runAdbAsRoot)
installApk.dependsOn(deletePreviousResultXml)
startInstalledApk.dependsOn(installApk)
installApk.dependsOn(waitForEmulatorToStart)
// Runs after the install: run-as only works once the debuggable build is on the device.
deletePreviousResultXml.dependsOn(installApk)
startInstalledApk.dependsOn(deletePreviousResultXml)
createDistFolder.dependsOn(startInstalledApk)
waitForUnitTestResultFile.dependsOn(createDistFolder)
copyResultToDist.dependsOn(waitForUnitTestResultFile)
Expand Down
2 changes: 1 addition & 1 deletion test-app/tools/make_sure_emulator_is_ready.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ var checkBootAnimationPropertyExec = require('child_process').exec,
processTimeout = 10 * 60 * 1000, // 10 minutes
searchInterval = 2 * 1000;

var runOnDeviceOrEmulator = process.argv[2];
var runOnDeviceOrEmulator = process.argv.slice(2).join(" ");

checkBootAnimationPropertyExec("empty", check)

Expand Down
91 changes: 48 additions & 43 deletions test-app/tools/try_to_find_test_result_file.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,18 @@
const { exec } = require("child_process");

const appId = "com.tns.testapplication";
const runOnDeviceOrEmulator = process.argv[2] || "";
const adbPrefix = `adb ${runOnDeviceOrEmulator} -e`;
const resultsPath = `/data/data/${appId}/android_unit_test_results.xml`;
const runOnDeviceOrEmulator = process.argv.slice(2).join(" ") || "-e";
const adbPrefix = `adb ${runOnDeviceOrEmulator}`;
const resultsFileName = "android_unit_test_results.xml";

const processTimeoutMs = 20 * 60 * 1000; // 20 minutes timeout (empirical constant)
const pollIntervalMs = 10 * 1000;
// adb can hang outright on a wedged or overloaded device; a hung call would also stall the
// overall timeout check, which only runs between calls.
const adbCallTimeoutMs = 30 * 1000;
// exec kills the child past maxBuffer (1 MiB by default); the results XML and a full logcat
// dump can both exceed that.
const adbExecOptions = { timeout: adbCallTimeoutMs, maxBuffer: 64 * 1024 * 1024 };
const scriptStartTime = new Date();
const logcatCutoffTime = new Date(scriptStartTime.getTime() - 60 * 1000); // 1 minute before start

Expand All @@ -16,7 +22,7 @@ console.error = console.log; // Redirect stderr to stdout for easier logcat capt

function execAndStream(command, streamOutput = false) {
return new Promise((resolve) => {
const child = exec(command, (error, stdout, stderr) => {
const child = exec(command, adbExecOptions, (error, stdout, stderr) => {
resolve({ error, stdout, stderr });
});

Expand Down Expand Up @@ -73,15 +79,15 @@ async function readAndFilterLogcat() {
// if we have a PID, filter by it
if (appPID !== -1) {
return new Promise((resolve) => {
exec(`${adbPrefix} logcat -d --pid=${appPID}`, (error, stdout) => {
exec(`${adbPrefix} logcat -d --pid=${appPID}`, adbExecOptions, (error, stdout) => {
resolve(filterLogcatFromCutoff(stdout || ""));
});
});
}
// otherwise, do a general logcat read
return new Promise((resolve) => {
console.log('Reading full logcat since app PID is unknown');
exec(`${adbPrefix} logcat -d`, (error, stdout) => {
exec(`${adbPrefix} logcat -d`, adbExecOptions, (error, stdout) => {
resolve(filterLogcatFromCutoff(stdout || ""));
});
});
Expand All @@ -97,27 +103,43 @@ async function exitWithLogcatDump() {
}
}

// A single slow or failed adb call on a loaded host is not a crash; only give up after
// several consecutive misses.
const maxConsecutiveLivenessMisses = 3;
let consecutiveLivenessMisses = 0;
let appPID = -1;

// pidof prints "<pid>". API 21-23 images have no pidof, so fall back to their ps, whose rows
// are "<user> <pid> ...".
function parseAppPid(output) {
const text = (output || "").trim();
const match = /^(\d+)/.exec(text) || /^\S+\s+(\d+)/.exec(text);
return match ? parseInt(match[1], 10) : NaN;
}

async function ensureProcessAlive() {
const { stdout } = await execAndStream(
`${adbPrefix} shell "ps | grep ${appId}"`
`${adbPrefix} shell "pidof ${appId} 2>/dev/null || ps | grep ${appId}"`
);

if (!stdout) {
const pid = parseAppPid(stdout);

if (!Number.isFinite(pid)) {
consecutiveLivenessMisses++;
if (consecutiveLivenessMisses < maxConsecutiveLivenessMisses) {
console.log(
`${appId} process not found (${consecutiveLivenessMisses}/${maxConsecutiveLivenessMisses}), checking again`
);
return;
}
console.error(`${appId} process died or never started!`);
await exitWithLogcatDump();
}
await updateAppPID();

console.log(`${appId} process is running`);
}
let appPID = -1;
async function updateAppPID() {
if (appPID !== -1) {
return appPID;
consecutiveLivenessMisses = 0;
if (appPID === -1) {
appPID = pid;
}
const { stdout } = await execAndStream(`${adbPrefix} shell "pidof ${appId}"`);
appPID = parseInt(stdout.trim(), 10);
return appPID;
console.log(`${appId} process is running`);
}

async function checkForErrorActivity() {
Expand All @@ -139,32 +161,15 @@ function isCompleteJunitXml(text) {
return /<testsuites[\s>]/.test(trimmed) && trimmed.includes("</testsuites>");
}

// The app writes into its private data dir. `run-as` reads it without root, as
// long as the installed build is debuggable.
async function tryPullResultsFile() {
const { error } = await execAndStream(`${adbPrefix} pull ${resultsPath}`);

if (!error) {
const fs = require("fs");
try {
const text = fs.readFileSync("android_unit_test_results.xml", "utf8");
if (isCompleteJunitXml(text)) {
console.log("Tests results file found!");
process.exit(0);
}
} catch (e) {
// Missing or unreadable; keep polling.
}
}

// Play Store / userdebug-less images reject `adb root` and cannot pull
// /data/data directly. Debug apps can still read their own files via run-as.
const localPath = "android_unit_test_results.xml";
const { error: runAsError, stdout } = await execAndStream(
`${adbPrefix} exec-out run-as ${appId} cat android_unit_test_results.xml`
const { error, stdout } = await execAndStream(
`${adbPrefix} exec-out run-as ${appId} cat ${resultsFileName}`
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
);
if (!runAsError && isCompleteJunitXml(stdout)) {
const fs = require("fs");
fs.writeFileSync(localPath, stdout);
console.log("Tests results file found via run-as!");
if (!error && isCompleteJunitXml(stdout)) {
require("fs").writeFileSync(resultsFileName, stdout);
console.log("Tests results file found!");
process.exit(0);
}
}
Expand Down
Loading