Split tests page into list and detail page

This commit splits the tests page into a list page and its detail page
for improving the performance of showing the page.
Previously, we rendered numerous tests list in one tests page. So it
causes the performance issue. Splitting the page reduces the number of
showing test lists. This is not perfect solution but it's better than
before at least.

Change-Id: If1a97740d09359ae17d0f0b7df8d36256ac99f47
Closes-Bug: #1548354
This commit is contained in:
Masayuki Igawa 2016-02-22 15:53:02 -05:00
parent 972e247b32
commit 8a0ab1d425
8 changed files with 288 additions and 109 deletions

View 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);

View File

@ -27,40 +27,15 @@ function TestsController($scope, healthService, testService, $location) {
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,
var sortedKeys = _.sortBy(_.keys(testsByHierarchy));
_.each(sortedKeys, function(key) {
if (!vm.chartData[key]) {
vm.chartData[key] = [{
key: key,
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);
});
});
};

View File

@ -31,6 +31,17 @@ function OnConfig($stateProvider, $locationProvider, $urlRouterProvider) {
templateUrl: 'tests.html',
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', {
url: '/job/:jobName',
controller: 'JobController as job',

View File

@ -100,6 +100,7 @@ function HealthService($http, config) {
service.getTests = function() {
return config.get().then(function(config) {
return $http.jsonp(config.apiRoot + '/tests', {
cache: true,
params: { callback: 'JSON_CALLBACK' }
});
});

View 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>

View File

@ -13,58 +13,18 @@
</div>
<div class="row">
<div class="col-lg-12">
<accordion close-others="false">
<div class="panel panel-body panel-default" ng-repeat="(key, value) in tests.chartData">
<accordion-group heading="Details for {{ 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="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 class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title">Details list</h3>
</div>
<div class="panel-body">
<div class="list-group">
<a ng-repeat="(key, value) in tests.chartData"
ui-sref="testsDetail({ key: key })"
class="list-group-item">{{ key }}</a>
</div>
</accordion-group>
</div>
</div>
</accordion>
</div>
</div>
</div>

View 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);
});
});

View File

@ -70,35 +70,8 @@ describe('TestsController', function() {
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
}]
values: [],
tests: []
}]
};
expect(testsController.chartData).toEqual(expectedChartData);