Commit 3e029cff authored by Piotr Bialecki's avatar Piotr Bialecki Committed by Commit Bot

WebXR - anchors - add automated instrumentation tests

Changes:
- added one more recording including a tracking loss
- added 3 test cases exercising anchors API
- slightly tweaked XrTestFramework to allow for specifying timeouts
explicitly so that the longest anchor test does not time out

Bug: 992035
Change-Id: Ied3db9b459c72cbc1826d8a9e2e7edec90cbaae4
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2314663
Commit-Queue: Piotr Bialecki <bialpio@chromium.org>
Reviewed-by: default avatarBrian Sheedy <bsheedy@chromium.org>
Reviewed-by: default avatarKlaus Weidner <klausw@chromium.org>
Cr-Commit-Position: refs/heads/master@{#792001}
parent 6d980ecc
......@@ -1334,6 +1334,7 @@ if (enable_vr || enable_arcore) {
testonly = true
sources = [
"javatests/src/org/chromium/chrome/browser/vr/WebXrArAnchorsTest.java",
"javatests/src/org/chromium/chrome/browser/vr/WebXrArHitTestTest.java",
"javatests/src/org/chromium/chrome/browser/vr/WebXrArSessionTest.java",
"javatests/src/org/chromium/chrome/browser/vr/WebXrArTestFramework.java",
......
// Copyright 2020 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package org.chromium.chrome.browser.vr;
import static org.chromium.chrome.browser.vr.WebXrArTestFramework.PAGE_LOAD_TIMEOUT_S;
import static org.chromium.chrome.browser.vr.XrTestFramework.POLL_TIMEOUT_SHORT_MS;
import android.os.Build;
import androidx.test.filters.LargeTest;
import androidx.test.filters.MediumTest;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.RuleChain;
import org.junit.runner.RunWith;
import org.chromium.base.test.params.ParameterAnnotations.ClassParameter;
import org.chromium.base.test.params.ParameterAnnotations.UseRunnerDelegate;
import org.chromium.base.test.params.ParameterSet;
import org.chromium.base.test.params.ParameterizedRunner;
import org.chromium.base.test.util.CommandLineFlags;
import org.chromium.base.test.util.MinAndroidSdkLevel;
import org.chromium.chrome.browser.flags.ChromeSwitches;
import org.chromium.chrome.browser.vr.rules.ArPlaybackFile;
import org.chromium.chrome.browser.vr.rules.XrActivityRestriction;
import org.chromium.chrome.browser.vr.util.ArTestRuleUtils;
import org.chromium.chrome.test.ChromeActivityTestRule;
import org.chromium.chrome.test.ChromeJUnit4RunnerDelegate;
import java.util.List;
import java.util.concurrent.Callable;
/**
* End-to-end tests for testing WebXR for AR's anchors behavior.
*/
@RunWith(ParameterizedRunner.class)
@UseRunnerDelegate(ChromeJUnit4RunnerDelegate.class)
@CommandLineFlags.Add({ChromeSwitches.DISABLE_FIRST_RUN_EXPERIENCE,
"enable-features=WebXR,WebXRARModule,WebXRHitTest,LogJsConsoleMessages"})
@MinAndroidSdkLevel(Build.VERSION_CODES.N) // WebXR for AR is only supported on N+
public class WebXrArAnchorsTest {
@ClassParameter
private static List<ParameterSet> sClassParams =
ArTestRuleUtils.generateDefaultTestRuleParameters();
@Rule
public RuleChain mRuleChain;
private ChromeActivityTestRule mTestRule;
private WebXrArTestFramework mWebXrArTestFramework;
public WebXrArAnchorsTest(Callable<ChromeActivityTestRule> callable) throws Exception {
mTestRule = callable.call();
mRuleChain = ArTestRuleUtils.wrapRuleInActivityRestrictionRule(mTestRule);
}
@Before
public void setUp() {
mWebXrArTestFramework = new WebXrArTestFramework(mTestRule);
}
/**
* Tests that anchor can be created off of a valid hit test result.
*/
@Test
@MediumTest
@XrActivityRestriction({XrActivityRestriction.SupportedActivity.ALL})
@ArPlaybackFile("chrome/test/data/xr/ar_playback_datasets/floor_session_12s_30fps.mp4")
public void testHitTestAnchorSucceedsWithPlane() {
mWebXrArTestFramework.loadFileAndAwaitInitialization(
"webxr_test_basic_anchors_hittest", PAGE_LOAD_TIMEOUT_S);
mWebXrArTestFramework.enterSessionWithUserGestureOrFail();
mWebXrArTestFramework.executeStepAndWait("stepStartHitTesting()");
mWebXrArTestFramework.endTest();
}
/**
* Tests that a free-floating anchor can be created when the session is stable.
*/
@Test
@MediumTest
@XrActivityRestriction({XrActivityRestriction.SupportedActivity.ALL})
@ArPlaybackFile("chrome/test/data/xr/ar_playback_datasets/floor_session_12s_30fps.mp4")
public void testFreeFloatingAnchorSucceeds() {
mWebXrArTestFramework.loadFileAndAwaitInitialization(
"webxr_test_basic_anchors_freefloating", PAGE_LOAD_TIMEOUT_S);
mWebXrArTestFramework.enterSessionWithUserGestureOrFail();
mWebXrArTestFramework.executeStepAndWait("stepStartTest()");
mWebXrArTestFramework.endTest();
}
/**
* Tests that an anchor gets updated (includes updating anchor position, tracking pause, and
* tracking recovery).
*/
@Test
@LargeTest
@XrActivityRestriction({XrActivityRestriction.SupportedActivity.ALL})
@ArPlaybackFile(
"chrome/test/data/xr/ar_playback_datasets/floor_session_with_tracking_loss_37s_30fps.mp4")
public void
testAnchorStates() {
mWebXrArTestFramework.loadFileAndAwaitInitialization(
"webxr_test_basic_anchors_updates", PAGE_LOAD_TIMEOUT_S);
mWebXrArTestFramework.enterSessionWithUserGestureOrFail();
// The recording is 37 seconds long, let's wait for a bit more than that before timing out.
mWebXrArTestFramework.executeStepAndWait("stepStartHitTesting()", 40 * 1000);
mWebXrArTestFramework.endTest();
// Time taken from start to end of JS test should not be less than 20 seconds (tracking loss
// happens later in the recording).
mWebXrArTestFramework.pollJavaScriptBooleanOrFail(
"time_taken_in_ms > (20 * 1000)", POLL_TIMEOUT_SHORT_MS);
}
}
......@@ -250,11 +250,23 @@ public abstract class XrTestFramework {
* @param webContents The WebContents for the tab the JavaScript is in.
*/
public static void executeStepAndWait(String stepFunction, WebContents webContents) {
executeStepAndWait(stepFunction, webContents, POLL_TIMEOUT_LONG_MS);
}
/**
* Executes a JavaScript step function using the given WebContents.
*
* @param stepFunction The JavaScript step function to call.
* @param webContents The WebContents for the tab the JavaScript is in.
* @param timeoutMs Timeout (in milliseconds) to wait for the JavaScript step.
*/
public static void executeStepAndWait(
String stepFunction, WebContents webContents, int timeoutMs) {
// Run the step and block
if (DEBUG_LOGS) Log.i(TAG, "executeStepAndWait " + stepFunction);
JavaScriptUtils.executeJavaScript(webContents, stepFunction);
if (DEBUG_LOGS) Log.i(TAG, "executeStepAndWait ...wait");
waitOnJavaScriptStep(webContents);
waitOnJavaScriptStep(webContents, timeoutMs);
if (DEBUG_LOGS) Log.i(TAG, "executeStepAndWait ...done");
}
......@@ -265,6 +277,17 @@ public abstract class XrTestFramework {
* @param webContents The WebContents for the tab the JavaScript step is in.
*/
public static void waitOnJavaScriptStep(WebContents webContents) {
waitOnJavaScriptStep(webContents, POLL_TIMEOUT_LONG_MS);
}
/**
* Waits for a JavaScript step to finish, asserting that the step finished instead of timing
* out.
*
* @param webContents The WebContents for the tab the JavaScript step is in.
* @param timeoutMs Timeout (in milliseconds) to wait for the JavaScript step.
*/
public static void waitOnJavaScriptStep(WebContents webContents, int timeoutMs) {
if (DEBUG_LOGS) Log.i(TAG, "waitOnJavaScriptStep");
// Make sure we aren't trying to wait on a JavaScript test step without the code to do so.
Assert.assertTrue("Attempted to wait on a JavaScript step without the code to do so. You "
......@@ -274,8 +297,7 @@ public abstract class XrTestFramework {
POLL_TIMEOUT_SHORT_MS, webContents)));
// Actually wait for the step to finish
boolean success =
pollJavaScriptBoolean("javascriptDone", POLL_TIMEOUT_LONG_MS, webContents);
boolean success = pollJavaScriptBoolean("javascriptDone", timeoutMs, webContents);
// Check what state we're in to make sure javascriptDone wasn't called because the test
// failed.
......@@ -513,7 +535,17 @@ public abstract class XrTestFramework {
* @param stepFunction The JavaScript step function to call.
*/
public void executeStepAndWait(String stepFunction) {
executeStepAndWait(stepFunction, getCurrentWebContents());
executeStepAndWait(stepFunction, POLL_TIMEOUT_LONG_MS);
}
/**
* Helper function to run executeStepAndWait using the current tab's WebContents.
*
* @param stepFunction The JavaScript step function to call.
* @param timeoutMs Timeout (in milliseconds) to wait for the JavaScript step.
*/
public void executeStepAndWait(String stepFunction, int timeoutMs) {
executeStepAndWait(stepFunction, getCurrentWebContents(), timeoutMs);
}
/**
......
0258f3cc10f00cd36eed290037737149f13c4051
\ No newline at end of file
<!--
Tests that free-floating anchors can be created in an AR session.
-->
<html>
<head>
<link rel="stylesheet" type="text/css" href="../resources/webxr_e2e.css">
</head>
<body>
<canvas id="webgl-canvas"></canvas>
<script src="../../../../../../third_party/blink/web_tests/resources/testharness.js"></script>
<script src="../resources/webxr_e2e.js"></script>
<script>var shouldAutoCreateNonImmersiveSession = false;</script>
<script src="../resources/webxr_boilerplate.js"></script>
<script>
setup({single_test: true});
const TestState = Object.freeze({
"Initial": 0,
"AnchorRequested": 1,
});
let testState = TestState.Initial;
function stepStartTest() {
const sessionInfo = sessionInfos[sessionTypes.AR];
const referenceSpace = sessionInfo.currentRefSpace;
testState = TestState.Initial;
onARFrameCallback = (session, frame) => {
switch(testState) {
case TestState.Initial: {
// In initial state, we should attempt to create an anchor once viewer pose is available.
const viewerPose = frame.getViewerPose(referenceSpace);
if(viewerPose && !viewerPose.emulatedPosition) {
frame.createAnchor(new XRRigidTransform(), referenceSpace).then((anchor) => {
done();
return;
}).catch((err) => {
// Fail the test.
assert_unreached("XRFrame.createAnchor() promise rejected.");
});
testState = TestState.AnchorRequested;
}
return;
}
default:
return;
}
};
}
</script>
</body>
</html>
<!--
Tests that anchors can be created off of AR hit test results when plane is present.
-->
<html>
<head>
<link rel="stylesheet" type="text/css" href="../resources/webxr_e2e.css">
</head>
<body>
<canvas id="webgl-canvas"></canvas>
<script src="../../../../../../third_party/blink/web_tests/resources/testharness.js"></script>
<script src="../resources/webxr_e2e.js"></script>
<script>var shouldAutoCreateNonImmersiveSession = false;</script>
<script src="../resources/webxr_boilerplate.js"></script>
<script>
setup({single_test: true});
const TestState = Object.freeze({
"Initial": 0,
"HitTestSourceAvailable": 1, // hitTestSource variable is guaranteed to be non-null in this state
"AnchorRequested": 2,
});
let testState = TestState.Initial;
let hitTestSource = null;
function stepStartHitTesting() {
const sessionInfo = sessionInfos[sessionTypes.AR];
sessionInfo.currentSession.requestHitTestSource({
space: sessionInfo.currentRefSpace,
offsetRay: new XRRay()
}).then((hts) => {
// Mark that the hit test source is available.
hitTestSource = hts;
testState = TestState.HitTestSourceAvailable;
}).catch((err) => {
assert_unreached("XRSession.requestHitTestSource() promise rejected.");
});
onARFrameCallback = (session, frame) => {
switch(testState) {
case TestState.Initial: {
// In initial state, there is nothing to do (we're waiting for hit test source).
return;
}
case TestState.HitTestSourceAvailable: {
// Since we already have a hit test source, let's get its results and create an anchor.
const results = frame.getHitTestResults(hitTestSource);
if(results.length) {
const result = results[0];
result.createAnchor().then((anchor) => {
// Mark the test as done, there is no need to pump rAFcb at this point anymore.
done();
}).catch((err) => {
// Fail the test.
assert_unreached("XRHitTestResult.createAnchor() promise rejected.");
});
// Mark that we are waiting for anchor creation.
testState = TestState.AnchorRequested;
}
return;
}
default:
return;
}
};
}
</script>
</body>
</html>
<!--
Tests that anchor gets updated as the state of the session changes.
-->
<html>
<head>
<link rel="stylesheet" type="text/css" href="../resources/webxr_e2e.css">
<meta name="timeout" content="long"> <!-- this is a long-running test! -->
</head>
<body>
<canvas id="webgl-canvas"></canvas>
<script src="../../../../../../third_party/blink/web_tests/resources/testharness.js"></script>
<script src="../resources/webxr_e2e.js"></script>
<script>var shouldAutoCreateNonImmersiveSession = false;</script>
<script src="../resources/webxr_boilerplate.js"></script>
<script>
setup({single_test: true});
const TestState = Object.freeze({
"Initial": 0,
"HitTestSourceAvailable": 1, // hitTestSource variable is guaranteed to be non-null in this state
"AnchorRequested": 2,
"AnchorAvailable": 3, // createdAnchor variable is guaranteed to be non-null in this state
"AnchorPoseAvailable": 4, // createdAnchor and createdAnchorPose variables are guaranteed to be non-null in this state
"AnchorPoseChanged": 5,
"AnchorPaused": 6,
"AnchorResumed": 7,
"AnchorRemoved": 8,
});
let testState = TestState.Initial;
let hitTestSource = null;
let createdAnchor = null;
let createdAnchorPose = null;
let time_taken_in_ms = null;
// Returns true if q1 and q2 represent the same (or almost the same) orientation.
// This also means that if they only differ by sign, they would still be considered
// equal (since they represent the same orientation, but take a different path).
// |q1|, |q2| - DOMPoints representing quaternions.
function orientation_almost_equal(q1, q2, epsilon) {
const dot = q1.x * q2.x
+ q1.y * q2.y
+ q1.z * q2.z
+ q1.w * q2.w;
const angle = Math.acos(2 * dot * dot -1);
return Math.abs(angle) < epsilon;
};
function position_almost_equal(p1, p2, epsilon) {
const x_diff = p1.x - p2.x;
const y_diff = p1.y - p2.y;
const z_diff = p1.z - p2.z;
const distance = Math.sqrt(x_diff * x_diff + y_diff * y_diff + z_diff * z_diff);
return distance < epsilon;
}
function stepStartHitTesting() {
const start_time = Date.now();
const sessionInfo = sessionInfos[sessionTypes.AR];
const referenceSpace = sessionInfo.currentRefSpace;
sessionInfo.currentSession.requestHitTestSource({
space: referenceSpace,
offsetRay: new XRRay()
}).then((hts) => {
// Mark that the hit test source is available.
hitTestSource = hts;
testState = TestState.HitTestSourceAvailable;
}).catch((err) => {
assert_unreached("XRSession.requestHitTestSource() promise rejected.");
});
onARFrameCallback = (session, frame) => {
switch(testState) {
case TestState.Initial: {
// In initial state, there is nothing to do (we're waiting for hit test source).
return;
}
case TestState.HitTestSourceAvailable: {
// Since we already have a hit test source, let's get its results and create an anchor.
const results = frame.getHitTestResults(hitTestSource);
if (results.length) {
const result = results[0];
result.createAnchor().then((anchor) => {
createdAnchor = anchor;
testState = TestState.AnchorAvailable;
hitTestSource.cancel();
hitTestSource = null;
}).catch((err) => {
// Fail the test.
assert_unreached("XRHitTestResult.createAnchor() promise rejected.");
});
// Mark that we are waiting for anchor creation.
testState = TestState.AnchorRequested;
}
return;
}
case TestState.AnchorAvailable: {
assert_true(frame.trackedAnchors.has(createdAnchor), "Created anchor should be tracked!");
const anchorPose = frame.getPose(createdAnchor.anchorSpace, referenceSpace);
if (anchorPose) {
testState = TestState.AnchorPoseAvailable;
createdAnchorPose = anchorPose;
}
return;
}
case TestState.AnchorPoseAvailable: {
assert_true(frame.trackedAnchors.has(createdAnchor), "Created anchor should be tracked!");
const currentAnchorPose = frame.getPose(createdAnchor.anchorSpace, referenceSpace);
if (currentAnchorPose) {
// We'll only leave this state if anchor pose is significantly different than previous anchor pose.
const previousAnchorPosition = createdAnchorPose.transform.position;
const currentAnchorPosition = currentAnchorPose.transform.position;
const previousAnchorOrientation = createdAnchorPose.transform.orientation;
const currentAnchorOrientation = currentAnchorPose.transform.orientation;
if (position_almost_equal(previousAnchorPosition, currentAnchorPosition, 0.05)
&& orientation_almost_equal(previousAnchorOrientation, currentAnchorOrientation, 5 * (Math.PI / 180))) {
// Anchor pose is still the same, keep waiting.
} else {
testState = TestState.AnchorPoseChanged;
}
}
return;
}
case TestState.AnchorPoseChanged: {
assert_true(frame.trackedAnchors.has(createdAnchor), "Created anchor should be tracked!");
// Now let's wait for the anchor tracking to become paused:
const currentAnchorPose = frame.getPose(createdAnchor.anchorSpace, referenceSpace);
if(!currentAnchorPose) {
testState = TestState.AnchorPaused;
}
return;
}
case TestState.AnchorPaused: {
assert_true(frame.trackedAnchors.has(createdAnchor), "Created anchor should be tracked!");
// Now wait for the tracking to be resumed:
const currentAnchorPose = frame.getPose(createdAnchor.anchorSpace, referenceSpace);
if(currentAnchorPose) {
testState = TestState.AnchorResumed;
}
return;
}
case TestState.AnchorResumed: {
assert_true(frame.trackedAnchors.has(createdAnchor), "Created anchor should be tracked!");
createdAnchor.delete();
assert_throws_dom('InvalidStateError', () => {
createdAnchor.anchorSpace;
});
testState = TestState.AnchorRemoved;
return;
}
case TestState.AnchorRemoved:
assert_true(!frame.trackedAnchors.has(createdAnchor), "Created anchor should not be tracked after it got removed!");
time_taken_in_ms = Date.now() - start_time;
done();
return;
default:
return;
}
};
}
</script>
</body>
</html>
......@@ -79,7 +79,7 @@ var immersiveSessionInit = {};
// AR sessions will use the `immersiveSessionInit` and `immersiveArSessionInit`
// to request a session. If they both contain the same keys, the one present in
// `immersiveArSessionInit` will be chosen.
var immersiveArSessionInit = { requiredFeatures: ['hit-test'] };
var immersiveArSessionInit = { requiredFeatures: ['hit-test', 'anchors'] };
var nonImmersiveSessionInit = {};
function getSessionType(session) {
......
......@@ -48,6 +48,7 @@ ScriptPromise XRHitTestResult::createAnchor(ScriptState* script_state,
if (!session_->IsFeatureEnabled(device::mojom::XRSessionFeature::ANCHORS)) {
exception_state.ThrowDOMException(DOMExceptionCode::kNotSupportedError,
XRSession::kAnchorsFeatureNotSupported);
DVLOG(3) << __func__ << ": anchors not supported on the session";
return {};
}
......@@ -65,6 +66,7 @@ ScriptPromise XRHitTestResult::createAnchor(ScriptState* script_state,
if (!reference_space_information) {
exception_state.ThrowDOMException(DOMExceptionCode::kInvalidStateError,
XRSession::kUnableToRetrieveMatrix);
DVLOG(3) << __func__ << ": unable to obtain stationary reference space";
return {};
}
......
......@@ -1021,6 +1021,9 @@ void XRSession::ProcessAnchorsData(
updated_anchors.insert(anchor->id, it->value);
it->value->Update(*anchor);
} else {
DVLOG(3) << __func__ << ": processing newly created anchor, anchor->id="
<< anchor->id;
auto resolver_it =
anchor_ids_to_pending_anchor_promises_.find(anchor->id);
if (resolver_it == anchor_ids_to_pending_anchor_promises_.end()) {
......@@ -1087,7 +1090,7 @@ void XRSession::ProcessHitTestData(
// We have received hit test results for hit test subscriptions - process
// each result and notify its corresponding hit test source about new
// results for the current frame.
DVLOG(3) << __func__ << "hit_test_subscriptions_data->results.size()="
DVLOG(3) << __func__ << ": hit_test_subscriptions_data->results.size()="
<< hit_test_subscriptions_data->results.size() << ", "
<< "hit_test_subscriptions_data->transient_input_results.size()="
<< hit_test_subscriptions_data->transient_input_results.size();
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment