Skip to content
Closed
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
459 changes: 459 additions & 0 deletions TIDEVICE_TO_PYMOBILEDEVICE3_MIGRATION.md

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -69,14 +69,37 @@ private void initializeTest(TestRunDevice testRunDevice, TestTask testTask, Test
}

@Override
protected void reInstallApp(TestRunDevice testRunDevice, TestTask testTask, Logger logger) {
protected void reInstallApp(TestRunDevice testRunDevice, TestTask testTask, Logger logger) throws Exception {
checkTestTaskCancel(testTask);
if (testTask.getNeedUninstall()) {
logger.info("📦 Uninstalling app: {}", testTask.getPkgName());
testRunDeviceOrchestrator.uninstallApp(testRunDevice, testTask.getPkgName(), logger);
ThreadUtils.safeSleep(3000);
} else if (testTask.getNeedClearData()) {
logger.info("🧹 Clearing app data: {}", testTask.getPkgName());
testRunDeviceOrchestrator.resetPackage(testRunDevice, testTask.getPkgName(), logger);
}

// Install the app (IPA) if not skipped
if (!testTask.getSkipInstall() && testTask.getAppFile() != null && testTask.getAppFile().exists()) {
logger.info("📲 Installing app from: {}", testTask.getAppFile().getAbsolutePath());
try {
boolean installed = testRunDeviceOrchestrator.installApp(testRunDevice, testTask.getAppFile().getAbsolutePath(), logger);
if (installed) {
logger.info("✅ App installed successfully");
} else {
logger.error("❌ App installation returned false");
throw new Exception("Failed to install app: " + testTask.getAppFile().getAbsolutePath());
}
} catch (Exception e) {
logger.error("❌ App installation failed: {}", e.getMessage());
throw e;
}
} else {
logger.info("⏭️ Skipping app installation (skipInstall={}, appFile={})",
testTask.getSkipInstall(),
testTask.getAppFile() != null ? testTask.getAppFile().getAbsolutePath() : "null");
}
}

private void unzipXctestFolder(File zipFile, TestRun testRun, Logger logger) {
Expand Down
Empty file modified android_client/gradlew
100644 → 100755
Empty file.
31 changes: 31 additions & 0 deletions center-application.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
spring:
security:
oauth2:
enabled: false
client:
provider:
azure-ad:
authorization-uri: https://login.microsoftonline.com/common/oauth2/v2.0/authorize
token-uri: https://login.microsoftonline.com/common/oauth2/v2.0/token
jwk-set-uri: https://login.microsoftonline.com/common/discovery/v2.0/keys
registration:
azure-client:
provider: azure-ad
client-id: dummy-client-id
client-secret: dummy-secret
authorization-grant-type: authorization_code
redirect-uri: http://localhost:9886/login/oauth2/code/azure-client
scope: "User.Read"
datasource:
url: jdbc:sqlite:./hydra_lab_center_db.sqlite
driver-class-name: org.sqlite.JDBC
username: sqlite
password: 98765432

app:
default-user: default@hydralab.com
storage:
type: LOCAL
location: ${user.dir}
agent-auth-mode: SECRET
api-auth-mode: SECRET
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ public enum CapabilityKeyword {
npm("--version"),
git("--version"),
tidevice("-v"),
pymobiledevice3("version"), // pymobiledevice3 uses 'version' subcommand
// maven("--version"),
gradle("--version"),
// xcode("--version"),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,8 @@ public IOSDriver getIOSDriver(DeviceInfo deviceInfo, Logger logger) {
caps.setCapability(IOSMobileCapabilityType.USE_PREBUILT_WDA, false);
caps.setCapability("useXctestrunFile", false);
caps.setCapability("skipLogCapture", true);
caps.setCapability("mjpegServerPort", IOSUtils.getMjpegServerPortByUdid(udid, logger, deviceInfo));
// Note: mjpegServerPort removed - Appium XCUITest driver handles MJPEG streaming internally
// Setting it explicitly was causing port conflicts with iproxy

int tryTimes = 3;
boolean sessionCreated = false;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,8 @@ public class IOSDeviceDriver extends AbstractDeviceDriver {
private final Map<String, DeviceInfo> iOSDeviceInfoMap = new HashMap<>();
private static final int MAJOR_APPIUM_VERSION = 1;
private static final int MINOR_APPIUM_VERSION = -1;
private static final int MAJOR_TIDEVICE_VERSION = 0;
private static final int MINOR_TIDEVICE_VERSION = 10;
private static final int MAJOR_PYMOBILEDEVICE3_VERSION = 0;
private static final int MINOR_PYMOBILEDEVICE3_VERSION = 0;

public IOSDeviceDriver(AgentManagementService agentManagementService,
AppiumServerManager appiumServerManager) {
Expand All @@ -64,7 +64,7 @@ public IOSDeviceDriver(AgentManagementService agentManagementService,
@Override
public void init() {
try {
ShellUtils.killProcessByCommandStr("tidevice", classLogger);
ShellUtils.killProcessByCommandStr("pymobiledevice3", classLogger);
IOSUtils.startIOSDeviceWatcher(classLogger, this);
} catch (Exception e) {
throw new HydraLabRuntimeException(500, "IOSDeviceDriver init failed", e);
Expand All @@ -80,12 +80,16 @@ public void execDeviceOperation(DeviceInfo deviceInfo, DeviceOperation operation
public List<EnvCapabilityRequirement> getEnvCapabilityRequirements() {
// todo XCCode / iTunes
return List.of(new EnvCapabilityRequirement(EnvCapability.CapabilityKeyword.appium, MAJOR_APPIUM_VERSION, MINOR_APPIUM_VERSION),
new EnvCapabilityRequirement(EnvCapability.CapabilityKeyword.tidevice, MAJOR_TIDEVICE_VERSION, MINOR_TIDEVICE_VERSION));
new EnvCapabilityRequirement(EnvCapability.CapabilityKeyword.pymobiledevice3, MAJOR_PYMOBILEDEVICE3_VERSION, MINOR_PYMOBILEDEVICE3_VERSION));
}

@Override
public void screenCapture(@NotNull DeviceInfo deviceInfo, @NotNull String path, @Nullable Logger logger) {
IOSUtils.takeScreenshot(deviceInfo.getSerialNum(), path, classLogger);
public void screenCapture(@NotNull DeviceInfo deviceInfo, @NotNull String path, @Nullable Logger logger) throws Exception {
boolean success = IOSUtils.takeScreenshot(deviceInfo.getSerialNum(), path, classLogger);
if (!success) {
throw new Exception("❌ iOS screenshot capture failed for device: " + deviceInfo.getSerialNum() +
". Ensure tunneld is running (for iOS 17+ device only): sudo python3 -m pymobiledevice3 remote tunneld");
}
}

@Override
Expand All @@ -97,7 +101,16 @@ public void wakeUpDevice(DeviceInfo deviceInfo, Logger logger) {
@Override
public void unlockDevice(@NotNull DeviceInfo deviceInfo, @Nullable Logger logger) {
classLogger.info("Unlocking may not work as expected, please keep your device wake.");
getAppiumServerManager().getIOSDriver(deviceInfo, logger).unlockDevice();
try {
getAppiumServerManager().getIOSDriver(deviceInfo, logger).unlockDevice();
} catch (Exception e) {
// Unlock via Appium is optional for XCTest execution (uses xcodebuild command)
// Log the error but don't fail the test run
classLogger.warn("Failed to unlock device via Appium (this is non-fatal for XCTest): " + e.getMessage());
if (logger != null) {
logger.warn("Device unlock via Appium failed but test can proceed with XCTest. Error: " + e.getMessage());
}
}
}

@Override
Expand Down Expand Up @@ -242,12 +255,47 @@ public void updateAllDeviceInfo() {

public DeviceInfo parseJsonToDevice(JSONObject deviceObject) {
DeviceInfo deviceInfo = new DeviceInfo();
String udid = deviceObject.getString("udid");
// pymobiledevice3 uses different field names than tidevice
// Try new format first (Identifier), fallback to old format (udid)
String udid = deviceObject.getString("Identifier");
if (udid == null || udid.isEmpty()) {
udid = deviceObject.getString("UniqueDeviceID");
}
if (udid == null || udid.isEmpty()) {
udid = deviceObject.getString("udid"); // fallback for tidevice compatibility
}
deviceInfo.setSerialNum(udid);
deviceInfo.setDeviceId(udid);
deviceInfo.setName(deviceObject.getString("name"));
deviceInfo.setModel(deviceObject.getString("market_name"));
deviceInfo.setOsVersion(deviceObject.getString("product_version"));

// Try new format (DeviceName), fallback to old format (name)
String name = deviceObject.getString("DeviceName");
if (name == null || name.isEmpty()) {
name = deviceObject.getString("name");
}
deviceInfo.setName(name);

// Try new format (ProductType mapped to model name), fallback to old format (market_name)
String productType = deviceObject.getString("ProductType");
String model = "-";
if (productType != null && !productType.isEmpty()) {
String mappedModel = AgentConstant.iOSProductModelMap.get(productType);
if (mappedModel != null && !mappedModel.isEmpty()) {
model = mappedModel;
} else {
model = productType; // use ProductType as fallback
}
} else {
model = deviceObject.getString("market_name"); // fallback for tidevice
}
deviceInfo.setModel(model != null ? model : "-");

// Try new format (ProductVersion), fallback to old format (product_version)
String osVersion = deviceObject.getString("ProductVersion");
if (osVersion == null || osVersion.isEmpty()) {
osVersion = deviceObject.getString("product_version");
}
deviceInfo.setOsVersion(osVersion);

deviceInfo.setBrand(iOSDeviceManufacturer);
deviceInfo.setManufacturer(iOSDeviceManufacturer);
deviceInfo.setOsSDKInt("");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,27 @@ abstract public class IOSAppiumScreenRecorder implements ScreenRecorder {
protected String recordDir;

protected boolean isStarted = false;
protected boolean isDriverInitialized = false;


public IOSAppiumScreenRecorder(DeviceDriver deviceDriver, DeviceInfo info, String recordDir) {
this.deviceDriver = deviceDriver;
this.deviceInfo = info;
this.recordDir = recordDir;

this.iosDriver = deviceDriver.getAppiumServerManager().getIOSDriver(deviceInfo, CLASS_LOGGER);
CLASS_LOGGER.info("🎬 Initializing iOS screen recorder for device: {} ({})", info.getName(), info.getSerialNum());
try {
this.iosDriver = deviceDriver.getAppiumServerManager().getIOSDriver(deviceInfo, CLASS_LOGGER);
if (this.iosDriver != null) {
CLASS_LOGGER.info("✅ IOSDriver initialized successfully for device: {}", info.getSerialNum());
isDriverInitialized = true;
} else {
CLASS_LOGGER.error("❌ Failed to initialize IOSDriver - driver is null. Ensure WDA is installed on device.");
}
} catch (Exception e) {
CLASS_LOGGER.error("❌ Failed to initialize IOSDriver: {}. Video recording will be disabled.", e.getMessage());
CLASS_LOGGER.error("💡 To fix: Ensure WDA (WebDriverAgent) is installed on the device and Appium server is running.");
}
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,58 +13,96 @@
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.text.SimpleDateFormat;
import java.time.Duration;
import java.util.Base64;

public class IOSAppiumScreenRecorderForMac extends IOSAppiumScreenRecorder {

public IOSAppiumScreenRecorderForMac(DeviceDriver deviceDriver, DeviceInfo info, String recordDir) {
super(deviceDriver, info, recordDir);
CLASS_LOGGER.info("🎬 IOSAppiumScreenRecorderForMac initialized. Record dir: {}", recordDir);
}

@Override
public void startRecord(int maxTimeInSecond) {
if (!isDriverInitialized || iosDriver == null) {
CLASS_LOGGER.error("❌ Cannot start recording - IOSDriver not initialized. Skipping video recording.");
CLASS_LOGGER.error("💡 Ensure WDA (WebDriverAgent) is installed on the iOS device.");
return;
}

int timeout = maxTimeInSecond > 0 ? maxTimeInSecond : DEFAULT_TIMEOUT_IN_SECOND;
CLASS_LOGGER.info("🎬 Starting iOS screen recording for device: {} (timeout: {}s)",
deviceInfo.getSerialNum(), timeout);
try {
FlowUtil.retryAndSleepWhenFalse(3, 10, () -> {
CLASS_LOGGER.info("📹 Calling iosDriver.startRecordingScreen() with 720p @ 30fps...");
iosDriver.startRecordingScreen(new IOSStartScreenRecordingOptions()
.enableForcedRestart()
.withFps(24)
.withFps(30) // 30 fps for smoother video
.withVideoType("h264")
.withVideoScale("720:360")
.withVideoScale("1280:720") // 720p resolution (was 720:360)
.withVideoQuality(IOSStartScreenRecordingOptions.VideoQuality.HIGH)
.withTimeLimit(Duration.ofSeconds(timeout)));
return true;
});
isStarted = true;
CLASS_LOGGER.info("✅ iOS screen recording started successfully for device: {}", deviceInfo.getSerialNum());
} catch (Exception e) {
System.out.println("-------------------------------Fail to Start recording, Ignore it to unblocking the following tests----------------------------");
e.printStackTrace();
System.out.println("-------------------------------------------------------Ignore End--------------------------------------------------------------");
CLASS_LOGGER.error("❌ Failed to start iOS screen recording: {}", e.getMessage());
CLASS_LOGGER.error("💡 Possible causes: WDA not running, Appium session expired, or device disconnected.");
CLASS_LOGGER.debug("Stack trace:", e);
}
}

@Override
public String finishRecording() {
if (!isStarted) {
CLASS_LOGGER.warn("⚠️ finishRecording() called but recording was never started. Returning null.");
return null;
}

if (iosDriver == null) {
CLASS_LOGGER.error("❌ Cannot stop recording - IOSDriver is null.");
isStarted = false;
return null;
}
SimpleDateFormat format = new SimpleDateFormat(
"yyyy-MM-dd-HH-mm-ss");

CLASS_LOGGER.info("⏹️ Stopping iOS screen recording for device: {}", deviceInfo.getSerialNum());
String destPath = "";
try {
// wait 5s to record more info after testing
CLASS_LOGGER.info("⏳ Waiting 5s before stopping recording...");
ThreadUtils.safeSleep(5000);

CLASS_LOGGER.info("📹 Calling iosDriver.stopRecordingScreen()...");
String base64String = iosDriver.stopRecordingScreen();

if (base64String == null || base64String.isEmpty()) {
CLASS_LOGGER.error("❌ stopRecordingScreen() returned empty data.");
isStarted = false;
return null;
}

byte[] data = Base64.getDecoder().decode(base64String);
destPath = new File(recordDir, Const.ScreenRecoderConfig.DEFAULT_FILE_NAME).getAbsolutePath();
Path path = Paths.get(destPath);
Files.write(path, data);
isStarted = false;

File videoFile = new File(destPath);
if (videoFile.exists() && videoFile.length() > 0) {
CLASS_LOGGER.info("✅ iOS screen recording saved successfully: {} ({}KB)",
destPath, videoFile.length() / 1024);
} else {
CLASS_LOGGER.error("❌ Video file was not created or is empty: {}", destPath);
return null;
}
} catch (Throwable e) {
System.out.println("-------------------------------Fail to Stop recording, Ignore it to unblocking the following tests-----------------------------");
e.printStackTrace();
System.out.println("-------------------------------------------------------Ignore End--------------------------------------------------------------");
CLASS_LOGGER.error("❌ Failed to stop iOS screen recording: {}", e.getMessage());
CLASS_LOGGER.error("💡 Possible causes: Recording timeout exceeded, WDA crashed, or device disconnected.");
CLASS_LOGGER.debug("Stack trace:", e);
isStarted = false;
return null;
}
return destPath;
Expand Down
Loading