Files
shaka-player/spec/networking_engine_spec.js
T
Joey Parrish e0783c1686 Fix flake in NetworkingEngine retry tests
This mocks the date and clock for retry tests to eliminate flake on
busy machines (such as our buildbot).

b/25727620

Change-Id: Ied0f1a3a4aa72ec9d8fbc5c4b36a3c91a489e668
2015-11-25 12:14:05 -08:00

572 lines
18 KiB
JavaScript

/**
* @license
* Copyright 2015 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
goog.require('shaka.net.NetworkingEngine');
goog.require('shaka.util.PublicPromise');
describe('NetworkingEngine', function() {
var networkingEngine;
var resolveScheme;
var rejectScheme;
var requestType;
beforeAll(function() {
requestType = shaka.net.NetworkingEngine.RequestType.SEGMENT;
});
beforeEach(function() {
networkingEngine = new shaka.net.NetworkingEngine();
resolveScheme = jasmine.createSpy('resolve scheme').and.callFake(
function() {
return Promise.resolve({data: new ArrayBuffer(5), headers: {}});
});
rejectScheme = jasmine.createSpy('reject scheme')
.and.callFake(function() { return Promise.reject(); });
shaka.net.NetworkingEngine.registerScheme('resolve', resolveScheme);
shaka.net.NetworkingEngine.registerScheme('reject', rejectScheme);
});
afterEach(function() {
shaka.net.NetworkingEngine.unregisterScheme('resolve');
shaka.net.NetworkingEngine.unregisterScheme('reject');
});
describe('retry', function() {
it('will retry', function(done) {
var request = {
uri: ['reject://foo'],
retryParameters: {maxAttempts: 2, baseDelay: 0}
};
rejectScheme.and.callFake(function() {
if (rejectScheme.calls.count() == 1)
return Promise.reject();
else
return Promise.resolve({data: new ArrayBuffer(0), headers: {}});
});
networkingEngine.request(requestType, request)
.catch(fail)
.then(function() {
expect(rejectScheme.calls.count()).toBe(2);
done();
});
});
it('will retry twice', function(done) {
var request = {
uri: ['reject://foo'],
retryParameters: {maxAttempts: 3, baseDelay: 0}
};
rejectScheme.and.callFake(function() {
if (rejectScheme.calls.count() < 3)
return Promise.reject();
else
return Promise.resolve({data: new ArrayBuffer(0), headers: {}});
});
networkingEngine.request(requestType, request)
.catch(fail)
.then(function() {
expect(rejectScheme.calls.count()).toBe(3);
done();
});
});
it('will fail overall', function(done) {
var request = {
uri: ['reject://foo'],
retryParameters: {maxAttempts: 3, baseDelay: 0}
};
networkingEngine.request(requestType, request)
.then(fail)
.catch(function() { expect(rejectScheme.calls.count()).toBe(3); })
.then(done);
});
describe('backoff', function() {
var baseDelay = 200;
var realSetTimeout;
var realRandom;
beforeAll(function() {
realSetTimeout = window.setTimeout;
window.setTimeout = jasmine.createSpy('setTimeout');
window.setTimeout.and.callFake(realSetTimeout);
realRandom = Math.random;
Math.random = function() { return 0.75; };
});
afterAll(function() {
Math.random = realRandom;
window.setTimeout = realSetTimeout;
});
beforeEach(function() {
window.setTimeout.calls.reset();
});
it('uses baseDelay', function(done) {
var request = {
uri: ['reject://foo'],
retryParameters: {
maxAttempts: 2,
baseDelay: baseDelay,
fuzzFactor: 0,
backoffFactor: 2
}
};
networkingEngine.request(requestType, request)
.then(fail)
.catch(function() {
expect(window.setTimeout.calls.count()).toBe(1);
expect(window.setTimeout)
.toHaveBeenCalledWith(jasmine.any(Function), baseDelay);
})
.then(done);
});
it('uses backoffFactor', function(done) {
var request = {
uri: ['reject://foo'],
retryParameters: {
maxAttempts: 3,
baseDelay: baseDelay,
fuzzFactor: 0,
backoffFactor: 2
}
};
networkingEngine.request(requestType, request)
.then(fail)
.catch(function() {
expect(window.setTimeout.calls.count()).toBe(2);
expect(window.setTimeout)
.toHaveBeenCalledWith(jasmine.any(Function), baseDelay);
expect(window.setTimeout)
.toHaveBeenCalledWith(jasmine.any(Function), baseDelay * 2);
})
.then(done);
});
it('uses fuzzFactor', function(done) {
var request = {
uri: ['reject://foo'],
retryParameters: {
maxAttempts: 2,
baseDelay: baseDelay,
fuzzFactor: 1,
backoffFactor: 1
}
};
networkingEngine.request(requestType, request)
.then(fail)
.catch(function() {
// (rand * 2.0) - 1.0 = (0.75 * 2.0) - 1.0 = 0.5
// 0.5 * fuzzFactor = 0.5 * 1 = 0.5
// delay * (1 + 0.5) = baseDelay * (1 + 0.5)
expect(window.setTimeout.calls.count()).toBe(1);
expect(window.setTimeout)
.toHaveBeenCalledWith(jasmine.any(Function), baseDelay * 1.5);
})
.then(done);
});
});
it('uses multiple URIs', function(done) {
var request = {
uri: ['reject://foo', 'resolve://foo'],
retryParameters: {maxAttempts: 3}
};
networkingEngine.request(requestType, request)
.catch(fail)
.then(function() {
expect(rejectScheme.calls.count()).toBe(1);
expect(resolveScheme.calls.count()).toBe(1);
done();
});
});
});
describe('request', function() {
it('uses registered schemes', function(done) {
networkingEngine.request(requestType, createRequest('resolve://foo'))
.catch(fail)
.then(function() {
expect(resolveScheme).toHaveBeenCalled();
done();
});
});
it('can unregister scheme', function(done) {
shaka.net.NetworkingEngine.unregisterScheme('resolve');
networkingEngine.request(requestType, createRequest('resolve://foo'))
.then(fail)
.catch(function() { expect(resolveScheme).not.toHaveBeenCalled(); })
.then(done);
});
it('rejects if scheme does not exist', function(done) {
networkingEngine.request(requestType, createRequest('foo://foo'))
.then(fail)
.catch(function() { expect(resolveScheme).not.toHaveBeenCalled(); })
.then(done);
});
it('returns the response object', function(done) {
networkingEngine.request(requestType, createRequest('resolve://foo'))
.catch(fail)
.then(function(response) {
expect(response).toBeTruthy();
expect(response.data).toBeTruthy();
expect(response.data.byteLength).toBe(5);
expect(response.headers).toBeTruthy();
done();
});
});
it('passes correct arguments to plugin', function(done) {
var request = {uri: ['resolve://foo'], method: 'POST'};
resolveScheme.and.callFake(function(uri, request) {
expect(uri).toBe(request.uri[0]);
expect(request).toBe(request);
return Promise.resolve();
});
networkingEngine.request(requestType, request).catch(fail).then(done);
});
});
describe('request filter', function() {
var filter;
beforeEach(function() {
filter = jasmine.createSpy('request filter');
networkingEngine.registerRequestFilter(filter);
});
afterEach(function() {
networkingEngine.unregisterRequestFilter(filter);
});
it('can be called', function(done) {
networkingEngine.request(requestType, createRequest('resolve://foo'))
.catch(fail)
.then(function() {
expect(filter).toHaveBeenCalled();
done();
});
});
it('called on failure', function(done) {
networkingEngine.request(requestType, createRequest('reject://foo'))
.then(fail)
.catch(function() { expect(filter).toHaveBeenCalled(); })
.then(done);
});
it('is given correct arguments', function(done) {
var request = {uri: ['resolve://foo']};
networkingEngine.request(requestType, request)
.catch(fail)
.then(function() {
expect(filter.calls.argsFor(0)[0]).toBe(requestType);
expect(filter.calls.argsFor(0)[1]).toBe(request);
expect(filter.calls.argsFor(0)[1].uri[0]).toBe(request.uri[0]);
done();
});
});
it('can modify uri', function(done) {
filter.and.callFake(function(type, request) {
request.uri = ['resolve://foo'];
});
networkingEngine.request(requestType, createRequest('reject://foo'))
.catch(fail)
.then(function() {
expect(filter).toHaveBeenCalled();
done();
});
});
it('can modify allowCrossSiteCredentials', function(done) {
filter.and.callFake(function(type, request) {
request.allowCrossSiteCredentials = true;
});
networkingEngine.request(requestType, createRequest('resolve://foo'))
.catch(fail)
.then(function() {
expect(filter).toHaveBeenCalled();
expect(resolveScheme).toHaveBeenCalled();
expect(resolveScheme.calls.argsFor(0)[1].allowCrossSiteCredentials)
.toBe(true);
done();
});
});
it('if throws will stop requests', function(done) {
var request = {
uri: ['resolve://foo'],
retryParameters: {maxAttempts: 3, baseRetryDelay: 0}
};
filter.and.throwError(new Error());
networkingEngine.request(requestType, request)
.then(fail)
.catch(function() {
expect(resolveScheme).not.toHaveBeenCalled();
expect(filter.calls.count()).toBe(1);
})
.then(done);
});
});
describe('response filter', function() {
var filter;
beforeEach(function() {
filter = jasmine.createSpy('response filter');
networkingEngine.registerResponseFilter(filter);
resolveScheme.and.callFake(function(request) {
var response = {data: new ArrayBuffer(100), headers: {}};
return Promise.resolve(response);
});
});
afterEach(function() {
networkingEngine.unregisterResponseFilter(filter);
});
it('can be called', function(done) {
networkingEngine.request(requestType, createRequest('resolve://foo'))
.catch(fail)
.then(function() {
expect(filter).toHaveBeenCalled();
done();
});
});
it('not called on failure', function(done) {
networkingEngine.request(requestType, createRequest('reject://foo'))
.then(fail)
.catch(function() { expect(filter).not.toHaveBeenCalled(); })
.then(done);
});
it('is given correct arguments', function(done) {
var request = {uri: ['resolve://foo']};
networkingEngine.request(requestType, request)
.catch(fail)
.then(function() {
expect(filter.calls.argsFor(0)[0]).toBe(requestType);
expect(filter.calls.argsFor(0)[1]).toBeTruthy();
expect(filter.calls.argsFor(0)[1].data).toBeTruthy();
expect(filter.calls.argsFor(0)[1].headers).toBeTruthy();
done();
});
});
it('can modify data', function(done) {
filter.and.callFake(function(type, response) {
response.data = new ArrayBuffer(5);
});
networkingEngine.request(requestType, createRequest('resolve://foo'))
.catch(fail)
.then(function(response) {
expect(filter).toHaveBeenCalled();
expect(response).toBeTruthy();
expect(response.data.byteLength).toBe(5);
done();
});
});
it('can modify headers', function(done) {
filter.and.callFake(function(type, response) {
expect(response.headers).toBeTruthy();
response.headers['DATE'] = 'CAT';
});
networkingEngine.request(requestType, createRequest('resolve://foo'))
.catch(fail)
.then(function(response) {
expect(filter).toHaveBeenCalled();
expect(response).toBeTruthy();
expect(response.headers['DATE']).toBe('CAT');
done();
});
});
it('if throws will stop requests', function(done) {
filter.and.throwError(new Error());
networkingEngine.request(requestType, createRequest('resolve://foo'))
.then(fail)
.catch(function() { expect(filter).toHaveBeenCalled(); })
.then(done);
});
it('if throws will retry', function(done) {
var request = {
uri: ['resolve://foo'],
retryParameters: {maxAttempts: 2, baseRetryDelay: 0}
};
filter.and.callFake(function() {
if (filter.calls.count() == 1) throw new Error();
});
networkingEngine.request(requestType, request)
.catch(fail)
.then(function() {
expect(resolveScheme.calls.count()).toBe(2);
expect(filter.calls.count()).toBe(2);
done();
});
});
});
describe('destroy', function() {
it('waits for all operations to complete', function(done) {
var request = {uri: ['resolve://foo']};
var p = new shaka.util.PublicPromise();
resolveScheme.and.returnValue(p);
var r1 = networkingEngine.request(requestType, request);
var r2 = networkingEngine.request(requestType, request);
capturePromiseStatus(r1);
capturePromiseStatus(r2);
expect(r1.status).toBe('pending');
expect(r2.status).toBe('pending');
var d = networkingEngine.destroy();
capturePromiseStatus(d);
expect(d.status).toBe('pending');
delay(0.1).then(function() {
expect(d.status).toBe('pending');
p.resolve();
return delay(0.1);
}).then(function() {
expect(r1.status).toBe('resolved');
expect(r2.status).toBe('resolved');
expect(d.status).toBe('resolved');
done();
});
});
it('resolves even when a request fails', function(done) {
var request = {uri: ['reject://foo']};
var p = new shaka.util.PublicPromise();
rejectScheme.and.returnValue(p);
var r1 = networkingEngine.request(requestType, request);
var r2 = networkingEngine.request(requestType, request);
capturePromiseStatus(r1);
capturePromiseStatus(r2);
expect(r1.status).toBe('pending');
expect(r2.status).toBe('pending');
var d = networkingEngine.destroy();
capturePromiseStatus(d);
expect(d.status).toBe('pending');
delay(0.1).then(function() {
expect(d.status).toBe('pending');
p.reject();
return delay(0.1);
}).then(function() {
expect(r1.status).toBe('rejected');
expect(r2.status).toBe('rejected');
expect(d.status).toBe('resolved');
done();
});
});
it('prevents new requests', function(done) {
var request = {uri: ['resolve://foo']};
var p = new shaka.util.PublicPromise();
resolveScheme.and.returnValue(p);
var r1 = networkingEngine.request(requestType, request);
capturePromiseStatus(r1);
expect(r1.status).toBe('pending');
// The request has already been made.
expect(resolveScheme.calls.count()).toBe(1);
var d = networkingEngine.destroy();
capturePromiseStatus(d);
expect(d.status).toBe('pending');
var r2 = networkingEngine.request(requestType, request);
capturePromiseStatus(r2);
expect(r2.status).toBe('pending');
// A new request has not been made.
expect(resolveScheme.calls.count()).toBe(1);
delay(0.1).then(function() {
expect(r1.status).toBe('pending');
expect(r2.status).toBe('rejected');
expect(d.status).toBe('pending');
p.resolve();
return delay(0.1);
}).then(function() {
expect(r1.status).toBe('resolved');
expect(r2.status).toBe('rejected');
expect(d.status).toBe('resolved');
expect(resolveScheme.calls.count()).toBe(1);
done();
});
});
it('does not allow further retries', function(done) {
var request = {
uri: ['reject://foo'],
retryParameters: {maxAttempts: 3, baseDelay: 0}
};
var p1 = new shaka.util.PublicPromise();
var p2 = new shaka.util.PublicPromise();
rejectScheme.and.callFake(function() {
return (rejectScheme.calls.count() == 1) ? p1 : p2;
});
var r1 = networkingEngine.request(requestType, request);
capturePromiseStatus(r1);
expect(r1.status).toBe('pending');
expect(rejectScheme.calls.count()).toBe(1);
var d = networkingEngine.destroy();
capturePromiseStatus(d);
expect(d.status).toBe('pending');
delay(0.1).then(function() {
expect(r1.status).toBe('pending');
expect(d.status).toBe('pending');
expect(rejectScheme.calls.count()).toBe(1);
// Reject the initial request.
p1.reject();
// Resolve any retry, but since we have already been destroyed, this
// promise should not be used.
p2.resolve();
return delay(0.1);
}).then(function() {
expect(d.status).toBe('resolved');
// The request was never retried.
expect(r1.status).toBe('rejected');
expect(rejectScheme.calls.count()).toBe(1);
done();
});
});
});
function createRequest(uri) {
return { uri: [uri] };
}
});