Skip to content

Commit dae9961

Browse files
authored
Merge pull request #464 from TESTARtool/master_android
Add Android logcat logic and refactor GenerateMode logic to report initialState issues
2 parents 469f784 + 78591d2 commit dae9961

33 files changed

Lines changed: 971 additions & 214 deletions

.gitattributes

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
*.sh text eol=lf

.github/workflows/test-ubuntu-android.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,5 +120,8 @@ jobs:
120120
- name: Run TESTAR android generic protocol
121121
run: ./gradlew runTestAndroidGenericOk
122122

123+
- name: Run TESTAR android generic protocol for the suspicious logcat message
124+
run: ./gradlew runTestAndroidGenericSuspiciousLog
125+
123126
- name: Run TESTAR android generic protocol with suspicious message
124127
run: ./gradlew runTestAndroidGenericSuspiciousTagStateModel

.github/workflows/test-windows-webdriver.yml

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -49,15 +49,25 @@ jobs:
4949
name: Java${{ matrix.java }}-runTestWebdriverSuspiciousTagStateModel-artifact
5050
path: D:/a/TESTAR_dev/TESTAR_dev/testar/target/install/testar/bin/webdriver_and_suspicious
5151

52-
- name: Run webdriver to login in parabank and detect browser console error
53-
run: ./gradlew runTestWebdriverParabankLoginAndConsoleError
54-
- name: Save runTestWebdriverParabankLoginAndConsoleError HTML report artifact
52+
- name: Run webdriver to detect a browser console error in parabank
53+
run: ./gradlew runTestWebdriverParabankConsoleError
54+
- name: Save runTestWebdriverParabankConsoleError HTML report artifact
5555
uses: actions/upload-artifact@v4
5656
# Only upload GitHub Actions results if this task fails (Can be replaced with 'if: always()')
5757
if: failure()
5858
with:
59-
name: Java${{ matrix.java }}-runTestWebdriverParabankLoginAndConsoleError-artifact
60-
path: D:/a/TESTAR_dev/TESTAR_dev/testar/target/install/testar/bin/webdriver_login_console_error
59+
name: Java${{ matrix.java }}-runTestWebdriverParabankConsoleError-artifact
60+
path: D:/a/TESTAR_dev/TESTAR_dev/testar/target/install/testar/bin/webdriver_console_error
61+
62+
- name: Run webdriver to login in parabank and detect a welcome suspicious tag
63+
run: ./gradlew runTestWebdriverParabankLogin
64+
- name: Save runTestWebdriverParabankLogin HTML report artifact
65+
uses: actions/upload-artifact@v4
66+
# Only upload GitHub Actions results if this task fails (Can be replaced with 'if: always()')
67+
if: failure()
68+
with:
69+
name: Java${{ matrix.java }}-runTestWebdriverParabankLogin-artifact
70+
path: D:/a/TESTAR_dev/TESTAR_dev/testar/target/install/testar/bin/webdriver_login_welcome
6171

6272
- name: Run webdriver to login in parabank using form filling
6373
run: ./gradlew runTestWebdriverParabankFormFilling

CHANGELOG

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,17 @@
1+
#TESTAR v2.7.21 (17-Feb-2026)
2+
- Add logic to detect Android logcat suspicious messages
3+
- Refactor the GenerateMode logic to report initialState issues
4+
- Add appium capabilities adb timeouts commands
5+
- Add gitattributes for LF EOL
6+
- Refactor android system actions to map only the state
7+
- Add buggy app APK for demos
8+
- Update testar main default paths
9+
- Make output directories creation issues verbose
10+
- Mark android driver timeout as unresponsive
11+
- Deduplicate and sort AndroidLogcatOracle messages
12+
- Add and update tests accordingly
13+
14+
115
#TESTAR v2.7.20 (3-Feb-2026)
216
- Fix android SPY is displayed property
317
- Add Rect overlap method

VERSION

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
2.7.20
1+
2.7.21

android/src/org/testar/monkey/alayer/android/AndroidAppiumFramework.java

Lines changed: 169 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
/***************************************************************************************************
22
*
3-
* Copyright (c) 2020 - 2025 Universitat Politecnica de Valencia - www.upv.es
4-
* Copyright (c) 2020 - 2025 Open Universiteit - www.ou.nl
3+
* Copyright (c) 2020 - 2026 Universitat Politecnica de Valencia - www.upv.es
4+
* Copyright (c) 2020 - 2026 Open Universiteit - www.ou.nl
55
*
66
* Redistribution and use in source and binary forms, with or without
77
* modification, are permitted provided that the following conditions are met:
@@ -75,6 +75,7 @@ public class AndroidAppiumFramework extends SUTBase {
7575
public static AndroidAppiumFramework androidSUT = null;
7676

7777
private static AndroidDriver driver = null;
78+
private static volatile boolean driverUnresponsive = false;
7879

7980
// Appium v2 do not use /wd/hub suffix anymore
8081
// It can be enabled using the "--base-path /wd/hub" command when launching the Appium server
@@ -138,6 +139,26 @@ public static AndroidDriver getDriver() {
138139
return driver;
139140
}
140141

142+
static void markDriverUnresponsive(Throwable t) {
143+
driverUnresponsive = true;
144+
if (t != null) {
145+
if(t.getMessage() != null) {
146+
System.err.println("AndroidAppiumFramework: Android driver is unresponsive: " + t.getClass().getSimpleName() + " - " + t.getMessage());
147+
} else {
148+
System.err.println("AndroidAppiumFramework: Android driver is unresponsive");
149+
t.printStackTrace();
150+
}
151+
}
152+
}
153+
154+
static boolean isDriverUnresponsive() {
155+
return driverUnresponsive;
156+
}
157+
158+
static void resetDriverUnresponsive() {
159+
driverUnresponsive = false;
160+
}
161+
141162
public static List<WebElement> findElements(By by){
142163
return driver.findElements(by);
143164
}
@@ -333,8 +354,12 @@ public static void generateText() {
333354
}
334355

335356
public static String getCurrentPackage() {
336-
String currentPackage = driver.getCurrentPackage();
337-
return currentPackage;
357+
try {
358+
return driver.getCurrentPackage();
359+
} catch (WebDriverException wde) {
360+
markDriverUnresponsive(wde);
361+
return "";
362+
}
338363
}
339364

340365
public static void pressKeyEvent(KeyEvent keyEvent){
@@ -380,7 +405,12 @@ public static void pushFile(String remotePath, File file){
380405
}
381406

382407
public static String getActivity() {
383-
return driver.currentActivity();
408+
try {
409+
return driver.currentActivity();
410+
} catch (WebDriverException wde) {
411+
markDriverUnresponsive(wde);
412+
return "";
413+
}
384414
}
385415

386416
public static String getScreenshotSpyMode(String stateID) throws IOException {
@@ -393,15 +423,27 @@ public static String getScreenshotSpyMode(String stateID) throws IOException {
393423
}
394424

395425
public static String getScreenshotState(State state) throws IOException {
396-
byte[] byteImage = driver.getScreenshotAs(OutputType.BYTES);
397-
InputStream is = new ByteArrayInputStream(byteImage);
398-
AWTCanvas canvas = AWTCanvas.fromInputStream(is);
399-
return ScreenshotSerialiser.saveStateshot(state.get(Tags.ConcreteID, "NoConcreteIdAvailable"), canvas);
426+
try {
427+
byte[] byteImage = driver.getScreenshotAs(OutputType.BYTES);
428+
InputStream is = new ByteArrayInputStream(byteImage);
429+
AWTCanvas canvas = AWTCanvas.fromInputStream(is);
430+
return ScreenshotSerialiser.saveStateshot(state.get(Tags.ConcreteID, "NoConcreteIdAvailable"), canvas);
431+
} catch (WebDriverException wde) {
432+
markDriverUnresponsive(wde);
433+
throw new IOException("Exception: AndroidDriver getScreenshotState failed", wde);
434+
}
400435
}
401436

402437
public static String getScreenshotAction(State state, Action action) throws IOException {
403-
byte[] byteImage = driver.getScreenshotAs(OutputType.BYTES);
404-
InputStream is = new ByteArrayInputStream(byteImage);
438+
byte[] byteImage;
439+
InputStream is;
440+
try {
441+
byteImage = driver.getScreenshotAs(OutputType.BYTES);
442+
is = new ByteArrayInputStream(byteImage);
443+
} catch (WebDriverException wde) {
444+
markDriverUnresponsive(wde);
445+
throw new IOException("Exception: AndroidDriver getScreenshotAction failed", wde);
446+
}
405447

406448
// Highlight the action on the screenshot:
407449
BufferedImage newBi = ImageIO.read(is);
@@ -432,9 +474,14 @@ public static String getScreenshotAction(State state, Action action) throws IOEx
432474
}
433475

434476
public static AWTCanvas getScreenshotBinary(State state) throws IOException {
435-
byte[] byteImage = driver.getScreenshotAs(OutputType.BYTES);
436-
InputStream is = new ByteArrayInputStream(byteImage);
437-
return AWTCanvas.fromInputStream(is);
477+
try {
478+
byte[] byteImage = driver.getScreenshotAs(OutputType.BYTES);
479+
InputStream is = new ByteArrayInputStream(byteImage);
480+
return AWTCanvas.fromInputStream(is);
481+
} catch (WebDriverException wde) {
482+
markDriverUnresponsive(wde);
483+
throw new IOException("Exception: AndroidDriver getScreenshotBinary failed", wde);
484+
}
438485
}
439486

440487
public static void terminateApp(String bundleId){
@@ -486,6 +533,98 @@ public static LogEntries getAppiumLogs() {
486533
return driver.manage().logs().get("driver");
487534
}
488535

536+
/**
537+
* Clear all logcat content.
538+
*/
539+
public static void clearLogcat() {
540+
try {
541+
mobileShell("logcat", List.of("-b", "all", "-c"), Duration.ofSeconds(10));
542+
} catch (Exception ignored) {}
543+
}
544+
545+
/**
546+
* Execute an Android shell command on the device via Appium ("mobile: shell").
547+
*/
548+
private static void mobileShell(String command, List<String> args, Duration timeout) {
549+
Map<String, Object> m = new HashMap<>();
550+
m.put("command", command);
551+
m.put("args", args);
552+
m.put("timeout", (int) timeout.toMillis());
553+
driver.executeScript("mobile: shell", m);
554+
}
555+
556+
/**
557+
* Dump logcat output (main + system + crash buffers).
558+
*/
559+
public static String dumpLogcatThreadtimeForPackage(String pkg) {
560+
if (pkg == null || pkg.isBlank()) return "";
561+
562+
// 1) get PID
563+
String pid = mobileShellStdout("pidof", List.of("-s", pkg), Duration.ofSeconds(5)).trim();
564+
if (pid.isEmpty()) return "";
565+
566+
// 2) dump logcat for that PID
567+
return mobileShellStdout(
568+
"logcat",
569+
List.of("-d", "-v", "threadtime", "--pid", pid, "-b", "main", "-b", "system", "-b", "crash"),
570+
Duration.ofSeconds(10)
571+
);
572+
}
573+
574+
/**
575+
* Execute an Android shell command on the device via Appium ("mobile: shell") and return stdout.
576+
*/
577+
private static String mobileShellStdout(String command, List<String> args, Duration timeout) {
578+
Map<String, Object> m = new HashMap<>();
579+
m.put("command", command);
580+
m.put("args", args);
581+
m.put("timeout", (int) timeout.toMillis());
582+
583+
Object raw;
584+
try {
585+
raw = driver.executeScript("mobile: shell", m);
586+
} catch (WebDriverException wde) {
587+
markDriverUnresponsive(wde);
588+
return "";
589+
}
590+
if (raw == null) return "";
591+
if (raw instanceof String) return (String)raw;
592+
if (raw instanceof Map<?, ?>) {
593+
Object out = ((Map<?, ?>)raw).get("stdout");
594+
return out == null ? "" : out.toString();
595+
}
596+
return raw.toString();
597+
}
598+
599+
public static String getAppPackageFromCapabilitiesOrCurrent() {
600+
if (driver == null) {
601+
return "";
602+
}
603+
604+
try {
605+
Object appPackage = driver.getCapabilities().getCapability("appium:appPackage");
606+
if (appPackage == null) {
607+
appPackage = driver.getCapabilities().getCapability("appPackage");
608+
}
609+
if (appPackage != null) {
610+
String pkg = appPackage.toString().trim();
611+
if (!pkg.isEmpty()) {
612+
return pkg;
613+
}
614+
}
615+
} catch (Exception ignored) {
616+
}
617+
618+
try {
619+
return driver.getCurrentPackage();
620+
} catch (WebDriverException wde) {
621+
markDriverUnresponsive(wde);
622+
return "";
623+
} catch (Exception ignored) {
624+
return "";
625+
}
626+
}
627+
489628
@Override
490629
public void stop() throws SystemStopException {
491630
driver.quit();
@@ -500,6 +639,10 @@ public boolean isRunning() {
500639
//driver.queryAppState(appId), equalTo(ApplicationState.RUNNING_IN_FOREGROUND)
501640
driver.getCurrentPackage();
502641
}
642+
catch (WebDriverException wde) {
643+
markDriverUnresponsive(wde);
644+
return false;
645+
}
503646
catch (Exception e) {
504647
return false;
505648
}
@@ -510,11 +653,21 @@ public boolean isRunning() {
510653
@Override
511654
public String getStatus() {
512655
//TODO: Check and select proper method to print the status
513-
return "Android current package : " + driver.getCurrentPackage();
656+
try {
657+
return "Android current package : " + driver.getCurrentPackage();
658+
} catch (WebDriverException wde) {
659+
markDriverUnresponsive(wde);
660+
return "Android current package : <unavailable>";
661+
}
514662
}
515663

516664
public static ApplicationState getStatus(String appId) {
517-
return driver.queryAppState(appId);
665+
try {
666+
return driver.queryAppState(appId);
667+
} catch (WebDriverException wde) {
668+
markDriverUnresponsive(wde);
669+
return ApplicationState.NOT_RUNNING;
670+
}
518671
}
519672

520673
@Override

android/src/org/testar/monkey/alayer/android/AndroidCapabilitiesFactory.java

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
/***************************************************************************************************
22
*
3-
* Copyright (c) 2025 Universitat Politecnica de Valencia - www.upv.es
4-
* Copyright (c) 2025 Open Universiteit - www.ou.nl
3+
* Copyright (c) 2025 - 2026 Universitat Politecnica de Valencia - www.upv.es
4+
* Copyright (c) 2025 - 2026 Open Universiteit - www.ou.nl
55
*
66
* Redistribution and use in source and binary forms, with or without
77
* modification, are permitted provided that the following conditions are met:
@@ -80,6 +80,13 @@ Result fromJsonObject(JsonObject json) {
8080

8181
cap.setCapability("appium:settings[allowInvisibleElements]", getBool(json, "allowInvisibleElements", false));
8282

83+
cap.setCapability("appium:ignoreHiddenApiPolicyError", getBool(json, "ignoreHiddenApiPolicyError", false));
84+
85+
// ADB / server timeouts
86+
cap.setCapability("appium:adbExecTimeout", getInt(json, "adbExecTimeout", 120000));
87+
cap.setCapability("appium:uiautomator2ServerInstallTimeout", getInt(json, "uiautomator2ServerInstallTimeout", 120000));
88+
cap.setCapability("appium:uiautomator2ServerLaunchTimeout", getInt(json, "uiautomator2ServerLaunchTimeout", 120000));
89+
8390
String appiumUrl = defaultAppiumUrl;
8491

8592
// If the APK is already installed we use appPackage identifier

android/src/org/testar/monkey/alayer/android/AndroidStateBuilder.java

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
/***************************************************************************************************
22
*
3-
* Copyright (c) 2020 - 2022 Universitat Politecnica de Valencia - www.upv.es
4-
* Copyright (c) 2020 - 2022 Open Universiteit - www.ou.nl
3+
* Copyright (c) 2020 - 2026 Universitat Politecnica de Valencia - www.upv.es
4+
* Copyright (c) 2020 - 2026 Open Universiteit - www.ou.nl
55
*
66
* Redistribution and use in source and binary forms, with or without
77
* modification, are permitted provided that the following conditions are met:
@@ -54,8 +54,29 @@ public AndroidStateBuilder(double timeOut) {
5454
@Override
5555
public State apply(SUT system) throws StateBuildException {
5656
try {
57+
// If the driver became unresponsive during non-state fetcher calls like actions or logact
58+
if (AndroidAppiumFramework.isDriverUnresponsive()) {
59+
AndroidAppiumFramework.resetDriverUnresponsive();
60+
AndroidRootElement rootElement = AndroidStateFetcher.buildRoot(system);
61+
AndroidState androidState = new AndroidState(rootElement);
62+
androidState.set(Tags.Role, Roles.Process);
63+
androidState.set(Tags.NotResponding, true);
64+
return androidState;
65+
}
66+
5767
Future<AndroidState> future = executor.submit(new AndroidStateFetcher(system));
5868
AndroidState state = future.get((long) (timeOut), TimeUnit.SECONDS);
69+
70+
// If the driver became unresponsive during state fetch calls
71+
if (AndroidAppiumFramework.isDriverUnresponsive()) {
72+
AndroidAppiumFramework.resetDriverUnresponsive();
73+
AndroidRootElement rootElement = AndroidStateFetcher.buildRoot(system);
74+
AndroidState androidState = new AndroidState(rootElement);
75+
androidState.set(Tags.Role, Roles.Process);
76+
androidState.set(Tags.NotResponding, true);
77+
return androidState;
78+
}
79+
5980
return state;
6081
}
6182
catch (InterruptedException | ExecutionException e) {

0 commit comments

Comments
 (0)