diff --git a/bundles.json b/bundles.json index d7edba5db7..aeeccff228 100644 --- a/bundles.json +++ b/bundles.json @@ -6,6 +6,7 @@ "platform/commonUI/edit", "platform/commonUI/dialog", "platform/commonUI/general", + "platform/telemetry", "example/persistence" ] \ No newline at end of file diff --git a/platform/telemetry/bundle.json b/platform/telemetry/bundle.json index ccd45217a0..10256fd17c 100644 --- a/platform/telemetry/bundle.json +++ b/platform/telemetry/bundle.json @@ -21,7 +21,7 @@ { "key": "telemetry", "implementation": "TelemetryCapability.js", - "depends": [ "telemetryService" ] + "depends": [ "$injector", "$q", "$log" ] } ] } diff --git a/platform/telemetry/src/TelemetryCapability.js b/platform/telemetry/src/TelemetryCapability.js index e5863bb93f..8a781880b9 100644 --- a/platform/telemetry/src/TelemetryCapability.js +++ b/platform/telemetry/src/TelemetryCapability.js @@ -12,10 +12,27 @@ define( * * @constructor */ - function TelemetryCapability(telemetryService, domainObject) { + function TelemetryCapability($injector, $q, $log, domainObject) { + var telemetryService; + + // We could depend on telemetryService directly, but + // there isn't a platform implementation of this; + function getTelemetryService() { + if (!telemetryService) { + try { + telemetryService = + $q.when($injector.get("telemetryService")); + } catch (e) { + $log.warn("Telemetry service unavailable"); + telemetryService = $q.reject(e); + } + } + return telemetryService; + } + function buildRequest(request) { var type = domainObject.getCapability("type"), - typeRequest = type.getDefinition().telemetry || {}, + typeRequest = (type && type.getDefinition().telemetry) || {}, modelTelemetry = domainObject.getModel().telemetry, fullRequest = Object.create(typeRequest); @@ -45,10 +62,15 @@ define( key = fullRequest.key; function getRelevantResponse(response) { - return (response[source] || {})[key] || {}; + return ((response || {})[source] || {})[key] || {}; } - return telemetryService.requestTelemetry([fullRequest]) + function requestTelemetryFromService(telemetryService) { + return telemetryService.requestTelemetry([fullRequest]); + } + + return getTelemetryService() + .then(requestTelemetryFromService) .then(getRelevantResponse); } @@ -57,12 +79,13 @@ define( getMetadata: function () { return buildRequest({}); } - //subscribe: subscribe }; } TelemetryCapability.appliesTo = function (model) { - return model.telemetry; + return (model && + model.telemetry && + model.telemetry.source) ? true : false; }; return TelemetryCapability; diff --git a/platform/telemetry/src/TelemetryController.js b/platform/telemetry/src/TelemetryController.js index 1061559181..67f8ad7186 100644 --- a/platform/telemetry/src/TelemetryController.js +++ b/platform/telemetry/src/TelemetryController.js @@ -10,33 +10,13 @@ define( /** * Serves as a reusable controller for views (or parts of views) - * which need to issue, use telemetry controls. + * which need to issue requests for telemetry data and use the + * results * * @constructor */ function TelemetryController($scope, $q, $timeout, $log) { - /* - Want a notion of "the data set": All the latest data. - It can look like: - { - "source": { - "key": { - ...Telemetry object... - } - } - } - Then, telemetry controller should provide: - { - // Element for which there is telemetry data available - elements: [ { <-- the objects to view - name: ...human-readable - metadata: ... - object: ...the domain object - data: ...telemetry data for that element - } ] - } - */ var self = { ids: [], response: {}, @@ -91,9 +71,7 @@ define( })); } - function promiseRelevantDomainObjects() { - var domainObject = $scope.domainObject; - + function promiseRelevantDomainObjects(domainObject) { if (!domainObject) { return $q.when([]); } @@ -130,6 +108,14 @@ define( domainObject.getId(), " but none was found." ].join("")); + + self.response[domainObject.getId()] = { + name: domainObject.getModel().name, + domainObject: domainObject, + metadata: {}, + pending: 0, + data: {} + }; } } @@ -149,8 +135,9 @@ define( } } - function getTelemetryObjects() { - promiseRelevantDomainObjects().then(buildResponseContainers); + function getTelemetryObjects(domainObject) { + promiseRelevantDomainObjects(domainObject) + .then(buildResponseContainers); } function startTimeout() { @@ -163,7 +150,7 @@ define( self.refreshing = false; startTimeout(); - }, 1000); + }, self.interval); } } diff --git a/platform/telemetry/test/TelemetryAggregatorSpec.js b/platform/telemetry/test/TelemetryAggregatorSpec.js new file mode 100644 index 0000000000..bb6a48df11 --- /dev/null +++ b/platform/telemetry/test/TelemetryAggregatorSpec.js @@ -0,0 +1,80 @@ +/*global define,Promise,describe,it,expect,beforeEach,waitsFor,jasmine*/ + +define( + ["../src/TelemetryAggregator"], + function (TelemetryAggregator) { + "use strict"; + + describe("The telemetry aggregator", function () { + var mockQ, + mockProviders, + aggregator; + + function mockPromise(value) { + return { + then: function (callback) { + return mockPromise(callback(value)); + } + }; + } + + function mockProvider(key, index) { + var provider = jasmine.createSpyObj( + "provider" + index, + [ "requestTelemetry" ] + ); + provider.requestTelemetry.andReturn({ someKey: key }); + return provider; + } + + beforeEach(function () { + mockQ = jasmine.createSpyObj("$q", [ "all" ]); + mockQ.all.andReturn(mockPromise([])); + + mockProviders = [ "a", "b", "c" ].map(mockProvider); + + aggregator = new TelemetryAggregator(mockQ, mockProviders); + }); + + it("passes requests to aggregated providers", function () { + var requests = [ + { someKey: "some value" }, + { someKey: "some other value" } + ]; + + aggregator.requestTelemetry(requests); + + mockProviders.forEach(function (mockProvider) { + expect(mockProvider.requestTelemetry) + .toHaveBeenCalledWith(requests); + }); + }); + + it("merges results from all providers", function () { + var capture = jasmine.createSpy("capture"); + + mockQ.all.andReturn(mockPromise([ + { someKey: "some value" }, + { someOtherKey: "some other value" } + ])); + + aggregator.requestTelemetry().then(capture); + + // Verify that aggregator results were run through + // $q.all + expect(mockQ.all).toHaveBeenCalledWith([ + { someKey: 'a' }, + { someKey: 'b' }, + { someKey: 'c' } + ]); + + expect(capture).toHaveBeenCalledWith({ + someKey: "some value", + someOtherKey: "some other value" + }); + }); + + + }); + } +); \ No newline at end of file diff --git a/platform/telemetry/test/TelemetryCapabilitySpec.js b/platform/telemetry/test/TelemetryCapabilitySpec.js new file mode 100644 index 0000000000..045da297df --- /dev/null +++ b/platform/telemetry/test/TelemetryCapabilitySpec.js @@ -0,0 +1,132 @@ +/*global define,Promise,describe,it,expect,beforeEach,waitsFor,jasmine*/ + +define( + ["../src/TelemetryCapability"], + function (TelemetryCapability) { + "use strict"; + + describe("The telemetry capability", function () { + var mockInjector, + mockQ, + mockLog, + mockDomainObject, + mockTelemetryService, + mockReject, + telemetry; + + + function mockPromise(value) { + return { + then: function (callback) { + return mockPromise(callback(value)); + } + }; + } + + beforeEach(function () { + mockInjector = jasmine.createSpyObj("$injector", ["get"]); + mockQ = jasmine.createSpyObj("$q", ["when", "reject"]); + mockLog = jasmine.createSpyObj("$log", ["warn", "info", "debug"]); + mockDomainObject = jasmine.createSpyObj( + "domainObject", + [ "getId", "getCapability", "getModel" ] + ); + mockTelemetryService = jasmine.createSpyObj( + "telemetryService", + [ "requestTelemetry" ] + ); + mockReject = jasmine.createSpyObj("reject", ["then"]); + + mockInjector.get.andReturn(mockTelemetryService); + + mockQ.when.andCallFake(mockPromise); + mockQ.reject.andReturn(mockReject); + + mockDomainObject.getId.andReturn("testId"); + mockDomainObject.getModel.andReturn({ + telemetry: { + source: "testSource", + key: "testKey" + } + }); + + // Bubble up... + mockReject.then.andReturn(mockReject); + + telemetry = new TelemetryCapability( + mockInjector, + mockQ, + mockLog, + mockDomainObject + ); + }); + + it("applies only to objects with telemetry sources", function () { + expect(TelemetryCapability.appliesTo({ + telemetry: { source: "testSource" } + })).toBeTruthy(); + expect(TelemetryCapability.appliesTo({ + telemetry: { xsource: "testSource" } + })).toBeFalsy(); + expect(TelemetryCapability.appliesTo({ + xtelemetry: { source: "testSource" } + })).toBeFalsy(); + expect(TelemetryCapability.appliesTo({})).toBeFalsy(); + expect(TelemetryCapability.appliesTo()).toBeFalsy(); + }); + + it("gets a telemetry service from the injector", function () { + telemetry.requestData(); + expect(mockInjector.get) + .toHaveBeenCalledWith("telemetryService"); + }); + + it("applies request arguments", function () { + telemetry.requestData({ start: 42 }); + expect(mockTelemetryService.requestTelemetry) + .toHaveBeenCalledWith([{ + id: "testId", // from domain object + source: "testSource", // from model + key: "testKey", // from model + start: 42 // from argument + }]); + + }); + + it("provides telemetry metadata", function () { + expect(telemetry.getMetadata()).toEqual({ + id: "testId", // from domain object + source: "testSource", + key: "testKey" + }); + }); + + it("uses domain object as a key if needed", function () { + // Don't include key in telemetry + mockDomainObject.getModel.andReturn({ + telemetry: { source: "testSource" } + }); + + // Should have used the domain object's ID + expect(telemetry.getMetadata()).toEqual({ + id: "testId", // from domain object + source: "testSource", // from model + key: "testId" // from domain object + }); + }); + + + it("warns if no telemetry service can be injected", function () { + mockInjector.get.andCallFake(function () { throw ""; }); + + // Verify precondition + expect(mockLog.warn).not.toHaveBeenCalled(); + + telemetry.requestData(); + + expect(mockLog.warn).toHaveBeenCalled(); + }); + + }); + } +); \ No newline at end of file diff --git a/platform/telemetry/test/TelemetryControllerSpec.js b/platform/telemetry/test/TelemetryControllerSpec.js new file mode 100644 index 0000000000..decc414e91 --- /dev/null +++ b/platform/telemetry/test/TelemetryControllerSpec.js @@ -0,0 +1,193 @@ +/*global define,Promise,describe,it,expect,beforeEach,waitsFor,jasmine*/ + +define( + ["../src/TelemetryController"], + function (TelemetryController) { + "use strict"; + + describe("The telemetry controller", function () { + var mockScope, + mockQ, + mockTimeout, + mockLog, + mockDomainObject, + mockTelemetry, + controller; + + function mockPromise(value) { + return (value && value.then) ? value : { + then: function (callback) { + return mockPromise(callback(value)); + } + }; + } + + beforeEach(function () { + mockScope = jasmine.createSpyObj( + "$scope", + [ "$on", "$broadcast", "$watch" ] + ); + mockQ = jasmine.createSpyObj("$q", [ "all", "when" ]); + mockTimeout = jasmine.createSpy("$timeout"); + mockLog = jasmine.createSpyObj("$log", ["warn", "info", "debug"]); + + mockDomainObject = jasmine.createSpyObj( + "domainObject", + [ + "getId", + "getCapability", + "getModel", + "hasCapability", + "useCapability" + ] + ); + + mockTelemetry = jasmine.createSpyObj( + "telemetry", + [ "requestData", "getMetadata" ] + ); + + mockQ.when.andCallFake(mockPromise); + mockQ.all.andReturn(mockPromise([mockDomainObject])); + + mockDomainObject.getId.andReturn("testId"); + mockDomainObject.getModel.andReturn({ name: "TEST" }); + mockDomainObject.useCapability.andReturn([]); + mockDomainObject.hasCapability.andReturn(true); + mockDomainObject.getCapability.andReturn(mockTelemetry); + + mockTelemetry.getMetadata.andReturn({ + source: "testSource", + key: "testKey" + }); + mockTelemetry.requestData.andReturn(mockPromise({ + telemetryKey: "some value" + })); + + controller = new TelemetryController( + mockScope, + mockQ, + mockTimeout, + mockLog + ); + }); + + it("watches the domain object in scope", function () { + expect(mockScope.$watch).toHaveBeenCalledWith( + "domainObject", + jasmine.any(Function) + ); + }); + + it("starts a refresh interval", function () { + expect(mockTimeout).toHaveBeenCalledWith( + jasmine.any(Function), + jasmine.any(Number) + ); + }); + + it("changes refresh interval on request", function () { + controller.setRefreshInterval(42); + + // Tick the clock; should issue a new request, with + // the new interval + mockTimeout.mostRecentCall.args[0](); + + expect(mockTimeout).toHaveBeenCalledWith( + jasmine.any(Function), + 42 + ); + }); + + it("requests data from domain objects", function () { + // Push into the scope... + mockScope.$watch.mostRecentCall.args[1](mockDomainObject); + + expect(mockTelemetry.requestData).toHaveBeenCalled(); + }); + + it("logs a warning if no telemetry capability exists", function () { + mockDomainObject.getCapability.andReturn(undefined); + + // Push into the scope... + mockScope.$watch.mostRecentCall.args[1](mockDomainObject); + + expect(mockLog.warn).toHaveBeenCalled(); + }); + + it("provides telemetry metadata", function () { + // Push into the scope... + mockScope.$watch.mostRecentCall.args[1](mockDomainObject); + + expect(controller.getMetadata()).toEqual([ + { source: "testSource", key: "testKey" } + ]); + }); + + it("provides telemetry-possessing domain objects", function () { + // Push into the scope... + mockScope.$watch.mostRecentCall.args[1](mockDomainObject); + + expect(controller.getTelemetryObjects()) + .toEqual([mockDomainObject]); + }); + + it("provides telemetry data", function () { + // Push into the scope... + mockScope.$watch.mostRecentCall.args[1](mockDomainObject); + + expect(controller.getResponse()) + .toEqual([{telemetryKey: "some value"}]); + }); + + it("provides telemetry data per-id", function () { + // Push into the scope... + mockScope.$watch.mostRecentCall.args[1](mockDomainObject); + + expect(controller.getResponse("testId")) + .toEqual({telemetryKey: "some value"}); + }); + + it("provides a check for pending requests", function () { + expect(controller.isRequestPending()).toBeFalsy(); + }); + + it("allows a request to be specified", function () { + // Push into the scope... + mockScope.$watch.mostRecentCall.args[1](mockDomainObject); + + controller.requestData({ someKey: "some request" }); + + expect(mockTelemetry.requestData).toHaveBeenCalledWith({ + someKey: "some request" + }); + }); + + it("allows an object to be removed from scope", function () { + // Push into the scope... + mockScope.$watch.mostRecentCall.args[1](undefined); + + expect(controller.getTelemetryObjects()) + .toEqual([]); + }); + + it("broadcasts when telemetry is available", function () { + // Push into the scope... + mockScope.$watch.mostRecentCall.args[1](mockDomainObject); + controller.requestData({ someKey: "some request" }); + + // Verify precondition + expect(mockScope.$broadcast).not.toHaveBeenCalled(); + + // Call the broadcast timeout + mockTimeout.mostRecentCall.args[0](); + + // Should have broadcast a telemetryUpdate + expect(mockScope.$broadcast) + .toHaveBeenCalledWith("telemetryUpdate"); + }); + + + }); + } +); \ No newline at end of file diff --git a/platform/telemetry/test/suite.json b/platform/telemetry/test/suite.json new file mode 100644 index 0000000000..e22a9280d7 --- /dev/null +++ b/platform/telemetry/test/suite.json @@ -0,0 +1,5 @@ +[ + "TelemetryAggregator", + "TelemetryCapability", + "TelemetryController" +] \ No newline at end of file