From 16efb058eafe48e8b58d9a07a6cbd99e7f9d59d7 Mon Sep 17 00:00:00 2001 From: Tomas Date: Wed, 23 Sep 2026 10:43:44 +0300 Subject: [PATCH 1/5] Cherry pick changes from live stream PR --- .../Editor/AndroidLogcatCaptureVideo.cs | 15 +--- .../Editor/AndroidLogcatDevice.cs | 80 +++++++++++++++++++ .../Editor/AndroidLogcatUtilities.cs | 28 +++++-- .../Editor/AndroidTools/AndroidBridge.cs | 21 +++++ .../Tests/Editor/AndroidLogcatFakeDevice.cs | 19 ++++- 5 files changed, 143 insertions(+), 20 deletions(-) diff --git a/com.unity.mobile.android-logcat/Editor/AndroidLogcatCaptureVideo.cs b/com.unity.mobile.android-logcat/Editor/AndroidLogcatCaptureVideo.cs index e501ead6..6e30c415 100644 --- a/com.unity.mobile.android-logcat/Editor/AndroidLogcatCaptureVideo.cs +++ b/com.unity.mobile.android-logcat/Editor/AndroidLogcatCaptureVideo.cs @@ -45,7 +45,7 @@ private void Cleanup() var device = m_RecordingOnDevice; StopRecording(); DeleteVideoOnDevice(device); - KillRemoteRecorder(m_Runtime, device); + AndroidLogcatUtilities.KillScreenRecordProcess(m_Runtime, device); m_Runtime = null; } @@ -94,15 +94,6 @@ internal bool IsRemoteRecorderActive(IAndroidLogcatDevice device) return AndroidLogcatUtilities.GetPidFromPackageName(m_Runtime.Tools.ADB, device, "screenrecord") != -1; } - internal static void KillRemoteRecorder(AndroidLogcatRuntimeBase runtime, IAndroidLogcatDevice device) - { - if (device == null) - return; - var pid = AndroidLogcatUtilities.GetPidFromPackageName(runtime.Tools.ADB, device, "screenrecord"); - if (pid != -1) - AndroidLogcatUtilities.KillProcesss(runtime.Tools.ADB, device, pid); - } - private void DeleteVideoOnHost(string path) { try @@ -155,7 +146,7 @@ internal void StartRecording(IAndroidLogcatDevice device, m_RecordingOnDevice = device; DeleteVideoOnHost(GetVideoPath(device)); - KillRemoteRecorder(m_Runtime, m_RecordingOnDevice); + AndroidLogcatUtilities.KillScreenRecordProcess(m_Runtime, m_RecordingOnDevice); // If for some reason screen recorder is still running, abort. if (IsRemoteRecorderActive(m_RecordingOnDevice)) @@ -249,7 +240,7 @@ private bool CollectRecording(string targetPath) if (!CopyVideoFromDevice(m_RecordingOnDevice, targetPath)) { result = false; - KillRemoteRecorder(m_Runtime, m_RecordingOnDevice); + AndroidLogcatUtilities.KillScreenRecordProcess(m_Runtime, m_RecordingOnDevice); } DeleteVideoOnDevice(m_RecordingOnDevice); diff --git a/com.unity.mobile.android-logcat/Editor/AndroidLogcatDevice.cs b/com.unity.mobile.android-logcat/Editor/AndroidLogcatDevice.cs index 43d3d6d2..745cc154 100644 --- a/com.unity.mobile.android-logcat/Editor/AndroidLogcatDevice.cs +++ b/com.unity.mobile.android-logcat/Editor/AndroidLogcatDevice.cs @@ -7,6 +7,19 @@ namespace Unity.Android.Logcat { + /// + /// How the device's screen is rotated. The numbers are Android's own + /// Surface.ROTATION_* values, which is what the user_rotation setting takes. + /// + internal enum AndroidDeviceRotation + { + Auto = -1, + Rotate0 = 0, + Rotate90 = 1, + Rotate180 = 2, + Rotate270 = 3 + } + internal abstract class IAndroidLogcatDevice { internal IAndroidLogcatActivityManager m_ActivityManager; @@ -53,6 +66,30 @@ internal enum DeviceState internal abstract void QueryDisplaySize(out Vector2 displaySize, out Vector2? overridenDisplaySize); + /// + /// Wakes the device's screen. A display that is off composes nothing, so + /// anything that reads the screen - a mirrored display, screenrecord - gets + /// nothing at all out of a sleeping device. + /// + /// Only a wake: a lock screen stays up, and streams perfectly well, because it + /// composes like any other screen. Best effort, too - a device that will not + /// take it is not an error, since it may well be showing something already. + /// + /// + internal abstract void WakeUp(); + + /// + /// Puts the device's screen to sleep, the counterpart of + /// and best effort in the same way. + /// + internal abstract void Sleep(); + + /// + /// Rotates the device's screen, or with + /// hands the rotation back to the accelerometer. + /// + internal abstract void SetRotation(AndroidDeviceRotation rotation); + protected void ParseDisplaySize(string input, out Vector2 displaySize, out Vector2? overridenDisplaySize) { displaySize = Vector2.zero; @@ -154,6 +191,7 @@ internal class AndroidLogcatDevice : IAndroidLogcatDevice private AndroidBridge.ADB m_ADB; private Version m_Version; private string m_DisplayName; + internal AndroidLogcatDevice(AndroidBridge.ADB adb, string deviceId) : base(new AndroidLogcatActivityManager(adb, deviceId)) { @@ -257,6 +295,48 @@ internal override string DisplayName } } + internal override void WakeUp() => SendPowerKey("KEYCODE_WAKEUP", "Failed to wake the device"); + + internal override void Sleep() => SendPowerKey("KEYCODE_SLEEP", "Failed to put the device to sleep"); + + void SendPowerKey(string keyCode, string failureMessage) + { + if (m_Device == null || State != DeviceState.Connected) + return; + + var args = $"-s {Id} shell input keyevent {keyCode}"; + try + { + var output = m_ADB.Run(new[] { args }, failureMessage); + AndroidLogcatInternalLog.Log($"adb {args}\n{output}"); + } + catch (Exception ex) + { + AndroidLogcatInternalLog.Log(ex.Message); + } + } + + internal override void SetRotation(AndroidDeviceRotation rotation) + { + if (m_Device == null || State != DeviceState.Connected) + return; + + // user_rotation is only obeyed while auto rotation is off, so the two + // settings are one operation: turn the accelerometer off and pin the + // rotation, or turn it back on and leave the pinned value alone. + var auto = rotation == AndroidDeviceRotation.Auto; + PutSystemSetting("accelerometer_rotation", auto ? 1 : 0); + if (!auto) + PutSystemSetting("user_rotation", (int)rotation); + } + + void PutSystemSetting(string name, int value) + { + var args = $"-s {Id} shell settings put system {name} {value}"; + AndroidLogcatInternalLog.Log($"adb {args}"); + m_ADB.Run(new[] { args }, $"Failed to set '{name}' to {value}"); + } + internal override void QueryDisplaySize(out Vector2 displaySize, out Vector2? overridenDisplaySize) { overridenDisplaySize = null; diff --git a/com.unity.mobile.android-logcat/Editor/AndroidLogcatUtilities.cs b/com.unity.mobile.android-logcat/Editor/AndroidLogcatUtilities.cs index 927ed824..6f9e1561 100644 --- a/com.unity.mobile.android-logcat/Editor/AndroidLogcatUtilities.cs +++ b/com.unity.mobile.android-logcat/Editor/AndroidLogcatUtilities.cs @@ -72,15 +72,22 @@ public static bool CaptureScreen(AndroidBridge.ADB adb, string deviceId, string public static string GetTemporaryPath(IAndroidLogcatDevice device, string name, string extension) { - string fileName = device != null ? device.Id : "NoDevice"; - if (device != null) - { - foreach (var p in Path.GetInvalidFileNameChars()) - fileName = fileName.Replace(p, '_'); - } + string fileName = device != null ? SanitizeFileName(device.Id) : "NoDevice"; fileName = $"{name}_{fileName}{extension}"; return Path.Combine(Application.dataPath, "..", "Temp", fileName).Replace("\\", "/"); } + + /// + /// Replaces anything the filesystem will not accept in a file name. A device id + /// can be an ip:port, and ':' is not allowed on Windows. + /// + public static string SanitizeFileName(string name) + { + foreach (var c in Path.GetInvalidFileNameChars()) + name = name.Replace(c, '_'); + return name; + } + /// /// Get the top activity on the given device. @@ -483,6 +490,15 @@ internal static bool ParseCrashLine(IReadOnlyList regexs, str return false; } + internal static void KillScreenRecordProcess(AndroidLogcatRuntimeBase runtime, IAndroidLogcatDevice device) + { + if (device == null) + return; + var pid = GetPidFromPackageName(runtime.Tools.ADB, device, "screenrecord"); + if (pid != -1) + KillProcesss(runtime.Tools.ADB, device, pid); + } + internal static void ShowAndroidIsNotInstalledMessage() { UnityEditor.EditorGUILayout.HelpBox("Android Logcat requires Android support to be installed.", UnityEditor.MessageType.Info); diff --git a/com.unity.mobile.android-logcat/Editor/AndroidTools/AndroidBridge.cs b/com.unity.mobile.android-logcat/Editor/AndroidTools/AndroidBridge.cs index e546713d..f4584e92 100644 --- a/com.unity.mobile.android-logcat/Editor/AndroidTools/AndroidBridge.cs +++ b/com.unity.mobile.android-logcat/Editor/AndroidTools/AndroidBridge.cs @@ -212,6 +212,7 @@ internal class AndroidExternalToolsSettings private static Type s_AndroidExternalToolsSettingsType; private static PropertyInfo s_NdkRootPathProperty; private static PropertyInfo s_SdkRootPathProperty; + private static PropertyInfo s_JdkRootPathProperty; private static Type UnderlyingType { @@ -251,6 +252,17 @@ private static PropertyInfo SdkRootPathProperty } } + private static PropertyInfo JdkRootPathProperty + { + get + { + if (s_JdkRootPathProperty != null) + return s_JdkRootPathProperty; + s_JdkRootPathProperty = UnderlyingType.GetProperty("jdkRootPath"); + return s_JdkRootPathProperty; + } + } + /// /// Matches to UnityEditor.Android.AndroidExternalToolsSettings.ndkRootPath /// @@ -268,6 +280,15 @@ public static string sdkRootPath get => (string)SdkRootPathProperty.GetValue(null); set => SdkRootPathProperty.SetValue(null, value); } + + /// + /// Matches to UnityEditor.Android.AndroidExternalToolsSettings.jdkRootPath + /// + public static string jdkRootPath + { + get => (string)JdkRootPathProperty.GetValue(null); + set => JdkRootPathProperty.SetValue(null, value); + } } } } diff --git a/com.unity.mobile.android-logcat/Tests/Editor/AndroidLogcatFakeDevice.cs b/com.unity.mobile.android-logcat/Tests/Editor/AndroidLogcatFakeDevice.cs index bdfdb3e0..ef20bedd 100644 --- a/com.unity.mobile.android-logcat/Tests/Editor/AndroidLogcatFakeDevice.cs +++ b/com.unity.mobile.android-logcat/Tests/Editor/AndroidLogcatFakeDevice.cs @@ -29,6 +29,21 @@ internal override string Id get { return m_DeviceId; } } + /// Nothing to wake. + internal override void WakeUp() + { + } + + /// Nothing to put to sleep. + internal override void Sleep() + { + } + + /// Nothing to rotate. + internal override void SetRotation(AndroidDeviceRotation rotation) + { + } + internal override void QueryDisplaySize(out Vector2 displaySize, out Vector2? overridenDisplaySize) { ParseDisplaySize(m_DisplayInfo, out displaySize, out overridenDisplaySize); @@ -39,9 +54,9 @@ internal void SetRawDisplayInfo(string displayInfo) m_DisplayInfo = displayInfo; } - internal override string DisplayName => throw new NotImplementedException(); + internal override string DisplayName => throw new NotImplementedException(nameof(DisplayName)); - internal override string ShortDisplayName => throw new NotImplementedException(); + internal override string ShortDisplayName => throw new NotImplementedException(nameof(ShortDisplayName)); protected override string GetTagPriorityAsString(string tag) { From c9ef4d83e0f6b8f31179bdc38531bdeb612ef045 Mon Sep 17 00:00:00 2001 From: Tomas Date: Wed, 23 Sep 2026 10:45:10 +0300 Subject: [PATCH 2/5] Switch integration tests to Pixel Pro --- .yamato/upm-ci.yml | 2 +- .yamato/upm-integration-ci.yml | 17 ++++----- .yamato/use-packed-package.py | 35 +++++++++++++++++++ .../Shared/AndroidLogcatConditionals.cs | 6 ++-- .../Editor/Shared/AndroidLogcatWorkspace.cs | 12 +++++++ 5 files changed, 61 insertions(+), 11 deletions(-) create mode 100644 .yamato/use-packed-package.py diff --git a/.yamato/upm-ci.yml b/.yamato/upm-ci.yml index f8f63045..94f6f7a4 100644 --- a/.yamato/upm-ci.yml +++ b/.yamato/upm-ci.yml @@ -44,7 +44,7 @@ test_{{ platform.name }}_{{ editor.version }}_{{ module_support.name }}: {% if platform.model %}model: {{ platform.model }}{% endif %} commands: - unity-downloader-cli -u {{ editor.version }} {{ platform.install_editor_command }} {{ module_support.install_command }} --wait - - {{ platform.move_alias }} ./disable_tests_csc.rsp ./com.unity.mobile.android-logcat/Tests/Editor/Integration/csc.rsp + - python .yamato/use-packed-package.py TestProjects/SampleProject1 com.unity.mobile.android-logcat - {{ platform.utr_cmd }} --suite=editor --editor-location=.Editor --testproject="TestProjects/SampleProject1" --extra-editor-arg=-buildTarget --extra-editor-arg=Android --artifacts_path=upm-ci~/test-results/editor-android/ - {{ platform.utr_cmd }} --suite=editor --editor-location=.Editor --testproject="TestProjects/TestWarnings" --artifacts_path=upm-ci~/test-results/editor-warnings-android/ artifacts: diff --git a/.yamato/upm-integration-ci.yml b/.yamato/upm-integration-ci.yml index 295f97cf..476eab84 100644 --- a/.yamato/upm-integration-ci.yml +++ b/.yamato/upm-integration-ci.yml @@ -3,19 +3,18 @@ --- {% for editor in test_editors %} test_integration_{{ editor.version }}: - name : Test Integration on {{ editor.version }} + name : Test Integration on {{ editor.version }} Pixel Pro Android 16 agent: - type: Unity::mobile::shield - image: mobile/android-package-ci-win:latest - flavor: b1.medium + image: mobile/android-windows-unity:v2.8084395 + type: Unity::mobile::pixel + flavor: b1.large + model: 8pro-Android16 commands: - - pip install unity-downloader-cli --index-url https://artifactory.prd.it.unity3d.com/artifactory/api/pypi/pypi/simple --upgrade + - gsudo choco install unity-downloader-cli -y -s https://artifactory.prd.it.unity3d.com/artifactory/api/nuget/unity-choco-local - unity-downloader-cli -u {{ editor.version }} -c editor -c android --wait - curl -s https://artifactory.prd.it.unity3d.com/artifactory/unity-tools-local/utr-standalone/utr.bat --output utr.bat - - move ./disable_tests_csc.rsp ./com.unity.mobile.android-logcat/Tests/Editor/csc.rsp + - python .yamato/use-packed-package.py TestProjects/SampleProject1 com.unity.mobile.android-logcat - | - REM Set the IP of the device. In case device gets lost, UTR will try to recconect to ANDROID_DEVICE_CONNECTION - set ANDROID_DEVICE_CONNECTION=%BOKKEN_DEVICE_IP% set ARTIFACTS_PATH=upm-ci~/test-results/integration-artifacts/ REM Editor will perform the connection instead of upm script, since editor might use different SDK (and adb) ./utr.bat --suite=editor --editor-location=.Editor --testproject="TestProjects/SampleProject1" --artifacts_path=upm-ci~/test-results/editor-android/ @@ -23,6 +22,8 @@ test_integration_{{ editor.version }}: integration_{{ editor.version }}_logs: paths: - "upm-ci~/test-results/**/*" + variables: + ANDROID_DEVICE_AVAILABLE: 1 dependencies: - .yamato/wrench/package-pack-jobs.yml#package_pack_-_mobile_android-logcat {% endfor %} diff --git a/.yamato/use-packed-package.py b/.yamato/use-packed-package.py new file mode 100644 index 00000000..f94cd492 --- /dev/null +++ b/.yamato/use-packed-package.py @@ -0,0 +1,35 @@ +"""Points a test project at the packed package instead of the source folder. + +The source folder has no server jar: it is a build output, gitignored, and built by +the jar job whose artifact the pack job folds into the tarball. Tests that push the +jar to a device therefore need the package the pack job produced - which is also what +a user installs, so this is the thing worth testing. + +Usage: python .yamato/use-packed-package.py +""" +import glob +import json +import os +import sys + +project, package = sys.argv[1], sys.argv[2] + +# Globbed rather than named with a version, the way the wrench validation jobs take +# their packages, so a version bump does not have to reach in here. +tarballs = glob.glob(os.path.join('upm-ci~', 'packages', package + '-*.tgz')) +if len(tarballs) != 1: + found = ', '.join(sorted(tarballs)) or 'nothing' + sys.exit(f"Expected one {package} tarball from the pack job, found {found}.") + +manifest_path = os.path.join(project, 'Packages', 'manifest.json') +with open(manifest_path, encoding='utf-8') as f: + manifest = json.load(f) + +previous = manifest['dependencies'].get(package) +manifest['dependencies'][package] = 'file:' + os.path.abspath(tarballs[0]).replace('\\', '/') + +with open(manifest_path, 'w', encoding='utf-8') as f: + json.dump(manifest, f, indent=2) + f.write('\n') + +print(f"{manifest_path}: {package} {previous} -> {manifest['dependencies'][package]}") diff --git a/com.unity.mobile.android-logcat/Tests/Editor/Shared/AndroidLogcatConditionals.cs b/com.unity.mobile.android-logcat/Tests/Editor/Shared/AndroidLogcatConditionals.cs index 741262a4..3acb169a 100644 --- a/com.unity.mobile.android-logcat/Tests/Editor/Shared/AndroidLogcatConditionals.cs +++ b/com.unity.mobile.android-logcat/Tests/Editor/Shared/AndroidLogcatConditionals.cs @@ -13,15 +13,17 @@ static OnLoad() var runningOnKatana = Workspace.IsRunningOnKatana(); var runningOnBuildServer = Workspace.IsRunningOnBuildServer(); var androidDeviceInfo = Workspace.GetAndroidDeviceInfo(); + var androidDeviceAvailable = Workspace.IsAndroidDeviceAvailable(); Console.WriteLine($"Running On Yamato: {runningOnYamato}"); Console.WriteLine($"Running On Katana: {runningOnKatana}"); Console.WriteLine($"Running On Build Server: {runningOnBuildServer}"); Console.WriteLine($"Android Device Info: {androidDeviceInfo}"); + Console.WriteLine($"Android Device Available: {androidDeviceAvailable}"); - // Ignore test only if running on Yamato or Katana and device info is not available + // Ignore test only if running on Yamato or Katana and no device is available // We always want our test to run when running locally - ConditionalIgnoreAttribute.AddConditionalIgnoreMapping(nameof(RequiresAndroidDeviceAttribute), runningOnBuildServer && string.IsNullOrEmpty(androidDeviceInfo)); + ConditionalIgnoreAttribute.AddConditionalIgnoreMapping(nameof(RequiresAndroidDeviceAttribute), runningOnBuildServer && !androidDeviceAvailable); } } diff --git a/com.unity.mobile.android-logcat/Tests/Editor/Shared/AndroidLogcatWorkspace.cs b/com.unity.mobile.android-logcat/Tests/Editor/Shared/AndroidLogcatWorkspace.cs index f477345f..95859b88 100644 --- a/com.unity.mobile.android-logcat/Tests/Editor/Shared/AndroidLogcatWorkspace.cs +++ b/com.unity.mobile.android-logcat/Tests/Editor/Shared/AndroidLogcatWorkspace.cs @@ -31,6 +31,18 @@ public static string GetAndroidDeviceInfo() return result; } + /// + /// Whether the job this runs in has a device attached. Stated by the job rather + /// than inferred from ANDROID_DEVICE_CONNECTION, which only holds something + /// when the device is reached over the network - a device on a usb cable looked + /// like no device at all, and every test that needs one was quietly ignored. + /// + public static bool IsAndroidDeviceAvailable() + { + var value = Environment.GetEnvironmentVariable("ANDROID_DEVICE_AVAILABLE"); + return value == "1" || string.Equals(value, "true", StringComparison.OrdinalIgnoreCase); + } + public static string GetAritfactsPath() { if (!Workspace.IsRunningOnBuildServer()) From 6ec6547e5197ef41bbc3b7f9007fc4a2ff5982c9 Mon Sep 17 00:00:00 2001 From: Tomas Date: Wed, 23 Sep 2026 10:45:22 +0300 Subject: [PATCH 3/5] formatting --- .../Editor/AndroidLogcatUtilities.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/com.unity.mobile.android-logcat/Editor/AndroidLogcatUtilities.cs b/com.unity.mobile.android-logcat/Editor/AndroidLogcatUtilities.cs index 6f9e1561..60cb6cbc 100644 --- a/com.unity.mobile.android-logcat/Editor/AndroidLogcatUtilities.cs +++ b/com.unity.mobile.android-logcat/Editor/AndroidLogcatUtilities.cs @@ -76,7 +76,7 @@ public static string GetTemporaryPath(IAndroidLogcatDevice device, string name, fileName = $"{name}_{fileName}{extension}"; return Path.Combine(Application.dataPath, "..", "Temp", fileName).Replace("\\", "/"); } - + /// /// Replaces anything the filesystem will not accept in a file name. A device id /// can be an ip:port, and ':' is not allowed on Windows. From 73dc549a642b01dd9d89c76bcab17ffe124c8ef1 Mon Sep 17 00:00:00 2001 From: Tomas Date: Wed, 23 Sep 2026 10:45:34 +0300 Subject: [PATCH 4/5] Cherry pick test changes --- .../AndroidLogcatIntegrationScreenCapture.cs | 33 ++-- .../AndroidLogcatIntegrationTestBase.cs | 167 +++++++++++++++++- 2 files changed, 184 insertions(+), 16 deletions(-) diff --git a/com.unity.mobile.android-logcat/Tests/Editor/Integration/AndroidLogcatIntegrationScreenCapture.cs b/com.unity.mobile.android-logcat/Tests/Editor/Integration/AndroidLogcatIntegrationScreenCapture.cs index df74f87d..10402144 100644 --- a/com.unity.mobile.android-logcat/Tests/Editor/Integration/AndroidLogcatIntegrationScreenCapture.cs +++ b/com.unity.mobile.android-logcat/Tests/Editor/Integration/AndroidLogcatIntegrationScreenCapture.cs @@ -27,21 +27,27 @@ protected void Deinit() private void Cleanup() { // Need to kill screen recorder before attempting to delete files - AndroidLogcatCaptureVideo.KillRemoteRecorder(Runtime, Device); + AndroidLogcatUtilities.KillScreenRecordProcess(Runtime, Device); SafeDeleteOnDevice(Device, AndroidLogcatCaptureVideo.VideoPathOnDevice); SafeDeleteOnHost(VideoPathOnHost); } - [UnityTest] - public IEnumerator CanGetScreenshot() + /// + /// Takes a screenshot and waits for it to land, which is where most of these tests + /// start. Returns the wait rather than yielding it, so the capture is queued as + /// soon as this is called. + /// + private IEnumerator CaptureScreenshot(string what = "Waiting for screenshot") { var completed = false; - Runtime.CaptureScreenshot.QueueScreenCapture(Device, () => - { - completed = true; - }); + Runtime.CaptureScreenshot.QueueScreenCapture(Device, () => completed = true); + return WaitForCondition(what, () => completed); + } - yield return WaitForCondition("Waiting for screenshot", () => completed); + [UnityTest] + public IEnumerator CanGetScreenshot() + { + yield return CaptureScreenshot(); var texture = Runtime.CaptureScreenshot.ImageTexture; Assert.IsNotNull(texture, "Expected to have a valid texture"); @@ -49,7 +55,7 @@ public IEnumerator CanGetScreenshot() Assert.Greater(texture.width, 10); Assert.Greater(texture.height, 10); - File.Copy(Runtime.CaptureScreenshot.GetImagePath(Device), Path.Combine(GetOrCreateArtifactsPath(), "screenshot.png"), true); + CopyToArtifacts("screenshot.png", Runtime.CaptureScreenshot.GetImagePath(Device)); } [UnityTest] @@ -70,8 +76,7 @@ public IEnumerator CanGetVideo() yield return WaitForCondition("Waiting for Android's screenrecord to become active", () => Runtime.CaptureVideo.IsRemoteRecorderActive(Device)); - var start = DateTime.Now; - yield return WaitForCondition("Recording video", () => (DateTime.Now - start).TotalSeconds > 5.0f); + yield return WaitFor(5.0, "Recording video"); var result = Runtime.CaptureVideo.StopRecording(); Assert.IsTrue(result, "Failed to stop the recording"); Assert.AreEqual(AndroidLogcatCaptureVideo.Result.Success, recordingResult); @@ -85,7 +90,7 @@ public IEnumerator CanGetVideo() AssertFileExistanceOnDevice(AndroidLogcatCaptureVideo.VideoPathOnDevice, false); AssertFileExistanceOnHost(VideoPathOnHost, true); - File.Copy(Runtime.CaptureVideo.GetVideoPath(Device), Path.Combine(GetOrCreateArtifactsPath(), "video.mp4"), true); + CopyToArtifacts("video.mp4", Runtime.CaptureVideo.GetVideoPath(Device)); } [UnityTest] @@ -107,7 +112,7 @@ public IEnumerator CanGetVideoWithTimeLimit() AssertFileExistanceOnDevice(AndroidLogcatCaptureVideo.VideoPathOnDevice, false); AssertFileExistanceOnHost(VideoPathOnHost, true); - File.Copy(Runtime.CaptureVideo.GetVideoPath(Device), Path.Combine(GetOrCreateArtifactsPath(), "video.mp4"), true); + CopyToArtifacts("video.mp4", Runtime.CaptureVideo.GetVideoPath(Device)); } [UnityTest] @@ -130,6 +135,6 @@ public IEnumerator CaptureVideoHandlesErrors() AssertFileExistanceOnHost(VideoPathOnHost, false); Debug.Log(errors); - File.WriteAllText(Path.Combine(GetOrCreateArtifactsPath(), "errors.txt"), errors); + ReportArtifact("errors.txt", errors); } } diff --git a/com.unity.mobile.android-logcat/Tests/Editor/Integration/AndroidLogcatIntegrationTestBase.cs b/com.unity.mobile.android-logcat/Tests/Editor/Integration/AndroidLogcatIntegrationTestBase.cs index 91cf7ccd..fa66325e 100644 --- a/com.unity.mobile.android-logcat/Tests/Editor/Integration/AndroidLogcatIntegrationTestBase.cs +++ b/com.unity.mobile.android-logcat/Tests/Editor/Integration/AndroidLogcatIntegrationTestBase.cs @@ -10,7 +10,7 @@ internal class AndroidLogcatIntegrationTestBase { - protected const float kDefaulTimeOut = 10.0f; + protected const float kDefaultTimeout = 30.0f; private AndroidLogcatRuntime m_Runtime; private IAndroidLogcatDevice m_Device; private int m_Ticks; @@ -60,6 +60,109 @@ protected void InitRuntime() throw new Exception("No Android Device connected?"); } + /// + /// A device that has dozed off composes nothing, so a mirrored display hands over + /// no frames and `screenrecord` never starts - both of which surface as a test + /// timing out for reasons that have nothing to do with the code under test. Waking + /// it is part of putting the device in a known state, and it is cheap enough to do + /// per test rather than once per fixture. + /// + [SetUp] + protected void PrepareDevice() + { + if (m_Device == null) + return; + + m_Device.WakeUp(); + + // Waking only lights the screen up, and a device left on its lock screen + // ignores Home and Overview entirely - so a test that expects the screen to + // change sees nothing move. Devices here have no secure lock, where this + // dismisses the keyguard outright. + RunAdb("Failed to dismiss the lock screen", "shell", "wm", "dismiss-keyguard"); + + // Cleared so that what the teardown collects is this test's log and not the + // one before it. + RunAdb("Failed to clear the device log", "logcat", "-c"); + } + + /// + /// What the device had to say and what it looked like when the test ended, kept as + /// artifacts. A failing live stream test says little by itself - the server writes + /// what went wrong on the device side to logcat, and the screen shows what the + /// device was actually doing. Both are gone by the time anyone looks. + /// + /// Nothing in here throws or logs an error: a teardown that fails would bury + /// whatever the test was failing on. + /// + /// + [TearDown] + protected void CollectDeviceState() + { + if (m_Device == null) + return; + + var name = AndroidLogcatUtilities.SanitizeFileName(TestContext.CurrentContext.Test.Name); + + try + { + CollectLogcat(name); + CollectScreenshot(name); + } + catch (Exception ex) + { + Log($"Failed to collect the device state: {ex}"); + } + } + + private void CollectLogcat(string name) + { + var log = RunAdb("Failed to read the device log", "logcat", "-d"); + if (!string.IsNullOrEmpty(log)) + ReportArtifact($"{name}-logcat.txt", log); + } + + /// + /// Captured here rather than with , + /// which reports its failures with Debug.LogError - and an unexpected error + /// log fails the test that is being torn down. + /// + private void CollectScreenshot(string name) + { + const string onDevice = "/sdcard/unity-logcat-test-screen.png"; + // A path nothing has written, so the file being there afterwards means this + // capture worked rather than an earlier one having left something behind. + var path = ArtifactPath($"{name}-screen.png"); + + var capture = RunAdb("Failed to capture the screen", "shell", $"screencap -p {onDevice}"); + var pull = RunAdb("Failed to pull the screenshot", "pull", onDevice, path); + SafeDeleteOnDevice(m_Device, onDevice); + + if (File.Exists(path)) + return; + + ReportArtifact("failed_to_capture_screenshot.txt", + $"{name}{Environment.NewLine}{capture}{Environment.NewLine}{pull}"); + } + + /// + /// Runs adb against the device under test. Never throws: this is housekeeping + /// around a test, and a device that will not answer should not be reported as the + /// test failing. + /// + private string RunAdb(string failureMessage, params string[] args) + { + try + { + return m_Runtime.Tools.ADB.Run(new[] { $"-s {m_Device.Id}" }.Concat(args).ToArray(), failureMessage); + } + catch (Exception ex) + { + Log($"{failureMessage}: {ex.Message}"); + return null; + } + } + [OneTimeTearDown] protected void ShutdownRuntime() { @@ -88,7 +191,7 @@ protected IEnumerator Waiting() #endif } - protected IEnumerator WaitForCondition(string name, Func condition, float timeOutInSeconds = kDefaulTimeOut, Func additionalErrorMessage = null) + protected IEnumerator WaitForCondition(string name, Func condition, float timeOutInSeconds = kDefaultTimeout, Func additionalErrorMessage = null) { m_Runtime.OnUpdate(); @@ -114,6 +217,66 @@ protected static void Log(string message) Debug.LogFormat(LogType.Log, LogOption.NoStacktrace, null, "{0}", message); } + /// + /// Waits out a stretch of time, for what cannot be watched for directly: a screen + /// settling after an app opens, a device falling asleep, a recording running long + /// enough to be worth stopping. + /// + protected IEnumerator WaitFor(double seconds, string what) + { + var start = DateTime.Now; + return WaitForCondition(what, () => (DateTime.Now - start).TotalSeconds > seconds); + } + + /// + /// Saves something into this test's artifacts folder, which Yamato collects and a + /// local run leaves behind to look at. A frame count only says the screen changed; + /// the picture says what it changed to. + /// + protected static void ReportArtifact(string fileName, Texture2D texture) + { + ReportArtifact(fileName, texture.EncodeToPNG()); + } + + protected static void ReportArtifact(string fileName, byte[] contents) + { + File.WriteAllBytes(ArtifactPath(fileName), contents); + } + + protected static void ReportArtifact(string fileName, string contents) + { + File.WriteAllText(ArtifactPath(fileName), contents); + } + + /// + /// A path in the artifacts folder that nothing has written yet, numbered from the + /// first one: "frame_0.png", then "frame_1.png" and so on. A test that runs more + /// than once into the same folder - a rerun, or two editor versions - keeps every + /// attempt rather than the last one. + /// + protected static string ArtifactPath(string fileName) + { + var directory = GetOrCreateArtifactsPath(); + var name = Path.GetFileNameWithoutExtension(fileName); + var extension = Path.GetExtension(fileName); + + for (var i = 0; ; i++) + { + var path = Path.Combine(directory, $"{name}_{i}{extension}"); + if (!File.Exists(path)) + return path; + } + } + + /// + /// The same, for something already written to disk - a screenshot or a recording + /// the code under test produced. + /// + protected static void CopyToArtifacts(string fileName, string sourcePath) + { + File.Copy(sourcePath, ArtifactPath(fileName)); + } + protected static string GetOrCreateArtifactsPath() { var root = Workspace.GetAritfactsPath(); From 9e97269df3ea9efa0a7158cbd6cbf9150055010a Mon Sep 17 00:00:00 2001 From: Tomas Dirvanauskas Date: Wed, 23 Sep 2026 11:32:05 +0300 Subject: [PATCH 5/5] Update .yamato/upm-integration-ci.yml Co-authored-by: Manuel Gil --- .yamato/upm-integration-ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.yamato/upm-integration-ci.yml b/.yamato/upm-integration-ci.yml index 476eab84..8674781b 100644 --- a/.yamato/upm-integration-ci.yml +++ b/.yamato/upm-integration-ci.yml @@ -5,7 +5,7 @@ test_integration_{{ editor.version }}: name : Test Integration on {{ editor.version }} Pixel Pro Android 16 agent: - image: mobile/android-windows-unity:v2.8084395 + image: mobile/android-windows-unity:v2.8585035 type: Unity::mobile::pixel flavor: b1.large model: 8pro-Android16