diff --git a/app/js/controllers/home.js b/app/js/controllers/home.js index d709c00..ccbc98e 100644 --- a/app/js/controllers/home.js +++ b/app/js/controllers/home.js @@ -9,10 +9,14 @@ function HomeCtrl($scope, $state, datasetService) { // ViewModel var vm = this; - vm.focus = $state.params.datasetId; + vm.focus = $state.params.artifactName; - datasetService.list().then(function(response) { - vm.tempest = response.data.tempest; + datasetService.groups().then(function(groups) { + vm.groups = groups; + + if (!vm.focus) { + vm.focus = groups[0]; + } }); // update the page url as the focus id changes, but don't reload @@ -20,7 +24,7 @@ function HomeCtrl($scope, $state, datasetService) { return vm.focus; }, function(value, old) { if (value !== old) { - $state.go('home', { datasetId: value }, { notify: false }); + $state.go('home', { artifactName: value }, { notify: false }); } }); diff --git a/app/js/controllers/test-details.js b/app/js/controllers/test-details.js index dc5736a..c67336b 100644 --- a/app/js/controllers/test-details.js +++ b/app/js/controllers/test-details.js @@ -2,10 +2,6 @@ var controllersModule = require('./_index'); -/** - * @ngInject - */ -var TestDetailsCtrl = /** * Responsible for making three calls to the dataset service. First, the * dataset corresponding to the given int id is loaded, then the raw and details @@ -13,78 +9,84 @@ var TestDetailsCtrl = * of the details JSON is kept in `originalDetails` so that information is not * lost when parsing. Progress of the dataset service calls is recorded and * displayed in a progress bar on `test-details.html`. -*/ -function($scope, $location, $stateParams, $log, datasetService, progressService) { + * @ngInject + */ +function TestDetailsCtrl( + $scope, $location, $stateParams, $log, $q, + datasetService, progressService) { var vm = this; - vm.datasetId = $stateParams.datasetId; - var testName = $stateParams.test; - vm.testName = testName; + vm.artifactName = $stateParams.artifactName; + vm.testName = $stateParams.test; progressService.start({ parent: 'div[role="main"] .panel-body' }); // load dataset, raw json, and details json - datasetService.get($stateParams.datasetId) - .then(function(response) { - vm.dataset = response; - vm.stats = response.stats; - return datasetService.raw(response); - }) - .then(function(raw) { - var item = null; - for (var t in raw.data) { - if (raw.data[t].name === testName) { - item = raw.data[t]; - } - } - vm.item = item; - progressService.inc(); - return datasetService.details(vm.dataset); - }) - .then(function(deets) { - vm.details = deets; - vm.originalDetails = angular.copy(deets.data[testName]); - vm.itemDetails = deets.data[testName]; - progressService.done(); - }) - .catch(function(error) { - $log.error(error); - progressService.done(); - }); + var statsArtifact = datasetService.artifact(vm.artifactName, 'subunit-stats'); + var subunitArtifact = datasetService.artifact(vm.artifactName, 'subunit'); + var detailsArtifact = datasetService.artifact(vm.artifactName, 'subunit-details'); + + var statsPromise = statsArtifact.then(function(response) { + vm.stats = response.data; + }); + + var subunitPromise = subunitArtifact.then(function(response) { + var item = null; + for (var t in response.data) { + if (response.data[t].name === vm.testName) { + item = response.data[t]; + } + } + vm.item = item; + progressService.inc(); + }); + + var detailsPromise = detailsArtifact.then(function(details) { + vm.details = details; + vm.originalDetails = angular.copy(details.data[vm.testName]); + vm.itemDetails = details.data[vm.testName]; + }).catch(function(ex) { + // ignore errors, details won't exist for deployer + }); + + $q.all([statsPromise, subunitPromise, detailsPromise]).catch(function(ex) { + $log.error(ex); + }).finally(function() { + progressService.done(); + }); - vm.parsePythonLogging = /** * This function changes the `itemDetails.pythonlogging` variable to only * show lines with the log levels specified by the four boolean parameters. - * EX: If the `showINFO` parameter is set to true, `itemDetails.pythonlogging` + * EX: If the `info` parameter is set to true, `itemDetails.pythonlogging` * will display lines that contain the text `INFO`. - * @param {boolean} showINFO - * @param {boolean} showDEBUG - * @param {boolean} showWARNING - * @param {boolean} showERROR + * @param {boolean} info + * @param {boolean} debug + * @param {boolean} warning + * @param {boolean} error */ - function(showINFO, showDEBUG, showWARNING, showERROR) { + vm.parsePythonLogging = function(info, debug, warning, error) { if (vm.originalDetails && vm.originalDetails.pythonlogging) { var log = vm.originalDetails.pythonlogging; var ret = []; var lines = log.split('\n'); for (var i in lines) { var line = lines[i]; - if (showINFO && line.includes("INFO")) { + if (info && line.includes("INFO")) { ret.push(line); } - if (showDEBUG && line.includes("DEBUG")) { + if (debug && line.includes("DEBUG")) { ret.push(line); } - if (showWARNING && line.includes("WARNING")) { + if (warning && line.includes("WARNING")) { ret.push(line); } - if (showERROR && line.includes("ERROR")) { + if (error && line.includes("ERROR")) { ret.push(line); } } vm.itemDetails.pythonlogging = ret.join('\n'); } }; +} -}; controllersModule.controller('TestDetailsController', TestDetailsCtrl); diff --git a/app/js/controllers/timeline.js b/app/js/controllers/timeline.js index 7f0c7c8..04f0c6b 100644 --- a/app/js/controllers/timeline.js +++ b/app/js/controllers/timeline.js @@ -9,12 +9,7 @@ function TimelineCtrl($scope, $location, $stateParams, datasetService) { // ViewModel var vm = this; - - datasetService.get($stateParams.datasetId).then(function(dataset) { - vm.dataset = dataset; - }, function(reason) { - vm.error = "Unable to load dataset: " + reason; - }); + vm.artifactName = $stateParams.artifactName; vm.hoveredItem = null; vm.selectedItem = null; diff --git a/app/js/directives/tempest-summary.js b/app/js/directives/tempest-summary.js index 4eac7db..ceddf41 100644 --- a/app/js/directives/tempest-summary.js +++ b/app/js/directives/tempest-summary.js @@ -7,27 +7,27 @@ var directivesModule = require('./_index.js'); */ function tempestSummary() { - /** - * @ngInject - */ - var controller = /** * Responsible for getting the basic run summary stats via the dataset service. * Also calculates the duration of the run - `timeDiff` - by subtracting the * run's start and end timestamps. + * @ngInject */ - function($scope, $attrs, datasetService) { - $scope.$watch('dataset', function(dataset) { - var stats = dataset.stats; - $scope.stats = stats; - $scope.timeDiff = (new Date(stats.end) - new Date(stats.start)) / 1000; + var controller = function($scope, $attrs, datasetService) { + $scope.$watch('artifactName', function(artifactName) { + datasetService.artifact(artifactName, 'subunit-stats').then(function(response) { + var stats = response.data; + $scope.stats = stats; + $scope.timeDiff = (new Date(stats.end) - new Date(stats.start)) / 1000; + }); }); }; return { restrict: 'EA', scope: { - 'dataset': '=' + 'index': '=', + 'artifactName': '=' }, controller: controller, templateUrl: 'directives/tempest-summary.html' diff --git a/app/js/directives/timeline-details.js b/app/js/directives/timeline-details.js index ddd71d5..37f2e4a 100644 --- a/app/js/directives/timeline-details.js +++ b/app/js/directives/timeline-details.js @@ -16,7 +16,7 @@ function timelineDetails() { return { restrict: 'EA', scope: { - 'dataset': '=', + 'artifactName': '=', 'item': '=' }, controller: controller, diff --git a/app/js/directives/timeline-dstat.js b/app/js/directives/timeline-dstat.js index 747a28b..9ab3907 100644 --- a/app/js/directives/timeline-dstat.js +++ b/app/js/directives/timeline-dstat.js @@ -7,7 +7,7 @@ var parseDstat = require('../util/dstat-parse'); var d3 = require('d3'); var getDstatLanes = function(data, mins, maxes) { - if (!data) { + if (!data || !data.length) { return []; } @@ -245,6 +245,10 @@ function timelineDstat($document, $window) { var bottom = y(laneIndex) + laneHeight; for (var pathIndex = 0; pathIndex < laneDef.length; pathIndex++) { + if (!region.data.length) { + continue; + } + var pathDef = laneDef[pathIndex]; var line = pathDef.type === 'line'; diff --git a/app/js/directives/timeline.js b/app/js/directives/timeline.js index 458cd41..596fdcf 100644 --- a/app/js/directives/timeline.js +++ b/app/js/directives/timeline.js @@ -378,6 +378,9 @@ function timeline($window, $log, datasetService, progressService) { var accessor = function(d) { return d.system_time; }; var minIndex = arrayUtil.binaryMinIndex(min, raw.entries, accessor); var maxIndex = arrayUtil.binaryMaxIndex(max, raw.entries, accessor); + if (minIndex < 0) { + minIndex = 0; + } self.dstat = { entries: raw.entries.slice(minIndex, maxIndex), @@ -388,8 +391,8 @@ function timeline($window, $log, datasetService, progressService) { $scope.$broadcast('dstatLoaded', self.dstat); }; - $scope.$watch('dataset', function(dataset) { - if (!dataset) { + $scope.$watch('artifactName', function(artifactName) { + if (!artifactName) { return; } @@ -398,11 +401,11 @@ function timeline($window, $log, datasetService, progressService) { // load dataset details (raw log entries and dstat) sequentially // we need to determine the initial date from the subunit data to parse // dstat - datasetService.raw(dataset).then(function(response) { + datasetService.artifact(artifactName, 'subunit').then(function(response) { progressService.set(0.33); initData(response.data); - return datasetService.dstat(dataset); + return datasetService.artifact('dstat'); }).then(function(response) { progressService.set(0.66); var firstDate = new Date(self.dataRaw[0].timestamps[0]); @@ -462,7 +465,7 @@ function timeline($window, $log, datasetService, progressService) { transclude: true, templateUrl: 'directives/timeline.html', scope: { - 'dataset': '=', + 'artifactName': '=', 'hoveredItem': '=', 'selectedItem': '=', 'preselect': '=' diff --git a/app/js/on_config.js b/app/js/on_config.js index 2f9624a..28405a1 100644 --- a/app/js/on_config.js +++ b/app/js/on_config.js @@ -6,15 +6,15 @@ function OnConfig($stateProvider, $locationProvider, $urlRouterProvider) { $stateProvider.state('home', { - url: '/{datasetId:int}', - params: { datasetId: 0 }, + url: '/{artifactName}', + params: { artifactName: null }, controller: 'HomeController as home', templateUrl: 'home.html', title: 'Home' }); $stateProvider.state('timeline', { - url: '/{datasetId:int}/timeline?test', + url: '/{artifactName}/timeline?test', controller: 'TimelineController as timeline', templateUrl: 'timeline.html', reloadOnSearch: false, @@ -22,7 +22,7 @@ function OnConfig($stateProvider, $locationProvider, $urlRouterProvider) { }); $stateProvider.state('testDetails', { - url: '/{datasetId:int}/test-details/{test}', + url: '/{artifactName}/test-details/{test}', controller: 'TestDetailsController', controllerAs: 'testDetails', templateUrl: 'test-details.html', diff --git a/app/js/services/dataset.js b/app/js/services/dataset.js index 53ef0ec..cad7bca 100644 --- a/app/js/services/dataset.js +++ b/app/js/services/dataset.js @@ -5,73 +5,258 @@ var servicesModule = require('./_index.js'); /** * @ngInject */ -function DatasetService($q, $http) { +function DatasetService($q, $http, $window) { var service = {}; - service.list = function() { + var config = null; + var datasets = null; + var artifacts = new Map(); + var deployer = false; + + /** + * Return a promise to fetch the dataset associated with the current URL path. + * This is only valid when in deployer mode. + * @return {Promise} an $http promise for the current deployer dataset + */ + var fetchDeployerDataset = function() { + // get uuid from first segment of url, but remove any defined config root + var path = $window.location.pathname; + if (config.root && path.startsWith(config.root)) { + path = path.replace(config.root, ''); + } + + // remove leading '/' (if any) + if (path.startsWith('/')) { + path = path.substr(1, path.length - 1); + } + + // trim to first segment if necessary + if (path.includes('/')) { + path = path.substring(0, path.indexOf('/')); + } + return $http({ cache: true, - url: 'data/config.json', - method: 'GET' + url: config.apiRoot + '/task', + method: 'POST', + data: { q: path } }); }; - service.get = function(id) { - return $q(function(resolve, reject) { - service.list().then(function(response) { - for (var i in response.data.tempest) { - var entry = response.data.tempest[i]; - if (entry.id === id) { - resolve(entry); - return; - } - } + /** + * Adds the given list of artifacts to the global artifact map, based on their + * `artifact_name` fields. + * @param {object[]} artifacts a list of artifacts + */ + var initArtifacts = function(list) { + list.forEach(function(artifact) { + if (artifacts.has(artifact.artifact_name)) { + artifacts.get(artifact.artifact_name).push(artifact); + } else { + artifacts.set(artifact.artifact_name, [artifact]); + } + }); + }; - reject("Dataset not found with ID: " + id); + service.config = function() { + return $q(function(resolve, reject) { + if (config) { + resolve({ config: config, datasets: datasets, artifacts: artifacts }); + return; + } + + $http({ + cache: true, + url: 'data/config.json', + method: 'GET' + }).then(function(response) { + config = response.data; + + if (config.deployer === true) { + deployer = true; + + fetchDeployerDataset().then(function(apiResponse) { + datasets = [ apiResponse.data ]; + initArtifacts(apiResponse.data.artifacts); + resolve({ + config: config, + datasets: datasets, + artifacts: artifacts + }); + }, function(reason) { + reject(reason); + }); + } else { + datasets = config.datasets; + + // merge all datasets into a 1-level grouping for now + config.datasets.forEach(function(dataset) { + initArtifacts(dataset.artifacts); + }); + + resolve({ + config: config, + datasets: datasets, + artifacts: artifacts + }); + } }, function(reason) { reject(reason); }); }); }; - service.raw = function(dataset) { - return $http({ - cache: true, - url: "data/" + dataset.raw, - method: 'GET' - }); - }; - - service.details = function(dataset) { - return $http({ - cache: true, - url: "data/" + dataset.details, - method: 'GET' - }); - }; - - service.tree = function(dataset) { - return $http({ - cache: true, - url: "data/" + dataset.tree, - method: 'GET' - }); - }; - - service.dstat = function(dataset) { + /** + * Lists all datasets. + * @return {Promise} a Promise for the global list of datasets + */ + service.list = function() { return $q(function(resolve, reject) { - if (!dataset.dstat) { - reject({ status: -1, statusText: 'Dstat not available for dataset.' }); - return; + /* eslint-disable angular/di */ + service.config().then(function(config) { + resolve(config.datasets); + }, reject); + /* eslint-enable angular/di */ + }); + }; + + /** + * Lists all artifact groups that contain at least one artifact. If `primary` + * is true (default), only groups with at least one primary artifact are + * returned. + * @return {Promise} a Promise for the global list of datasets + */ + service.groups = function(primary) { + if (typeof primary === 'undefined') { + primary = true; + } + + return $q(function(resolve, reject) { + /* eslint-disable angular/di */ + service.config().then(function(config) { + var ret = []; + config.artifacts.forEach(function(entries, name) { + if (primary) { + entries = entries.filter(function(artifact) { + return artifact.primary; + }); + } + + if (entries.length > 0) { + ret.push(name); + } + }); + + resolve(ret); + }, reject); + /* eslint-enable angular/di */ + }); + }; + + /** + * Gets the dataset with the given ID. Note that for deployer instances, there + * will only ever be a single dataset (id #0). In most cases, dataset #0 + * should be treated as the 'primary' dataset (and should almost always be the + * only one configured). + * @param {number} id the index of the dataset to get + * @return {Promise} a Promise to retreive the specified dataset + */ + service.get = function(id) { + return $q(function(resolve, reject) { + /* eslint-disable angular/di */ + service.config().then(function(config) { + var dataset = config.datasets[id]; + if (dataset) { + resolve(dataset); + } else { + reject("Dataset not found with ID: " + id); + } + }, function(reason) { + reject(reason); + }); + /* eslint-enable angular/di */ + }); + }; + + /** + * Fetch all artifacts with the given `artifact_name` field. This should be + * the primary method for differentiating between artifacts. If no artifact + * name is given, this returns a flat list of all artifacts (via a Promise). + * @param {string} [name] an `artifact_name` field value + * @return {Promise} a promise for a list of matching artifacts + */ + service.artifacts = function(name) { + return $q(function(resolve, reject) { + /* eslint-disable angular/di */ + service.config().then(function(config) { + if (typeof name === 'undefined') { + var ret = []; + config.datasets.forEach(function(dataset) { + ret.push.apply(ret, dataset.artifacts); + }); + resolve(ret); + } else { + var group = config.artifacts.get(name); + if (group && group.length > 0) { + resolve(group); + } else { + reject('No artifacts found with name: ' + name); + } + } + }, reject); + /* eslint-enable angular/di */ + }); + }; + + var _loadArtifact = function(artifact, resolve, reject, message) { + if (artifact) { + var url = null; + if (deployer) { + url = config.apiRoot + '/blob/' + artifact.id; + } else { + url = 'data/' + artifact.path; } resolve($http({ cache: true, - url: "data/" + dataset.dstat, + url: url, method: 'GET' })); - }); + } else { + reject('No artifact found matching ' + message); + } + }; + + /** + * Fetch the artifact with the given `artifact_name` and `artifact_type` + * fields. If only one parameter is provided, only `artifact_type` is + * considered. + * @param {string} [name] an `artifact_name` field value + * @param {string} type an `artifact_type` field value (e.g. 'subunit') + * @return {Promise} a Promise for the actual data associated with the + * artifact + */ + service.artifact = function(name, type) { + if (arguments.length === 1) { + type = arguments[0]; + + return $q(function(resolve, reject) { + service.artifacts().then(function(all) { + _loadArtifact(all.find(function(a) { + return a.artifact_type === type; + }), resolve, reject, 'type=' + type); + }); + }); + } else { + return $q(function(resolve, reject) { + service.artifacts(name).then(function(group) { + _loadArtifact(group.find(function(a) { + return a.artifact_type === type; + }), resolve, reject, 'name=' + name + ', type=' + type); + }, reject); + }); + } }; return service; diff --git a/app/views/directives/tempest-summary.html b/app/views/directives/tempest-summary.html index 83828c1..b02aca3 100644 --- a/app/views/directives/tempest-summary.html +++ b/app/views/directives/tempest-summary.html @@ -1,7 +1,7 @@

- {{dataset.name}} + {{artifactName}} {{stats.start | date:'d MMM, yyyy'}} @@ -33,7 +33,7 @@
Details + ui-sref="timeline({artifactName: artifactName})">Details

@@ -45,7 +45,7 @@
+ ui-sref="testDetails({artifactName: artifactName, test: fail.name})">

{{fail.name | split:'.' | slice:-2 | join:'.'}}

diff --git a/app/views/directives/timeline-details.html b/app/views/directives/timeline-details.html index a5c7f7d..616fe9a 100644 --- a/app/views/directives/timeline-details.html +++ b/app/views/directives/timeline-details.html @@ -57,7 +57,7 @@
diff --git a/app/views/home.html b/app/views/home.html index ff1f5a7..fb8ef08 100644 --- a/app/views/home.html +++ b/app/views/home.html @@ -6,18 +6,20 @@
-
+
No tempest datasets could be loaded!
-
+
- +
-
+

Additional Datasets

@@ -25,18 +27,18 @@
- @@ -26,7 +25,8 @@
- +
diff --git a/stackviz/export.py b/stackviz/export.py index 05793fe..b8bcab4 100644 --- a/stackviz/export.py +++ b/stackviz/export.py @@ -21,12 +21,31 @@ import os import shutil from argparse import ArgumentParser -from functools import partial from stackviz.parser import tempest_subunit _base = os.path.dirname(os.path.abspath(__file__)) -_tempest_count = 0 + + +def environment_params(): + r = {} + + if 'ZUUL_PROJECT' in os.environ: + r['change_project'] = os.environ['ZUUL_PROJECT'] + + if 'ZUUL_CHANGE' in os.environ: + r['change_id'] = os.environ['ZUUL_CHANGE'] + + if 'ZUUL_PATCHSET' in os.environ: + r['revision'] = os.environ['ZUUL_PATCHSET'] + + if 'ZUUL_PIPELINE' in os.environ: + r['pipeline'] = os.environ['ZUUL_PIPELINE'] + + if 'JOB_NAME' in os.environ: + r['name'] = os.environ['JOB_NAME'] + + return r def open_compressed(output_dir, file_name, compress): @@ -46,40 +65,61 @@ def json_date_handler(object): return None -def export_tempest_tree(stream, output_stream): - converted = tempest_subunit.convert_stream(stream, strip_details=True) - tree = tempest_subunit.reorganize(converted) - json.dump(tree, output_stream, default=json_date_handler) - output_stream.close() +def build_artifact(path, artifact_name, artifact_type, content_type, primary, + compress): + ret = { + 'path': path, + 'artifact_name': artifact_name, + 'artifact_type': artifact_type, + 'content_type': content_type, + 'primary': primary + } + + if compress: + ret['content_encoding'] = 'gzip' + + return ret -def export_tempest_raw(stream, output_stream): - converted = tempest_subunit.convert_stream(stream, strip_details=True) - json.dump(converted, output_stream, default=json_date_handler) - output_stream.close() +def export_tempest_raw(name, subunit, output_dir, prefix, compress): + converted = tempest_subunit.convert_stream(subunit, strip_details=True) + + stream, path = open_compressed(output_dir, + prefix + '-raw.json', + compress) + json.dump(converted, stream, default=json_date_handler) + stream.close() + + return converted, build_artifact(path, name, + 'subunit', 'application/json', + True, compress) -def export_tempest_details(stream, output_stream): - converted = tempest_subunit.convert_stream(stream) - +def export_tempest_details(name, subunit, output_dir, prefix, compress): + converted = tempest_subunit.convert_stream(subunit, strip_details=False) output = {} for entry in converted: output[entry['name']] = entry['details'] - json.dump(output, output_stream, default=json_date_handler) - output_stream.close() + stream, path = open_compressed(output_dir, + prefix + '-details.json', + compress) + json.dump(output, stream, default=json_date_handler) + stream.close() + + return build_artifact(path, name, + 'subunit-details', 'application/json', + False, compress) -def get_stats(stream): - converted = tempest_subunit.convert_stream(stream, strip_details=False) - +def export_stats(name, subunit_parsed, output_dir, prefix, compress): start = None end = None total_duration = 0 failures = [] skips = [] - for entry in converted: + for entry in subunit_parsed: # find min/max dates entry_start, entry_end = entry['timestamps'] if start is None or entry_start < start: @@ -112,56 +152,44 @@ def get_stats(stream): 'details': entry['details'].get('reason') }) - return { - 'count': len(converted), + stream, path = open_compressed( + output_dir, prefix + '-stats.json', compress) + + json.dump({ + 'count': len(subunit_parsed), 'start': start, 'end': end, 'total_duration': total_duration, 'failures': failures, 'skips': skips - } + }, stream, default=json_date_handler) + stream.close() + + return build_artifact(path, name, + 'subunit-stats', 'application/json', + False, compress) -def export_tempest(provider, output_dir, dstat, compress): - global _tempest_count - +def export_tempest(provider, output_dir, compress): ret = [] for i in range(provider.count): - path_base = 'tempest_%s_%d' % (provider.name, i) - if provider.count > 1: - name = '%s (%d)' % (provider.description, i) - else: - name = provider.description + prefix = '%s-%d' % (provider.name, i) - open_ = partial(open_compressed, - output_dir=output_dir, - compress=compress) + # convert and save raw (without details) + raw, artifact = export_tempest_raw(provider.name, + provider.get_stream(i), + output_dir, prefix, compress) + ret.append(artifact) - stream_raw, path_raw = open_(file_name=path_base + '_raw.json') - export_tempest_raw(provider.get_stream(i), stream_raw) + # convert and save details + ret.append(export_tempest_details(provider.name, + provider.get_stream(i), + output_dir, prefix, compress)) - stream_tree, path_tree = open_(file_name=path_base + '_tree.json') - export_tempest_tree(provider.get_stream(i), stream_tree) - - stream_details, path_details = open_( - file_name=path_base + '_details.json') - export_tempest_details(provider.get_stream(i), stream_details) - - stats = get_stats(provider.get_stream(i)) - - entry = { - 'id': _tempest_count, - 'name': name, - 'raw': path_raw, - 'tree': path_tree, - 'details': path_details, - 'stats': stats - } - entry.update({'dstat': dstat} if dstat else {}) - - ret.append(entry) - _tempest_count += 1 + # generate and save stats + ret.append(export_stats(provider.name, raw, output_dir, prefix, + compress)) return ret @@ -170,7 +198,7 @@ def export_dstat(path, output_dir, compress): f = open(path, 'rb') out_stream, out_file = open_compressed( output_dir, - 'dstat_log.csv', + 'dstat.csv', compress) shutil.copyfileobj(f, out_stream) @@ -178,7 +206,9 @@ def export_dstat(path, output_dir, compress): f.close() out_stream.close() - return out_file + return build_artifact(out_file, os.path.basename(path), + 'dstat', 'text/csv', + False, compress) def main(): @@ -190,12 +220,15 @@ def main(): parser.add_argument("-z", "--gzip", help="Enable gzip compression for data files.", action="store_true") + parser.add_argument("-e", "--env", + help="Include Zuul metadata from environment " + "variables.", + action="store_true") parser.add_argument("-f", "--stream-file", action="append", help="Include the given direct subunit stream; can be " "used multiple times.") parser.add_argument("-r", "--repository", - action="append", help="A directory containing a `.testrepository` to " "include; can be used multiple times.") parser.add_argument("-i", "--stdin", @@ -211,28 +244,42 @@ def main(): if not os.path.exists(args.path): os.mkdir(args.path) - dstat = None + artifacts = [] + dataset = { + 'name': None, + 'url': None, + 'status': None, + 'ci_username': None, + 'pipeline': None, + 'change_id': None, + 'revision': None, + 'change_project': None, + 'change_subject': None, + 'artifacts': artifacts + } + + if args.env: + dataset.update(environment_params()) + if args.dstat: print("Exporting DStat log") dstat = export_dstat(args.dstat, args.path, args.gzip) + artifacts.append(dstat) providers = tempest_subunit.get_providers( args.repository, args.stream_file, args.stdin) - tempest_config_entries = [] - for provider in providers.values(): print("Exporting Tempest provider: %s (%d)" % (provider.description, provider.count)) - tempest_config_entries.extend( - export_tempest(provider, args.path, dstat, args.gzip) - ) + artifacts.extend(export_tempest(provider, args.path, args.gzip)) with open(os.path.join(args.path, 'config.json'), 'w') as f: json.dump({ - 'tempest': tempest_config_entries + 'deployer': False, + 'datasets': [dataset] }, f, default=json_date_handler) diff --git a/stackviz/parser/tempest_subunit.py b/stackviz/parser/tempest_subunit.py index 868c314..60a4198 100644 --- a/stackviz/parser/tempest_subunit.py +++ b/stackviz/parser/tempest_subunit.py @@ -97,7 +97,7 @@ class RepositoryProvider(SubunitProvider): @property def name(self): - return "repo_%s" % os.path.basename(self.repository_path) + return os.path.basename(self.repository_path) @property def description(self): @@ -120,7 +120,7 @@ class FileProvider(SubunitProvider): @property def name(self): - return "file_%s" % os.path.basename(self.path) + return os.path.basename(self.path) @property def description(self): diff --git a/test/unit/services/test-dataset.js b/test/unit/services/test-dataset.js index 1b6d5a5..2d4eccc 100644 --- a/test/unit/services/test-dataset.js +++ b/test/unit/services/test-dataset.js @@ -1,17 +1,39 @@ /*global angular */ - 'use strict'; describe('Unit: DatasetService', function() { var service, httpBackend; - var exampleConfig = {"tempest": [ - {"raw": "tempest_file_freshlog_0_raw.json", - "details": "tempest_file_freshlog_0_details.json", - "tree": "tempest_file_freshlog_0_tree.json", - "id": 0, - "name": "Subunit File: freshlog"} - ]}; + + var mockConfig = { + "deployer": false, + "datasets": [{ + "status": null, "ci_username": null, "pipeline": null, + "change_project": null, "name": null, "url": null, + "change_id": null, "change_subject": null, "revision": null, + "artifacts": [ + { + "artifact_type": "dstat", "path": "dstat.csv", "primary": false, + "content_type": "text/csv", "artifact_name": "dstat-csv.txt" + }, { + "artifact_type": "subunit", "primary": true, + "path": "testrepository.subunit-0-raw.json", + "content_type": "application/json", + "artifact_name": "testrepository.subunit" + }, { + "artifact_type": "subunit-details", "primary": false, + "path": "testrepository.subunit-0-details.json", + "content_type": "application/json", + "artifact_name": "testrepository.subunit" + }, { + "artifact_type": "subunit-stats", "primary": false, + "path": "testrepository.subunit-0-stats.json", + "content_type": "application/json", + "artifact_name": "testrepository.subunit" + } + ] + }] + }; beforeEach(function() { // instantiate the app module @@ -21,6 +43,8 @@ describe('Unit: DatasetService', function() { angular.mock.inject(function(datasetService, $httpBackend) { service = datasetService; httpBackend = $httpBackend; + + httpBackend.whenGET("data/config.json").respond(mockConfig); }); }); @@ -28,40 +52,45 @@ describe('Unit: DatasetService', function() { expect(service).toBeDefined(); }); - it('should return config.json', function() { - httpBackend.whenGET("data/config.json").respond(exampleConfig); - service.list().then(function(config) { - expect(config.data).toEqual(exampleConfig); + it('should return the loaded configuration', function() { + service.config().then(function(config) { + expect(config.config).toEqual(mockConfig); }); httpBackend.flush(); }); - it('should GET the raw file from a dataset', function() { - httpBackend.whenGET(exampleConfig.raw).respond(exampleConfig.raw); - service.raw(exampleConfig).then(function(raw) { - expect(raw).toEqual(exampleConfig.raw); + it('should only have valid primary artifacts', function() { + service.groups(true).then(function(groups) { + expect(groups.length).toEqual(1); + expect(groups).toContain('testrepository.subunit'); + }, function() { + fail('callback should return'); }); + + httpBackend.flush(); }); - it('should GET the details file from a dataset', function() { - httpBackend.whenGET(exampleConfig.details).respond(exampleConfig.details); - service.details(exampleConfig).then(function(details) { - expect(details).toEqual(exampleConfig.details); + it('should find all artifacts matching a particular name', function() { + service.artifacts('testrepository.subunit').then(function(artifacts) { + expect(artifacts.length).toEqual(3); + }, function() { + fail('callback should return'); }); + + httpBackend.flush(); }); - it('should GET the tree file from a dataset', function() { - httpBackend.whenGET(exampleConfig.tree).respond(exampleConfig.tree); - service.tree(exampleConfig).then(function(tree) { - expect(tree).toEqual(exampleConfig.tree); + it('should load an artifact', function() { + httpBackend.whenGET('data/testrepository.subunit-0-raw.json').respond({ + mock: true }); - }); - it('should GET the dstat file from a dataset', function() { - httpBackend.whenGET(exampleConfig.dstat).respond(exampleConfig.dstat); - service.dstat(exampleConfig).then(function(dstat) { - expect(dstat).toEqual(exampleConfig.dstat); + service.artifact('testrepository.subunit', 'subunit').then(function(resp) { + expect(resp.data).toEqual({ mock: true }); + }, function(ex) { + fail('promise should return successfully: ' + ex); }); - }); + httpBackend.flush(); + }); });