blob: 5140c1b729cf27653138b5b9cb9748de19afea04 [file] [log] [blame]
// Copyright 2018 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/browser/vr/test/xr_browser_test.h"
#include <cstring>
#include "base/base_paths.h"
#include "base/command_line.h"
#include "base/debug/debugger.h"
#include "base/environment.h"
#include "base/files/file_path.h"
#include "base/files/file_util.h"
#include "base/functional/bind.h"
#include "base/functional/callback.h"
#include "base/path_service.h"
#include "base/strings/string_split.h"
#include "base/strings/utf_string_conversions.h"
#include "base/task/single_thread_task_runner.h"
#include "base/time/time.h"
#include "build/build_config.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/test/base/chrome_test_utils.h"
#include "content/public/browser/web_contents.h"
#include "content/public/common/content_features.h"
#include "content/public/common/content_switches.h"
#include "content/public/test/browser_test_utils.h"
#include "url/gurl.h"
#if BUILDFLAG(IS_ANDROID)
#include "base/android/path_utils.h"
#include "chrome/browser/android/tab_android.h"
#include "chrome/browser/ui/android/tab_model/tab_model.h"
#include "chrome/browser/ui/android/tab_model/tab_model_list.h"
#else
#include "chrome/browser/ui/browser.h"
#include "chrome/browser/ui/browser_finder.h"
#include "chrome/browser/ui/browser_tabstrip.h"
#endif
namespace vr {
constexpr base::TimeDelta XrBrowserTestBase::kPollCheckIntervalShort;
constexpr base::TimeDelta XrBrowserTestBase::kPollCheckIntervalLong;
constexpr base::TimeDelta XrBrowserTestBase::kPollTimeoutShort;
constexpr base::TimeDelta XrBrowserTestBase::kPollTimeoutMedium;
constexpr base::TimeDelta XrBrowserTestBase::kPollTimeoutLong;
constexpr char XrBrowserTestBase::kOpenXrConfigPathEnvVar[];
constexpr char XrBrowserTestBase::kOpenXrConfigPathVal[];
constexpr char XrBrowserTestBase::kTestFileDir[];
constexpr char XrBrowserTestBase::kSwitchIgnoreRuntimeRequirements[];
const std::vector<std::string> XrBrowserTestBase::kRequiredTestSwitches{
#if BUILDFLAG(IS_WIN)
"enable-gpu", "enable-pixel-output-in-tests",
"run-through-xr-wrapper-script", "enable-unsafe-swiftshader"
#endif
};
const std::vector<std::pair<std::string, std::string>>
XrBrowserTestBase::kRequiredTestSwitchesWithValues{
#if BUILDFLAG(IS_WIN)
std::pair<std::string, std::string>("test-launcher-jobs", "1")
#endif
};
XrBrowserTestBase::XrBrowserTestBase() : env_(base::Environment::Create()) {
enable_features_.push_back(features::kLogJsConsoleMessages);
}
XrBrowserTestBase::~XrBrowserTestBase() = default;
base::FilePath::StringType UTF8ToWideIfNecessary(std::string input) {
#if BUILDFLAG(IS_WIN)
return base::UTF8ToWide(input);
#else
return input;
#endif // BUILDFLAG(IS_WIN)
}
std::string WideToUTF8IfNecessary(base::FilePath::StringType input) {
#if BUILDFLAG(IS_WIN)
return base::WideToUTF8(input);
#else
return input;
#endif // BUILDFLAG(IS_WIN)
}
// Returns an std::string consisting of the given path relative to the test
// executable's path, e.g. if the executable is in out/Debug and the given path
// is "test", the returned string should be out/Debug/test.
std::string MakeExecutableRelative(const char* path) {
base::FilePath executable_path;
#if BUILDFLAG(IS_ANDROID)
NOTREACHED();
#else
EXPECT_TRUE(
base::PathService::Get(base::BasePathKey::FILE_EXE, &executable_path));
executable_path = executable_path.DirName();
// We need an std::string that is an absolute file path, which requires
// platform-specific logic since Windows uses std::wstring instead of
// std::string for FilePaths, but SetVar only accepts std::string.
return WideToUTF8IfNecessary(
base::MakeAbsoluteFilePath(
executable_path.Append(base::FilePath(UTF8ToWideIfNecessary(path))))
.value());
#endif
}
void XrBrowserTestBase::SetUp() {
// Check whether the required flags were passed to the test - without these,
// we can fail in ways that are non-obvious, so fail more explicitly here if
// they aren't present.
auto* cmd_line = base::CommandLine::ForCurrentProcess();
for (auto req_switch : kRequiredTestSwitches) {
ASSERT_TRUE(cmd_line->HasSwitch(req_switch))
<< "Missing switch " << req_switch << " required to run tests properly";
}
for (auto req_switch_pair : kRequiredTestSwitchesWithValues) {
ASSERT_TRUE(cmd_line->HasSwitch(req_switch_pair.first))
<< "Missing switch " << req_switch_pair.first
<< " required to run tests properly";
ASSERT_TRUE(cmd_line->GetSwitchValueASCII(req_switch_pair.first) ==
req_switch_pair.second)
<< "Have required switch " << req_switch_pair.first
<< ", but not required value " << req_switch_pair.second;
}
// Get the set of runtime requirements to ignore.
if (cmd_line->HasSwitch(kSwitchIgnoreRuntimeRequirements)) {
auto reqs = cmd_line->GetSwitchValueASCII(kSwitchIgnoreRuntimeRequirements);
if (reqs != "") {
for (auto req : base::SplitString(
reqs, ",", base::WhitespaceHandling::TRIM_WHITESPACE,
base::SplitResult::SPLIT_WANT_NONEMPTY)) {
ignored_requirements_.insert(req);
}
}
}
// Check whether we meet all runtime requirements for this test.
XR_CONDITIONAL_SKIP_PRETEST(runtime_requirements_, ignored_requirements_,
&test_skipped_at_startup_)
// OpenXr on Android cannot use the environment variable as the loader cannot
// read it on Android at present.
#if !BUILDFLAG(IS_ANDROID)
// Set the environment variable to use the mock OpenXR client.
// If the kOpenXrConfigPathEnvVar environment variable is set, the OpenXR
// loader will look for the OpenXR runtime specified in that json file. The
// json file contains the path to the runtime, relative to the json file
// itself. Otherwise, the OpenXR loader loads the active OpenXR runtime
// installed on the system, which is specified by a registry key.
ASSERT_TRUE(env_->SetVar(kOpenXrConfigPathEnvVar,
MakeExecutableRelative(kOpenXrConfigPathVal)))
<< "Failed to set OpenXR JSON location environment variable";
#endif
// Set any command line flags that subclasses have set, e.g. enabling features
// or specific runtimes.
for (const auto& switch_string : append_switches_) {
cmd_line->AppendSwitch(switch_string);
}
for (const auto& blink_feature : enable_blink_features_) {
cmd_line->AppendSwitchASCII(switches::kEnableBlinkFeatures, blink_feature);
}
#if defined(MEMORY_SANITIZER)
// Surprisingly enough, there is no constant for this.
// TODO(crbug.com/40564748): Once generic wrapper scripts for tests are
// supported, move the logic to avoid passing --enable-gpu to GN.
if (cmd_line->HasSwitch("enable-gpu")) {
LOG(WARNING) << "Ignoring --enable-gpu switch, which is incompatible with "
"MSan builds.";
cmd_line->RemoveSwitch("enable-gpu");
}
#endif
scoped_feature_list_.InitWithFeatures(enable_features_, disable_features_);
PlatformBrowserTest::SetUp();
}
void XrBrowserTestBase::TearDown() {
if (test_skipped_at_startup_) {
// Since we didn't complete startup, no need to do teardown, either. Doing
// so can result in hitting a DCHECK.
return;
}
PlatformBrowserTest::TearDown();
}
XrBrowserTestBase::RuntimeType XrBrowserTestBase::GetRuntimeType() const {
return XrBrowserTestBase::RuntimeType::RUNTIME_NONE;
}
GURL XrBrowserTestBase::GetUrlForFile(const std::string& test_name) {
// GetURL requires that the path start with /.
return GetEmbeddedServer()->GetURL(std::string("/") + kTestFileDir +
test_name + ".html");
}
net::EmbeddedTestServer* XrBrowserTestBase::GetEmbeddedServer() {
if (server_ == nullptr) {
server_ = std::make_unique<net::EmbeddedTestServer>(
net::EmbeddedTestServer::Type::TYPE_HTTPS);
// We need to serve from the root in order for the inclusion of the
// test harness from //third_party to work.
server_->ServeFilesFromSourceDirectory(".");
EXPECT_TRUE(server_->Start()) << "Failed to start embedded test server";
}
return server_.get();
}
content::WebContents* XrBrowserTestBase::GetCurrentWebContents() {
#if !BUILDFLAG(IS_ANDROID)
// `chrome_test_utils::GetActiveWebContents()` doesn't properly account for
// the presence of an incognito browser, and only looks in the browser
// returned by the base class, which doesn't get overridden by the incognito
// browser.
if (incognito_) {
Browser* incognito_browser = chrome::FindTabbedBrowser(
browser()->profile()->GetPrimaryOTRProfile(/*create_if_needed=*/false),
/*match_original_profiles=*/false);
return incognito_browser->tab_strip_model()->GetActiveWebContents();
}
#endif
return chrome_test_utils::GetActiveWebContents(this);
}
void XrBrowserTestBase::SetIncognito() {
incognito_ = true;
OpenNewTab(url::kAboutBlankURL);
}
void XrBrowserTestBase::OpenNewTab(const std::string& url) {
OpenNewTab(url, incognito_);
}
void XrBrowserTestBase::OpenNewTab(const std::string& url, bool incognito) {
#if BUILDFLAG(IS_ANDROID)
auto* profile = chrome_test_utils::GetProfile(this);
if (incognito) {
profile = profile->GetPrimaryOTRProfile(/*create_if_needed=*/true);
}
TabModel* tab_model =
TabModelList::GetTabModelForWebContents(GetCurrentWebContents());
TabAndroid* first_tab = TabAndroid::FromWebContents(GetCurrentWebContents());
std::unique_ptr<content::WebContents> contents =
content::WebContents::Create(content::WebContents::CreateParams(profile));
EXPECT_TRUE(content::NavigateToURL(contents.get(), GURL(url)));
// TabModel takes ownership of the WebContents, so we release it here.
tab_model->CreateTab(first_tab, contents.release(), true);
#else
if (incognito) {
OpenURLOffTheRecord(browser()->profile(), GURL(url));
} else {
// -1 is a special index value used to append to the end of the tab list.
chrome::AddTabAt(browser(), GURL(url), /*index=*/-1, /*foreground=*/true);
}
#endif
}
void XrBrowserTestBase::LoadFileAndAwaitInitialization(
const std::string& test_name) {
OnBeforeLoadFile();
GURL url = GetUrlForFile(test_name);
ASSERT_TRUE(content::NavigateToURL(GetCurrentWebContents(), url));
ASSERT_TRUE(PollJavaScriptBoolean("isInitializationComplete()",
kPollTimeoutMedium,
GetCurrentWebContents()))
<< "Timed out waiting for JavaScript test initialization.";
#if BUILDFLAG(IS_WIN)
// Now that the browser is opened and has focus, keep track of this window so
// that we can restore the proper focus after entering each session. This is
// required for tests that create multiple sessions to work properly.
hwnd_ = GetForegroundWindow();
#endif
}
void XrBrowserTestBase::RunJavaScriptOrFail(
const std::string& js_expression,
content::WebContents* web_contents) {
if (javascript_failed_) {
LogJavaScriptFailure();
return;
}
ASSERT_TRUE(content::ExecJs(web_contents, js_expression))
<< "Failed to run given JavaScript: " << js_expression;
}
bool XrBrowserTestBase::RunJavaScriptAndExtractBoolOrFail(
const std::string& js_expression,
content::WebContents* web_contents) {
if (javascript_failed_) {
LogJavaScriptFailure();
return false;
}
DLOG(INFO) << "Run JavaScript: " << js_expression;
return content::EvalJs(web_contents, js_expression).ExtractBool();
}
std::string XrBrowserTestBase::RunJavaScriptAndExtractStringOrFail(
const std::string& js_expression,
content::WebContents* web_contents) {
if (javascript_failed_) {
LogJavaScriptFailure();
return "";
}
return content::EvalJs(web_contents, js_expression).ExtractString();
}
bool XrBrowserTestBase::PollJavaScriptBoolean(
const std::string& bool_expression,
const base::TimeDelta& timeout,
content::WebContents* web_contents) {
bool result = false;
base::RunLoop wait_loop(base::RunLoop::Type::kNestableTasksAllowed);
// Lambda used because otherwise BindRepeating gets confused about which
// version of RunJavaScriptAndExtractBoolOrFail to use.
BlockOnCondition(base::BindRepeating(
[](XrBrowserTestBase* base, std::string expression,
content::WebContents* contents) {
return base->RunJavaScriptAndExtractBoolOrFail(
expression, contents);
},
this, bool_expression, web_contents),
&result, &wait_loop, base::Time::Now(), timeout);
wait_loop.Run();
return result;
}
void XrBrowserTestBase::PollJavaScriptBooleanOrFail(
const std::string& bool_expression,
const base::TimeDelta& timeout,
content::WebContents* web_contents) {
ASSERT_TRUE(PollJavaScriptBoolean(bool_expression, timeout, web_contents))
<< "Timed out polling JavaScript boolean expression: " << bool_expression;
}
void XrBrowserTestBase::BlockOnCondition(
base::RepeatingCallback<bool()> condition,
bool* result,
base::RunLoop* wait_loop,
const base::Time& start_time,
const base::TimeDelta& timeout,
const base::TimeDelta& period) {
if (!*result) {
*result = condition.Run();
}
if (*result) {
if (wait_loop->running()) {
wait_loop->Quit();
return;
}
// In the case where the condition is met fast enough that the given
// RunLoop hasn't started yet, spin until it's available.
base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask(
FROM_HERE,
base::BindOnce(&XrBrowserTestBase::BlockOnCondition,
base::Unretained(this), std::move(condition),
base::Unretained(result), base::Unretained(wait_loop),
start_time, timeout, period));
return;
}
if (base::Time::Now() - start_time > timeout &&
!base::debug::BeingDebugged()) {
wait_loop->Quit();
return;
}
base::SingleThreadTaskRunner::GetCurrentDefault()->PostDelayedTask(
FROM_HERE,
base::BindOnce(&XrBrowserTestBase::BlockOnCondition,
base::Unretained(this), std::move(condition),
base::Unretained(result), base::Unretained(wait_loop),
start_time, timeout, period),
period);
}
void XrBrowserTestBase::WaitOnJavaScriptStep(
content::WebContents* web_contents) {
// Make sure we aren't trying to wait on a JavaScript test step without the
// code to do so.
bool code_available = RunJavaScriptAndExtractBoolOrFail(
"typeof javascriptDone !== 'undefined'", web_contents);
ASSERT_TRUE(code_available) << "Attempted to wait on a JavaScript test step "
<< "without the code to do so. You either forgot "
<< "to import webxr_e2e.js or "
<< "are incorrectly using a C++ function.";
// Actually wait for the step to finish.
bool success =
PollJavaScriptBoolean("javascriptDone", kPollTimeoutLong, web_contents);
// Check what state we're in to make sure javascriptDone wasn't called
// because the test failed.
XrBrowserTestBase::TestStatus test_status = CheckTestStatus(web_contents);
if (!success || test_status == XrBrowserTestBase::TestStatus::STATUS_FAILED) {
// Failure states: Either polling failed or polling succeeded, but because
// the test failed.
std::string reason;
if (!success) {
reason = "Timed out waiting for JavaScript step to finish.";
} else {
reason =
"JavaScript testharness reported failure while waiting for "
"JavaScript step to finish";
}
std::string result_string =
RunJavaScriptAndExtractStringOrFail("resultString", web_contents);
if (result_string.empty()) {
reason +=
" Did not obtain specific failure reason from JavaScript "
"testharness.";
} else {
reason +=
" JavaScript testharness reported failure reason: " + result_string;
}
// Store that we've failed waiting for a JavaScript step so we can abort
// further attempts to run JavaScript, which has the potential to do weird
// things and produce non-useful output due to JavaScript code continuing
// to run when it's in a known bad state.
// This is a workaround for the fact that FAIL() and other gtest macros that
// cause test failures only abort the current function. Thus, a failure here
// will show up as a test failure, but there's nothing that actually stops
// the test from continuing to run since FAIL() is not being called in the
// main test body.
javascript_failed_ = true;
// Newlines to help the failure reason stick out.
LOG(ERROR) << "\n\n\nvvvvvvvvvvvvvvvvv Useful Stack vvvvvvvvvvvvvvvvv\n\n";
FAIL() << reason;
}
// Reset the synchronization boolean.
RunJavaScriptOrFail("javascriptDone = false", web_contents);
}
void XrBrowserTestBase::ExecuteStepAndWait(const std::string& step_function,
content::WebContents* web_contents) {
RunJavaScriptOrFail(step_function, web_contents);
WaitOnJavaScriptStep(web_contents);
}
XrBrowserTestBase::TestStatus XrBrowserTestBase::CheckTestStatus(
content::WebContents* web_contents) {
std::string result_string =
RunJavaScriptAndExtractStringOrFail("resultString", web_contents);
bool test_passed =
RunJavaScriptAndExtractBoolOrFail("testPassed", web_contents);
if (test_passed) {
return XrBrowserTestBase::TestStatus::STATUS_PASSED;
} else if (!test_passed && result_string.empty()) {
return XrBrowserTestBase::TestStatus::STATUS_RUNNING;
}
// !test_passed && result_string != ""
return XrBrowserTestBase::TestStatus::STATUS_FAILED;
}
void XrBrowserTestBase::EndTest(content::WebContents* web_contents) {
switch (CheckTestStatus(web_contents)) {
case XrBrowserTestBase::TestStatus::STATUS_PASSED:
break;
case XrBrowserTestBase::TestStatus::STATUS_FAILED:
FAIL() << "JavaScript testharness failed with reason: "
<< RunJavaScriptAndExtractStringOrFail("resultString",
web_contents);
case XrBrowserTestBase::TestStatus::STATUS_RUNNING:
FAIL() << "Attempted to end test in C++ without finishing in JavaScript.";
default:
FAIL() << "Received unknown test status.";
}
}
void XrBrowserTestBase::AssertNoJavaScriptErrors(
content::WebContents* web_contents) {
if (CheckTestStatus(web_contents) ==
XrBrowserTestBase::TestStatus::STATUS_FAILED) {
FAIL() << "JavaScript testharness failed with reason: "
<< RunJavaScriptAndExtractStringOrFail("resultString", web_contents);
}
}
void XrBrowserTestBase::RunJavaScriptOrFail(const std::string& js_expression) {
RunJavaScriptOrFail(js_expression, GetCurrentWebContents());
}
bool XrBrowserTestBase::RunJavaScriptAndExtractBoolOrFail(
const std::string& js_expression) {
return RunJavaScriptAndExtractBoolOrFail(js_expression,
GetCurrentWebContents());
}
std::string XrBrowserTestBase::RunJavaScriptAndExtractStringOrFail(
const std::string& js_expression) {
return RunJavaScriptAndExtractStringOrFail(js_expression,
GetCurrentWebContents());
}
bool XrBrowserTestBase::PollJavaScriptBoolean(
const std::string& bool_expression,
const base::TimeDelta& timeout) {
return PollJavaScriptBoolean(bool_expression, timeout,
GetCurrentWebContents());
}
void XrBrowserTestBase::PollJavaScriptBooleanOrFail(
const std::string& bool_expression,
const base::TimeDelta& timeout) {
PollJavaScriptBooleanOrFail(bool_expression, timeout,
GetCurrentWebContents());
}
void XrBrowserTestBase::WaitOnJavaScriptStep() {
WaitOnJavaScriptStep(GetCurrentWebContents());
}
void XrBrowserTestBase::ExecuteStepAndWait(const std::string& step_function) {
ExecuteStepAndWait(step_function, GetCurrentWebContents());
}
void XrBrowserTestBase::EndTest() {
EndTest(GetCurrentWebContents());
}
void XrBrowserTestBase::AssertNoJavaScriptErrors() {
AssertNoJavaScriptErrors(GetCurrentWebContents());
}
void XrBrowserTestBase::LogJavaScriptFailure() {
LOG(ERROR) << "HEY! LISTEN! Not running requested JavaScript due to previous "
"failure. Failures below this are likely garbage. Look for the "
"useful stack above.";
}
} // namespace vr