Merge "Split tests page into list and detail page"
This commit is contained in:
commit
b2267c824e
82
app/js/controllers/tests-detail.js
Normal file
82
app/js/controllers/tests-detail.js
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
var controllersModule = require('./_index');
|
||||||
|
var _ = require('underscore');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @ngInject
|
||||||
|
*/
|
||||||
|
function TestsDetailController($scope, healthService, testService, key, $location) {
|
||||||
|
|
||||||
|
// ViewModel
|
||||||
|
var vm = this;
|
||||||
|
vm.searchTest = '';
|
||||||
|
vm.key = decodeURIComponent(key);
|
||||||
|
|
||||||
|
vm.processData = function(data) {
|
||||||
|
vm.chartData = {};
|
||||||
|
|
||||||
|
var testsByHierarchy = _.groupBy(data.tests, function(test) {
|
||||||
|
var testId = testService.removeIdNoise(test.test_id);
|
||||||
|
var keyMatcher = /^(\w*)\./g;
|
||||||
|
var matches = keyMatcher.exec(testId);
|
||||||
|
|
||||||
|
if (matches) {
|
||||||
|
return matches[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'Others';
|
||||||
|
});
|
||||||
|
|
||||||
|
var getTestFailureAvg = function(test) {
|
||||||
|
return test.failure / test.run_count;
|
||||||
|
};
|
||||||
|
|
||||||
|
_.each(testsByHierarchy, function(tests, hierarchy, list) {
|
||||||
|
if (!vm.chartData[hierarchy]) {
|
||||||
|
vm.chartData[hierarchy] = [{
|
||||||
|
key: hierarchy,
|
||||||
|
values: [],
|
||||||
|
tests: []
|
||||||
|
}];
|
||||||
|
}
|
||||||
|
|
||||||
|
var orderedTests = _.sortBy(tests, function(test) {
|
||||||
|
return getTestFailureAvg(test) * -1;
|
||||||
|
});
|
||||||
|
|
||||||
|
var topFailures = _.first(orderedTests, 10);
|
||||||
|
|
||||||
|
topFailures.forEach(function(test) {
|
||||||
|
var failureAverage = getTestFailureAvg(test);
|
||||||
|
if (!isNaN(failureAverage) && parseFloat(failureAverage) > 0.01) {
|
||||||
|
var chartData = {
|
||||||
|
label: test.test_id,
|
||||||
|
value: failureAverage
|
||||||
|
};
|
||||||
|
vm.chartData[hierarchy][0].values.push(chartData);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
orderedTests.forEach(function(test) {
|
||||||
|
test.failureAverage = getTestFailureAvg(test);
|
||||||
|
vm.chartData[hierarchy][0].tests.push(test);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
vm.loadData = function() {
|
||||||
|
healthService.getTests().then(function(response) {
|
||||||
|
vm.processData(response.data);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
vm.searchTest = $location.search().searchTest || '';
|
||||||
|
|
||||||
|
vm.loadData();
|
||||||
|
|
||||||
|
vm.onSearchChange = function() {
|
||||||
|
$location.search('searchTest', $scope.testsDetail.searchTest);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
controllersModule.controller('TestsDetailController', TestsDetailController);
|
@ -27,40 +27,15 @@ function TestsController($scope, healthService, testService, $location) {
|
|||||||
return 'Others';
|
return 'Others';
|
||||||
});
|
});
|
||||||
|
|
||||||
var getTestFailureAvg = function(test) {
|
var sortedKeys = _.sortBy(_.keys(testsByHierarchy));
|
||||||
return test.failure / test.run_count;
|
_.each(sortedKeys, function(key) {
|
||||||
};
|
if (!vm.chartData[key]) {
|
||||||
|
vm.chartData[key] = [{
|
||||||
_.each(testsByHierarchy, function(tests, hierarchy, list) {
|
key: key,
|
||||||
if (!vm.chartData[hierarchy]) {
|
|
||||||
vm.chartData[hierarchy] = [{
|
|
||||||
key: hierarchy,
|
|
||||||
values: [],
|
values: [],
|
||||||
tests: []
|
tests: []
|
||||||
}];
|
}];
|
||||||
}
|
}
|
||||||
|
|
||||||
var orderedTests = _.sortBy(tests, function(test) {
|
|
||||||
return getTestFailureAvg(test) * -1;
|
|
||||||
});
|
|
||||||
|
|
||||||
var topFailures = _.first(orderedTests, 10);
|
|
||||||
|
|
||||||
topFailures.forEach(function(test) {
|
|
||||||
var failureAverage = getTestFailureAvg(test);
|
|
||||||
if (!isNaN(failureAverage) && parseFloat(failureAverage) > 0.01) {
|
|
||||||
var chartData = {
|
|
||||||
label: test.test_id,
|
|
||||||
value: failureAverage
|
|
||||||
};
|
|
||||||
vm.chartData[hierarchy][0].values.push(chartData);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
orderedTests.forEach(function(test) {
|
|
||||||
test.failureAverage = getTestFailureAvg(test);
|
|
||||||
vm.chartData[hierarchy][0].tests.push(test);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -31,6 +31,17 @@ function OnConfig($stateProvider, $locationProvider, $urlRouterProvider) {
|
|||||||
templateUrl: 'tests.html',
|
templateUrl: 'tests.html',
|
||||||
title: 'Tests'
|
title: 'Tests'
|
||||||
})
|
})
|
||||||
|
.state('testsDetail', {
|
||||||
|
url: '/tests/:key',
|
||||||
|
controller: 'TestsDetailController as testsDetail',
|
||||||
|
templateUrl: 'tests-detail.html',
|
||||||
|
title: 'Tests Detail',
|
||||||
|
resolve: /*@ngInject*/ {
|
||||||
|
'key': function($stateParams) {
|
||||||
|
return $stateParams.key;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
.state('job', {
|
.state('job', {
|
||||||
url: '/job/:jobName',
|
url: '/job/:jobName',
|
||||||
controller: 'JobController as job',
|
controller: 'JobController as job',
|
||||||
|
@ -100,6 +100,7 @@ function HealthService($http, config) {
|
|||||||
service.getTests = function() {
|
service.getTests = function() {
|
||||||
return config.get().then(function(config) {
|
return config.get().then(function(config) {
|
||||||
return $http.jsonp(config.apiRoot + '/tests', {
|
return $http.jsonp(config.apiRoot + '/tests', {
|
||||||
|
cache: true,
|
||||||
params: { callback: 'JSON_CALLBACK' }
|
params: { callback: 'JSON_CALLBACK' }
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
70
app/views/tests-detail.html
Normal file
70
app/views/tests-detail.html
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
<header class="bs-header">
|
||||||
|
<div class="container">
|
||||||
|
<h1 class="page-header">Tests Detail</h1>
|
||||||
|
<crumb-menu></crumb-menu>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="container">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-lg-12">
|
||||||
|
<loading-indicator></loading-indicator>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-lg-12">
|
||||||
|
<accordion close-others="false">
|
||||||
|
<div class="panel panel-body panel-default">
|
||||||
|
<accordion-group heading="Details for {{ testsDetail.key }}" is-open="true">
|
||||||
|
<div>
|
||||||
|
<form>
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="input-group">
|
||||||
|
<div class="input-group-addon"><i class="fa fa-search"></i></div>
|
||||||
|
<input type="text" class="form-control"
|
||||||
|
placeholder="Search for test"
|
||||||
|
ng-model="testsDetail.searchTest"
|
||||||
|
ng-model-options="{debounce: 250}"
|
||||||
|
ng-change="testsDetail.onSearchChange()">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
<table table-sort data="testsDetail.chartData[testsDetail.key][0]['tests']" class="table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th sort-field="test_id" class="text-left">
|
||||||
|
Test ID
|
||||||
|
</th>
|
||||||
|
<th sort-field="success" class="text-right" style="min-width:80px">
|
||||||
|
Passed
|
||||||
|
</th>
|
||||||
|
<th sort-field="failure" class="text-right" style="min-width:80px">
|
||||||
|
Failed
|
||||||
|
</th>
|
||||||
|
<th sort-default='reversed' sort-field="failureAverage" class="text-right" style="min-width:95px">
|
||||||
|
Failure %
|
||||||
|
</th>
|
||||||
|
<th sort-field="run_time" class="text-right" style="min-width:80px">
|
||||||
|
Avg. Runtime (secs.)
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr table-ref="table" ng-repeat="test in table.dataSorted | filter:testsDetail.searchTest">
|
||||||
|
<td class="text-left">
|
||||||
|
<a ui-sref="test({ testId: test.test_id })"> {{test.test_id | limitTo: 110}}</a>
|
||||||
|
</td>
|
||||||
|
<td class="text-right">{{ test.success | number }}</td>
|
||||||
|
<td class="text-right">{{ test.failure | number }}</td>
|
||||||
|
<td class="text-right">{{ test.failureAverage * 100 | number: 2 }}%</td>
|
||||||
|
<td class="text-right">{{ test.run_time | number: 2 }}</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</accordion-group>
|
||||||
|
</div>
|
||||||
|
</accordion>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
@ -13,58 +13,18 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-lg-12">
|
<div class="col-lg-12">
|
||||||
<accordion close-others="false">
|
<div class="panel panel-default">
|
||||||
<div class="panel panel-body panel-default" ng-repeat="(key, value) in tests.chartData">
|
<div class="panel-heading">
|
||||||
<accordion-group heading="Details for {{ key }}" is-open="true">
|
<h3 class="panel-title">Details list</h3>
|
||||||
<div>
|
</div>
|
||||||
<form>
|
<div class="panel-body">
|
||||||
<div class="form-group">
|
<div class="list-group">
|
||||||
<div class="input-group">
|
<a ng-repeat="(key, value) in tests.chartData"
|
||||||
<div class="input-group-addon"><i class="fa fa-search"></i></div>
|
ui-sref="testsDetail({ key: key })"
|
||||||
<input type="text" class="form-control"
|
class="list-group-item">{{ key }}</a>
|
||||||
placeholder="Search for test"
|
|
||||||
ng-model="tests.searchTest"
|
|
||||||
ng-model-options="{debounce: 250}"
|
|
||||||
ng-change="tests.onSearchChange()">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
<table table-sort data="tests.chartData[key][0]['tests']" class="table">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th sort-field="test_id" class="text-left">
|
|
||||||
Test ID
|
|
||||||
</th>
|
|
||||||
<th sort-field="success" class="text-right" style="min-width:80px">
|
|
||||||
Passed
|
|
||||||
</th>
|
|
||||||
<th sort-field="failure" class="text-right" style="min-width:80px">
|
|
||||||
Failed
|
|
||||||
</th>
|
|
||||||
<th sort-default='reversed' sort-field="failureAverage" class="text-right" style="min-width:95px">
|
|
||||||
Failure %
|
|
||||||
</th>
|
|
||||||
<th sort-field="run_time" class="text-right" style="min-width:80px">
|
|
||||||
Avg. Runtime (secs.)
|
|
||||||
</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<tr table-ref="table" ng-repeat="test in table.dataSorted | filter:tests.searchTest">
|
|
||||||
<td class="text-left">
|
|
||||||
<a ui-sref="test({ testId: test.test_id })"> {{test.test_id | limitTo: 110}}</a>
|
|
||||||
</td>
|
|
||||||
<td class="text-right">{{ test.success | number }}</td>
|
|
||||||
<td class="text-right">{{ test.failure | number }}</td>
|
|
||||||
<td class="text-right">{{ test.failureAverage * 100 | number: 2 }}%</td>
|
|
||||||
<td class="text-right">{{ test.run_time | number: 2 }}</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
</div>
|
||||||
</accordion-group>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</accordion>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
107
test/unit/controllers/tests_detail_spec.js
Normal file
107
test/unit/controllers/tests_detail_spec.js
Normal file
@ -0,0 +1,107 @@
|
|||||||
|
describe('TestsDetailController', function() {
|
||||||
|
beforeEach(function() {
|
||||||
|
module('app');
|
||||||
|
module('app.controllers');
|
||||||
|
});
|
||||||
|
|
||||||
|
var $scope, $httpBackend, $controller, healthService;
|
||||||
|
var API_ROOT = 'http://8.8.4.4:8080';
|
||||||
|
var DEFAULT_START_DATE = new Date();
|
||||||
|
|
||||||
|
beforeEach(inject(function($rootScope, _$httpBackend_, _$controller_, _healthService_) {
|
||||||
|
$httpBackend = _$httpBackend_;
|
||||||
|
|
||||||
|
mockConfigService();
|
||||||
|
mockHealthService();
|
||||||
|
|
||||||
|
$scope = $rootScope.$new();
|
||||||
|
$controller = _$controller_;
|
||||||
|
healthService = _healthService_;
|
||||||
|
}));
|
||||||
|
|
||||||
|
function mockHealthService() {
|
||||||
|
var expectedResponse = {
|
||||||
|
tests: [
|
||||||
|
{
|
||||||
|
failure: 5592,
|
||||||
|
id: '00187173-ab23-4181-9a15-e291a0d8e2d1',
|
||||||
|
run_count: 55920,
|
||||||
|
run_time: 0.608151,
|
||||||
|
success: 55920,
|
||||||
|
test_id: 'tempest.api.identity.admin.v2.test_users.one'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
failure: 0,
|
||||||
|
id: '001c6860-c966-4c0b-9928-ecccd162bed0',
|
||||||
|
run_count: 4939,
|
||||||
|
run_time: 5.97596,
|
||||||
|
success: 4939,
|
||||||
|
test_id: 'tempest.api.volume.admin.test_snapshots_actions.two'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
failure: 1,
|
||||||
|
id: '002a15e0-f6d1-472a-bd66-bb13ac4d77aa',
|
||||||
|
run_count: 32292,
|
||||||
|
run_time: 1.18864,
|
||||||
|
success: 32291,
|
||||||
|
test_id: 'tempest.api.network.test_routers.three'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
var endpoint = API_ROOT + '/tests?callback=JSON_CALLBACK';
|
||||||
|
$httpBackend.expectJSONP(endpoint)
|
||||||
|
.respond(200, expectedResponse);
|
||||||
|
}
|
||||||
|
|
||||||
|
function mockConfigService() {
|
||||||
|
var expectedResponse = { apiRoot: API_ROOT };
|
||||||
|
var endpoint = 'config.json';
|
||||||
|
$httpBackend.expectGET(endpoint).respond(200, expectedResponse);
|
||||||
|
}
|
||||||
|
|
||||||
|
it('should process chart data correctly', function() {
|
||||||
|
var testsDetailController = $controller('TestsDetailController', {
|
||||||
|
healthService: healthService,
|
||||||
|
$scope: $scope,
|
||||||
|
key: 'tempest'
|
||||||
|
});
|
||||||
|
$httpBackend.flush();
|
||||||
|
|
||||||
|
var expectedChartData = {
|
||||||
|
'tempest': [{
|
||||||
|
key: 'tempest',
|
||||||
|
values: [{
|
||||||
|
label: 'tempest.api.identity.admin.v2.test_users.one',
|
||||||
|
value: 0.1
|
||||||
|
}],
|
||||||
|
tests: [{
|
||||||
|
failure: 5592,
|
||||||
|
id: '00187173-ab23-4181-9a15-e291a0d8e2d1',
|
||||||
|
run_count: 55920,
|
||||||
|
run_time: 0.608151,
|
||||||
|
success: 55920,
|
||||||
|
test_id: 'tempest.api.identity.admin.v2.test_users.one',
|
||||||
|
failureAverage: 0.1
|
||||||
|
}, {
|
||||||
|
failure: 1,
|
||||||
|
id: '002a15e0-f6d1-472a-bd66-bb13ac4d77aa',
|
||||||
|
run_count: 32292,
|
||||||
|
run_time: 1.18864,
|
||||||
|
success: 32291,
|
||||||
|
test_id: 'tempest.api.network.test_routers.three',
|
||||||
|
failureAverage: 0.0000309674222717701
|
||||||
|
}, {
|
||||||
|
failure: 0,
|
||||||
|
id: '001c6860-c966-4c0b-9928-ecccd162bed0',
|
||||||
|
run_count: 4939,
|
||||||
|
run_time: 5.97596,
|
||||||
|
success: 4939,
|
||||||
|
test_id: 'tempest.api.volume.admin.test_snapshots_actions.two',
|
||||||
|
failureAverage: 0
|
||||||
|
}]
|
||||||
|
}]
|
||||||
|
};
|
||||||
|
expect(testsDetailController.chartData).toEqual(expectedChartData);
|
||||||
|
});
|
||||||
|
});
|
@ -70,35 +70,8 @@ describe('TestsController', function() {
|
|||||||
var expectedChartData = {
|
var expectedChartData = {
|
||||||
'tempest': [{
|
'tempest': [{
|
||||||
key: 'tempest',
|
key: 'tempest',
|
||||||
values: [{
|
values: [],
|
||||||
label: 'tempest.api.identity.admin.v2.test_users.one',
|
tests: []
|
||||||
value: 0.1
|
|
||||||
}],
|
|
||||||
tests: [{
|
|
||||||
failure: 5592,
|
|
||||||
id: '00187173-ab23-4181-9a15-e291a0d8e2d1',
|
|
||||||
run_count: 55920,
|
|
||||||
run_time: 0.608151,
|
|
||||||
success: 55920,
|
|
||||||
test_id: 'tempest.api.identity.admin.v2.test_users.one',
|
|
||||||
failureAverage: 0.1
|
|
||||||
}, {
|
|
||||||
failure: 1,
|
|
||||||
id: '002a15e0-f6d1-472a-bd66-bb13ac4d77aa',
|
|
||||||
run_count: 32292,
|
|
||||||
run_time: 1.18864,
|
|
||||||
success: 32291,
|
|
||||||
test_id: 'tempest.api.network.test_routers.three',
|
|
||||||
failureAverage: 0.0000309674222717701
|
|
||||||
}, {
|
|
||||||
failure: 0,
|
|
||||||
id: '001c6860-c966-4c0b-9928-ecccd162bed0',
|
|
||||||
run_count: 4939,
|
|
||||||
run_time: 5.97596,
|
|
||||||
success: 4939,
|
|
||||||
test_id: 'tempest.api.volume.admin.test_snapshots_actions.two',
|
|
||||||
failureAverage: 0
|
|
||||||
}]
|
|
||||||
}]
|
}]
|
||||||
};
|
};
|
||||||
expect(testsController.chartData).toEqual(expectedChartData);
|
expect(testsController.chartData).toEqual(expectedChartData);
|
||||||
|
Loading…
Reference in New Issue
Block a user