blob: bbc3973b407af8d895a2d9a70f8a0e9776001059 [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';
// 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.
self.RingBuffer = class {
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 };
}
value() {
return this.data_[this.bufferPosition_];
}
[Symbol.iterator]() {
return this;
}
};
class DefaultSensorTraits {
// https://w3c.github.io/sensors/#threshold-check-algorithm
static isSignificantlyDifferent(reading1, reading2) {
return true;
}
// https://w3c.github.io/sensors/#reading-quantization-algorithm
static roundToMultiple(reading) {
return reading;
}
// https://w3c.github.io/ambient-light/#ambient-light-threshold-check-algorithm
static areReadingsEqual(reading1, reading2) {
return false;
}
}
class AmbientLightSensorTraits extends DefaultSensorTraits {
// https://w3c.github.io/ambient-light/#reduce-sensor-accuracy
static #ROUNDING_MULTIPLE = 50;
static #SIGNIFICANCE_THRESHOLD = 25;
// https://w3c.github.io/ambient-light/#ambient-light-threshold-check-algorithm
static isSignificantlyDifferent([illuminance1], [illuminance2]) {
return Math.abs(illuminance1 - illuminance2) >=
this.#SIGNIFICANCE_THRESHOLD;
}
// https://w3c.github.io/ambient-light/#ambient-light-reading-quantization-algorithm
static roundToMultiple(reading) {
const illuminance = reading[0];
const scaledValue =
illuminance / AmbientLightSensorTraits.#ROUNDING_MULTIPLE;
let roundedReading = reading.splice();
if (illuminance < 0.0) {
roundedReading[0] = -AmbientLightSensorTraits.#ROUNDING_MULTIPLE *
Math.floor(-scaledValue + 0.5);
} else {
roundedReading[0] = AmbientLightSensorTraits.#ROUNDING_MULTIPLE *
Math.floor(scaledValue + 0.5);
}
return roundedReading;
}
// https://w3c.github.io/ambient-light/#ambient-light-threshold-check-algorithm
static areReadingsEqual([illuminance1], [illuminance2]) {
return illuminance1 === illuminance2;
}
}
self.GenericSensorTest = (() => {
// Default sensor frequency in default configurations.
const DEFAULT_FREQUENCY = 5;
// Class that mocks Sensor interface defined in
// https://cs.chromium.org/chromium/src/services/device/public/mojom/sensor.mojom
class MockSensor {
static #BUFFER_OFFSET_TIMESTAMP = 1;
static #BUFFER_OFFSET_READINGS = 2;
constructor(sensorRequest, buffer, reportingMode, sensorType) {
this.client_ = null;
this.startShouldFail_ = false;
this.notifyOnReadingChange_ = true;
this.reportingMode_ = reportingMode;
this.sensorType_ = sensorType;
this.sensorReadingTimerId_ = null;
this.readingData_ = null;
this.requestedFrequencies_ = [];
// The Blink implementation (third_party/blink/renderer/modules/sensor/sensor.cc)
// sets a timestamp by creating a DOMHighResTimeStamp from a given platform timestamp.
// In this mock implementation we use a starting value
// and an increment step value that resemble a platform timestamp reasonably enough.
this.timestamp_ = window.performance.timeOrigin;
// |buffer| represents a SensorReadingSharedBuffer on the C++ side in
// Chromium. It consists, in this order, of a
// SensorReadingField<OneWriterSeqLock> (an 8-byte union that includes
// 32-bit integer used by the lock class), and a SensorReading consisting
// of an 8-byte timestamp and 4 8-byte reading fields.
//
// |this.buffer_[0]| is zeroed by default, which allows OneWriterSeqLock
// to work with our custom memory buffer that did not actually create a
// OneWriterSeqLock instance. It is never changed manually here.
//
// Use MockSensor.#BUFFER_OFFSET_TIMESTAMP and
// MockSensor.#BUFFER_OFFSET_READINGS to access the other positions in
// |this.buffer_| without having to hardcode magic numbers in the code.
this.buffer_ = buffer;
this.buffer_.fill(0);
this.receiver_ = new SensorReceiver(this);
this.receiver_.$.bindHandle(sensorRequest.handle);
this.lastRawReading_ = null;
this.lastRoundedReading_ = null;
if (sensorType == SensorType.AMBIENT_LIGHT) {
this.sensorTraits = AmbientLightSensorTraits;
} else {
this.sensorTraits = DefaultSensorTraits;
}
}
// Returns default configuration.
async getDefaultConfiguration() {
return { frequency: DEFAULT_FREQUENCY };
}
// Adds configuration for the sensor and starts reporting fake data
// through setSensorReading function.
async addConfiguration(configuration) {
this.requestedFrequencies_.push(configuration.frequency);
// Sort using descending order.
this.requestedFrequencies_.sort(
(first, second) => { return second - first });
if (!this.startShouldFail_ )
this.startReading();
return { success: !this.startShouldFail_ };
}
// Removes sensor configuration from the list of active configurations and
// stops notification about sensor reading changes if
// requestedFrequencies_ is empty.
removeConfiguration(configuration) {
const index = this.requestedFrequencies_.indexOf(configuration.frequency);
if (index == -1)
return;
this.requestedFrequencies_.splice(index, 1);
if (this.requestedFrequencies_.length === 0)
this.stopReading();
}
// ConfigureReadingChangeNotifications(bool enabled)
// Configures whether to report a reading change when in ON_CHANGE
// reporting mode.
configureReadingChangeNotifications(notifyOnReadingChange) {
this.notifyOnReadingChange_ = notifyOnReadingChange;
}
resume() {
this.startReading();
}
suspend() {
this.stopReading();
}
// Mock functions
// Resets mock Sensor state.
reset() {
this.stopReading();
this.startShouldFail_ = false;
this.requestedFrequencies_ = [];
this.notifyOnReadingChange_ = true;
this.readingData_ = null;
this.buffer_.fill(0);
this.receiver_.$.close();
this.lastRawReading_ = null;
this.lastRoundedReading_ = null;
}
// Sets fake data that is used to deliver sensor reading updates.
setSensorReading(readingData) {
this.readingData_ = new RingBuffer(readingData);
}
// This is a workaround to accommodate Blink's Device Orientation
// implementation. In general, all tests should use setSensorReading()
// instead.
setSensorReadingImmediately(readingData) {
this.setSensorReading(readingData);
const reading = this.readingData_.value();
this.buffer_.set(reading, MockSensor.#BUFFER_OFFSET_READINGS);
this.buffer_[MockSensor.#BUFFER_OFFSET_TIMESTAMP] = this.timestamp_++;
}
// Sets flag that forces sensor to fail when addConfiguration is invoked.
setStartShouldFail(shouldFail) {
this.startShouldFail_ = shouldFail;
}
startReading() {
if (this.readingData_ != null) {
this.stopReading();
}
let maxFrequencyUsed = this.requestedFrequencies_[0];
let timeout = (1 / maxFrequencyUsed) * 1000;
this.sensorReadingTimerId_ = window.setInterval(() => {
if (this.readingData_) {
// |buffer_| is a TypedArray, so we need to make sure pass an
// array to set().
const reading = this.readingData_.next().value;
if (!Array.isArray(reading)) {
throw new TypeError("startReading(): The readings passed to " +
"setSensorReading() must be arrays");
}
if (this.reportingMode_ == ReportingMode.ON_CHANGE &&
this.lastRawReading_ !== null &&
!this.sensorTraits.isSignificantlyDifferent(
this.lastRawReading_, reading)) {
// In case new value is not significantly different compared to
// old value, new value is not sent.
return;
}
this.lastRawReading_ = reading.slice();
const roundedReading = this.sensorTraits.roundToMultiple(reading);
if (this.reportingMode_ == ReportingMode.ON_CHANGE &&
this.lastRoundedReading_ !== null &&
this.sensorTraits.areReadingsEqual(
roundedReading, this.lastRoundedReading_)) {
// In case new rounded value is not different compared to old
// value, new value is not sent.
return;
}
this.buffer_.set(roundedReading, MockSensor.#BUFFER_OFFSET_READINGS);
this.lastRoundedReading_ = roundedReading;
}
// For all tests sensor reading should have monotonically
// increasing timestamp.
this.buffer_[MockSensor.#BUFFER_OFFSET_TIMESTAMP] = this.timestamp_++;
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() {
if (this.requestedFrequencies_.length == 0) {
throw new Error("getSamplingFrequency(): No configured frequency");
}
return this.requestedFrequencies_[0];
}
isReadingData() {
return this.sensorReadingTimerId_ != null;
}
}
// Class that mocks SensorProvider interface defined in
// https://cs.chromium.org/chromium/src/services/device/public/mojom/sensor_provider.mojom
class MockSensorProvider {
constructor() {
this.readingSizeInBytes_ =
Number(SensorInitParams_READ_BUFFER_SIZE_FOR_TESTS);
this.sharedBufferSizeInBytes_ =
this.readingSizeInBytes_ * (SensorType.MAX_VALUE + 1);
let rv = Mojo.createSharedBuffer(this.sharedBufferSizeInBytes_);
if (rv.result != Mojo.RESULT_OK) {
throw new Error('MockSensorProvider: Failed to create shared buffer');
}
const handle = rv.handle;
rv = handle.mapBuffer(0, this.sharedBufferSizeInBytes_);
if (rv.result != Mojo.RESULT_OK) {
throw new Error("MockSensorProvider: Failed to map shared buffer");
}
this.shmemArrayBuffer_ = rv.buffer;
rv = handle.duplicateBufferHandle({readOnly: true});
if (rv.result != Mojo.RESULT_OK) {
throw new Error(
'MockSensorProvider: failed to duplicate shared buffer');
}
this.readOnlySharedBufferHandle_ = rv.handle;
this.activeSensors_ = new Map();
this.resolveFuncs_ = new Map();
this.getSensorShouldFail_ = new Map();
this.permissionsDenied_ = new Map();
this.maxFrequency_ = 60;
this.minFrequency_ = 1;
this.mojomSensorType_ = new Map([
['Accelerometer', SensorType.ACCELEROMETER],
['LinearAccelerationSensor', SensorType.LINEAR_ACCELERATION],
['GravitySensor', SensorType.GRAVITY],
['AmbientLightSensor', SensorType.AMBIENT_LIGHT],
['Gyroscope', SensorType.GYROSCOPE],
['Magnetometer', SensorType.MAGNETOMETER],
['AbsoluteOrientationSensor',
SensorType.ABSOLUTE_ORIENTATION_QUATERNION],
['AbsoluteOrientationEulerAngles',
SensorType.ABSOLUTE_ORIENTATION_EULER_ANGLES],
['RelativeOrientationSensor',
SensorType.RELATIVE_ORIENTATION_QUATERNION],
['RelativeOrientationEulerAngles',
SensorType.RELATIVE_ORIENTATION_EULER_ANGLES],
['ProximitySensor', SensorType.PROXIMITY]
]);
this.receiver_ = new SensorProviderReceiver(this);
this.interceptor_ =
new MojoInterfaceInterceptor(SensorProvider.$interfaceName);
this.interceptor_.oninterfacerequest = e => {
this.bindToPipe(e.handle);
};
this.interceptor_.start();
}
// Returns initialized Sensor proxy to the client.
async getSensor(type) {
if (this.getSensorShouldFail_.get(type)) {
return {result: SensorCreationResult.ERROR_NOT_AVAILABLE,
initParams: null};
}
if (this.permissionsDenied_.get(type)) {
return {result: SensorCreationResult.ERROR_NOT_ALLOWED,
initParams: null};
}
const offset = type * this.readingSizeInBytes_;
const reportingMode = ReportingMode.ON_CHANGE;
const sensor = new SensorRemote();
if (!this.activeSensors_.has(type)) {
const shmemView = new Float64Array(
this.shmemArrayBuffer_, offset,
this.readingSizeInBytes_ / Float64Array.BYTES_PER_ELEMENT);
const mockSensor = new MockSensor(
sensor.$.bindNewPipeAndPassReceiver(), shmemView, reportingMode,
type);
this.activeSensors_.set(type, mockSensor);
this.activeSensors_.get(type).client_ = new SensorClientRemote();
}
const rv = this.readOnlySharedBufferHandle_.duplicateBufferHandle(
{readOnly: true});
if (rv.result != Mojo.RESULT_OK) {
throw new Error('getSensor(): failed to duplicate shared buffer');
}
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 (type == SensorType.AMBIENT_LIGHT || type == SensorType.MAGNETOMETER) {
this.maxFrequency_ = Math.min(10, this.maxFrequency_);
}
const client = this.activeSensors_.get(type).client_;
const initParams = {
sensor,
clientReceiver: client.$.bindNewPipeAndPassReceiver(),
memory: {buffer: rv.handle},
bufferOffset: BigInt(offset),
mode: reportingMode,
defaultConfiguration: defaultConfig,
minimumFrequency: this.minFrequency_,
maximumFrequency: this.maxFrequency_
};
if (this.resolveFuncs_.has(type)) {
for (let resolveFunc of this.resolveFuncs_.get(type)) {
resolveFunc(this.activeSensors_.get(type));
}
this.resolveFuncs_.delete(type);
}
return {result: SensorCreationResult.SUCCESS, initParams};
}
// Binds object to mojo message pipe
bindToPipe(pipe) {
this.receiver_.$.bindHandle(pipe);
}
// Mock functions
// 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.getSensorShouldFail_.clear();
this.permissionsDenied_.clear();
this.maxFrequency_ = 60;
this.minFrequency_ = 1;
this.receiver_.$.close();
this.interceptor_.stop();
}
// Sets flag that forces mock SensorProvider to fail when getSensor() is
// invoked.
setGetSensorShouldFail(sensorType, shouldFail) {
this.getSensorShouldFail_.set(this.mojomSensorType_.get(sensorType),
shouldFail);
}
setPermissionsDenied(sensorType, permissionsDenied) {
this.permissionsDenied_.set(this.mojomSensorType_.get(sensorType),
permissionsDenied);
}
// Returns mock sensor that was created in getSensor to the layout test.
getCreatedSensor(sensorType) {
const type = this.mojomSensorType_.get(sensorType);
if (typeof type != "number") {
throw new TypeError(`getCreatedSensor(): Invalid sensor type ${sensorType}`);
}
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);
});
}
// 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;
}
}
let testInternal = {
initialized: false,
sensorProvider: null
}
class GenericSensorTestChromium {
constructor() {
Object.freeze(this); // Make it immutable.
}
async initialize() {
if (testInternal.initialized)
throw new Error('Call reset() before initialize().');
// Grant sensor permissions for Chromium testdriver.
// testdriver.js only works in the top-level browsing context, so do
// nothing if we're in e.g. an iframe.
if (window.parent === window) {
for (const entry
of ['accelerometer', 'gyroscope', 'magnetometer',
'ambient-light-sensor']) {
await test_driver.set_permission({name: entry}, 'granted', false);
}
}
testInternal.sensorProvider = new MockSensorProvider;
testInternal.initialized = true;
}
// Resets state of sensor mocks between test runs.
async reset() {
if (!testInternal.initialized)
throw new Error('Call initialize() before reset().');
testInternal.sensorProvider.reset();
testInternal.sensorProvider = null;
testInternal.initialized = false;
// Wait for an event loop iteration to let any pending mojo commands in
// the sensor provider finish.
await new Promise(resolve => setTimeout(resolve, 0));
}
getSensorProvider() {
return testInternal.sensorProvider;
}
}
return GenericSensorTestChromium;
})();