blob: a707441fd8b64c118a130ff3aacfe51e8c62328a [file] [log] [blame]
import {ReportingMode, Sensor, SensorClientRemote, SensorReceiver, SensorRemote, SensorType} from '/gen/services/device/public/mojom/sensor.mojom.m.js';
import {SensorCreationResult, SensorInitParams_READ_BUFFER_SIZE_FOR_TESTS, SensorProvider, SensorProviderReceiver} from '/gen/services/device/public/mojom/sensor_provider.mojom.m.js';
// Default sensor frequency in default configurations.
const DEFAULT_FREQUENCY = 5;
// A "sliding window" that iterates over |data| and returns one item at a
// time, advancing and wrapping around as needed. |data| must be an array of
// arrays.
class RingBuffer {
constructor(data) {
this.bufferPosition_ = 0;
// Validate |data|'s format and deep-copy every element.
this.data_ = Array.from(data, element => {
if (!Array.isArray(element)) {
throw new TypeError('Every |data| element must be an array.');
}
return Array.from(element);
})
}
next() {
const value = this.data_[this.bufferPosition_];
this.bufferPosition_ = (this.bufferPosition_ + 1) % this.data_.length;
return { done: false, value: value };
}
[Symbol.iterator]() {
return this;
}
}
export function sensorMocks() {
// Class that mocks Sensor interface defined in sensor.mojom
class MockSensor {
constructor(sensorRequest, sharedBufferHandle, offset, size, reportingMode) {
this.client_ = null;
this.startShouldFail_ = false;
this.notifyOnReadingChange_ = true;
this.reportingMode_ = reportingMode;
this.sensorReadingTimerId_ = null;
this.readingData_ = null;
this.suspendCalled_ = null;
this.resumeCalled_ = null;
this.addConfigurationCalled_ = null;
this.removeConfigurationCalled_ = null;
this.requestedFrequencies_ = [];
const rv = sharedBufferHandle.mapBuffer(offset, size);
assert_equals(rv.result, Mojo.RESULT_OK, "Failed to map shared buffer");
this.bufferArray_ = rv.buffer;
this.buffer_ = new Float64Array(this.bufferArray_);
this.resetBuffer();
this.receiver_ = new SensorReceiver(this);
this.receiver_.$.bindHandle(sensorRequest.handle);
}
// device.mojom.Sensor implementation
// Mojo functions that return a value must be async and return an object
// whose keys match the names declared in Mojo.
// GetDefaultConfiguration() => (SensorConfiguration configuration)
// Returns default configuration.
async getDefaultConfiguration() {
return { frequency: DEFAULT_FREQUENCY };
}
// AddConfiguration(SensorConfiguration configuration) => (bool success)
// Adds configuration for the sensor and starts reporting fake data
// through setSensorReading function.
async addConfiguration(configuration) {
assert_not_equals(configuration, null, "Invalid sensor configuration.");
this.requestedFrequencies_.push(configuration.frequency);
// Sort using descending order.
this.requestedFrequencies_.sort(
(first, second) => { return second - first });
if (!this.startShouldFail_ )
this.startReading();
if (this.addConfigurationCalled_ != null)
this.addConfigurationCalled_(this);
return { success: !this.startShouldFail_ };
}
// RemoveConfiguration(SensorConfiguration configuration)
// Removes sensor configuration from the list of active configurations and
// stops notification about sensor reading changes if
// requestedFrequencies_ is empty.
removeConfiguration(configuration) {
if (this.removeConfigurationCalled_ != null) {
this.removeConfigurationCalled_(this);
}
const index = this.requestedFrequencies_.indexOf(configuration.frequency);
if (index == -1)
return;
this.requestedFrequencies_.splice(index, 1);
if (this.requestedFrequencies_.length === 0)
this.stopReading();
}
// Suspend()
suspend() {
this.stopReading();
if (this.suspendCalled_ != null) {
this.suspendCalled_(this);
}
}
// Resume()
resume() {
assert_equals(this.sensorReadingTimerId_, null);
this.startReading();
if (this.resumeCalled_ != null) {
this.resumeCalled_(this);
}
}
// ConfigureReadingChangeNotifications(bool enabled)
// Configures whether to report a reading change when in ON_CHANGE
// reporting mode.
configureReadingChangeNotifications(notifyOnReadingChange) {
this.notifyOnReadingChange_ = notifyOnReadingChange;
}
// Mock functions
// Resets mock Sensor state.
reset() {
this.stopReading();
this.startShouldFail_ = false;
this.notifyOnReadingChange_ = true;
this.readingData_ = null;
this.requestedFrequencies_ = [];
this.suspendCalled_ = null;
this.resumeCalled_ = null;
this.addConfigurationCalled_ = null;
this.removeConfigurationCalled_ = null;
this.resetBuffer();
this.bufferArray_ = null;
this.receiver_.$.close();
}
// Zeroes shared buffer.
resetBuffer() {
this.buffer_.fill(0);
}
// Sets fake data that is used to deliver sensor reading updates.
async setSensorReading(readingData) {
this.readingData_ = new RingBuffer(readingData);
return this;
}
// Sets flag that forces sensor to fail when addConfiguration is invoked.
setStartShouldFail(shouldFail) {
this.startShouldFail_ = shouldFail;
}
// Returns resolved promise if suspend() was called, rejected otherwise.
suspendCalled() {
return new Promise(resolve => {
this.suspendCalled_ = resolve;
});
}
// Returns resolved promise if resume() was called, rejected otherwise.
resumeCalled() {
return new Promise(resolve => {
this.resumeCalled_ = resolve;
});
}
// Resolves promise when addConfiguration() is called.
addConfigurationCalled() {
return new Promise(resolve => {
this.addConfigurationCalled_ = resolve;
});
}
// Resolves promise when removeConfiguration() is called.
removeConfigurationCalled() {
return new Promise(resolve => {
this.removeConfigurationCalled_ = resolve;
});
}
startReading() {
if (this.readingData_ != null) {
this.stopReading();
const maxFrequencyUsed = this.requestedFrequencies_[0];
const timeout = (1 / maxFrequencyUsed) * 1000;
this.sensorReadingTimerId_ = window.setInterval(() => {
if (this.readingData_) {
// |buffer_| is a TypedArray, so we need to make sure we pass an
// array to set().
const reading = this.readingData_.next().value;
assert_true(Array.isArray(reading), "The readings passed to " +
"setSensorReading() must arrays.");
this.buffer_.set(reading, 2);
// For all tests sensor reading should have monotonically
// increasing timestamp in seconds.
this.buffer_[1] = window.performance.now() * 0.001;
}
if (this.reportingMode_ === ReportingMode.ON_CHANGE &&
this.notifyOnReadingChange_) {
this.client_.sensorReadingChanged();
}
}, timeout);
}
}
stopReading() {
if (this.sensorReadingTimerId_ != null) {
window.clearInterval(this.sensorReadingTimerId_);
this.sensorReadingTimerId_ = null;
}
}
getSamplingFrequency() {
assert_true(this.requestedFrequencies_.length > 0);
return this.requestedFrequencies_[0];
}
}
// This class aggregates information about a given sensor type that is used by
// MockSensorProvider when it is asked to create a new MockSensor.
class SensorTypeSettings {
constructor(mojoSensorType) {
this.mojoSensorType_ = mojoSensorType;
assert_greater_than_equal(mojoSensorType, SensorType.MIN_VALUE);
assert_less_than_equal(mojoSensorType, SensorType.MAX_VALUE);
this.shouldDenyRequests_ = false;
this.unavailable_ = false;
}
get mojoSensorType() {
return this.mojoSensorType_;
}
get shouldDenyRequests() {
return this.shouldDenyRequests_;
}
set shouldDenyRequests(deny) {
this.shouldDenyRequests_ = deny;
}
get unavailable() {
return this.unavailable_;
}
set unavailable(is_unavailable) {
this.unavailable_ = is_unavailable;
}
}
// Maps a given device.mojom.SensorType enum value to a suitable name as a
// string.
function getSensorTypeName(mojoSensorType) {
switch (mojoSensorType) {
case SensorType.ACCELEROMETER:
return 'Accelerometer';
case SensorType.LINEAR_ACCELERATION:
return 'LinearAccelerationSensor';
case SensorType.AMBIENT_LIGHT:
return 'AmbientLightSensor';
case SensorType.GRAVITY:
return 'GravitySensor';
case SensorType.GYROSCOPE:
return 'Gyroscope';
case SensorType.MAGNETOMETER:
return 'Magnetometer';
case SensorType.ABSOLUTE_ORIENTATION_QUATERNION:
return 'AbsoluteOrientationSensor';
case SensorType.ABSOLUTE_ORIENTATION_EULER_ANGLES:
return 'AbsoluteOrientationEulerAngles';
case SensorType.RELATIVE_ORIENTATION_QUATERNION:
return 'RelativeOrientationSensor';
case SensorType.RELATIVE_ORIENTATION_EULER_ANGLES:
return 'RelativeOrientationEulerAngles';
}
}
// Class that mocks SensorProvider interface defined in
// sensor_provider.mojom
class MockSensorProvider {
constructor() {
this.readingSizeInBytes_ =
Number(SensorInitParams_READ_BUFFER_SIZE_FOR_TESTS);
this.sharedBufferSizeInBytes_ =
this.readingSizeInBytes_ * (SensorType.MAX_VALUE + 1);
const rv = Mojo.createSharedBuffer(this.sharedBufferSizeInBytes_);
assert_equals(rv.result, Mojo.RESULT_OK, "Failed to create buffer");
this.sharedBufferHandle_ = rv.handle;
this.activeSensors_ = new Map();
this.resolveFuncs_ = new Map();
this.isContinuous_ = false;
this.maxFrequency_ = 60;
this.minFrequency_ = 1;
this.resetSensorTypeSettings();
this.receiver_ = new SensorProviderReceiver(this);
this.interceptor_ =
new MojoInterfaceInterceptor(SensorProvider.$interfaceName);
this.interceptor_.oninterfacerequest =
e => this.receiver_.$.bindHandle(e.handle);
this.interceptor_.start();
}
// device.mojom.SensorProvider implementation
// Mojo functions that return a value must be async and return an object
// whose keys match the names declared in Mojo.
// GetSensor(SensorType type) => (SensorCreationResult result,
// SensorInitParams? init_params)
// Returns initialized Sensor proxy to the client.
async getSensor(mojoSensorType) {
const sensorSettings = this.sensorTypeSettings_.get(getSensorTypeName(mojoSensorType));
if (sensorSettings.unavailable) {
return {result: SensorCreationResult.ERROR_NOT_AVAILABLE,
initParams: null};
}
if (sensorSettings.shouldDenyRequests) {
return {result: SensorCreationResult.ERROR_NOT_ALLOWED,
initParams: null};
}
const offset = mojoSensorType * this.readingSizeInBytes_;
const reportingMode = this.isContinuous_ ?
ReportingMode.CONTINUOUS : ReportingMode.ON_CHANGE;
const sensorPtr = new SensorRemote();
if (!this.activeSensors_.has(mojoSensorType)) {
const mockSensor = new MockSensor(
sensorPtr.$.bindNewPipeAndPassReceiver(), this.sharedBufferHandle_,
offset, this.readingSizeInBytes_, reportingMode);
this.activeSensors_.set(mojoSensorType, mockSensor);
this.activeSensors_.get(mojoSensorType).client_ = new SensorClientRemote();
}
const rv = this.sharedBufferHandle_.duplicateBufferHandle();
assert_equals(rv.result, Mojo.RESULT_OK);
const defaultConfig = { frequency: DEFAULT_FREQUENCY };
// Consider sensor traits to meet assertions in C++ code (see
// services/device/public/cpp/generic_sensor/sensor_traits.h)
if (mojoSensorType == SensorType.AMBIENT_LIGHT ||
mojoSensorType == SensorType.MAGNETOMETER) {
this.maxFrequency_ = Math.min(10, this.maxFrequency_);
}
const client = this.activeSensors_.get(mojoSensorType).client_;
const initParams = {
sensor: sensorPtr,
clientReceiver: client.$.bindNewPipeAndPassReceiver(),
memory: rv.handle,
bufferOffset: BigInt(offset),
mode: reportingMode,
defaultConfiguration: defaultConfig,
minimumFrequency: this.minFrequency_,
maximumFrequency: this.maxFrequency_
};
if (this.resolveFuncs_.has(mojoSensorType)) {
for (let resolveFunc of this.resolveFuncs_.get(mojoSensorType)) {
resolveFunc(this.activeSensors_.get(mojoSensorType));
}
this.resolveFuncs_.delete(mojoSensorType);
}
return {result: SensorCreationResult.SUCCESS, initParams};
}
// Mock functions
// Returns a SensorTypeSettings instance corresponding to the name |type|, a
// string.
getSensorTypeSettings(type) {
return this.sensorTypeSettings_.get(type);
}
// Recreates |this.sensorTypeSettings_| with a new map and values reset to
// their defaults.
resetSensorTypeSettings() {
this.sensorTypeSettings_ = new Map([
['Accelerometer', new SensorTypeSettings(SensorType.ACCELEROMETER)],
['LinearAccelerationSensor', new SensorTypeSettings(SensorType.LINEAR_ACCELERATION)],
['AmbientLightSensor', new SensorTypeSettings(SensorType.AMBIENT_LIGHT)],
['GravitySensor', new SensorTypeSettings(SensorType.GRAVITY)],
['Gyroscope', new SensorTypeSettings(SensorType.GYROSCOPE)],
['Magnetometer', new SensorTypeSettings(SensorType.MAGNETOMETER)],
['AbsoluteOrientationSensor', new SensorTypeSettings(SensorType.ABSOLUTE_ORIENTATION_QUATERNION)],
['AbsoluteOrientationEulerAngles', new SensorTypeSettings(SensorType.ABSOLUTE_ORIENTATION_EULER_ANGLES)],
['RelativeOrientationSensor', new SensorTypeSettings(SensorType.RELATIVE_ORIENTATION_QUATERNION)],
['RelativeOrientationEulerAngles', new SensorTypeSettings(SensorType.RELATIVE_ORIENTATION_EULER_ANGLES)]
]);
}
// Resets state of mock SensorProvider between test runs.
reset() {
for (const sensor of this.activeSensors_.values()) {
sensor.reset();
}
this.activeSensors_.clear();
this.resolveFuncs_.clear();
this.resetSensorTypeSettings();
this.maxFrequency_ = 60;
this.minFrequency_ = 1;
this.isContinuous_ = false;
this.receiver_.$.close();
this.interceptor_.stop();
}
// Returns mock sensor that was created in getSensor to the layout test.
getCreatedSensor(sensorName) {
const type = this.sensorTypeSettings_.get(sensorName).mojoSensorType;
if (this.activeSensors_.has(type)) {
return Promise.resolve(this.activeSensors_.get(type));
}
return new Promise(resolve => {
if (!this.resolveFuncs_.has(type)) {
this.resolveFuncs_.set(type, []);
}
this.resolveFuncs_.get(type).push(resolve);
});
}
// Forces sensor to use |reportingMode| as an update mode.
setContinuousReportingMode() {
this.isContinuous_ = true;
}
// Sets the maximum frequency for a concrete sensor.
setMaximumSupportedFrequency(frequency) {
this.maxFrequency_ = frequency;
}
// Sets the minimum frequency for a concrete sensor.
setMinimumSupportedFrequency(frequency) {
this.minFrequency_ = frequency;
}
}
return new MockSensorProvider();
}
export function sensor_test(func, name, properties) {
promise_test(async t => {
const sensorProvider = sensorMocks();
// Clean up and reset mock sensor stubs asynchronously, so that the blink
// side closes its proxies and notifies JS sensor objects before new test is
// started.
try {
await func(t, sensorProvider);
} finally {
sensorProvider.reset();
await new Promise(resolve => { setTimeout(resolve, 0); });
};
}, name, properties);
}
export async function setMockSensorDataForType(sensorProvider, sensorType, mockDataArray) {
const createdSensor = await sensorProvider.getCreatedSensor(sensorType);
return createdSensor.setSensorReading([mockDataArray]);
}
// Returns a promise that will be resolved when an event equal to the given
// event is fired.
export function waitForEvent(expectedEvent, targetWindow = window) {
const stringify = (thing, targetWindow) => {
if (thing instanceof targetWindow.Object && thing.constructor !== targetWindow.Object) {
let str = '{';
for (let key of Object.keys(Object.getPrototypeOf(thing))) {
str += JSON.stringify(key) + ': ' + stringify(thing[key], targetWindow) + ', ';
}
return str + '}';
} else if (thing instanceof Number) {
return thing.toFixed(6);
}
return JSON.stringify(thing);
};
return new Promise((resolve, reject) => {
let events = [];
let timeoutId = null;
const expectedEventString = stringify(expectedEvent, window);
function listener(event) {
const eventString = stringify(event, targetWindow);
if (eventString === expectedEventString) {
targetWindow.clearTimeout(timeoutId);
targetWindow.removeEventListener(expectedEvent.type, listener);
resolve();
} else {
events.push(eventString);
}
}
targetWindow.addEventListener(expectedEvent.type, listener);
timeoutId = targetWindow.setTimeout(() => {
targetWindow.removeEventListener(expectedEvent.type, listener);
let errorMessage = 'Timeout waiting for expected event: ' + expectedEventString;
if (events.length == 0) {
errorMessage += ', no events were fired';
} else {
errorMessage += ', received events: '
for (let event of events) {
errorMessage += event + ', ';
}
}
reject(errorMessage);
}, 500);
});
}