mirror of
https://github.com/shaka-project/shaka-player.git
synced 2026-06-17 16:26:39 +03:00
7e6a0f38ff
This corrects/normalizes license headers in misc. files, such as config files, docs, build tools, tests, and externs. This does not affect the compiled output, and is only done for consistency. Issue #2638 Change-Id: I9d8da2de55243b08d7df2b743aac73c6f15e858a
505 lines
16 KiB
JavaScript
505 lines
16 KiB
JavaScript
/*! @license
|
|
* Shaka Player
|
|
* Copyright 2016 Google LLC
|
|
* SPDX-License-Identifier: Apache-2.0
|
|
*/
|
|
|
|
describe('Walker', () => {
|
|
const AbortableOperation = shaka.util.AbortableOperation;
|
|
|
|
// For our tests, we will not use the payload in our routing logic. To
|
|
// avoid distracting the reader with payload details, hide them here.
|
|
const payload = {
|
|
factory: null,
|
|
mediaElement: null,
|
|
mimeType: null,
|
|
startTime: null,
|
|
startTimeOfLoad: NaN,
|
|
uri: null,
|
|
};
|
|
|
|
// The graph topology that we will be using for our tests.
|
|
//
|
|
// [ A ] ---> [ B ] ---> [ E ]
|
|
// ^ |
|
|
// | v
|
|
// [ D ] <--- [ C ]
|
|
//
|
|
/** @type {shaka.routing.Node} */
|
|
const nodeA = {name: 'a'};
|
|
/** @type {shaka.routing.Node} */
|
|
const nodeB = {name: 'b'};
|
|
/** @type {shaka.routing.Node} */
|
|
const nodeC = {name: 'c'};
|
|
/** @type {shaka.routing.Node} */
|
|
const nodeD = {name: 'd'};
|
|
/** @type {shaka.routing.Node} */
|
|
const nodeE = {name: 'e'};
|
|
|
|
/**
|
|
* @param {shaka.routing.Node} at
|
|
* @param {shaka.routing.Node} goingTo
|
|
* @return {shaka.routing.Node}
|
|
*/
|
|
function getNext(at, goingTo) {
|
|
// In this graph, where you start determines where you go. The one exception
|
|
// is node B, which acts as a fork between nodes C and E. This fork is
|
|
// important in testing interrupts.
|
|
|
|
let goTo = null;
|
|
|
|
if (at == nodeA) {
|
|
goTo = nodeB;
|
|
}
|
|
|
|
if (at == nodeB) {
|
|
goTo = goingTo == nodeE ? nodeE : nodeC;
|
|
}
|
|
|
|
if (at == nodeC) {
|
|
goTo = nodeD;
|
|
}
|
|
|
|
if (at == nodeD) {
|
|
goTo = nodeA;
|
|
}
|
|
|
|
goog.asserts.assert(goTo, 'We should have found a next step.');
|
|
return goTo;
|
|
}
|
|
|
|
/** @type {!shaka.routing.Walker} */
|
|
let walker;
|
|
|
|
/** @type {!jasmine.Spy} */
|
|
let enterNodeSpy;
|
|
|
|
/** @type {!jasmine.Spy} */
|
|
let handleErrorSpy;
|
|
|
|
/** @type {!jasmine.Spy} */
|
|
let idleSpy;
|
|
|
|
beforeEach(() => {
|
|
enterNodeSpy = jasmine.createSpy('enterNode');
|
|
enterNodeSpy.and.returnValue(AbortableOperation.completed(undefined));
|
|
|
|
handleErrorSpy = jasmine.createSpy('handleError');
|
|
|
|
idleSpy = jasmine.createSpy('idle');
|
|
|
|
const implementation = {
|
|
getNext: (at, has, goingTo, wants) => getNext(at, goingTo),
|
|
enterNode: shaka.test.Util.spyFunc(enterNodeSpy),
|
|
handleError: shaka.test.Util.spyFunc(handleErrorSpy),
|
|
onIdle: shaka.test.Util.spyFunc(idleSpy),
|
|
};
|
|
|
|
walker = new shaka.routing.Walker(nodeA, payload, implementation);
|
|
});
|
|
|
|
afterEach(async () => {
|
|
await walker.destroy();
|
|
});
|
|
|
|
it('enters idle after initialization', async () => {
|
|
await waitOnSpy(idleSpy);
|
|
});
|
|
|
|
it('enters idle after completing route', async () => {
|
|
// Execute a route but then wait a couple interrupter cycles to allow the
|
|
// walker time to idle.
|
|
await completesRoute(startNewRoute(nodeD, /* interruptible= */ false));
|
|
await waitOnSpy(idleSpy);
|
|
});
|
|
|
|
it('enters idle after error', async () => {
|
|
// The specific error does not matter.
|
|
const error = new shaka.util.Error(
|
|
shaka.util.Error.Severity.CRITICAL,
|
|
shaka.util.Error.Category.TEXT,
|
|
shaka.util.Error.Code.BAD_ENCODING);
|
|
|
|
enterNodeSpy.and.callFake((at) => {
|
|
return at == nodeC ?
|
|
shaka.util.AbortableOperation.failed(error) :
|
|
shaka.util.AbortableOperation.completed(undefined);
|
|
});
|
|
|
|
handleErrorSpy.and.returnValue(nodeA);
|
|
|
|
// Go to nodeD, passing through nodeC. This will fail, calling handleError,
|
|
// and returning the walker to nodeA. Wait a couple interrupter cycles to
|
|
// allow the walker time to idle. The route needs to be interruptible since
|
|
// we are going to return an aborted operation.
|
|
await failsRoute(startNewRoute(nodeD, /* interruptible= */ true));
|
|
await waitOnSpy(idleSpy);
|
|
});
|
|
|
|
// The walker should move node-by-node, so starting in node A (see beforeEach)
|
|
// and going to nodeD, we should see the walker enter nodeB, nodeC, and nodeD.
|
|
// We won't see it enter nodeA because it starts there and therefore never
|
|
// "enters" that node.
|
|
it('moves node-by-node', async () => {
|
|
// We don't expect any errors in this test.
|
|
handleErrorSpy.and.callFake(fail);
|
|
|
|
await completesRoute(startNewRoute(nodeD, /* interruptible= */ false));
|
|
|
|
const steps = getStepsTaken();
|
|
expect(steps).toEqual([nodeB, nodeC, nodeD]);
|
|
});
|
|
|
|
// We want to make sure that if a route is registered as interruptible, we
|
|
// can interrupt the route and start a new route.
|
|
//
|
|
// For this we will have the walker start going to node E, but when it
|
|
// enters node B (the fork) we will interrupt it and tell it to go to
|
|
// node A.
|
|
it('can interrupt interruptible routes', async () => {
|
|
// We don't expect any errors in this test.
|
|
handleErrorSpy.and.callFake(fail);
|
|
|
|
/** @type {!shaka.util.PublicPromise} */
|
|
const atA = new shaka.util.PublicPromise();
|
|
const interrupt = () => {
|
|
const goToA = startNewRoute(nodeA, /* interruptible= */ false);
|
|
goToA.onEnd = () => atA.resolve();
|
|
};
|
|
|
|
const goingToE = startNewRoute(nodeE, /* interruptible= */ true);
|
|
goingToE.onEnter = (node) => {
|
|
if (node == nodeB) {
|
|
interrupt();
|
|
}
|
|
};
|
|
|
|
await atA;
|
|
|
|
const steps = getStepsTaken();
|
|
expect(steps).toEqual([nodeB, nodeC, nodeD, nodeA]);
|
|
});
|
|
|
|
// We want to make sure that a non-interruptible route cannot be interrupted
|
|
// by starting a new route. To do this, we are going to start off by going to
|
|
// node c. When we get to node b, we will try to start a new route to node e.
|
|
//
|
|
// If the route was interrupted, we would go to straight to node e (node b is
|
|
// a fork in the graph). However, since we expect the first route to finish,
|
|
// we expect to see the walker go to node c and then continue around to get to
|
|
// node e.
|
|
it('cannot interrupt non-interruptible routes', async () => {
|
|
// We don't expect any errors in this test.
|
|
handleErrorSpy.and.callFake(fail);
|
|
|
|
/** @type {!shaka.util.PublicPromise} */
|
|
const atE = new shaka.util.PublicPromise();
|
|
|
|
const interrupt = () => {
|
|
const goToE = startNewRoute(nodeE, /* interruptible= */ false);
|
|
goToE.onEnd = () => atE.resolve();
|
|
};
|
|
|
|
// Create a "trap" so that once we enter node B (the fork) that we will
|
|
// issue a new route - this will ensure that we are trying to interrupt a
|
|
// route mid-execution.
|
|
const goingToC = startNewRoute(nodeC, /* interruptible= */ false);
|
|
goingToC.onEnter = (node) => {
|
|
interrupt();
|
|
};
|
|
|
|
await atE;
|
|
|
|
const steps = getStepsTaken();
|
|
expect(steps).toEqual([
|
|
// First route.
|
|
nodeB, nodeC,
|
|
// Second route.
|
|
nodeD, nodeA, nodeB, nodeE,
|
|
]);
|
|
});
|
|
|
|
// We do not want to execute steps for a route that will be interrupted
|
|
// right after it starts. For this example, we queue-up three routes:
|
|
// 1. Non-interruptible
|
|
// 2. Interruptible
|
|
// 3. Interruptible
|
|
//
|
|
// What we expect to see is that Route 1 finishes, Route 2 starts but takes
|
|
// no steps, and Route 3 finishes.
|
|
it('does not take steps for route interrupted before starting', async () => {
|
|
/**
|
|
* When a route starts, it will assign this value to its id (1, 2, or 3)
|
|
* so we always know who was the most recent route to start.
|
|
*
|
|
* @type {?number}
|
|
*/
|
|
let currentRoute = null;
|
|
|
|
/**
|
|
* We use this set to know who took steps. When the walker takes a step
|
|
* we will add |currentRoute| to this set. That way we will know what
|
|
* route we took steps on.
|
|
*
|
|
* @type {!Array.<?number>}
|
|
*/
|
|
const tookSteps = [];
|
|
|
|
enterNodeSpy.and.callFake((node) => {
|
|
tookSteps.push(currentRoute);
|
|
return AbortableOperation.completed(undefined);
|
|
});
|
|
|
|
const route1 = startNewRoute(nodeD, /* interruptible= */ false);
|
|
route1.onStart = () => {
|
|
currentRoute = 1;
|
|
};
|
|
|
|
const route2 = startNewRoute(nodeC, /* interruptible= */ true);
|
|
route2.onStart = () => {
|
|
currentRoute = 2;
|
|
};
|
|
|
|
const route3 = startNewRoute(nodeE, /* interruptible= */ true);
|
|
route3.onStart = () => {
|
|
currentRoute = 3;
|
|
};
|
|
|
|
// Wait until we get to the end of route 3, that should be the end.
|
|
await completesRoute(route3);
|
|
|
|
// Make sure we had the correct routes when taking each step.
|
|
expect(tookSteps).toEqual([
|
|
1, // A to B
|
|
1, // B to C
|
|
1, // C to D
|
|
3, // D to A
|
|
3, // A to B
|
|
3, // B to E
|
|
]);
|
|
});
|
|
|
|
// When we destroy the walker, it should cancel all routes - even the
|
|
// non-interruptible routes.
|
|
it('cancels all routes when destroyed', async () => {
|
|
// Start-up a couple routes, and then destroy the walker. We expect to see
|
|
// both routes have their |onCancel| callbacks called. We make the first
|
|
// be non-interruptible and the second route interruptible so that we can
|
|
// see both types be cancelled by |destroy|. The non-interruptible route
|
|
// must before first or else it would interrupt the other route.
|
|
const goToC = startNewRoute(nodeC, /* interruptible= */ false);
|
|
const goToB = startNewRoute(nodeB, /* interruptible= */ true);
|
|
|
|
/** @type {!jasmine.Spy} */
|
|
const canceledCSpy = jasmine.createSpy('cancel c');
|
|
goToC.onCancel = shaka.test.Util.spyFunc(canceledCSpy);
|
|
|
|
/** @type {!jasmine.Spy} */
|
|
const canceledBSpy = jasmine.createSpy('cancel b');
|
|
goToB.onCancel = shaka.test.Util.spyFunc(canceledBSpy);
|
|
|
|
await walker.destroy();
|
|
|
|
expect(canceledCSpy).toHaveBeenCalled();
|
|
expect(canceledBSpy).toHaveBeenCalled();
|
|
});
|
|
|
|
it('calls handleError when step fails', async () => {
|
|
// The specific error does not matter.
|
|
const error = new shaka.util.Error(
|
|
shaka.util.Error.Severity.CRITICAL,
|
|
shaka.util.Error.Category.TEXT,
|
|
shaka.util.Error.Code.BAD_ENCODING);
|
|
|
|
// Make the walker fail when it hits node C. This should allow us to
|
|
// exercise the |handleError| path.
|
|
enterNodeSpy.and.callFake((at) => {
|
|
return at == nodeC ?
|
|
shaka.util.AbortableOperation.failed(error) :
|
|
shaka.util.AbortableOperation.completed(undefined);
|
|
});
|
|
|
|
// We want to handle the error and return to a safe state, so just put the
|
|
// walker back at node A.
|
|
handleErrorSpy.and.returnValue(nodeA);
|
|
|
|
// Go to D (passing through C). This should throw an error, so wait for the
|
|
// error to be seen. The route must be abortable because we are going to
|
|
// throw an abortable error.
|
|
await failsRoute(startNewRoute(nodeD, /* interruptible= */ true));
|
|
|
|
expect(handleErrorSpy).toHaveBeenCalled();
|
|
});
|
|
|
|
// When we interrupt a route that has a step that can be aborted, it should
|
|
// abort the operation and enter the error recovery mode.
|
|
//
|
|
// To model this we will make a node be a never-resolving node. We will enter
|
|
// the node and then get stuck. From there we will request a new route, the
|
|
// blocked op will abort, and then we will go to our new destination.
|
|
it('can abort current step', async () => {
|
|
// Because we need a node to start at after resetting, we will just use A.
|
|
// There is no special reason for node A.
|
|
handleErrorSpy.and.returnValue(Promise.resolve(nodeA));
|
|
|
|
// Block when we enter node C so that we can re-route to node E and finish a
|
|
// path.
|
|
blockWalkerAt(nodeC);
|
|
|
|
// Wait for us to enter node d before continuing. We introduce a small delay
|
|
// to ensure that we are "stuck" on the abortable operation.
|
|
const goingToD = startNewRoute(nodeD, /* interruptible= */ true);
|
|
await waitUntilEntering(goingToD, nodeC);
|
|
await shaka.test.Util.shortDelay();
|
|
|
|
await completesRoute(startNewRoute(nodeE, /* interruptible= */ true));
|
|
expect(handleErrorSpy).toHaveBeenCalled();
|
|
});
|
|
|
|
// If we are in the middle of a node and |destroy| is called, we want to
|
|
// ensure that we exit as soon as possible. If the current step is abortable
|
|
// then we want to abort.
|
|
it('can abort current step with destroy', async () => {
|
|
// Because we need a node to start at after resetting, we will just use A.
|
|
// There is no special reason for node A.
|
|
handleErrorSpy.and.returnValue(Promise.resolve(nodeA));
|
|
|
|
// Block when we enter node C so that we can re-route to node E and finish a
|
|
// path.
|
|
blockWalkerAt(nodeC);
|
|
|
|
// Wait for us to enter node d before continuing. We introduce a small delay
|
|
// to ensure that we are "stuck" on the abortable operation.
|
|
const goingToD = startNewRoute(nodeD, /* interruptible= */ true);
|
|
await waitUntilEntering(goingToD, nodeC);
|
|
await shaka.test.Util.shortDelay();
|
|
|
|
// We are "stuck" in nodeC. We will now destroy the walker which should
|
|
// abort the nodeC step, enter error recovery mode, and then shutdown.
|
|
await walker.destroy();
|
|
expect(handleErrorSpy).toHaveBeenCalled();
|
|
});
|
|
|
|
/**
|
|
* Ask the walker to start a new route. Since the requests from our tests
|
|
* are very basic, wrapping the call should not hide too much information.
|
|
*
|
|
* @param {shaka.routing.Node} goingTo
|
|
* @param {boolean} interruptible
|
|
*
|
|
* @return {shaka.routing.Walker.Listeners}
|
|
*/
|
|
function startNewRoute(goingTo, interruptible) {
|
|
return walker.startNewRoute((currentPayload) => {
|
|
return {
|
|
node: goingTo,
|
|
payload: payload,
|
|
interruptible: interruptible,
|
|
};
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Get the series of nodes that the walker went through during its "journey".
|
|
*
|
|
* @return {!Array.<shaka.routing.Node>}
|
|
*/
|
|
function getStepsTaken() {
|
|
// Use |onNode| to get the steps that completed.
|
|
return enterNodeSpy.calls.allArgs().map((args) => args[0]);
|
|
}
|
|
|
|
/**
|
|
* Configure the |enterNodeSpy| so that we will block when we enter |node|. In
|
|
* order to unblock, the route will need to be interrupted.
|
|
*
|
|
* @param {shaka.routing.Node} node
|
|
*/
|
|
function blockWalkerAt(node) {
|
|
/** @type {!shaka.util.AbortableOperation} */
|
|
const completedOp = AbortableOperation.completed(undefined);
|
|
|
|
/** @type {!shaka.util.PublicPromise} */
|
|
const waitForever = new shaka.util.PublicPromise();
|
|
|
|
/** @type {!shaka.util.AbortableOperation} */
|
|
const blockingOp = new AbortableOperation(
|
|
waitForever,
|
|
() => {
|
|
waitForever.reject(new shaka.util.Error(
|
|
shaka.util.Error.Severity.CRITICAL,
|
|
shaka.util.Error.Category.PLAYER,
|
|
shaka.util.Error.Code.OPERATION_ABORTED));
|
|
|
|
return waitForever;
|
|
});
|
|
|
|
enterNodeSpy.and.callFake((at) => {
|
|
return at == node ? blockingOp : completedOp;
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Create a promise that will resolve when we enter |node| while on the route
|
|
* that produced |events|.
|
|
*
|
|
* @param {shaka.routing.Walker.Listeners} events
|
|
* @param {shaka.routing.Node} node
|
|
* @return {!Promise}
|
|
*/
|
|
function waitUntilEntering(events, node) {
|
|
return new Promise((resolve) => {
|
|
events.onEnter = (node) => {
|
|
if (node == nodeC) {
|
|
resolve();
|
|
}
|
|
};
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Create a promise from a walker's route's listeners. This assumes that the
|
|
* route should finish. The promise will resolve if the route completes and
|
|
* will reject if the route fails to complete for any reason.
|
|
*
|
|
* @param {shaka.routing.Walker.Listeners} events
|
|
* @return {!Promise}
|
|
*/
|
|
function completesRoute(events) {
|
|
return new Promise((resolve, reject) => {
|
|
events.onEnd = resolve;
|
|
events.onCancel = reject;
|
|
events.onError = reject;
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Create a promise from a walker's route's listeners. This assumes that the
|
|
* route should not finish. The promise will resolve if the route fails and
|
|
* will reject if the route completes.
|
|
*
|
|
* @param {shaka.routing.Walker.Listeners} events
|
|
* @return {!Promise}
|
|
*/
|
|
function failsRoute(events) {
|
|
return new Promise((resolve, reject) => {
|
|
events.onEnd = reject;
|
|
events.onCancel = resolve;
|
|
events.onError = resolve;
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Wrap a spy in a promise so that the promise will resolve once the spy is
|
|
* called.
|
|
*
|
|
* @param {!jasmine.Spy} spy
|
|
* @return {!Promise}
|
|
*/
|
|
function waitOnSpy(spy) {
|
|
return new Promise((resolve) => {
|
|
spy.and.callFake(resolve);
|
|
});
|
|
}
|
|
});
|