blob: 2f241f14fd442e7d3addacc2197cc41745be51fc [file] [log] [blame]
// Copyright 2019 The Chromium OS Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "dev-install/dev_install.h"
#include <unistd.h>
#include <istream>
#include <sstream>
#include <string>
#include <base/files/file_path.h>
#include <base/files/file_util.h>
#include <base/files/scoped_temp_dir.h>
#include <gmock/gmock.h>
#include <gtest/gtest.h>
using ::testing::_;
using ::testing::Return;
namespace dev_install {
namespace {
class DevInstallMock : public DevInstall {
public:
MOCK_METHOD(bool, IsDevMode, (), (const, override));
MOCK_METHOD(bool,
PromptUser,
(std::istream&, const std::string&),
(override));
MOCK_METHOD(bool, ClearStateDir, (const base::FilePath&), (override));
MOCK_METHOD(bool,
InitializeStateDir,
(const base::FilePath& dir),
(override));
MOCK_METHOD(bool,
DownloadAndInstallBootstrapPackages,
(const base::FilePath&),
(override));
MOCK_METHOD(bool, ConfigurePortage, (), (override));
MOCK_METHOD(bool, InstallExtraPackages, (), (override));
};
class DevInstallTest : public ::testing::Test {
public:
void SetUp() override {
// Set the default to dev mode enabled. Most tests want that.
ON_CALL(dev_install_, IsDevMode()).WillByDefault(Return(true));
// Ignore stateful setup for most tests.
ON_CALL(dev_install_, InitializeStateDir(_)).WillByDefault(Return(true));
// Ignore bootstrap for most tests.
ON_CALL(dev_install_, DownloadAndInstallBootstrapPackages(_))
.WillByDefault(Return(true));
// Ignore portage setup for most tests.
ON_CALL(dev_install_, ConfigurePortage()).WillByDefault(Return(true));
// Ignore extra setup for most tests.
ON_CALL(dev_install_, InstallExtraPackages()).WillByDefault(Return(true));
// Most tests should run with a path that doesn't exist.
dev_install_.SetStateDirForTest(base::FilePath("/.path-does-not-exist"));
}
protected:
DevInstallMock dev_install_;
};
} // namespace
// Check default run through.
TEST_F(DevInstallTest, Run) {
EXPECT_EQ(0, dev_install_.Run());
}
// Systems not in dev mode should abort.
TEST_F(DevInstallTest, NonDevMode) {
EXPECT_CALL(dev_install_, IsDevMode()).WillOnce(Return(false));
EXPECT_CALL(dev_install_, ClearStateDir(_)).Times(0);
EXPECT_EQ(2, dev_install_.Run());
}
// Check system has been initialized.
TEST_F(DevInstallTest, AlreadyInitialized) {
dev_install_.SetStateDirForTest(base::FilePath("/"));
ASSERT_EQ(4, dev_install_.Run());
}
// Check --reinstall passed.
TEST_F(DevInstallTest, RunReinstallWorked) {
dev_install_.SetReinstallForTest(true);
EXPECT_CALL(dev_install_, ClearStateDir(_)).WillOnce(Return(true));
ASSERT_EQ(0, dev_install_.Run());
}
// Check when --reinstall is requested but clearing fails.
TEST_F(DevInstallTest, RunReinstallFails) {
dev_install_.SetReinstallForTest(true);
EXPECT_CALL(dev_install_, ClearStateDir(_)).WillOnce(Return(false));
ASSERT_EQ(1, dev_install_.Run());
}
// Check --uninstall passed.
TEST_F(DevInstallTest, RunUninstall) {
dev_install_.SetUninstallForTest(true);
EXPECT_CALL(dev_install_, ClearStateDir(_)).WillOnce(Return(true));
ASSERT_EQ(0, dev_install_.Run());
}
// Stateful setup failures.
TEST_F(DevInstallTest, StatefulSetupFailure) {
EXPECT_CALL(dev_install_, InitializeStateDir(_)).WillOnce(Return(false));
ASSERT_EQ(5, dev_install_.Run());
}
// We only bootstrap before exiting.
TEST_F(DevInstallTest, BootstrapOnly) {
dev_install_.SetBootstrapForTest(true);
ASSERT_EQ(0, dev_install_.Run());
}
// Bootstrap failures.
TEST_F(DevInstallTest, BootstrapFailure) {
EXPECT_CALL(dev_install_, DownloadAndInstallBootstrapPackages(_))
.WillOnce(Return(false));
ASSERT_EQ(7, dev_install_.Run());
}
// Portage setup failures.
TEST_F(DevInstallTest, PortageFailure) {
EXPECT_CALL(dev_install_, ConfigurePortage()).WillOnce(Return(false));
ASSERT_EQ(8, dev_install_.Run());
}
// Extra package failures.
TEST_F(DevInstallTest, ExtraPackagesFailure) {
EXPECT_CALL(dev_install_, InstallExtraPackages()).WillOnce(Return(false));
ASSERT_EQ(9, dev_install_.Run());
}
namespace {
class PromptUserTest : public ::testing::Test {
protected:
DevInstall dev_install_;
};
} // namespace
// The --yes flag should pass w/out prompting the user.
TEST_F(PromptUserTest, Forced) {
dev_install_.SetYesForTest(true);
std::stringstream stream("");
EXPECT_TRUE(dev_install_.PromptUser(stream, ""));
}
// EOF input should fail.
TEST_F(PromptUserTest, Eof) {
std::stringstream stream("");
EXPECT_FALSE(dev_install_.PromptUser(stream, ""));
}
// Default input (hitting enter) should fail.
TEST_F(PromptUserTest, Default) {
std::stringstream stream("\n");
EXPECT_FALSE(dev_install_.PromptUser(stream, ""));
}
// Entering "n" should fail.
TEST_F(PromptUserTest, No) {
std::stringstream stream("n\n");
EXPECT_FALSE(dev_install_.PromptUser(stream, ""));
}
// Entering "y" should pass.
TEST_F(PromptUserTest, Yes) {
std::stringstream stream("y\n");
EXPECT_TRUE(dev_install_.PromptUser(stream, ""));
}
namespace {
class DeletePathTest : public ::testing::Test {
public:
void SetUp() override {
ASSERT_TRUE(scoped_temp_dir_.CreateUniqueTempDir());
test_dir_ = scoped_temp_dir_.GetPath();
dev_install_.SetStateDirForTest(test_dir_);
}
protected:
DevInstall dev_install_;
base::FilePath test_dir_;
base::ScopedTempDir scoped_temp_dir_;
};
} // namespace
// Check missing dir.
TEST_F(DeletePathTest, Missing) {
struct stat st = {};
EXPECT_TRUE(dev_install_.DeletePath(st, test_dir_.Append("foo")));
}
// Check deleting dir contents leaves the dir alone.
TEST_F(DeletePathTest, Empty) {
struct stat st = {};
EXPECT_TRUE(dev_install_.DeletePath(st, test_dir_));
EXPECT_TRUE(base::PathExists(test_dir_));
}
// Check mounted deletion.
TEST_F(DeletePathTest, Mounted) {
struct stat st = {};
const base::FilePath subdir = test_dir_.Append("subdir");
EXPECT_TRUE(base::CreateDirectory(subdir));
EXPECT_FALSE(dev_install_.DeletePath(st, test_dir_));
EXPECT_TRUE(base::PathExists(subdir));
}
// Check recursive deletion.
TEST_F(DeletePathTest, Works) {
struct stat st;
EXPECT_EQ(0, stat(test_dir_.value().c_str(), &st));
EXPECT_EQ(3, base::WriteFile(test_dir_.Append("file"), "123", 3));
EXPECT_EQ(0, symlink("x", test_dir_.Append("broken-sym").value().c_str()));
EXPECT_EQ(0, symlink("file", test_dir_.Append("file-sym").value().c_str()));
EXPECT_EQ(0, symlink(".", test_dir_.Append("dir-sym").value().c_str()));
EXPECT_EQ(0, symlink("subdir", test_dir_.Append("dir-sym2").value().c_str()));
const base::FilePath subdir = test_dir_.Append("subdir");
EXPECT_TRUE(base::CreateDirectory(subdir));
EXPECT_EQ(3, base::WriteFile(subdir.Append("file"), "123", 3));
const base::FilePath subsubdir = test_dir_.Append("subdir");
EXPECT_TRUE(base::CreateDirectory(subsubdir));
EXPECT_EQ(3, base::WriteFile(subsubdir.Append("file"), "123", 3));
EXPECT_TRUE(dev_install_.DeletePath(st, test_dir_));
EXPECT_TRUE(base::PathExists(test_dir_));
EXPECT_EQ(0, rmdir(test_dir_.value().c_str()));
}
namespace {
class CreateMissingDirectoryTest : public ::testing::Test {
public:
void SetUp() {
ASSERT_TRUE(scoped_temp_dir_.CreateUniqueTempDir());
test_dir_ = scoped_temp_dir_.GetPath();
}
protected:
DevInstall dev_install_;
base::FilePath test_dir_;
base::ScopedTempDir scoped_temp_dir_;
};
} // namespace
// Create dirs that don't yet exist.
TEST_F(CreateMissingDirectoryTest, Works) {
const base::FilePath dir = test_dir_.Append("test");
ASSERT_TRUE(dev_install_.CreateMissingDirectory(dir));
int mode;
ASSERT_TRUE(base::GetPosixFilePermissions(dir, &mode));
ASSERT_EQ(0755, mode);
ASSERT_TRUE(dev_install_.CreateMissingDirectory(dir));
}
// If a dir already exists, should do nothing.
TEST_F(CreateMissingDirectoryTest, Existing) {
ASSERT_TRUE(dev_install_.CreateMissingDirectory(test_dir_));
ASSERT_TRUE(dev_install_.CreateMissingDirectory(test_dir_));
}
namespace {
// We could mock out DeletePath, but it's easy to lightly validate it.
class ClearStateDirMock : public DevInstall {
public:
MOCK_METHOD(bool,
PromptUser,
(std::istream&, const std::string&),
(override));
};
class ClearStateDirTest : public ::testing::Test {
public:
void SetUp() {
ASSERT_TRUE(scoped_temp_dir_.CreateUniqueTempDir());
test_dir_ = scoped_temp_dir_.GetPath();
}
protected:
ClearStateDirMock dev_install_;
base::FilePath test_dir_;
base::ScopedTempDir scoped_temp_dir_;
};
} // namespace
// Check user rejecting things.
TEST_F(ClearStateDirTest, Cancel) {
EXPECT_CALL(dev_install_, PromptUser(_, _)).WillOnce(Return(false));
const base::FilePath subdir = test_dir_.Append("subdir");
ASSERT_TRUE(base::CreateDirectory(subdir));
ASSERT_FALSE(dev_install_.ClearStateDir(test_dir_));
ASSERT_TRUE(base::PathExists(subdir));
}
// Check missing dir is handled.
TEST_F(ClearStateDirTest, Missing) {
EXPECT_CALL(dev_install_, PromptUser(_, _)).WillOnce(Return(true));
ASSERT_TRUE(dev_install_.ClearStateDir(test_dir_.Append("subdir")));
ASSERT_TRUE(base::PathExists(test_dir_));
}
// Check empty dir is handled.
TEST_F(ClearStateDirTest, Empty) {
EXPECT_CALL(dev_install_, PromptUser(_, _)).WillOnce(Return(true));
ASSERT_TRUE(dev_install_.ClearStateDir(test_dir_));
ASSERT_TRUE(base::PathExists(test_dir_));
}
// Check dir with contents is cleared.
TEST_F(ClearStateDirTest, Works) {
EXPECT_CALL(dev_install_, PromptUser(_, _)).WillOnce(Return(true));
const base::FilePath subdir = test_dir_.Append("subdir");
ASSERT_TRUE(base::CreateDirectory(subdir));
ASSERT_TRUE(dev_install_.ClearStateDir(test_dir_));
ASSERT_FALSE(base::PathExists(subdir));
}
namespace {
class InitializeStateDirTest : public ::testing::Test {
public:
void SetUp() {
ASSERT_TRUE(scoped_temp_dir_.CreateUniqueTempDir());
test_dir_ = scoped_temp_dir_.GetPath();
}
protected:
DevInstall dev_install_;
base::FilePath test_dir_;
base::ScopedTempDir scoped_temp_dir_;
};
} // namespace
// Check stateful is set up correctly.
TEST_F(InitializeStateDirTest, Works) {
// Make sure we fully set things up.
ASSERT_TRUE(dev_install_.InitializeStateDir(test_dir_));
ASSERT_TRUE(base::IsLink(test_dir_.Append("usr")));
ASSERT_TRUE(base::IsLink(test_dir_.Append("local")));
ASSERT_TRUE(base::IsLink(test_dir_.Append("local")));
const base::FilePath etc = test_dir_.Append("etc");
ASSERT_TRUE(base::PathExists(etc));
ASSERT_TRUE(base::IsLink(etc.Append("passwd")));
ASSERT_TRUE(base::IsLink(etc.Append("group")));
const base::FilePath tmp = test_dir_.Append("tmp");
ASSERT_TRUE(base::PathExists(tmp));
// Can't use base::GetPosixFilePermissions as that blocks +t mode.
struct stat st;
ASSERT_EQ(0, stat(tmp.value().c_str(), &st));
ASSERT_EQ(01777, st.st_mode & 07777);
// Calling a second time should be fine.
ASSERT_TRUE(dev_install_.InitializeStateDir(test_dir_));
}
// Check we handle errors gracefully.
TEST_F(InitializeStateDirTest, Fails) {
// Create a broken /etc symlink.
ASSERT_TRUE(
base::CreateSymbolicLink(base::FilePath("foo"), test_dir_.Append("etc")));
ASSERT_FALSE(dev_install_.InitializeStateDir(test_dir_));
}
namespace {
class LoadRuntimeSettingsTest : public ::testing::Test {
public:
void SetUp() {
ASSERT_TRUE(scoped_temp_dir_.CreateUniqueTempDir());
test_dir_ = scoped_temp_dir_.GetPath();
}
protected:
DevInstall dev_install_;
base::FilePath test_dir_;
base::ScopedTempDir scoped_temp_dir_;
};
} // namespace
// Check loading state works.
TEST_F(LoadRuntimeSettingsTest, Works) {
const base::FilePath lsb_release = test_dir_.Append("lsb-release");
std::string data{
"CHROMEOS_DEVSERVER=https://foo\n"
"CHROMEOS_RELEASE_BOARD=betty\n"
"CHROMEOS_RELEASE_CHROME_MILESTONE=79\n"
"CHROMEOS_RELEASE_VERSION=100.10.1\n"};
ASSERT_EQ(base::WriteFile(lsb_release, data.c_str(), data.size()),
data.size());
ASSERT_TRUE(dev_install_.LoadRuntimeSettings(lsb_release));
ASSERT_EQ(dev_install_.GetDevserverUrlForTest(), "https://foo");
ASSERT_EQ(dev_install_.GetBoardForTest(), "betty");
ASSERT_EQ(dev_install_.GetBinhostVersionForTest(), "100.10.1");
}
// Check loading empty state works.
TEST_F(LoadRuntimeSettingsTest, Empty) {
const base::FilePath lsb_release = test_dir_.Append("lsb-release");
std::string data{""};
ASSERT_EQ(base::WriteFile(lsb_release, data.c_str(), data.size()),
data.size());
ASSERT_TRUE(dev_install_.LoadRuntimeSettings(lsb_release));
}
// Check loading state doesn't abort with missing file.
TEST_F(LoadRuntimeSettingsTest, Missing) {
ASSERT_TRUE(dev_install_.LoadRuntimeSettings(test_dir_.Append("asdf")));
}
namespace {
class BootstrapPackagesMock : public DevInstall {
public:
MOCK_METHOD(bool,
DownloadAndInstallBootstrapPackage,
(const std::string&),
(override));
};
class BootstrapPackagesTest : public ::testing::Test {
public:
void SetUp() {
// Have the install step pass by default.
ON_CALL(dev_install_, DownloadAndInstallBootstrapPackage(_))
.WillByDefault(Return(true));
ASSERT_TRUE(scoped_temp_dir_.CreateUniqueTempDir());
test_dir_ = scoped_temp_dir_.GetPath();
dev_install_.SetStateDirForTest(test_dir_);
}
protected:
BootstrapPackagesMock dev_install_;
base::FilePath test_dir_;
base::ScopedTempDir scoped_temp_dir_;
};
} // namespace
// Check bootstrap works in general.
TEST_F(BootstrapPackagesTest, Works) {
const base::FilePath listing = test_dir_.Append("bootstrap.packages");
std::string data{
"foo/bar-123\n"
"cat/pkg-1.0\n"};
ASSERT_EQ(base::WriteFile(listing, data.c_str(), data.size()), data.size());
ON_CALL(dev_install_, DownloadAndInstallBootstrapPackage(_))
.WillByDefault(Return(false));
EXPECT_CALL(dev_install_, DownloadAndInstallBootstrapPackage("foo/bar-123"))
.WillOnce(Return(true));
EXPECT_CALL(dev_install_, DownloadAndInstallBootstrapPackage("cat/pkg-1.0"))
.WillOnce(Return(true));
const base::FilePath bindir = test_dir_.Append("usr/bin");
ASSERT_TRUE(base::CreateDirectory(bindir));
ASSERT_TRUE(dev_install_.DownloadAndInstallBootstrapPackages(listing));
// We assert the symlinks exist. We assume the targets are valid for now.
base::FilePath target;
ASSERT_TRUE(base::ReadSymbolicLink(bindir.Append("python"), &target));
ASSERT_TRUE(base::ReadSymbolicLink(bindir.Append("python2"), &target));
ASSERT_TRUE(base::ReadSymbolicLink(bindir.Append("python3"), &target));
}
// Check missing bootstrap list fails.
TEST_F(BootstrapPackagesTest, Missing) {
const base::FilePath listing = test_dir_.Append("bootstrap.packages");
ASSERT_FALSE(dev_install_.DownloadAndInstallBootstrapPackages(listing));
}
// Check empty bootstrap list fails.
TEST_F(BootstrapPackagesTest, Empty) {
const base::FilePath listing = test_dir_.Append("bootstrap.packages");
ASSERT_EQ(base::WriteFile(listing, "", 0), 0);
ASSERT_FALSE(dev_install_.DownloadAndInstallBootstrapPackages(listing));
}
// Check mid-bootstrap failure behavior.
TEST_F(BootstrapPackagesTest, PackageFailed) {
const base::FilePath listing = test_dir_.Append("bootstrap.packages");
std::string data{"cat/pkg-3"};
ASSERT_EQ(base::WriteFile(listing, data.c_str(), data.size()), data.size());
EXPECT_CALL(dev_install_, DownloadAndInstallBootstrapPackage("cat/pkg-3"))
.WillOnce(Return(false));
const base::FilePath bindir = test_dir_.Append("usr/bin");
ASSERT_TRUE(base::CreateDirectory(bindir));
ASSERT_FALSE(dev_install_.DownloadAndInstallBootstrapPackages(listing));
}
namespace {
class ConfigurePortageTest : public ::testing::Test {
public:
void SetUp() {
ASSERT_TRUE(scoped_temp_dir_.CreateUniqueTempDir());
test_dir_ = scoped_temp_dir_.GetPath();
dev_install_.SetStateDirForTest(test_dir_);
}
protected:
BootstrapPackagesMock dev_install_;
base::FilePath test_dir_;
base::ScopedTempDir scoped_temp_dir_;
};
} // namespace
// Check setup works in general.
TEST_F(ConfigurePortageTest, Works) {
std::string data;
// The exact path doesn't matter here, but we create a deep one to mimic
// common scenarios on real devices.
const auto portage_internal_dir = test_dir_.Append(
"lib64/python3.6/site-packages/portage/package/ebuild/_config");
const auto portage_internal_file =
portage_internal_dir.Append("special_env_vars.py");
ASSERT_TRUE(base::CreateDirectory(portage_internal_dir));
data = "foo\nenviron_whitelist = []\n\nbar\n";
ASSERT_TRUE(base::WriteFile(portage_internal_file, data.data(), data.size()));
// Create a symlink to mimic real devices to detect recursive search issues.
ASSERT_EQ(0, symlink(".", test_dir_.Append("usr").value().c_str()));
ASSERT_EQ(0, symlink(".", test_dir_.Append("local").value().c_str()));
// Check basic profile setup worked.
EXPECT_TRUE(dev_install_.ConfigurePortage());
const base::FilePath portage_dir = test_dir_.Append("etc/portage");
EXPECT_TRUE(base::PathExists(portage_dir));
// Verify make.conf has valid ROOT= setting.
EXPECT_TRUE(base::ReadFileToString(portage_dir.Append("make.conf"), &data));
EXPECT_NE(data.find("ROOT=\"" + test_dir_.value() + "\"\n"),
std::string::npos);
// Check internal portage hacking.
EXPECT_TRUE(base::ReadFileToString(portage_internal_file, &data));
EXPECT_EQ(data, "foo\nenviron_whitelist = ['LD_LIBRARY_PATH']\n\nbar\n");
}
} // namespace dev_install