Reland "crosier: Add ARC support"
This is a reland of commit b873bfb45ae306bf9d6de69524c6e1088f375af9
Original change's description:
> crosier: Add ARC support
>
> - Add ChromeOSIntegrationArcMixin to setup arc for tests;
> - Add AdbHelper to wrap adb commands;
> - A simple test to install an apk and create an ARC window;
>
> Bug: b:306225047
> Cq-Include-Trybots: luci.chrome.try:chromeos-betty-pi-arc-chrome
> Change-Id: I5590f0118788a1ec287d5f6fc6cb1faa5309fa7d
> Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/5062637
> Reviewed-by: Yury Khmel <khmel@chromium.org>
> Reviewed-by: James Cook <jamescook@chromium.org>
> Commit-Queue: Xiyuan Xia <xiyuan@chromium.org>
> Cr-Commit-Position: refs/heads/main@{#1245472}
Bug: b:306225047
Change-Id: I1a6c87db282b5eb8a8f8b8371719379a2b6dcfbd
Cq-Include-Trybots: luci.chrome.try:chromeos-betty-pi-arc-chrome
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/5190366
Reviewed-by: James Cook <jamescook@chromium.org>
Commit-Queue: Xiyuan Xia <xiyuan@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1258659}
diff --git a/chrome/browser/ash/arc/arc_integration_test.cc b/chrome/browser/ash/arc/arc_integration_test.cc
new file mode 100644
index 0000000..0b17fb7f
--- /dev/null
+++ b/chrome/browser/ash/arc/arc_integration_test.cc
@@ -0,0 +1,56 @@
+// Copyright 2024 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "ash/constants/app_types.h"
+#include "chrome/browser/ash/login/test/session_manager_state_waiter.h"
+#include "chrome/test/base/chromeos/crosier/ash_integration_test.h"
+#include "chrome/test/base/chromeos/crosier/chromeos_integration_arc_mixin.h"
+#include "chrome/test/base/chromeos/crosier/chromeos_integration_login_mixin.h"
+#include "ui/aura/client/aura_constants.h"
+#include "ui/aura/window.h"
+
+class ArcIntegrationTest : public AshIntegrationTest {
+ public:
+ ArcIntegrationTest() {
+ // This is needed to keep the browser test running after dismissing login
+ // screen. Otherwise, login screen destruction releases its ScopedKeepAlive
+ // which triggers shutdown from ShutdownIfNoBrowsers.
+ set_exit_when_last_browser_closes(false);
+
+ login_mixin().SetMode(ChromeOSIntegrationLoginMixin::Mode::kTestLogin);
+ arc_mixin().SetMode(ChromeOSIntegrationArcMixin::Mode::kEnabled);
+ }
+
+ ArcIntegrationTest(const ArcIntegrationTest&) = delete;
+ ArcIntegrationTest& operator=(const ArcIntegrationTest&) = delete;
+
+ ~ArcIntegrationTest() override = default;
+
+ // InteractiveAshTest:
+ void SetUpOnMainThread() override {
+ InteractiveAshTest::SetUpOnMainThread();
+
+ login_mixin().Login();
+ ash::test::WaitForPrimaryUserSessionStart();
+ arc_mixin().WaitForBootAndConnectAdb();
+ }
+};
+
+IN_PROC_BROWSER_TEST_F(ArcIntegrationTest, CreateWindow) {
+ // Test android apps are used by tast and deployed via "tast-local-apks-cros"
+ // package.
+ const base::FilePath kTestApk(
+ "/usr/local/libexec/tast/apks/local/cros/ArcKeyCharacterMapTest.apk");
+ ASSERT_TRUE(arc_mixin().InstallApk(kTestApk));
+
+ constexpr char kActivity[] = "org.chromium.arc.testapp.kcm.MainActivity";
+ constexpr char kPackage[] = "org.chromium.arc.testapp.kcm";
+
+ aura::Window* window =
+ arc_mixin().LaunchAndWaitForWindow(kPackage, kActivity);
+ ASSERT_NE(window, nullptr);
+
+ int window_app_type = window->GetProperty(aura::client::kAppType);
+ EXPECT_EQ(window_app_type, static_cast<int>(ash::AppType::ARC_APP));
+}
diff --git a/chrome/test/BUILD.gn b/chrome/test/BUILD.gn
index a5b8390..f47370b 100644
--- a/chrome/test/BUILD.gn
+++ b/chrome/test/BUILD.gn
@@ -247,6 +247,15 @@
]
deps += [ "//ui/ozone/platform/drm:ui_controls" ]
+
+ if (is_chrome_branded) {
+ sources += [
+ "base/chromeos/crosier/adb_helper.cc",
+ "base/chromeos/crosier/adb_helper.h",
+ "base/chromeos/crosier/chromeos_integration_arc_mixin.cc",
+ "base/chromeos/crosier/chromeos_integration_arc_mixin.h",
+ ]
+ }
}
deps += [
@@ -1108,7 +1117,10 @@
]
if (is_chrome_branded && is_chromeos_ash) {
- sources += [ "base/chromeos/crosier/test_accounts_test.cc" ]
+ sources += [
+ "../browser/ash/arc/arc_integration_test.cc",
+ "base/chromeos/crosier/test_accounts_test.cc",
+ ]
}
data = [
diff --git a/chrome/test/base/chromeos/crosier/adb_helper.cc b/chrome/test/base/chromeos/crosier/adb_helper.cc
new file mode 100644
index 0000000..96f0f00
--- /dev/null
+++ b/chrome/test/base/chromeos/crosier/adb_helper.cc
@@ -0,0 +1,204 @@
+// Copyright 2024 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "chrome/test/base/chromeos/crosier/adb_helper.h"
+
+#include "base/files/file_util.h"
+#include "base/files/scoped_temp_dir.h"
+#include "base/logging.h"
+#include "base/run_loop.h"
+#include "base/strings/strcat.h"
+#include "base/strings/string_split.h"
+#include "base/strings/string_util.h"
+#include "base/strings/stringprintf.h"
+#include "base/task/single_thread_task_runner.h"
+#include "base/threading/thread_restrictions.h"
+#include "base/time/time.h"
+#include "chrome/test/base/chromeos/crosier/helper/test_sudo_helper_client.h"
+
+namespace {
+
+// Android vendtor key to authorize adb connection.
+constexpr char kArcKey[] = R"(-----BEGIN PRIVATE KEY-----
+MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCnHNzujonYRLoI
+F2pyJX1SSrqmiT/3rTRCP1X0pj1V/sPGwgvIr+3QjZehLUGRQL0wneBNXd6EVrST
+drO4cOPwSxRJjCf+/PtS1nwkz+o/BGn5yhNppdSro7aPoQxEVM8qLtN5Ke9tx/zE
+ggxpF8D3XBC6Los9lAkyesZI6xqXESeofOYu3Hndzfbz8rAjC0X+p6Sx561Bt1dn
+T7k2cP0mwWfITjW8tAhzmKgL4tGcgmoLhMHl9JgScFBhW2Nd0QAR4ACyVvryJ/Xa
+2L6T2YpUjqWEDbiJNEApFb+m+smIbyGz0H/Kj9znoRs84z3/8rfyNQOyf7oqBpr2
+52XG4totAgMBAAECggEARisKYWicXKDO9CLQ4Uj4jBswsEilAVxKux5Y+zbqPjeR
+AN3tkMC+PHmXl2enRlRGnClOS24ExtCZVenboLBWJUmBJTiieqDC7o985QAgPYGe
+9fFxoUSuPbuqJjjbK73olq++v/tpu1Djw6dPirkcn0CbDXIJqTuFeRqwM2H0ckVl
+mVGUDgATckY0HWPyTBIzwBYIQTvAYzqFHmztcUahQrfi9XqxnySI91no8X6fR323
+R8WQ44atLWO5TPCu5JEHCwuTzsGEG7dEEtRQUxAsH11QC7S53tqf10u40aT3bXUh
+XV62ol9Zk7h3UrrlT1h1Ae+EtgIbhwv23poBEHpRQQKBgQDeUJwLfWQj0xHO+Jgl
+gbMCfiPYvjJ9yVcW4ET4UYnO6A9bf0aHOYdDcumScWHrA1bJEFZ/cqRvqUZsbSsB
++thxa7gjdpZzBeSzd7M+Ygrodi6KM/ojSQMsen/EbRFerZBvsXimtRb88NxTBIW1
+RXRPLRhHt+VYEF/wOVkNZ5c2eQKBgQDAbwNkkVFTD8yQJFxZZgr1F/g/nR2IC1Yb
+ylusFztLG998olxUKcWGGMoF7JjlM6pY3nt8qJFKek9bRJqyWSqS4/pKR7QTU4Nl
+a+gECuD3f28qGFgmay+B7Fyi9xmBAsGINyVxvGyKH95y3QICw1V0Q8uuNwJW2feo
+3+UD2/rkVQKBgFloh+ljC4QQ3gekGOR0rf6hpl8D1yCZecn8diB8AnVRBOQiYsX9
+j/XDYEaCDQRMOnnwdSkafSFfLbBrkzFfpe6viMXSap1l0F2RFWhQW9yzsvHoB4Br
+W7hmp73is2qlWQJimIhLKiyd3a4RkoidnzI8i5hEUBtDsqHVHohykfDZAoGABNhG
+q5eFBqRVMCPaN138VKNf2qon/i7a4iQ8Hp8PHRr8i3TDAlNy56dkHrYQO2ULmuUv
+Erpjvg5KRS/6/RaFneEjgg9AF2R44GrREpj7hP+uWs72GTGFpq2+v1OdTsQ0/yr0
+RGLMEMYwoY+y50Lnud+jFyXHZ0xhkdzhNTGqpWkCgYBigHVt/p8uKlTqhlSl6QXw
+1AyaV/TmfDjzWaNjmnE+DxQfXzPi9G+cXONdwD0AlRM1NnBRN+smh2B4RBeU515d
+x5RpTRFgzayt0I4Rt6QewKmAER3FbbPzaww2pkfH1zr4GJrKQuceWzxUf46K38xl
+yee+dcuGhs9IGBOEEF7lFA==
+-----END PRIVATE KEY-----)";
+
+// Install the vendor key in the given path and return the full path to the key
+// file.
+base::FilePath InstallVendorKey(const base::FilePath& path) {
+ base::FilePath key_file = path.Append("crosier.adb_key");
+ base::WriteFile(key_file, kArcKey, std::size(kArcKey));
+
+ constexpr char command_template[] = R"(
+ KEY_DIR="%s"
+ chown -R root.root "$KEY_DIR"
+ chmod 0755 "$KEY_DIR"
+ chmod 0600 "%s"
+ )";
+ auto result = TestSudoHelperClient().RunCommand(base::StringPrintf(
+ command_template, path.value().c_str(), key_file.value().c_str()));
+ CHECK_EQ(result.return_code, 0);
+
+ return key_file;
+}
+
+// Removes the vendor key files.
+void RemoveVendorKey(base::ScopedTempDir dir) {
+ if (!dir.IsValid()) {
+ return;
+ }
+
+ constexpr char command_template[] = R"(
+ rm -rf "%s"
+ )";
+ auto result = TestSudoHelperClient().RunCommand(
+ base::StringPrintf(command_template, dir.Take().value().c_str()));
+ CHECK_EQ(result.return_code, 0);
+}
+
+void KillServer() {
+ // `adb kill-server` is not reliable (crbug.com/855325).
+ // Not using `killall` since it can wait for orphan adb processes indefinitely
+ // (b/137797801).
+ //
+ // Use `kill -9` directly.
+ TestSudoHelperClient().RunCommand(R"(
+ while pgrep adb; do
+ kill -9 $(pgrep adb)
+ sleep 0.1
+ done
+ )");
+}
+
+void GiveItSomeTime(base::TimeDelta t) {
+ base::RunLoop run_loop;
+ base::SingleThreadTaskRunner::GetCurrentDefault()->PostDelayedTask(
+ FROM_HERE, run_loop.QuitClosure(), t);
+ run_loop.Run();
+}
+
+} // namespace
+
+AdbHelper::AdbHelper() = default;
+
+AdbHelper::~AdbHelper() {
+ if (!initialized_) {
+ return;
+ }
+
+ KillServer();
+
+ base::ScopedAllowBlockingForTesting allow_blocking;
+ RemoveVendorKey(std::move(vendor_key_dir_));
+}
+
+void AdbHelper::Intialize() {
+ initialized_ = true;
+
+ KillServer();
+
+ base::ScopedAllowBlockingForTesting allow_blocking;
+ CHECK(vendor_key_dir_.CreateUniqueTempDir())
+ << "Failed to create temp dir to hold vendor key.";
+ vendor_key_file_ = InstallVendorKey(vendor_key_dir_.GetPath());
+ CHECK(!vendor_key_file_.empty());
+
+ constexpr char command_template[] = R"(
+ ADB_VENDOR_KEYS=%s adb start-server
+ )";
+ auto result = TestSudoHelperClient().RunCommand(
+ base::StringPrintf(command_template, vendor_key_file_.value().c_str()));
+ CHECK_EQ(result.return_code, 0);
+
+ WaitForDevice();
+ CHECK(!serial_.empty());
+}
+
+bool AdbHelper::InstallApk(const base::FilePath& apk_path) {
+ {
+ base::ScopedAllowBlockingForTesting allow_blocking;
+ if (!base::PathExists(apk_path)) {
+ LOG(ERROR) << "Apk file does not exist: " << apk_path;
+ return false;
+ }
+ }
+
+ // Disable apk check.
+ auto result = TestSudoHelperClient().RunCommand(R"(
+ adb shell settings put global verifier_verify_adb_installs 0
+ )");
+ CHECK_EQ(result.return_code, 0);
+
+ // Install the apk.
+ return Command(
+ base::StringPrintf(R"(install "%s")", apk_path.value().c_str()));
+}
+
+bool AdbHelper::Command(const std::string_view command) {
+ auto result = TestSudoHelperClient().RunCommand(
+ base::StrCat({"ADB_VENDOR_KEYS=", vendor_key_file_.value(), " adb -s ",
+ serial_, " ", command}));
+ return result.return_code == 0;
+}
+
+void AdbHelper::WaitForDevice() {
+ constexpr char kEmulatorPrefix[] = "emulator-";
+ constexpr char kStateDevice[] = "device";
+
+ constexpr char command_template[] = R"(
+ ADB_VENDOR_KEYS=%s adb devices
+ )";
+ const std::string adb_devices =
+ base::StringPrintf(command_template, vendor_key_file_.value().c_str());
+
+ do {
+ GiveItSomeTime(base::Milliseconds(500));
+
+ // List all devices.
+ auto result = TestSudoHelperClient().RunCommand(adb_devices);
+ CHECK_EQ(result.return_code, 0);
+
+ // Search for the first emulator device with "device" state.
+ const auto lines = base::SplitString(
+ result.output, "\n", base::TRIM_WHITESPACE, base::SPLIT_WANT_NONEMPTY);
+ for (const auto& line : lines) {
+ const auto fields = base::SplitString(line, " \t", base::TRIM_WHITESPACE,
+ base::SPLIT_WANT_NONEMPTY);
+ if (fields.size() != 2) {
+ continue;
+ }
+
+ if (base::StartsWith(fields[0], kEmulatorPrefix) &&
+ fields[1] == kStateDevice) {
+ serial_ = fields[0];
+ break;
+ }
+ }
+ } while (serial_.empty());
+}
diff --git a/chrome/test/base/chromeos/crosier/adb_helper.h b/chrome/test/base/chromeos/crosier/adb_helper.h
new file mode 100644
index 0000000..00dc8161
--- /dev/null
+++ b/chrome/test/base/chromeos/crosier/adb_helper.h
@@ -0,0 +1,48 @@
+// Copyright 2024 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef CHROME_TEST_BASE_CHROMEOS_CROSIER_ADB_HELPER_H_
+#define CHROME_TEST_BASE_CHROMEOS_CROSIER_ADB_HELPER_H_
+
+#include <string_view>
+
+#include "base/files/scoped_temp_dir.h"
+#include "chrome/test/base/chromeos/crosier/helper/test_sudo_helper_client.h"
+
+namespace base {
+class FilePath;
+}
+
+// Helper to run adb command via the TestSudoHelper.
+class AdbHelper {
+ public:
+ AdbHelper();
+ AdbHelper(const AdbHelper&) = delete;
+ AdbHelper& operator=(const AdbHelper&) = delete;
+ ~AdbHelper();
+
+ // Starts adb server and connect to the first "emulator" device.
+ void Intialize();
+
+ // Installs the apk at the given path on the DUT.
+ bool InstallApk(const base::FilePath& apk_path);
+
+ // Runs the given command via adb.
+ bool Command(const std::string_view command);
+
+ private:
+ // Waits for the first emulator device to be ready and extract serial.
+ void WaitForDevice();
+
+ bool initialized_ = false;
+
+ // Device serial that is passed with "-s" to adb.
+ std::string serial_;
+
+ // A temp dir to store Android vendor keys.
+ base::ScopedTempDir vendor_key_dir_;
+ base::FilePath vendor_key_file_;
+};
+
+#endif // CHROME_TEST_BASE_CHROMEOS_CROSIER_ADB_HELPER_H_
diff --git a/chrome/test/base/chromeos/crosier/ash_integration_test.cc b/chrome/test/base/chromeos/crosier/ash_integration_test.cc
index d662c7e..3c609290 100644
--- a/chrome/test/base/chromeos/crosier/ash_integration_test.cc
+++ b/chrome/test/base/chromeos/crosier/ash_integration_test.cc
@@ -27,6 +27,9 @@
namespace {
+// A dir on DUT to host wayland socket and arc-bridge sockets.
+inline constexpr char kRunChrome[] = "/run/chrome";
+
// Simulates a failure for a Gaia URL request.
std::unique_ptr<net::test_server::HttpResponse> HandleGaiaURL(
const net::test_server::HttpRequest& request) {
@@ -44,14 +47,6 @@
CHECK(command_line);
OverrideGaiaUrlForLacros(command_line);
-
- // Enable the Wayland server.
- command_line->AppendSwitch(ash::switches::kAshEnableWaylandServer);
-
- // Set up XDG_RUNTIME_DIR for Wayland.
- std::unique_ptr<base::Environment> env(base::Environment::Create());
- CHECK(scoped_temp_dir_xdg_.CreateUniqueTempDir());
- env->SetVar("XDG_RUNTIME_DIR", scoped_temp_dir_xdg_.GetPath().AsUTF8Unsafe());
}
void AshIntegrationTest::SetUpLacrosBrowserManager() {
@@ -67,9 +62,9 @@
void AshIntegrationTest::WaitForAshFullyStarted() {
CHECK(base::CommandLine::ForCurrentProcess()->HasSwitch(
ash::switches::kAshEnableWaylandServer))
- << "Did you forget to call SetUpCommandLineForLacros?";
+ << "Wayland server should be enabled.";
base::ScopedAllowBlockingForTesting allow_blocking;
- base::FilePath xdg_path = scoped_temp_dir_xdg_.GetPath();
+ base::FilePath xdg_path(kRunChrome);
base::RepeatingTimer timer;
base::RunLoop run_loop1;
timer.Start(FROM_HERE, base::Milliseconds(100),
@@ -97,6 +92,17 @@
CHECK(extra_parts->did_post_browser_start());
}
+void AshIntegrationTest::SetUpCommandLine(base::CommandLine* command_line) {
+ InteractiveAshTest::SetUpCommandLine(command_line);
+
+ // Enable the Wayland server.
+ command_line->AppendSwitch(ash::switches::kAshEnableWaylandServer);
+
+ // Set up XDG_RUNTIME_DIR for Wayland.
+ std::unique_ptr<base::Environment> env(base::Environment::Create());
+ env->SetVar("XDG_RUNTIME_DIR", kRunChrome);
+}
+
void AshIntegrationTest::SetUpOnMainThread() {
InteractiveAshTest::SetUpOnMainThread();
diff --git a/chrome/test/base/chromeos/crosier/ash_integration_test.h b/chrome/test/base/chromeos/crosier/ash_integration_test.h
index 18dbe29b..828e017 100644
--- a/chrome/test/base/chromeos/crosier/ash_integration_test.h
+++ b/chrome/test/base/chromeos/crosier/ash_integration_test.h
@@ -12,6 +12,10 @@
#include "chrome/test/base/chromeos/crosier/chromeos_integration_test_mixin.h"
#include "chrome/test/base/chromeos/crosier/interactive_ash_test.h"
+#if BUILDFLAG(GOOGLE_CHROME_BRANDING)
+#include "chrome/test/base/chromeos/crosier/chromeos_integration_arc_mixin.h"
+#endif // BUILDFLAG(GOOGLE_CHROME_BRANDING)
+
namespace base {
class CommandLine;
}
@@ -51,10 +55,15 @@
void WaitForAshFullyStarted();
// MixinBasedInProcessBrowserTest:
+ void SetUpCommandLine(base::CommandLine* command_line) override;
void SetUpOnMainThread() override;
ChromeOSIntegrationLoginMixin& login_mixin() { return login_mixin_; }
+#if BUILDFLAG(GOOGLE_CHROME_BRANDING)
+ ChromeOSIntegrationArcMixin& arc_mixin() { return arc_mixin_; }
+#endif // BUILDFLAG(GOOGLE_CHROME_BRANDING)
+
private:
// Overrides the Gaia URL to point to a local test server that produces an
// error, which is expected behavior in test environments.
@@ -67,8 +76,10 @@
// Login support.
ChromeOSIntegrationLoginMixin login_mixin_{&mixin_host_};
- // Directory used by Wayland/Lacros in environment variable XDG_RUNTIME_DIR.
- base::ScopedTempDir scoped_temp_dir_xdg_;
+#if BUILDFLAG(GOOGLE_CHROME_BRANDING)
+ // ARC is only supported on the branded build.
+ ChromeOSIntegrationArcMixin arc_mixin_{&mixin_host_, login_mixin_};
+#endif // BUILDFLAG(GOOGLE_CHROME_BRANDING)
std::unique_ptr<net::test_server::EmbeddedTestServer> https_server_;
};
diff --git a/chrome/test/base/chromeos/crosier/ash_integration_test_test.cc b/chrome/test/base/chromeos/crosier/ash_integration_test_test.cc
index 36dafda7..8964566 100644
--- a/chrome/test/base/chromeos/crosier/ash_integration_test_test.cc
+++ b/chrome/test/base/chromeos/crosier/ash_integration_test_test.cc
@@ -11,7 +11,7 @@
class AshIntegrationTestTest : public AshIntegrationTest {
public:
void SetUpCommandLine(base::CommandLine* command_line) override {
- InteractiveAshTest::SetUpCommandLine(command_line);
+ AshIntegrationTest::SetUpCommandLine(command_line);
SetUpCommandLineForLacros(command_line);
}
};
diff --git a/chrome/test/base/chromeos/crosier/chromeos_integration_arc_mixin.cc b/chrome/test/base/chromeos/crosier/chromeos_integration_arc_mixin.cc
new file mode 100644
index 0000000..e91f2b8
--- /dev/null
+++ b/chrome/test/base/chromeos/crosier/chromeos_integration_arc_mixin.cc
@@ -0,0 +1,278 @@
+// Copyright 2024 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "chrome/test/base/chromeos/crosier/chromeos_integration_arc_mixin.h"
+
+#include "ash/components/arc/metrics/arc_metrics_constants.h"
+#include "ash/constants/ash_switches.h"
+#include "ash/public/cpp/window_properties.h"
+#include "ash/shell.h"
+#include "ash/test/active_window_waiter.h"
+#include "base/command_line.h"
+#include "base/files/file_path.h"
+#include "base/files/file_util.h"
+#include "base/memory/raw_ptr.h"
+#include "base/run_loop.h"
+#include "base/scoped_observation.h"
+#include "base/strings/string_util.h"
+#include "chrome/browser/ash/app_list/arc/arc_app_list_prefs.h"
+#include "chrome/browser/ash/app_list/arc/arc_app_utils.h"
+#include "chrome/browser/ash/arc/boot_phase_monitor/arc_boot_phase_monitor_bridge.h"
+#include "chrome/common/chrome_switches.h"
+#include "chrome/test/base/chromeos/crosier/chromeos_integration_login_mixin.h"
+#include "chrome/test/base/chromeos/crosier/helper/test_sudo_helper_client.h"
+#include "chromeos/ash/components/browser_context_helper/browser_context_helper.h"
+#include "components/user_manager/user_manager.h"
+#include "content/public/browser/browser_context.h"
+#include "ui/aura/client/aura_constants.h"
+#include "ui/aura/window.h"
+#include "ui/aura/window_observer.h"
+#include "ui/events/event_constants.h"
+
+namespace {
+
+// ArcBootWaiter waits for boot completed from `ArcBootPhaseMonitorBridge`.
+class ArcBootWaiter : public arc::ArcBootPhaseMonitorBridge::Observer {
+ public:
+ ArcBootWaiter() = default;
+ ~ArcBootWaiter() override = default;
+
+ void Wait() {
+ const user_manager::User* primary_user =
+ user_manager::UserManager::Get()->GetPrimaryUser();
+ CHECK(primary_user);
+
+ auto* browser_context =
+ ash::BrowserContextHelper::Get()->GetBrowserContextByUser(primary_user);
+ arc::ArcBootPhaseMonitorBridge* boot_bridge =
+ arc::ArcBootPhaseMonitorBridge::GetForBrowserContext(browser_context);
+ CHECK(boot_bridge);
+
+ scoped_observation_.Observe(boot_bridge);
+
+ wait_loop_.Run();
+ }
+
+ // arc::ArcBootPhaseMonitorBridge::Observer:
+ void OnBootCompleted() override { wait_loop_.Quit(); }
+
+ private:
+ base::RunLoop wait_loop_;
+ base::ScopedObservation<arc::ArcBootPhaseMonitorBridge,
+ arc::ArcBootPhaseMonitorBridge::Observer>
+ scoped_observation_{this};
+};
+
+// AppReadyWaiter waits until the given `app_id` is ready and launchable.
+class AppReadyWaiter : public ArcAppListPrefs::Observer {
+ public:
+ AppReadyWaiter(ArcAppListPrefs* arc_app_list_prefs,
+ const std::string_view app_id)
+ : prefs_(arc_app_list_prefs), app_id_(app_id) {
+ scoped_observation_.Observe(arc_app_list_prefs);
+ }
+
+ void Wait() {
+ if (IsAppReadyAndLaunchable()) {
+ return;
+ }
+
+ wait_loop_.Run();
+
+ CHECK(IsAppReadyAndLaunchable());
+ }
+
+ // ArcAppListPrefs::Observer:
+ void OnAppRegistered(const std::string& app_id,
+ const ArcAppListPrefs::AppInfo& app_info) override {
+ if (app_id == app_id_ && IsAppReadyAndLaunchable()) {
+ wait_loop_.Quit();
+ }
+ }
+ void OnAppStatesChanged(const std::string& app_id,
+ const ArcAppListPrefs::AppInfo& app_info) override {
+ if (app_id == app_id_ && IsAppReadyAndLaunchable()) {
+ wait_loop_.Quit();
+ }
+ }
+
+ private:
+ bool IsAppReadyAndLaunchable() const {
+ auto app_info = prefs_->GetApp(app_id_);
+ if (!app_info) {
+ return false;
+ }
+
+ return app_info->ready && app_info->launchable;
+ }
+
+ const raw_ptr<ArcAppListPrefs> prefs_;
+ const std::string app_id_;
+ base::ScopedObservation<ArcAppListPrefs, ArcAppListPrefs::Observer>
+ scoped_observation_{this};
+ base::RunLoop wait_loop_;
+};
+
+// WindowAppIdWaiter waits for `ash::kAppIDKey` property set on a given window.
+class WindowAppIdWaiter : public aura::WindowObserver {
+ public:
+ explicit WindowAppIdWaiter(aura::Window* window) {
+ observation_.Observe(window);
+ }
+
+ const std::string* Wait() {
+ found_app_id_ = observation_.GetSource()->GetProperty(ash::kAppIDKey);
+ if (!found_app_id_) {
+ run_loop_.Run();
+ }
+
+ return found_app_id_.get();
+ }
+
+ // aura::WindowObserver:
+ void OnWindowPropertyChanged(aura::Window* window,
+ const void* key,
+ intptr_t old) override {
+ if (key != ash::kAppIDKey) {
+ return;
+ }
+
+ found_app_id_ = window->GetProperty(ash::kAppIDKey);
+ run_loop_.Quit();
+ }
+ void OnWindowDestroyed(aura::Window* window) override {
+ observation_.Reset();
+ run_loop_.Quit();
+ }
+
+ private:
+ base::RunLoop run_loop_;
+ raw_ptr<std::string> found_app_id_ = nullptr;
+ base::ScopedObservation<aura::Window, aura::WindowObserver> observation_{
+ this};
+};
+
+// Returns whether ARCVM should be used based on tast_use_flags.txt.
+bool ShouldEnableArcVm() {
+ base::ScopedAllowBlockingForTesting allow_blocking;
+ std::string use_flags;
+ CHECK(base::ReadFileToString(
+ base::FilePath("/usr/local/etc/tast_use_flags.txt"), &use_flags));
+ return base::Contains(use_flags, "arcvm") &&
+ !base::Contains(use_flags, "arcpp");
+}
+
+// Gets the active user's browser context.
+content::BrowserContext* GetActiveUserBrowserContext() {
+ auto* user = user_manager::UserManager::Get()->GetActiveUser();
+ return ash::BrowserContextHelper::Get()->GetBrowserContextByUser(user);
+}
+
+void WaitForAppRegister(const std::string& app_id) {
+ AppReadyWaiter(ArcAppListPrefs::Get(GetActiveUserBrowserContext()), app_id)
+ .Wait();
+}
+
+} // namespace
+
+ChromeOSIntegrationArcMixin::ChromeOSIntegrationArcMixin(
+ InProcessBrowserTestMixinHost* host,
+ const ChromeOSIntegrationLoginMixin& login_mixin)
+ : InProcessBrowserTestMixin(host), login_mixin_(login_mixin) {}
+
+ChromeOSIntegrationArcMixin::~ChromeOSIntegrationArcMixin() = default;
+
+void ChromeOSIntegrationArcMixin::SetMode(Mode mode) {
+ CHECK(!setup_called_);
+ mode_ = mode;
+}
+
+void ChromeOSIntegrationArcMixin::SetUp() {
+ setup_called_ = true;
+}
+
+void ChromeOSIntegrationArcMixin::WaitForBootAndConnectAdb() {
+ ArcBootWaiter().Wait();
+ adb_helper_.Intialize();
+
+ const bool needs_play_store = (mode_ == Mode::kSupported);
+ if (!needs_play_store) {
+ // Disable play store. Otherwise it crashes.
+ CHECK(adb_helper_.Command(
+ "shell pm disable-user --user 0 com.android.vending"));
+ }
+}
+
+bool ChromeOSIntegrationArcMixin::InstallApk(const base::FilePath& apk_path) {
+ return adb_helper_.InstallApk(apk_path);
+}
+
+aura::Window* ChromeOSIntegrationArcMixin::LaunchAndWaitForWindow(
+ const std::string& package,
+ const std::string& activity) {
+ const std::string app_id = ArcAppListPrefs::GetAppId(package, activity);
+ WaitForAppRegister(app_id);
+
+ // Launch the given activity.
+ CHECK(arc::LaunchApp(GetActiveUserBrowserContext(), app_id, ui::EF_NONE,
+ arc::UserInteractionType::NOT_USER_INITIATED));
+
+ // Wait for the activity window to be activated.
+ aura::Window* const window =
+ ash::ActiveWindowWaiter(ash::Shell::GetPrimaryRootWindow()).Wait();
+ const std::string* window_app_id = WindowAppIdWaiter(window).Wait();
+ CHECK(window_app_id);
+ CHECK(!window_app_id->empty());
+ CHECK_EQ(*window_app_id, app_id);
+ return window;
+}
+
+void ChromeOSIntegrationArcMixin::SetUpCommandLine(
+ base::CommandLine* command_line) {
+ if (mode_ == Mode::kNone) {
+ command_line->AppendSwitchASCII(ash::switches::kArcAvailability, "none");
+ return;
+ }
+
+ CHECK(login_mixin_.mode() != ChromeOSIntegrationLoginMixin::Mode::kStubLogin)
+ << "ARC does not work with stub login.";
+
+ // User data dir needs to be "/home/chronos". Otherwise,
+ // `IsArcCompatibleFileSystemUsedForUser()` returns false and ARC could not be
+ // enabled.
+ command_line->AppendSwitchASCII(::switches::kUserDataDir, "/home/chronos");
+
+ if (ShouldEnableArcVm()) {
+ command_line->AppendSwitch(ash::switches::kEnableArcVm);
+ }
+
+ // Common setup for both "Enabled" and "Supported" modes. The switches here
+ // are from "tast-tests/cros/local/chrome/internal/setup/restart.go".
+ // Reference: http://shortn/_P4IIm7c7aY
+ command_line->AppendSwitch(ash::switches::kArcDisableAppSync);
+ command_line->AppendSwitch(ash::switches::kArcDisablePlayAutoInstall);
+ command_line->AppendSwitch(ash::switches::kArcDisableLocaleSync);
+ command_line->AppendSwitchASCII(ash::switches::kArcPlayStoreAutoUpdate,
+ "off");
+ command_line->AppendSwitch(ash::switches::kArcDisableMediaStoreMaintenance);
+ command_line->AppendSwitch(ash::switches::kDisableArcCpuRestriction);
+
+ if (mode_ == Mode::kEnabled) {
+ command_line->AppendSwitch(ash::switches::kDisableArcOptInVerification);
+ command_line->AppendSwitchASCII(ash::switches::kArcStartMode,
+ "always-start-with-no-play-store");
+
+ // The "installed" mode needs `kEnableArcFeature` to work.
+ // See "IsArcAvailable()" in ash/components/arc/arc_util.cc.
+ command_line->AppendSwitchASCII(ash::switches::kArcAvailability,
+ "installed");
+ scoped_feature_list_.emplace();
+ scoped_feature_list_->InitFromCommandLine("EnableARC", base::EmptyString());
+ }
+
+ if (mode_ == Mode::kSupported) {
+ command_line->AppendSwitchASCII(ash::switches::kArcAvailability,
+ "officially-supported");
+ }
+}
diff --git a/chrome/test/base/chromeos/crosier/chromeos_integration_arc_mixin.h b/chrome/test/base/chromeos/crosier/chromeos_integration_arc_mixin.h
new file mode 100644
index 0000000..eb1e1b3
--- /dev/null
+++ b/chrome/test/base/chromeos/crosier/chromeos_integration_arc_mixin.h
@@ -0,0 +1,76 @@
+// Copyright 2024 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef CHROME_TEST_BASE_CHROMEOS_CROSIER_CHROMEOS_INTEGRATION_ARC_MIXIN_H_
+#define CHROME_TEST_BASE_CHROMEOS_CROSIER_CHROMEOS_INTEGRATION_ARC_MIXIN_H_
+
+#include <string>
+
+#include "base/test/scoped_feature_list.h"
+#include "chrome/test/base/chromeos/crosier/adb_helper.h"
+#include "chrome/test/base/mixin_based_in_process_browser_test.h"
+#include "third_party/abseil-cpp/absl/types/optional.h"
+
+namespace aura {
+class Window;
+}
+
+namespace base {
+class FilePath;
+}
+
+class ChromeOSIntegrationLoginMixin;
+
+// ChromeOSIntegrationArcMixin provides ARC support to ChromeOS integration
+// test.
+class ChromeOSIntegrationArcMixin : public InProcessBrowserTestMixin {
+ public:
+ enum class Mode {
+ // ARC is not enabled.
+ kNone,
+
+ // ARC is enabled with no play store.
+ kEnabled,
+
+ // Full ARC support. ARC is enabled and could perform play store optin. This
+ // requires Gaia identity.
+ // TODO(b/306225047): Implement this.
+ kSupported,
+ };
+
+ ChromeOSIntegrationArcMixin(InProcessBrowserTestMixinHost* host,
+ const ChromeOSIntegrationLoginMixin& login_mixin);
+ ChromeOSIntegrationArcMixin(const ChromeOSIntegrationArcMixin&) = delete;
+ ChromeOSIntegrationArcMixin& operator=(const ChromeOSIntegrationArcMixin&) =
+ delete;
+ ~ChromeOSIntegrationArcMixin() override;
+
+ // Sets the ARC mode. Must be called before SetUp.
+ void SetMode(Mode mode);
+
+ // Waits for ARC boot_completed and connect adb.
+ void WaitForBootAndConnectAdb();
+
+ // Installs the apk at the given path on the DUT.
+ bool InstallApk(const base::FilePath& apk_path);
+
+ // Launches the give activity and wait for its window to be activated.
+ aura::Window* LaunchAndWaitForWindow(const std::string& package,
+ const std::string& activity);
+
+ // InProcessBrowserTestMixin:
+ void SetUp() override;
+ void SetUpCommandLine(base::CommandLine* command_line) override;
+
+ private:
+ const ChromeOSIntegrationLoginMixin& login_mixin_;
+
+ bool setup_called_ = false;
+ Mode mode_ = Mode::kNone;
+ absl::optional<base::test::ScopedFeatureList> scoped_feature_list_;
+
+ AdbHelper adb_helper_;
+};
+
+#endif // CHROME_TEST_BASE_CHROMEOS_CROSIER_CHROMEOS_INTEGRATION_ARC_MIXIN_H_
diff --git a/chrome/test/base/chromeos/crosier/helper/test_sudo_helper.py b/chrome/test/base/chromeos/crosier/helper/test_sudo_helper.py
index eb26b6e..8421838 100755
--- a/chrome/test/base/chromeos/crosier/helper/test_sudo_helper.py
+++ b/chrome/test/base/chromeos/crosier/helper/test_sudo_helper.py
@@ -26,6 +26,7 @@
import logging
import os
from pathlib import Path
+import resource
import socket
import subprocess
import sys
@@ -136,19 +137,45 @@
def run(self):
try:
+ container_root_dir = "/run/containers"
+ os.makedirs(container_root_dir, mode=0o755, exist_ok=True)
+
+ sm_env = {}
+ sm_env["CONTAINER_ROOT_DIR"] = container_root_dir
+
+ # Set limits etc before execute session_manager. This should match
+ # the limits in `ui.conf`.
+ def preexec():
+ resource.setrlimit(resource.RLIMIT_NICE, (40, 40))
+ resource.setrlimit(resource.RLIMIT_RTPRIO, (10, 10))
+
args = [
+ "/usr/bin/runcon",
+ "-t",
+ "cros_session_manager",
"/sbin/session_manager",
("--chrome-command=%s" % str(THIS_FILE.parent / "fake_chrome")),
]
logging.info("Starting session manager: args=%s", str(args))
self._session_manager_proc = subprocess.Popen(
- args, stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT)
+ args,
+ stdout=subprocess.DEVNULL,
+ stderr=subprocess.STDOUT,
+ cwd="/",
+ env=sm_env,
+ preexec_fn=preexec)
_wait_for_fake_chrome()
_send_code_and_string(self._sock, 0, "started")
except Exception as e:
logging.error("Exception: %s", e)
- _send_code_and_string(self._sock, 0xFF, str(e))
+
+ # Ignore BrokenPipeError since the client might be gone already.
+ try:
+ _send_code_and_string(self._sock, 0xFF, str(e))
+ except BrokenPipeError:
+ pass
+
self._session_manager_proc = None
return
@@ -166,7 +193,12 @@
self._session_manager_proc = None
- _send_code_and_string(self._sock, 0, "stopped")
+ # Ignore BrokenPipeError since the client might be gone already.
+ try:
+ _send_code_and_string(self._sock, 0, "stopped")
+ except BrokenPipeError:
+ pass
+
self._sock.close()
def stop(self):