GUI -- TopoView - Migrated helper functions to topoModel.js.
- moved randomized functions to random.js (so we can mock them).
Change-Id: Ic56ce64c036d36f34798f0df9f03a7d09335a2ab
diff --git a/web/gui/src/main/webapp/app/fw/util/random.js b/web/gui/src/main/webapp/app/fw/util/random.js
new file mode 100644
index 0000000..2298a94
--- /dev/null
+++ b/web/gui/src/main/webapp/app/fw/util/random.js
@@ -0,0 +1,51 @@
+/*
+ * Copyright 2014,2015 Open Networking Laboratory
+ *
+ * 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.
+ */
+
+/*
+ ONOS GUI -- Random -- Encapsulated randomness
+ */
+(function () {
+ 'use strict';
+
+ var $log, fs;
+
+ var halfRoot2 = 0.7071;
+
+ // given some value, s, returns an integer between -s/2 and s/2
+ // e.g. s = 100; result in the range [-50..50)
+ function spread(s) {
+ return Math.floor((Math.random() * s) - s / 2);
+ }
+
+ // for a given dimension, d, choose a random value somewhere between
+ // 0 and d where the value is within (d / (2 * sqrt(2))) of d/2.
+ function randDim(d) {
+ return d / 2 + spread(d * halfRoot2);
+ }
+
+ angular.module('onosUtil')
+ .factory('RandomService', ['$log', 'FnService',
+
+ function (_$log_, _fs_) {
+ $log = _$log_;
+ fs = _fs_;
+
+ return {
+ spread: spread,
+ randDim: randDim
+ };
+ }]);
+}());
diff --git a/web/gui/src/main/webapp/app/index.html b/web/gui/src/main/webapp/app/index.html
index c0e9228..b7b8af0 100644
--- a/web/gui/src/main/webapp/app/index.html
+++ b/web/gui/src/main/webapp/app/index.html
@@ -35,6 +35,7 @@
<script src="fw/util/util.js"></script>
<script src="fw/util/fn.js"></script>
+ <script src="fw/util/random.js"></script>
<script src="fw/util/theme.js"></script>
<script src="fw/util/keys.js"></script>
@@ -81,6 +82,7 @@
<script src="view/topo/topo.js"></script>
<script src="view/topo/topoEvent.js"></script>
<script src="view/topo/topoForce.js"></script>
+ <script src="view/topo/topoModel.js"></script>
<script src="view/topo/topoPanel.js"></script>
<script src="view/topo/topoInst.js"></script>
<script src="view/device/device.js"></script>
diff --git a/web/gui/src/main/webapp/app/view/topo/topo.js b/web/gui/src/main/webapp/app/view/topo/topo.js
index f227290..a512704 100644
--- a/web/gui/src/main/webapp/app/view/topo/topo.js
+++ b/web/gui/src/main/webapp/app/view/topo/topo.js
@@ -130,8 +130,8 @@
// callback invoked when the SVG view has been resized..
- function svgResized(dim) {
- tfs.resize(dim);
+ function svgResized(s) {
+ tfs.newDim([s.width, s.height]);
}
// --- Background Map ------------------------------------------------
@@ -203,6 +203,7 @@
_ks_, _zs_, _gs_, _ms_, _sus_, tes, _tfs_, tps, _tis_) {
var self = this,
projection,
+ dim,
uplink = {
// provides function calls back into this space
showNoDevs: showNoDevs,
@@ -230,6 +231,7 @@
tes.closeSock();
tps.destroyPanels();
tis.destroyInst();
+ tfs.destroyForce();
});
// svg layer and initialization of components
@@ -237,6 +239,7 @@
svg = ovtopo.select('svg');
// set the svg size to match that of the window, less the masthead
svg.attr(fs.windowSize(mast.mastHeight()));
+ dim = [svg.attr('width'), svg.attr('height')];
setUpKeys();
setUpDefs();
@@ -250,7 +253,7 @@
);
forceG = zoomLayer.append('g').attr('id', 'topo-force');
- tfs.initForce(forceG, uplink, svg.attr('width'), svg.attr('height'));
+ tfs.initForce(forceG, uplink, dim);
tis.initInst();
tps.initPanels();
tes.openSock();
diff --git a/web/gui/src/main/webapp/app/view/topo/topoForce.js b/web/gui/src/main/webapp/app/view/topo/topoForce.js
index e189ef1..e29c6c0 100644
--- a/web/gui/src/main/webapp/app/view/topo/topoForce.js
+++ b/web/gui/src/main/webapp/app/view/topo/topoForce.js
@@ -15,15 +15,15 @@
*/
/*
- ONOS GUI -- Topology Event Module.
- Defines event handling for events received from the server.
+ ONOS GUI -- Topology Force Module.
+ Visualization of the topology in an SVG layer, using a D3 Force Layout.
*/
(function () {
'use strict';
// injected refs
- var $log, fs, sus, is, ts, flash, tis, icfg, uplink;
+ var $log, fs, sus, is, ts, flash, tis, tms, icfg, uplink;
// configuration
var labelConfig = {
@@ -48,7 +48,7 @@
light: {
baseColor: '#666',
inColor: '#66f',
- outColor: '#f00',
+ outColor: '#f00'
},
dark: {
baseColor: '#aaa',
@@ -76,7 +76,7 @@
showOffline = true, // whether offline devices are displayed
oblique = false, // whether we are in the oblique view
nodeLock = false, // whether nodes can be dragged or not (locked)
- width, height, // the width and height of the force layout
+ dim, // the dimensions of the force layout [w,h]
hovered, // the node over which the mouse is hovering
selections = {}, // what is currently selected
selectOrder = []; // the order in which we made selections
@@ -131,7 +131,7 @@
return;
}
- d = createDeviceNode(data);
+ d = tms.createDeviceNode(data);
network.nodes.push(d);
lu[id] = d;
@@ -149,7 +149,7 @@
if (d) {
wasOnline = d.online;
angular.extend(d, data);
- if (positionNode(d, true)) {
+ if (tms.positionNode(d, true)) {
sendUpdateMeta(d);
}
updateNodes();
@@ -185,7 +185,7 @@
return;
}
- d = createHostNode(data);
+ d = tms.createHostNode(data);
network.nodes.push(d);
lu[id] = d;
@@ -193,7 +193,7 @@
updateNodes();
- lnk = createHostLink(data);
+ lnk = tms.createHostLink(data);
if (lnk) {
$log.debug("Created new host-link.. ", lnk.key);
@@ -213,7 +213,7 @@
d = lu[id];
if (d) {
angular.extend(d, data);
- if (positionNode(d, true)) {
+ if (tms.positionNode(d, true)) {
sendUpdateMeta(d);
}
updateNodes();
@@ -251,7 +251,7 @@
}
// no backing store link yet
- d = createLink(data);
+ d = tms.createLink(data);
if (d) {
network.links.push(d);
lu[d.key] = d;
@@ -290,42 +290,6 @@
restyleLinkElement(ldata);
}
- function createLink(link) {
- var lnk = linkEndPoints(link.src, link.dst);
-
- if (!lnk) {
- return null;
- }
-
- angular.extend(lnk, {
- key: link.id,
- class: 'link',
- fromSource: link,
-
- // functions to aggregate dual link state
- type: function () {
- var s = lnk.fromSource,
- t = lnk.fromTarget;
- return (s && s.type) || (t && t.type) || defaultLinkType;
- },
- online: function () {
- var s = lnk.fromSource,
- t = lnk.fromTarget,
- both = lnk.source.online && lnk.target.online;
- return both && ((s && s.online) || (t && t.online));
- },
- linkWidth: function () {
- var s = lnk.fromSource,
- t = lnk.fromTarget,
- ws = (s && s.linkWidth) || 0,
- wt = (t && t.linkWidth) || 0;
- return Math.max(ws, wt);
- }
- });
- return lnk;
- }
-
-
function makeNodeKey(d, what) {
var port = what + 'Port';
return d[what] + '/' + d[port];
@@ -342,8 +306,7 @@
.domain([1, 12])
.range([widthRatio, 12 * widthRatio])
.clamp(true),
- allLinkTypes = 'direct indirect optical tunnel',
- defaultLinkType = 'direct';
+ allLinkTypes = 'direct indirect optical tunnel';
function restyleLinkElement(ldata) {
// this fn's job is to look at raw links and decide what svg classes
@@ -568,7 +531,7 @@
// if we are not clearing the position data (unpinning),
// attach the x, y, longitude, latitude...
if (!clearPos) {
- ll = lngLatFromCoord([d.x, d.y]);
+ ll = tms.lngLatFromCoord([d.x, d.y]);
metaUi = {
x: d.x,
y: d.y,
@@ -588,171 +551,11 @@
$log.debug('TODO: requestTrafficForMode()...');
}
- // ==========================
- // === Devices and hosts - helper functions
-
- function coordFromLngLat(loc) {
- var p = uplink.projection();
- return p ? p([loc.lng, loc.lat]) : [0, 0];
- }
-
- function lngLatFromCoord(coord) {
- var p = uplink.projection();
- return p ? p.invert(coord) : [0, 0];
- }
-
- function positionNode(node, forUpdate) {
- var meta = node.metaUi,
- x = meta && meta.x,
- y = meta && meta.y,
- xy;
-
- // If we have [x,y] already, use that...
- if (x && y) {
- node.fixed = true;
- node.px = node.x = x;
- node.py = node.y = y;
- return;
- }
-
- var location = node.location,
- coord;
-
- if (location && location.type === 'latlng') {
- coord = coordFromLngLat(location);
- node.fixed = true;
- node.px = node.x = coord[0];
- node.py = node.y = coord[1];
- return true;
- }
-
- // if this is a node update (not a node add).. skip randomizer
- if (forUpdate) {
- return;
- }
-
- // Note: Placing incoming unpinned nodes at exactly the same point
- // (center of the view) causes them to explode outwards when
- // the force layout kicks in. So, we spread them out a bit
- // initially, to provide a more serene layout convergence.
- // Additionally, if the node is a host, we place it near
- // the device it is connected to.
-
- function spread(s) {
- return Math.floor((Math.random() * s) - s/2);
- }
-
- function randDim(dim) {
- return dim / 2 + spread(dim * 0.7071);
- }
-
- function rand() {
- return {
- x: randDim(width),
- y: randDim(height)
- };
- }
-
- function near(node) {
- var min = 12,
- dx = spread(12),
- dy = spread(12);
- return {
- x: node.x + min + dx,
- y: node.y + min + dy
- };
- }
-
- function getDevice(cp) {
- var d = lu[cp.device];
- return d || rand();
- }
-
- xy = (node.class === 'host') ? near(getDevice(node.cp)) : rand();
- angular.extend(node, xy);
- }
-
- function createDeviceNode(device) {
- // start with the object as is
- var node = device,
- type = device.type,
- svgCls = type ? 'node device ' + type : 'node device';
-
- // Augment as needed...
- node.class = 'device';
- node.svgClass = device.online ? svgCls + ' online' : svgCls;
- positionNode(node);
- return node;
- }
-
- function createHostNode(host) {
- var node = host;
-
- // Augment as needed...
- node.class = 'host';
- if (!node.type) {
- node.type = 'endstation';
- }
- node.svgClass = 'node host ' + node.type;
- positionNode(node);
- return node;
- }
-
- function createHostLink(host) {
- var src = host.id,
- dst = host.cp.device,
- id = host.ingress,
- lnk = linkEndPoints(src, dst);
-
- if (!lnk) {
- return null;
- }
-
- // Synthesize link ...
- angular.extend(lnk, {
- key: id,
- class: 'link',
-
- type: function () { return 'hostLink'; },
- online: function () {
- // hostlink target is edge switch
- return lnk.target.online;
- },
- linkWidth: function () { return 1; }
- });
- return lnk;
- }
-
- function linkEndPoints(srcId, dstId) {
- var srcNode = lu[srcId],
- dstNode = lu[dstId],
- sMiss = !srcNode ? missMsg('src', srcId) : '',
- dMiss = !dstNode ? missMsg('dst', dstId) : '';
-
- if (sMiss || dMiss) {
- $log.error('Node(s) not on map for link:\n' + sMiss + dMiss);
- //logicError('Node(s) not on map for link:\n' + sMiss + dMiss);
- return null;
- }
- return {
- source: srcNode,
- target: dstNode,
- x1: srcNode.x,
- y1: srcNode.y,
- x2: dstNode.x,
- y2: dstNode.y
- };
- }
-
- function missMsg(what, id) {
- return '\n[' + what + '] "' + id + '" missing ';
- }
// ==========================
// === Devices and hosts - D3 rendering
function nodeMouseOver(m) {
- // TODO
if (!m.dragStarted) {
$log.debug("MouseOver()...", m);
if (hovered != m) {
@@ -763,7 +566,6 @@
}
function nodeMouseOut(m) {
- // TODO
if (!m.dragStarted) {
if (hovered) {
hovered = null;
@@ -1031,12 +833,12 @@
var node = d.el;
node.classed('online', d.online);
updateDeviceLabel(d);
- positionNode(d, true);
+ tms.positionNode(d, true);
}
function hostExisting(d) {
updateHostLabel(d);
- positionNode(d, true);
+ tms.positionNode(d, true);
}
function deviceEnter(d) {
@@ -1424,9 +1226,9 @@
angular.module('ovTopo')
.factory('TopoForceService',
['$log', 'FnService', 'SvgUtilService', 'IconService', 'ThemeService',
- 'FlashService', 'TopoInstService',
+ 'FlashService', 'TopoInstService', 'TopoModelService',
- function (_$log_, _fs_, _sus_, _is_, _ts_, _flash_, _tis_) {
+ function (_$log_, _fs_, _sus_, _is_, _ts_, _flash_, _tis_, _tms_) {
$log = _$log_;
fs = _fs_;
sus = _sus_;
@@ -1434,18 +1236,24 @@
ts = _ts_;
flash = _flash_;
tis = _tis_;
+ tms = _tms_;
icfg = is.iconConfig();
// forceG is the SVG group to display the force layout in
// xlink is the cross-link api from the main topo source file
- // w, h are the initial dimensions of the SVG
+ // dim is the initial dimensions of the SVG as [w,h]
// opts are, well, optional :)
- function initForce(forceG, _uplink_, w, h, opts) {
- $log.debug('initForce().. WxH = ' + w + 'x' + h);
+ function initForce(forceG, _uplink_, _dim_, opts) {
uplink = _uplink_;
- width = w;
- height = h;
+ dim = _dim_;
+
+ $log.debug('initForce().. dim = ' + dim);
+
+ tms.initModel({
+ projection: uplink.projection,
+ lookup: network.lookup
+ }, dim);
settings = angular.extend({}, defaultSettings, opts);
@@ -1458,7 +1266,7 @@
node = nodeG.selectAll('.node');
force = d3.layout.force()
- .size([w, h])
+ .size(dim)
.nodes(network.nodes)
.links(network.links)
.gravity(settings.gravity)
@@ -1472,16 +1280,21 @@
selectObject, atDragEnd, dragEnabled, clickEnabled);
}
- function resize(dim) {
- width = dim.width;
- height = dim.height;
- force.size([width, height]);
+ function newDim(_dim_) {
+ dim = _dim_;
+ force.size(dim);
+ tms.newDim(dim);
// Review -- do we need to nudge the layout ?
}
+ function destroyForce() {
+
+ }
+
return {
initForce: initForce,
- resize: resize,
+ newDim: newDim,
+ destroyForce: destroyForce,
updateDeviceColors: updateDeviceColors,
toggleHosts: toggleHosts,
diff --git a/web/gui/src/main/webapp/app/view/topo/topoModel.js b/web/gui/src/main/webapp/app/view/topo/topoModel.js
new file mode 100644
index 0000000..015fbdd
--- /dev/null
+++ b/web/gui/src/main/webapp/app/view/topo/topoModel.js
@@ -0,0 +1,255 @@
+/*
+ * Copyright 2015 Open Networking Laboratory
+ *
+ * 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.
+ */
+
+/*
+ ONOS GUI -- Topology Model Module.
+ Auxiliary functions for the model of the topology; that is, our internal
+ representations of devices, hosts, links, etc.
+ */
+
+(function () {
+ 'use strict';
+
+ // injected refs
+ var $log, fs, rnd, api;
+
+ var dim; // dimensions of layout, as [w,h]
+
+ // configuration 'constants'
+ var defaultLinkType = 'direct',
+ nearDist = 15;
+
+
+ function coordFromLngLat(loc) {
+ var p = api.projection();
+ return p ? p([loc.lng, loc.lat]) : [0, 0];
+ }
+
+ function lngLatFromCoord(coord) {
+ var p = api.projection();
+ return p ? p.invert(coord) : [0, 0];
+ }
+
+ function positionNode(node, forUpdate) {
+ var meta = node.metaUi,
+ x = meta && meta.x,
+ y = meta && meta.y,
+ xy;
+
+ // If we have [x,y] already, use that...
+ if (x && y) {
+ node.fixed = true;
+ node.px = node.x = x;
+ node.py = node.y = y;
+ return;
+ }
+
+ var location = node.location,
+ coord;
+
+ if (location && location.type === 'latlng') {
+ coord = coordFromLngLat(location);
+ node.fixed = true;
+ node.px = node.x = coord[0];
+ node.py = node.y = coord[1];
+ return true;
+ }
+
+ // if this is a node update (not a node add).. skip randomizer
+ if (forUpdate) {
+ return;
+ }
+
+ // Note: Placing incoming unpinned nodes at exactly the same point
+ // (center of the view) causes them to explode outwards when
+ // the force layout kicks in. So, we spread them out a bit
+ // initially, to provide a more serene layout convergence.
+ // Additionally, if the node is a host, we place it near
+ // the device it is connected to.
+
+ function rand() {
+ return {
+ x: rnd.randDim(dim[0]),
+ y: rnd.randDim(dim[1])
+ };
+ }
+
+ function near(node) {
+ return {
+ x: node.x + nearDist + rnd.spread(nearDist),
+ y: node.y + nearDist + rnd.spread(nearDist)
+ };
+ }
+
+ function getDevice(cp) {
+ var d = api.lookup[cp.device];
+ return d || rand();
+ }
+
+ xy = (node.class === 'host') ? near(getDevice(node.cp)) : rand();
+ angular.extend(node, xy);
+ }
+
+ function mkSvgCls(dh, t, on) {
+ var ndh = 'node ' + dh,
+ ndht = t ? ndh + ' ' + t : ndh;
+ return on ? ndht + ' online' : ndht;
+ }
+
+ function createDeviceNode(device) {
+ var node = device;
+
+ // Augment as needed...
+ node.class = 'device';
+ node.svgClass = mkSvgCls('device', device.type, device.online);
+ positionNode(node);
+ return node;
+ }
+
+ function createHostNode(host) {
+ var node = host;
+
+ // Augment as needed...
+ node.class = 'host';
+ if (!node.type) {
+ node.type = 'endstation';
+ }
+ node.svgClass = mkSvgCls('host', node.type);
+ positionNode(node);
+ return node;
+ }
+
+ function createHostLink(host) {
+ var src = host.id,
+ dst = host.cp.device,
+ id = host.ingress,
+ lnk = linkEndPoints(src, dst);
+
+ if (!lnk) {
+ return null;
+ }
+
+ // Synthesize link ...
+ angular.extend(lnk, {
+ key: id,
+ class: 'link',
+
+ type: function () { return 'hostLink'; },
+ online: function () {
+ // hostlink target is edge switch
+ return lnk.target.online;
+ },
+ linkWidth: function () { return 1; }
+ });
+ return lnk;
+ }
+
+ function createLink(link) {
+ var lnk = linkEndPoints(link.src, link.dst);
+
+ if (!lnk) {
+ return null;
+ }
+
+ angular.extend(lnk, {
+ key: link.id,
+ class: 'link',
+ fromSource: link,
+
+ // functions to aggregate dual link state
+ type: function () {
+ var s = lnk.fromSource,
+ t = lnk.fromTarget;
+ return (s && s.type) || (t && t.type) || defaultLinkType;
+ },
+ online: function () {
+ var s = lnk.fromSource,
+ t = lnk.fromTarget,
+ both = lnk.source.online && lnk.target.online;
+ return both && ((s && s.online) || (t && t.online));
+ },
+ linkWidth: function () {
+ var s = lnk.fromSource,
+ t = lnk.fromTarget,
+ ws = (s && s.linkWidth) || 0,
+ wt = (t && t.linkWidth) || 0;
+ return Math.max(ws, wt);
+ }
+ });
+ return lnk;
+ }
+
+
+ function linkEndPoints(srcId, dstId) {
+ var srcNode = api.lookup[srcId],
+ dstNode = api.lookup[dstId],
+ sMiss = !srcNode ? missMsg('src', srcId) : '',
+ dMiss = !dstNode ? missMsg('dst', dstId) : '';
+
+ if (sMiss || dMiss) {
+ $log.error('Node(s) not on map for link:' + sMiss + dMiss);
+ //logicError('Node(s) not on map for link:\n' + sMiss + dMiss);
+ return null;
+ }
+ return {
+ source: srcNode,
+ target: dstNode,
+ x1: srcNode.x,
+ y1: srcNode.y,
+ x2: dstNode.x,
+ y2: dstNode.y
+ };
+ }
+
+ function missMsg(what, id) {
+ return '\n[' + what + '] "' + id + '" missing';
+ }
+
+ // ==========================
+ // Module definition
+
+ angular.module('ovTopo')
+ .factory('TopoModelService',
+ ['$log', 'FnService', 'RandomService',
+
+ function (_$log_, _fs_, _rnd_) {
+ $log = _$log_;
+ fs = _fs_;
+ rnd = _rnd_;
+
+ function initModel(_api_, _dim_) {
+ api = _api_;
+ dim = _dim_;
+ }
+
+ function newDim(_dim_) {
+ dim = _dim_;
+ }
+
+ return {
+ initModel: initModel,
+ newDim: newDim,
+
+ positionNode: positionNode,
+ createDeviceNode: createDeviceNode,
+ createHostNode: createHostNode,
+ createHostLink: createHostLink,
+ createLink: createLink,
+ coordFromLngLat: coordFromLngLat,
+ lngLatFromCoord: lngLatFromCoord,
+ }
+ }]);
+}());
diff --git a/web/gui/src/main/webapp/tests/app/fw/util/random-spec.js b/web/gui/src/main/webapp/tests/app/fw/util/random-spec.js
new file mode 100644
index 0000000..c4c61f1
--- /dev/null
+++ b/web/gui/src/main/webapp/tests/app/fw/util/random-spec.js
@@ -0,0 +1,110 @@
+/*
+ * Copyright 2014,2015 Open Networking Laboratory
+ *
+ * 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.
+ */
+
+/*
+ ONOS GUI -- Util -- Random Service - Unit Tests
+ */
+describe('factory: fw/util/random.js', function() {
+ var rnd, $log, fs;
+
+ beforeEach(module('onosUtil'));
+
+ beforeEach(inject(function (RandomService, _$log_, FnService) {
+ rnd = RandomService;
+ $log = _$log_;
+ fs = FnService;
+ }));
+
+ // interesting use of a custom matcher...
+ beforeEach(function () {
+ jasmine.addMatchers({
+ toBeWithinOf: function () {
+ return {
+ compare: function (actual, distance, base) {
+ var lower = base - distance,
+ upper = base + distance,
+ result = {};
+
+ result.pass = Math.abs(actual - base) <= distance;
+
+ if (result.pass) {
+ // for negation with ".not"
+ result.message = 'Expected ' + actual +
+ ' to be outside ' + lower + ' and ' +
+ upper + ' (inclusive)';
+ } else {
+ result.message = 'Expected ' + actual +
+ ' to be between ' + lower + ' and ' +
+ upper + ' (inclusive)';
+ }
+ return result;
+ }
+ }
+ }
+ });
+ });
+
+ it('should define RandomService', function () {
+ expect(rnd).toBeDefined();
+ });
+
+ it('should define api functions', function () {
+ expect(fs.areFunctions(rnd, [
+ 'spread', 'randDim'
+ ])).toBeTruthy();
+ });
+
+ // really, can only do this heuristically.. hope this doesn't break
+ it('should spread results across the range', function () {
+ var load = 1000,
+ s = 12,
+ low = 0,
+ high = 0,
+ i, res,
+ which = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
+ minCount = load / s * 0.5; // generous error
+
+ for (i=0; i<load; i++) {
+ res = rnd.spread(s);
+ if (res < low) low = res;
+ if (res > high) high = res;
+ which[res + s/2]++;
+ }
+ expect(low).toBe(-6);
+ expect(high).toBe(5);
+
+ // check we got a good number of hits in each bucket
+ for (i=0; i<s; i++) {
+ expect(which[i]).toBeGreaterThan(minCount);
+ }
+ });
+
+ // really, can only do this heuristically.. hope this doesn't break
+ it('should choose results across the dimension', function () {
+ var load = 1000,
+ dim = 100,
+ low = 999,
+ high = 0,
+ i, res;
+
+ for (i=0; i<load; i++) {
+ res = rnd.randDim(dim);
+ if (res < low) low = res;
+ if (res > high) high = res;
+ expect(res).toBeWithinOf(36, 50);
+ }
+ });
+});
diff --git a/web/gui/src/main/webapp/tests/app/fw/util/theme-spec.js b/web/gui/src/main/webapp/tests/app/fw/util/theme-spec.js
index cf1841b..1d400ed 100644
--- a/web/gui/src/main/webapp/tests/app/fw/util/theme-spec.js
+++ b/web/gui/src/main/webapp/tests/app/fw/util/theme-spec.js
@@ -29,7 +29,7 @@
ts.init();
}));
- it('should define MapService', function () {
+ it('should define ThemeService', function () {
expect(ts).toBeDefined();
});
diff --git a/web/gui/src/main/webapp/tests/app/view/topo/topoForce-spec.js b/web/gui/src/main/webapp/tests/app/view/topo/topoForce-spec.js
index dfaebf5..546f2f9 100644
--- a/web/gui/src/main/webapp/tests/app/view/topo/topoForce-spec.js
+++ b/web/gui/src/main/webapp/tests/app/view/topo/topoForce-spec.js
@@ -34,8 +34,11 @@
it('should define api functions', function () {
expect(fs.areFunctions(tfs, [
- 'initForce', 'resize', 'updateDeviceColors',
- 'toggleHosts', 'toggleOffline','cycleDeviceLabels', 'unpin',
+ 'initForce', 'newDim', 'destroyForce',
+
+ 'updateDeviceColors', 'toggleHosts', 'toggleOffline',
+ 'cycleDeviceLabels', 'unpin',
+
'addDevice', 'updateDevice', 'removeDevice',
'addHost', 'updateHost', 'removeHost',
'addLink', 'updateLink', 'removeLink'
diff --git a/web/gui/src/main/webapp/tests/app/view/topo/topoModel-spec.js b/web/gui/src/main/webapp/tests/app/view/topo/topoModel-spec.js
new file mode 100644
index 0000000..a0d488b
--- /dev/null
+++ b/web/gui/src/main/webapp/tests/app/view/topo/topoModel-spec.js
@@ -0,0 +1,403 @@
+/*
+ * Copyright 2015 Open Networking Laboratory
+ *
+ * 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.
+ */
+
+/*
+ ONOS GUI -- Topo View -- Topo Model Service - Unit Tests
+ */
+describe('factory: view/topo/topoModel.js', function() {
+ var $log, fs, rnd, tms;
+
+ // stop random numbers from being quite so random
+ var mockRandom = {
+ // mock spread returns s + 1
+ spread: function (s) {
+ return s + 1;
+ },
+ // mock random dimension returns d / 2 - 1
+ randDim: function (d) {
+ return d/2 - 1;
+ },
+ mock: 'yup'
+ };
+
+ // to mock out the [lng,lat] <=> [x,y] transformations, we will
+ // add/subtract 2000, 3000 respectively:
+ // lng:2005 === x:5, lat:3004 === y:4
+
+ var mockProjection = function (lnglat) {
+ return [lnglat[0] - 2000, lnglat[1] - 3000];
+ };
+
+ mockProjection.invert = function (xy) {
+ return [xy[0] + 2000, xy[1] + 3000];
+ };
+
+ // our test device lookup
+ var lu = {
+ dev1: {
+ 'class': 'device',
+ id: 'dev1',
+ x: 17,
+ y: 27,
+ online: true
+ },
+ dev2: {
+ 'class': 'device',
+ id: 'dev2',
+ x: 18,
+ y: 28,
+ online: true
+ },
+ host1: {
+ 'class': 'host',
+ id: 'host1',
+ x: 23,
+ y: 33,
+ cp: {
+ device: 'dev1',
+ port: 7
+ },
+ ingress: 'dev1/7-host1'
+ },
+ host2: {
+ 'class': 'host',
+ id: 'host2',
+ x: 24,
+ y: 34,
+ cp: {
+ device: 'dev0',
+ port: 0
+ },
+ ingress: 'dev0/0-host2'
+ }
+ };
+
+ // our test api
+ var api = {
+ projection: function () { return mockProjection; },
+ lookup: lu
+ };
+
+ // our test dimensions and well known locations..
+ var dim = [20, 40],
+ randLoc = [9, 19], // random location using randDim(): d/2-1
+ randHostLoc = [40, 50], // host "near" random location
+ // given that 'nearDist' = 15
+ // and spread(15) = 16
+ // 9 + 15 + 16 = 40; 19 + 15 + 16 = 50
+ nearDev1 = [48,58], // [17+15+16, 27+15+16]
+ dev1Loc = [17,27],
+ dev2Loc = [18,28],
+ host1Loc = [23,33],
+ host2Loc = [24,34];
+
+ // implement some custom matchers...
+ beforeEach(function () {
+ jasmine.addMatchers({
+ toBePositionedAt: function () {
+ return {
+ compare: function (actual, xy) {
+ var result = {},
+ actCoord = [actual.x, actual.y];
+
+ result.pass = (actual.x === xy[0]) && (actual.y === xy[1]);
+
+ if (result.pass) {
+ // for negation with ".not"
+ result.message = 'Expected [' + actCoord +
+ '] NOT to be positioned at [' + xy + ']';
+ } else {
+ result.message = 'Expected [' + actCoord +
+ '] to be positioned at [' + xy + ']';
+ }
+ return result;
+ }
+ }
+ },
+ toHaveEndPoints: function () {
+ return {
+ compare: function (actual, xy1, xy2) {
+ var result = {};
+
+ result.pass = (actual.x1 === xy1[0]) && (actual.y1 === xy1[1]) &&
+ (actual.x2 === xy2[0]) && (actual.y2 === xy2[1]);
+
+ if (result.pass) {
+ // for negation with ".not"
+ result.message = 'Expected ' + actual +
+ ' NOT to have endpoints [' + xy1 + ']-[' + xy2 + ']';
+ } else {
+ result.message = 'Expected ' + actual +
+ ' to have endpoints [' + xy1 + ']-[' + xy2 + ']';
+ }
+ return result;
+ }
+ }
+ },
+ toBeFixed: function () {
+ return {
+ compare: function (actual) {
+ var result = {
+ pass: actual.fixed
+ };
+ if (result.pass) {
+ result.message = 'Expected ' + actual +
+ ' NOT to be fixed!';
+ } else {
+ result.message = 'Expected ' + actual +
+ ' to be fixed!';
+ }
+ return result;
+ }
+ }
+ }
+ });
+ });
+
+ beforeEach(module('ovTopo', 'onosUtil'));
+
+ beforeEach(function () {
+ module(function ($provide) {
+ $provide.value('RandomService', mockRandom);
+ });
+ });
+
+ beforeEach(inject(function (_$log_, FnService, RandomService, TopoModelService) {
+ $log = _$log_;
+ fs = FnService;
+ rnd = RandomService;
+ tms = TopoModelService;
+ tms.initModel(api, dim);
+ }));
+
+
+ it('should install the mock random service', function () {
+ expect(rnd.mock).toBe('yup');
+ expect(rnd.spread(4)).toBe(5);
+ expect(rnd.randDim(8)).toBe(3);
+ });
+
+ it('should install the mock projection', function () {
+ expect(tms.coordFromLngLat({lng: 2005, lat: 3004})).toEqual([5,4]);
+ expect(tms.lngLatFromCoord([5,4])).toEqual([2005,3004]);
+ });
+
+ it('should define TopoModelService', function () {
+ expect(tms).toBeDefined();
+ });
+
+ it('should define api functions', function () {
+ expect(fs.areFunctions(tms, [
+ 'initModel', 'newDim',
+ 'positionNode', 'createDeviceNode', 'createHostNode',
+ 'createHostLink', 'createLink',
+ 'coordFromLngLat', 'lngLatFromCoord'
+ ])).toBeTruthy();
+ });
+
+ // === unit tests for positionNode()
+
+ it('should position a node using meta x/y', function () {
+ var node = {
+ metaUi: { x:37, y:48 }
+ };
+ tms.positionNode(node);
+ expect(node).toBePositionedAt([37,48]);
+ expect(node).toBeFixed();
+ });
+
+ it('should position a node by translating lng/lat', function () {
+ var node = {
+ location: {
+ type: 'latlng',
+ lng: 2008,
+ lat: 3009
+ }
+ };
+ tms.positionNode(node);
+ expect(node).toBePositionedAt([8,9]);
+ expect(node).toBeFixed();
+ });
+
+ it('should position a device with no location randomly', function () {
+ var node = { 'class': 'device' };
+ tms.positionNode(node);
+ expect(node).toBePositionedAt(randLoc);
+ expect(node).not.toBeFixed();
+ });
+
+ it('should position a device randomly even if x/y set', function () {
+ var node = { 'class': 'device', x: 1, y: 2 };
+ tms.positionNode(node);
+ expect(node).toBePositionedAt(randLoc);
+ expect(node).not.toBeFixed();
+ });
+
+ it('should NOT reposition a device randomly on update', function () {
+ var node = { 'class': 'device', x: 1, y: 2 };
+ tms.positionNode(node, true);
+ expect(node).toBePositionedAt([1,2]);
+ expect(node).not.toBeFixed();
+ });
+
+ it('should position a host close to its device', function () {
+ var node = { 'class': 'host', cp: { device: 'dev1' } };
+ tms.positionNode(node);
+
+ // note: nearDist is 15; spread(15) adds 16; dev1 at [17,27]
+
+ expect(node).toBePositionedAt(nearDev1);
+ expect(node).not.toBeFixed();
+ });
+
+ it('should randomize host with no assoc device', function () {
+ var node = { 'class': 'host', cp: { device: 'dev0' } };
+ tms.positionNode(node);
+
+ // note: no device gives 'rand loc' [9,19]
+ // nearDist is 15; spread(15) adds 16
+
+ expect(node).toBePositionedAt(randHostLoc);
+ expect(node).not.toBeFixed();
+ });
+
+ // === unit tests for createDeviceNode()
+
+ it('should create a basic device node', function () {
+ var node = tms.createDeviceNode({ id: 'foo' });
+ expect(node).toBePositionedAt(randLoc);
+ expect(node).not.toBeFixed();
+ expect(node.class).toEqual('device');
+ expect(node.svgClass).toEqual('node device');
+ expect(node.id).toEqual('foo');
+ });
+
+ it('should create device node with type', function () {
+ var node = tms.createDeviceNode({ id: 'foo', type: 'cool' });
+ expect(node).toBePositionedAt(randLoc);
+ expect(node).not.toBeFixed();
+ expect(node.class).toEqual('device');
+ expect(node.svgClass).toEqual('node device cool');
+ expect(node.id).toEqual('foo');
+ });
+
+ it('should create online device node with type', function () {
+ var node = tms.createDeviceNode({ id: 'foo', type: 'cool', online: true });
+ expect(node).toBePositionedAt(randLoc);
+ expect(node).not.toBeFixed();
+ expect(node.class).toEqual('device');
+ expect(node.svgClass).toEqual('node device cool online');
+ expect(node.id).toEqual('foo');
+ });
+
+ it('should create online device node with type and lng/lat', function () {
+ var node = tms.createDeviceNode({
+ id: 'foo',
+ type: 'yowser',
+ online: true,
+ location: {
+ type: 'latlng',
+ lng: 2048,
+ lat: 3096
+ }
+ });
+ expect(node).toBePositionedAt([48,96]);
+ expect(node).toBeFixed();
+ expect(node.class).toEqual('device');
+ expect(node.svgClass).toEqual('node device yowser online');
+ expect(node.id).toEqual('foo');
+ });
+
+ // === unit tests for createHostNode()
+
+ it('should create a basic host node', function () {
+ var node = tms.createHostNode({ id: 'bar', cp: { device: 'dev0' } });
+ expect(node).toBePositionedAt(randHostLoc);
+ expect(node).not.toBeFixed();
+ expect(node.class).toEqual('host');
+ expect(node.svgClass).toEqual('node host endstation');
+ expect(node.id).toEqual('bar');
+ });
+
+ it('should create a host with type', function () {
+ var node = tms.createHostNode({
+ id: 'bar',
+ type: 'classic',
+ cp: { device: 'dev1' }
+ });
+ expect(node).toBePositionedAt(nearDev1);
+ expect(node).not.toBeFixed();
+ expect(node.class).toEqual('host');
+ expect(node.svgClass).toEqual('node host classic');
+ expect(node.id).toEqual('bar');
+ });
+
+ // === unit tests for createHostLink()
+
+ it('should create a basic host link', function () {
+ var link = tms.createHostLink(lu.host1);
+ expect(link.source).toEqual(lu.host1);
+ expect(link.target).toEqual(lu.dev1);
+ expect(link).toHaveEndPoints(host1Loc, dev1Loc);
+ expect(link.key).toEqual('dev1/7-host1');
+ expect(link.class).toEqual('link');
+ expect(link.type()).toEqual('hostLink');
+ expect(link.linkWidth()).toEqual(1);
+ expect(link.online()).toEqual(true);
+ });
+
+ it('should return null for failed endpoint lookup', function () {
+ spyOn($log, 'error');
+ var link = tms.createHostLink(lu.host2);
+ expect(link).toBeNull();
+ expect($log.error).toHaveBeenCalledWith(
+ 'Node(s) not on map for link:\n[dst] "dev0" missing'
+ );
+ });
+
+ // === unit tests for createLink()
+
+ it('should return null for missing endpoints', function () {
+ spyOn($log, 'error');
+ var link = tms.createLink({src: 'dev0', dst: 'dev00'});
+ expect(link).toBeNull();
+ expect($log.error).toHaveBeenCalledWith(
+ 'Node(s) not on map for link:\n[src] "dev0" missing\n[dst] "dev00" missing'
+ );
+ });
+
+ it('should create a basic link', function () {
+ var linkData = {
+ src: 'dev1',
+ dst: 'dev2',
+ id: 'baz',
+ type: 'zoo',
+ online: true,
+ linkWidth: 1.5
+ },
+ link = tms.createLink(linkData);
+ expect(link.source).toEqual(lu.dev1);
+ expect(link.target).toEqual(lu.dev2);
+ expect(link).toHaveEndPoints(dev1Loc, dev2Loc);
+ expect(link.key).toEqual('baz');
+ expect(link.class).toEqual('link');
+ expect(link.fromSource).toBe(linkData);
+ expect(link.type()).toEqual('zoo');
+ expect(link.online()).toEqual(true);
+ expect(link.linkWidth()).toEqual(1.5);
+ });
+
+});