blob: 7ae04a99f4c8a645c8d4f4a06fd55fe2aef622c8 [file] [log] [blame]
// Copyright 2015 Google Inc. All rights reserved.
//
// 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.
'use strict';
// Controllers
angular.module('cherry.batchResult', [])
.controller('ActiveBatchResultListCtrl', ['$scope', '$location', 'rtdb', 'rpc', function($scope, $location, rtdb, rpc)
{
rtdb.bind('ActiveBatchResultList', 'activeBatchResultList', $scope);
}])
// Batch result view.
.controller('BatchResultCtrl', ['$scope', '$location', '$stateParams', 'rtdb', 'rpc', function($scope, $location, $stateParams, rtdb, rpc)
{
var batchResultId = $stateParams.batchResultId || $scope.batchResultId;
var getSubBatchTestFilters = function() { throw new Error('getSubBatchTestFilters callback not set'); };
angular.extend($scope,
{
batchResultId: batchResultId,
stopExecution: function()
{
rpc.call('rtdb.StopBatchExecution', { batchResultId:batchResultId });
},
continueExecution: function()
{
rpc.call('rtdb.ContinueBatchExecution', { batchResultId:batchResultId });
},
setSubBatchTestFiltersCallback: function(callback) { getSubBatchTestFilters = callback; },
anyNodesForSubBatch: false,
setAnyNodesForSubBatch: function(v) { $scope.anyNodesForSubBatch = v; },
executeSubBatch: function()
{
rpc.call('rtdb.ExecuteSubTestBatch', { originalBatchResultId:batchResultId, testNameFilters:getSubBatchTestFilters() })
.then(function(batchResultId)
{
console.log(' executeSubBatch(): created batch result ' + batchResultId);
$location.url('/results/batch/' + batchResultId);
},
function(reason)
{
console.log('executeSubBatch() failed: ' + reason);
});
},
});
$scope.$watch('batchResult.status', function(status)
{
$scope.isStoppable = status === BATCH_STATUS_CODE.RUNNING || status === BATCH_STATUS_CODE.PENDING || status === BATCH_STATUS_CODE.IMPORTING;
$scope.isContinuable = status === BATCH_STATUS_CODE.INTERRUPTED || status === BATCH_STATUS_CODE.CANCELED;
$scope.isInitialized = status && status !== BATCH_STATUS_CODE.IMPORTING && status !== BATCH_STATUS_CODE.INITIALIZING; // \note When status is still undefined, we assume it's initializing.
});
$scope.$watch('batchResult.execParams.deviceId', function(deviceId)
{
if (deviceId)
rtdb.bind('DeviceConfig', deviceId, $scope, { valueName:'deviceConfig' });
});
$scope.$watch('deviceConfig.name', function(deviceConfigName)
{
if ($scope.deviceConfig)
$scope.deviceConfigName = deviceConfigNameHumanReadable($scope.deviceConfig);
});
$scope.batchResultNameInputWidth = function()
{
var inputPadding = $('#batchResultNameInput').outerWidth(true) - $('#batchResultNameInput').width();
return $('#batchResultNameWidthHelper').width() + 2 + inputPadding; // +2 seems good enough to keep the blinking bar thingy visible even at the right edge.
};
$scope.initProgress = function()
{
if ($scope.batchResult)
return $scope.batchResult.initProgress;
return 0.0;
};
// Watch changes on the batch result's name input, and send a name update
// request after the input has been unchanged for some time.
var nameUpdateDelayMilliseconds = 200;
var nameInputLastModified;
var nameUpdateSendTimeoutHandle;
var lastAssignedName;
$scope.$watch('batchResultNameInput', function(name)
{
if (lastAssignedName !== undefined && name !== lastAssignedName)
{
nameInputLastModified = new Date();
if (nameUpdateSendTimeoutHandle !== undefined)
clearTimeout(nameUpdateSendTimeoutHandle);
nameUpdateSendTimeoutHandle = setTimeout(function()
{
nameUpdateSendTimeoutHandle = undefined;
rpc.call('rtdb.SetBatchResultName', { batchResultId:batchResultId, name:name });
}, nameUpdateDelayMilliseconds);
lastAssignedName = name;
}
});
// Watch updates to the batch result's name from the server, and update the
// input's value after it's hasn't been modified by the user in some time.
var nameInputUpdateTimeoutHandle = undefined;
$scope.$watch('batchResult.name', function(name)
{
if (name !== undefined)
{
var insideTimeout = false;
var updateInputValue = function()
{
if (nameInputUpdateTimeoutHandle !== undefined)
clearTimeout(nameInputUpdateTimeoutHandle);
if (nameInputLastModified === undefined || (new Date()).getTime() - nameInputLastModified.getTime() > nameUpdateDelayMilliseconds)
{
nameInputUpdateTimeoutHandle = undefined;
$scope.batchResultNameInput = name;
lastAssignedName = name;
if (insideTimeout)
$scope.$digest();
}
else
{
insideTimeout = true;
nameInputUpdateTimeoutHandle = setTimeout(updateInputValue, nameUpdateDelayMilliseconds);
}
};
updateInputValue();
}
});
rtdb.bind('BatchResult', batchResultId, $scope, { valueName:'batchResult' });
$scope.titleExpr('batchResult.name + " - " + deviceConfigName');
}])
// Test case list.
.controller('TestCaseListCtrl', ['$scope', '$stateParams', 'rtdb', 'rpc', function($scope, $stateParams, rtdb, rpc)
{
var batchResultId = $stateParams.batchResultId || $scope.batchResultId;
var initiallySelectedNodePath = $stateParams.testGroupPath || $stateParams.testCasePath || '';
var latestClickedNodePath = undefined;
var selectInitialNode = function()
{
$scope.treeAccess.selectNode(getTestCaseTreeNodeIdFromPath($scope.testCaseTree, initiallySelectedNodePath));
};
$scope.setTreeAccess = function(access)
{
$scope.treeAccess = access;
$scope.setSubBatchTestFiltersCallback(function()
{
return genTestCaseTreeSubsetFilter($scope.testCaseTree, $scope.treeAccess.nodeSelected);
});
if ($scope.testCaseTree)
selectInitialNode();
};
$scope.treeSelectionType = function(event)
{
if (event.button === 0)
{
if (event.shiftKey)
return 'area';
if (event.ctrlKey)
return 'multiple';
return 'single';
}
}
$scope.maybeUnselectNodes = function(event)
{
if ($scope.treeAccess && !event.ctrlKey && !event.shiftKey)
$scope.treeAccess.unselectAll();
};
$scope.$watch('treeAccess.anySelected', $scope.setAnyNodesForSubBatch);
$scope.testCasePathFilter = '';
var refilter = function()
{
if ($scope.testCaseTree)
{
var pathFilterLower = $scope.testCasePathFilter.toLowerCase();
filterTestCaseTree($scope.testCaseTree, function(node) { return node.type !== 'testCase' || node.path.toLowerCase().indexOf(pathFilterLower) !== -1; });
}
};
$scope.$watch('testCaseList.paths', function(caseList)
{
if (caseList)
{
console.log('test case list updated with ' + caseList.length + ' items');
$scope.testCaseTree = genTestCaseTree(caseList, 'All Results');
setTestCaseTreeInitialDepthExpansion($scope.testCaseTree, 2);
setTestCaseTreeInitialPathExpansion($scope.testCaseTree, initiallySelectedNodePath);
if ($scope.treeAccess)
selectInitialNode();
refilter();
}
});
$scope.$watch('testCasePathFilter', refilter);
rtdb.bind('TestCaseList', batchResultId, $scope, { valueName:'testCaseList' });
}])
// Stats for selected group node in tree.
// \todo [petri] same as TreeGroupCtrl?
.controller('TestGroupStatsCtrl', ['$scope', '$stateParams', 'rtdb', 'rpc', function($scope, $stateParams, rtdb, rpc)
{
var objId = $stateParams.batchResultId + '/' + $stateParams.testGroupPath;
rtdb.bind('TestCaseTreeGroup', objId, $scope, { invalidateMode: 'scope' });
$scope.testGroupPath = $stateParams.testGroupPath;
// Batch name if displaying a comparison view
$scope.batchResultName = "";
if ($scope.batchResults && $scope.batchResultIds && $stateParams.batchResultId)
{
var batchResult = $scope.batchResults[$scope.batchResultIds.indexOf($stateParams.batchResultId)];
if (batchResult)
$scope.batchResultName = batchResult.name;
}
angular.extend($scope,
{
getNumResults: function(value)
{
if (value)
return value.numSuccess +
value.numFailure +
value.numCrash +
value.numTimeout +
value.numQualityWarning +
value.numCompatibilityWarning +
value.numNotSupported +
value.numResourceError +
value.numInternalError;
else
return '0';
},
getResultTypePercentage: function(value)
{
if (value)
{
var numHits = 0;
for (var i = 1; i < arguments.length; ++i)
numHits += value[arguments[i]]
return percentageString(numHits, value.numTotalCases);
}
else
return '0';
},
getResultTypeCount: function(value)
{
if (value)
{
var numHits = 0;
for (var i = 1; i < arguments.length; ++i)
numHits += value[arguments[i]]
return numHits;
}
else
return '0';
},
});
}])
// Treeview selected group node.
.controller('TreeGroupCtrl', ['$scope', 'rtdb', 'rpc', function($scope, rtdb, rpc)
{
angular.extend($scope,
{
value: undefined,
init: function(objId)
{
rtdb.bind('TestCaseTreeGroup', objId, $scope, { invalidateMode: 'scope' });
},
getNumResults: function()
{
if ($scope.value)
return $scope.value.numSuccess +
$scope.value.numFailure +
$scope.value.numCrash +
$scope.value.numTimeout +
$scope.value.numQualityWarning +
$scope.value.numCompatibilityWarning +
$scope.value.numNotSupported +
$scope.value.numResourceError +
$scope.value.numInternalError;
else
return "0";
},
getResultTypePercentage: function()
{
if ($scope.value)
{
var numHits = 0;
for (var i = 0; i < arguments.length; ++i)
numHits += $scope.value[arguments[i]]
return percentageString(numHits, $scope.value.numTotalCases);
}
else
return '0';
},
});
}])
// Treeview selected leaf node.
.controller('TreeLeafCtrl', ['$scope', 'rtdb', 'rpc', function($scope, rtdb, rpc)
{
var resultFloat = undefined;
var onUpdate = function()
{
var isNumeric = $scope.value.caseType === TEST_CASE_TYPE.PERFORMANCE ||
$scope.value.caseType === TEST_CASE_TYPE.ACCURACY;
if (isNumeric)
{
resultFloat = parseFloat($scope.value.result);
if (isFinite(resultFloat))
{
if ($scope.groupShared.bestPerformanceResult === undefined ||
$scope.groupShared.bestPerformanceResult < resultFloat)
$scope.groupShared.bestPerformanceResult = resultFloat;
}
else
isNumeric = false;
}
$scope.isNumericResult = isNumeric;
};
angular.extend($scope,
{
value: undefined,
isNumericResult: false,
getResultPercentage: function()
{
if (resultFloat !== undefined)
return percentageString(Math.max(0, resultFloat), $scope.groupShared.bestPerformanceResult);
else
return '0';
},
init: function(obj)
{
// \note obj can be either a header object's id (a string) or the header object itself.
if (_.isString(obj))
{
rtdb.bind('TestCaseHeader', obj, $scope, { onUpdate: onUpdate, invalidateMode: 'scope' });
}
else
{
$scope.value = obj;
onUpdate();
}
},
});
}])
;
function deviceConfigNameHumanReadable(config)
{
return config.name || (config.isADBDevice ? 'Unknown Android device (' + config.adbSerialNumber + ')' : '');
}
function percentageString(num, den)
{
// \note Slightly less than 100.0 is used so that we don't get
// a total of greater than 100 due to precision issues
// when summing percentages.
return (num * 99.99 / den).toFixed(2);
}