diff --git a/rally-jobs/rally.yaml b/rally-jobs/rally.yaml index 65060f5271..3adeb10583 100644 --- a/rally-jobs/rally.yaml +++ b/rally-jobs/rally.yaml @@ -589,6 +589,16 @@ min: 20 max: 80 + Dummy.dummy_output: + - + runner: + type: "constant" + times: 20 + concurrency: 10 + sla: + failure_rate: + max: 0 + Dummy.dummy_with_scenario_output: - runner: diff --git a/rally/plugins/common/scenarios/dummy/dummy.py b/rally/plugins/common/scenarios/dummy/dummy.py index d6b8c5bcdb..ba9eb3df1a 100644 --- a/rally/plugins/common/scenarios/dummy/dummy.py +++ b/rally/plugins/common/scenarios/dummy/dummy.py @@ -80,6 +80,63 @@ class Dummy(scenario.Scenario): % exception_probability ) + @scenario.configure() + def dummy_output(self, random_range=25): + """Generate dummy output. + + This scenario generates example of output data. + :param random_range: max int limit for generated random values + """ + rand = lambda n: [n, random.randint(1, random_range)] + desc = "This is a description text for %s" + + self.add_output(additive={"title": "Additive Stat Table", + "description": desc % "Additive Stat Table", + "chart": "OutputStatsTable", + "items": [rand("foo stat"), rand("bar stat"), + rand("spam stat")]}) + + self.add_output(additive={"title": "Additive Foo StackedArea", + "description": ( + desc % "Additive Foo StackedArea"), + "chart": "OutputStackedAreaChart", + "items": [rand("foo 1"), rand("foo 2")]}) + + self.add_output(additive={"title": ("Additive Bar StackedArea " + "(no description)"), + "description": "", + "chart": "OutputStackedAreaChart", + "items": [rand("bar %d" % i) + for i in range(1, 7)]}) + + self.add_output(additive={"title": "Additive Spam Pie", + "description": desc % "Additive Spam Pie", + "chart": "OutputAvgChart", + "items": [rand("spam %d" % i) + for i in range(1, 4)]}, + complete={"title": "Complete StackedArea", + "description": desc % "Complete StackedArea", + "widget": "StackedArea", + "data": [ + [name, [rand(i) for i in range(30)]] + for name in ("alpha", "beta", "gamma")]}) + + self.add_output( + complete={"title": "Complete Pie (no description)", + "description": "", + "widget": "Pie", + "data": [rand("delta"), rand("epsilon"), rand("zeta"), + rand("theta"), rand("lambda"), rand("omega")]}) + + data = {"cols": ["mu column", "xi column", "pi column", + "tau column", "chi column"], + "rows": [([name + " row"] + [rand(i)[1] for i in range(4)]) + for name in ("iota", "nu", "rho", "phi", "psi")]} + self.add_output(complete={"title": "Complete Table", + "description": desc % "Complete Table", + "widget": "Table", + "data": data}) + @scenario.configure() def dummy_with_scenario_output(self): """Return a dummy scenario output. diff --git a/rally/ui/templates/base.html b/rally/ui/templates/base.html index cfc364ec8b..6fda35ac5c 100644 --- a/rally/ui/templates/base.html +++ b/rally/ui/templates/base.html @@ -12,8 +12,8 @@ p { margin:0; padding:5px 0 } p.thesis { padding:10px 0 } h1 { color:#666; margin:0 0 20px; font-size:30px; font-weight:normal } - h2 { color:#777; margin:20px 0 10px; font-size:25px; font-weight:normal } - h3 { color:#666; margin:13px 0 4px; font-size:18px; font-weight:normal } + h2, .h2 { color:#666; margin:24px 0 6px; font-size:25px; font-weight:normal } + h3, .h3 { color:#777; margin:12px 0 4px; font-size:18px; font-weight:normal } table { border-collapse:collapse; border-spacing:0; width:100%; font-size:12px; margin:0 0 10px } table th { text-align:left; padding:8px; color:#000; border:2px solid #ddd; border-width:0 0 2px 0 } table th.sortable { cursor:pointer } @@ -21,7 +21,6 @@ table.compact td { padding:4px 8px } table.striped tr:nth-child(odd) td { background:#f9f9f9 } table.linked tbody tr:hover { background:#f9f9f9; cursor:pointer } - .richcolor td { color:#036; font-weight:bold } .rich, .rich td { font-weight:bold } .code { padding:10px; font-size:13px; color:#333; background:#f6f6f6; border:1px solid #e5e5e5; border-radius:4px } diff --git a/rally/ui/templates/task/report.html b/rally/ui/templates/task/report.html index 13bfa81b3c..0ea7c92dc9 100644 --- a/rally/ui/templates/task/report.html +++ b/rally/ui/templates/task/report.html @@ -23,46 +23,41 @@ {% endblock %} {% block js_before %} +{% raw %} "use strict"; if (typeof angular === "object") { angular.module("TaskApp", []).controller( "TaskController", ["$scope", "$location", function($scope, $location) { - +{% endraw %} + $scope.source = {{ source }}; + $scope.scenarios = {{ data }}; +{% raw %} $scope.location = { - /* This is a junior brother of angular's $location, that allows non-`#' - symbol in uri, like `#/path/hash' instead of `#/path#hash' */ - _splitter: "/", + /* #/path/hash/sub/div */ normalize: function(str) { /* Remove unwanted characters from string */ if (typeof str !== "string") { return "" } return str.replace(/[^\w\-\.]/g, "") }, - _parseUri: function(uriStr) { - /* :returns: {path:string, hash:string} */ - var self = this; - var obj = {path: "", hash: ""}; - angular.forEach(uriStr.split(self._splitter), function(v){ - var s = self.normalize(v); - if (! s) { return } - if (! this.path) { this.path = s } else if (! this.hash) { this.hash = s } - }, obj) - return obj - }, uri: function(obj) { /* Getter/Setter */ - if (! obj) { return this._parseUri($location.url()) } - if (obj.path && obj.hash) { - $location.url(obj.path + this._splitter + obj.hash) - } else if (obj.path) { - $location.url(obj.path) - } else { - $location.url("/") + if (! obj) { + var uri = {path: "", hash: "", sub: "", div: ""}; + var arr = ["div", "sub", "hash", "path"]; + angular.forEach($location.url().split("/"), function(value){ + var v = $scope.location.normalize(value); + if (v) { var k = arr.pop(); if (k) { this[k] = v }} + }, uri); + return uri } + var arr = [obj.path, obj.hash, obj.sub, obj.div], res = []; + for (var i in arr) { if (! arr[i]) { break }; res.push(arr[i]) } + return $location.url("/" + res.join("/")) }, path: function(path, hash) { /* Getter/Setter */ - var uri = this.uri(); if (path === "") { return this.uri({}) } path = this.normalize(path); + var uri = this.uri(); if (! path) { return uri.path } uri.path = path; var _hash = this.normalize(hash); @@ -71,23 +66,27 @@ }, hash: function(hash) { /* Getter/Setter */ - var uri = this.uri(); - if (! hash) { return uri.hash } - return this.uri({path:uri.path, hash:hash}) + if (hash) { this.uri({path:this.uri().path, hash:hash}) } + return this.uri().hash } } /* Dispatch */ $scope.route = function(uri) { - if (! ($scope.scenarios && $scope.scenarios.length)) { - return - } + if (! $scope.scenarios_map) { return } if (uri.path in $scope.scenarios_map) { $scope.view = {is_scenario:true}; $scope.scenario = $scope.scenarios_map[uri.path]; $scope.nav_idx = $scope.nav_map[uri.path]; - $scope.showTab(uri.hash); + if ($scope.scenario.iterations.histogram.views.length) { + $scope.mainHistogram = $scope.scenario.iterations.histogram.views[0] + } + if ($scope.scenario.atomic.histogram.views.length) { + $scope.atomicHistogram = $scope.scenario.atomic.histogram.views[0] + } + $scope.outputIteration = 0; + $scope.showTab(uri); } else { $scope.scenario = null; if (uri.path === "source") { @@ -102,11 +101,7 @@ $scope.route($scope.location.uri()) }); - /* Navigation */ - - $scope.showNav = function(nav_idx) { - $scope.nav_idx = nav_idx - } + $scope.showNav = function(nav_idx) { $scope.nav_idx = nav_idx } /* Tabs */ @@ -121,8 +116,8 @@ visible: function(){ return !! $scope.scenario.atomic.pie.length } },{ id: "output", - name: "Output", - visible: function(){ return !! $scope.scenario.additive_output.length } + name: "Scenario Data", + visible: function(){ return $scope.scenario.output.length } },{ id: "failures", name: "Failures", @@ -137,19 +132,33 @@ angular.forEach($scope.tabs, function(tab){ this[tab.id] = tab }, $scope.tabs_map); - $scope.showTab = function(tab_id) { - $scope.tab = tab_id in $scope.tabs_map ? tab_id : "overview" + $scope.showTab = function(uri) { + $scope.tab = uri.hash in $scope.tabs_map ? uri.hash : "overview"; + if (! $scope.scenario.output) { + var has_additive = !! $scope.scenario.additive_output.length; + var has_complete = !! ($scope.scenario.complete_output.length + && $scope.scenario.complete_output[0].length); + $scope.scenario.output = { + has_additive: has_additive, + has_complete: has_complete, + length: has_additive + has_complete, + active: has_additive ? "additive" : (has_complete ? "complete" : "") + } + } + if (uri.hash === "output") { + if (uri.sub && $scope.scenario.output["has_" + uri.sub]) { + $scope.scenario.output.active = uri.sub + } + } } for (var i in $scope.tabs) { if ($scope.tabs[i].id === $scope.location.hash()) { $scope.tab = $scope.tabs[i].id } - $scope.tabs[i].isVisible = function(){ + $scope.tabs[i].isVisible = function() { if ($scope.scenario) { - if (this.visible()) { - return true - } + if (this.visible()) { return true } /* If tab should be hidden but is selected - show another one */ if (this.id === $scope.location.hash()) { for (var i in $scope.tabs) { @@ -165,149 +174,6 @@ } } - /* Charts */ - - var Charts = { - _render: function(selector, data, chart){ - nv.addGraph(function() { - d3.select(selector) - .datum(data) - .transition() - .duration(0) - .call(chart); - nv.utils.windowResize(chart.update) - }) - }, - /* NOTE(amaretskiy): this is actually a result of - d3.scale.category20().range(), excluding red color (#d62728) - which is reserved for errors */ - _colors: ["#1f77b4", "#aec7e8", "#ff7f0e", "#ffbb78", "#2ca02c", - "#98df8a", "#ff9896", "#9467bd", "#c5b0d5", "#8c564b", - "#c49c94", "#e377c2", "#f7b6d2", "#7f7f7f", "#c7c7c7", - "#bcbd22", "#dbdb8d", "#17becf", "#9edae5"], - pie: function(selector, data){ - var chart = nv.models.pieChart() - .x(function(d) { return d.key }) - .y(function(d) { return d.values }) - .showLabels(true) - .labelType("percent") - .donut(true) - .donutRatio(0.25) - .donutLabelsOutside(true) - .color(function(d){ - if (d.data && d.data.color) { - return d.data.color - } - }); - - var data_ = [], - colors = [], - colors_map = {errors: "#d62728"}; - for (var i in data) { - var key = data[i][0]; - if (! (key in colors_map)) { - if (! colors.length) { colors = this._colors.slice() } - colors_map[key] = colors.shift() - } - data_.push({key:key, values:data[i][1], color:colors_map[key]}) - } - this._render(selector, data_, chart) - }, - stack: function(selector, data, conf){ - var chart = nv.models.stackedAreaChart() - .x(function(d) { return d[0] }) - .y(function(d) { return d[1] }) - .clipEdge(true) - .showControls(conf.controls) - .useInteractiveGuideline(conf.guide); - chart.xAxis - .axisLabel(conf.xLabel || "") - .showMaxMin(false) - .tickFormat(d3.format(conf.xFormat || "d")); - chart.yAxis - .axisLabel(conf.yLabel || "") - .tickFormat(d3.format(conf.yFormat || ",.3f")); - var data_ = []; - for (var i in data) { - var d = {key:data[i][0], values:data[i][1]}; - if (d.key === "failed_duration") { - d.color = "#d62728" - } - data_.push(d) - } - this._render(selector, data_, chart) - }, - histogram: function(selector, data){ - var chart = nv.models.multiBarChart() - .reduceXTicks(true) - .showControls(false) - .transitionDuration(0) - .groupSpacing(0.05); - chart.legend - .radioButtonMode(true) - chart.xAxis - .axisLabel("Duration (seconds)") - .tickFormat(d3.format(",.2f")); - chart.yAxis - .axisLabel("Iterations (frequency)") - .tickFormat(d3.format("d")); - this._render(selector, data, chart) - } - }; - - $scope.renderTotal = function() { - if (! $scope.scenario) { - return - } - - Charts.stack( - "#total-stack", $scope.scenario.iterations.iter, - {xLabel: "Iteration sequence number", - controls: true, - guide: true}); - - if ($scope.scenario.load_profile.length) { - Charts.stack( - "#load-profile-stack", - $scope.scenario.load_profile, - {xLabel: "Timeline (seconds)", - xFormat: ",.2f", yFormat: "d"}) - } - - Charts.pie("#total-pie", $scope.scenario.iterations.pie); - - if ($scope.scenario.iterations.histogram.data.length) { - var idx = this.totalHistogramModel.id; - Charts.histogram("#total-histogram", - $scope.scenario.iterations.histogram.data[idx]) - } - } - - $scope.renderDetails = function() { - if (! $scope.scenario) { - return - } - Charts.stack( - "#atomic-stack", - $scope.scenario.atomic.iter, - {xLabel: "Iteration sequence number", - controls: true, - guide: true}); - if ($scope.scenario.atomic.pie) { - Charts.pie("#atomic-pie", $scope.scenario.atomic.pie) - } - if ($scope.scenario.atomic.histogram.data.length) { - var idx = this.atomicHistogramModel.id; - Charts.histogram("#atomic-histogram", $scope.scenario.atomic.histogram.data[idx]) - } - } - - $scope.renderOutput = function() { - if ($scope.scenario && $scope.scenario.additive_output.length) { - Charts.stack("#output-stack", $scope.scenario.additive_output[0].data, {}) - } - } - $scope.showError = function(message) { return (function (e) { e.style.display = "block"; @@ -318,23 +184,16 @@ /* Initialization */ angular.element(document).ready(function(){ - $scope.source = {{ source }}; - $scope.scenarios = {{ data }}; if (! $scope.scenarios.length) { return $scope.showError("No data...") } - $scope.totalHistogramModel = {label:'', id:0}; - $scope.atomicHistogramModel = {label:'', id:0}; /* Compose data mapping */ $scope.nav = []; $scope.nav_map = {}; $scope.scenarios_map = {}; - var scenario_ref = $scope.location.path(); - var met = []; - var itr = 0; - var cls_idx = 0; + var met = [], itr = 0, cls_idx = 0; var prev_cls, prev_met; for (var idx in $scope.scenarios) { @@ -350,21 +209,13 @@ cls_idx += 1 } - if (prev_met !== sc.met) { - itr = 1 - } - + if (prev_met !== sc.met) { itr = 1 }; sc.ref = $scope.location.normalize(sc.cls+"."+sc.met+(itr > 1 ? "-"+itr : "")); $scope.scenarios_map[sc.ref] = sc; $scope.nav_map[sc.ref] = cls_idx; - var current_ref = $scope.location.path(); - if (sc.ref === current_ref) { - scenario_ref = sc.ref - } - met.push({name:sc.name, itr:itr, idx:idx, ref:sc.ref}); prev_met = sc.met; - itr += 1 + itr += 1; } if (met.length) { @@ -374,11 +225,161 @@ /* Start */ var uri = $scope.location.uri(); - uri.path = scenario_ref; + uri.path = $scope.location.path(); $scope.route(uri); $scope.$digest() }); - }])} + }]) + .directive("widget", function($compile) { + + var Chart = { + _render: function(node, data, chart){ + nv.addGraph(function() { + d3.select(node).datum(data).transition().duration(0).call(chart); + nv.utils.windowResize(chart.update) + }) + }, + /* NOTE(amaretskiy): this is actually a result of + d3.scale.category20().range(), excluding red color (#d62728) + which is reserved for errors */ + _colors: ["#1f77b4", "#aec7e8", "#ff7f0e", "#ffbb78", "#2ca02c", + "#98df8a", "#ff9896", "#9467bd", "#c5b0d5", "#8c564b", + "#c49c94", "#e377c2", "#f7b6d2", "#7f7f7f", "#c7c7c7", + "#bcbd22", "#dbdb8d", "#17becf", "#9edae5"], + _widgets: { + Pie: "pie", + StackedArea: "stack", + Histogram: "histogram" + }, + get_chart: function(widget) { + if (widget in this._widgets) { + var name = this._widgets[widget]; + return Chart[name] + } + return function() { console.log("Error: unexpected widget:", widget) } + }, + pie: function(node, data) { + var chart = nv.models.pieChart() + .x(function(d) { return d.key }) + .y(function(d) { return d.values }) + .showLabels(true) + .labelType("percent") + .donut(true) + .donutRatio(0.25) + .donutLabelsOutside(true) + .color(function(d){ + if (d.data && d.data.color) { return d.data.color } + }); + + var data_ = [], colors = [], colors_map = {errors: "#d62728"}; + for (var i in data) { + var key = data[i][0]; + if (! (key in colors_map)) { + if (! colors.length) { colors = Chart._colors.slice() } + colors_map[key] = colors.shift() + } + data_.push({key:key, values:data[i][1], color:colors_map[key]}) + } + Chart._render(node, data_, chart) + }, + stack: function(node, data, opts) { + var chart = nv.models.stackedAreaChart() + .x(function(d) { return d[0] }) + .y(function(d) { return d[1] }) + .useInteractiveGuideline(opts.guide) + .showControls(opts.controls) + .clipEdge(true); + chart.xAxis + .tickFormat(d3.format(opts.xformat || "d")) + .axisLabel(opts.xname || "") + .showMaxMin(false); + chart.yAxis + .tickFormat(d3.format(opts.yformat || ",.3f")) + .axisLabel(opts.yname || ""); + var data_ = []; + for (var i in data) { + var d = {key:data[i][0], values:data[i][1]}; + if (d.key === "failed_duration") { + d.color = "#d62728" + } + data_.push(d) + } + Chart._render(node, data_, chart) + }, + histogram: function(node, data) { + var chart = nv.models.multiBarChart() + .reduceXTicks(true) + .showControls(false) + .transitionDuration(0) + .groupSpacing(0.05); + chart + .legend.radioButtonMode(true); + chart.xAxis + .axisLabel("Duration (seconds)") + .tickFormat(d3.format(",.2f")); + chart.yAxis + .axisLabel("Iterations (frequency)") + .tickFormat(d3.format("d")); + Chart._render(node, data, chart) + } + }; + + return { + restrict: "A", + scope: { data: "=" }, + link: function(scope, element, attrs) { + scope.$watch("data", function(data) { + if (! data) { return console.log("Chart has no data to render!") } + if (attrs.widget === "Table") { + var ng_class = attrs.lastrowClass ? " ng-class='{"+attrs.lastrowClass+":$last}'" : ""; + var template = "" + + "" + + "" + + "" + + "" + + "
{{i}}
{{i}}" + + "
"; + var el = element.empty().append($compile(template)(scope)).children()[0] + } else { + /* Hide widget if not enough data */ + if (attrs.widget === "StackedArea") { + if ((! data.length) || (data[0].length < 1) || (data[0][1].length < 2)) { + return element.empty().css({display:"none"}) + } + } + else if (attrs.widget === "Pie") { + if (! data.length) { + return element.empty().css({display:"none"}) + } + } + + var options = { + xname: attrs.nameX || "", + yname: attrs.nameY || "", + xformat: attrs.formatX || "d", + yformat: attrs.formatY || ",.3f", + controls: attrs.controls === "true", + guide: attrs.guide === "true" + }; + var el = element.addClass("chart").css({display:"block"}).html("").children()[0]; + Chart.get_chart(attrs.widget)(el, data, options) + } + + if (attrs.description) { + var desc_el = angular.element("
").addClass(attrs.descriptionClass || "h3").text(attrs.description); + angular.element(el).parent().prepend(desc_el) + } + + if (attrs.title) { + var title_el = angular.element("
").addClass(attrs.titleClass || "h2").text(attrs.title); + angular.element(el).parent().prepend(title_el) + } + }); + } + } + }) + } +{% endraw %} {% endblock %} {% block css %} @@ -402,14 +403,15 @@ .failure-mesg { color:#900 } .failure-trace { color:#333; white-space:pre; overflow:auto } - .chart { height:300px } - .chart .chart-dropdown { float:right; margin:0 35px 0 } - .chart.lesser { padding:0; margin:0; float:left; width:40% } - .chart.larger { padding:0; margin:0; float:left; width:59% } + .link { color:#428BCA; padding:5px 15px 5px 5px; text-decoration:underline; cursor:pointer } + .link.active { color:#333; text-decoration:none } + + .chart { padding:0; margin:0 } + .chart svg { height:300px; padding:0; margin:0 } + .chart.lower svg { height:180px } .expandable { cursor:pointer } .clearfix { clear:both } - .top-margin { margin-top:40px !important } .sortable > .arrow { display:inline-block; width:12px; height:inherit; color:#c90 } .content-main { margin:0 5px; display:block; float:left } {% endblock %} @@ -488,32 +490,28 @@ - Iterations - Runner - Errors - Success (SLA) @@ -558,8 +556,6 @@
diff --git a/samples/tasks/scenarios/dummy/dummy_output.json b/samples/tasks/scenarios/dummy/dummy_output.json new file mode 100644 index 0000000000..8ef42984eb --- /dev/null +++ b/samples/tasks/scenarios/dummy/dummy_output.json @@ -0,0 +1,20 @@ +{ + "Dummy.dummy_output": [ + { + "args": { + "random_range": 25 + }, + "runner": { + "type": "constant", + "times": 50, + "concurrency": 5 + }, + "context": { + "users": { + "tenants": 1, + "users_per_tenant": 1 + } + } + } + ] +} diff --git a/samples/tasks/scenarios/dummy/dummy_output.yaml b/samples/tasks/scenarios/dummy/dummy_output.yaml new file mode 100644 index 0000000000..fe99edfe77 --- /dev/null +++ b/samples/tasks/scenarios/dummy/dummy_output.yaml @@ -0,0 +1,13 @@ +--- + Dummy.dummy_output: + - + args: + random_range: 25 + runner: + type: "constant" + times: 50 + concurrency: 5 + context: + users: + tenants: 1 + users_per_tenant: 1 diff --git a/tests/unit/plugins/common/scenarios/dummy/test_dummy.py b/tests/unit/plugins/common/scenarios/dummy/test_dummy.py index 31b335f065..7f1b01417d 100644 --- a/tests/unit/plugins/common/scenarios/dummy/test_dummy.py +++ b/tests/unit/plugins/common/scenarios/dummy/test_dummy.py @@ -51,6 +51,58 @@ class DummyTestCase(test.TestCase): scenario.dummy_exception_probability, exception_probability=1) + @mock.patch("rally.plugins.common.scenarios.dummy.dummy.random") + def test_dummy_output(self, mock_random): + mock_random.randint.side_effect = lambda min_, max_: max_ + desc = "This is a description text for %s" + for random_range, exp in (None, 25), (1, 1), (42, 42): + scenario = dummy.Dummy(test.get_test_context()) + if random_range is None: + scenario.dummy_output() + else: + scenario.dummy_output(random_range=random_range) + expected = { + "additive": [ + {"chart": "OutputStatsTable", + "items": [[s + " stat", exp] + for s in ("foo", "bar", "spam")], + "title": "Additive Stat Table", + "description": desc % "Additive Stat Table"}, + {"chart": "OutputStackedAreaChart", + "items": [["foo 1", exp], ["foo 2", exp]], + "title": "Additive Foo StackedArea", + "description": desc % "Additive Foo StackedArea"}, + {"chart": "OutputStackedAreaChart", + "items": [["bar %d" % i, exp] for i in range(1, 7)], + "title": "Additive Bar StackedArea (no description)", + "description": ""}, + {"chart": "OutputAvgChart", + "items": [["spam %d" % i, exp] for i in range(1, 4)], + "title": "Additive Spam Pie", + "description": desc % "Additive Spam Pie"}], + "complete": [ + {"data": [["alpha", [[i, exp] for i in range(30)]], + ["beta", [[i, exp] for i in range(30)]], + ["gamma", [[i, exp] for i in range(30)]]], + "title": "Complete StackedArea", + "description": desc % "Complete StackedArea", + "widget": "StackedArea"}, + {"data": [["delta", exp], ["epsilon", exp], ["zeta", exp], + ["theta", exp], ["lambda", exp], ["omega", exp]], + "title": "Complete Pie (no description)", + "description": "", + "widget": "Pie"}, + {"data": { + "cols": ["mu column", "xi column", "pi column", + "tau column", "chi column"], + "rows": [ + [r + " row", exp, exp, exp, exp] + for r in ("iota", "nu", "rho", "phi", "psi")]}, + "title": "Complete Table", + "description": desc % "Complete Table", + "widget": "Table"}]} + self.assertEqual(expected, scenario._output) + def test_dummy_dummy_with_scenario_output(self): scenario = dummy.Dummy(test.get_test_context()) result = scenario.dummy_with_scenario_output() diff --git a/tests/unit/task/test_scenario.py b/tests/unit/task/test_scenario.py index 8aaef95ad2..71cb5316e5 100644 --- a/tests/unit/task/test_scenario.py +++ b/tests/unit/task/test_scenario.py @@ -223,6 +223,9 @@ class ScenarioTestCase(test.TestCase): def test_add_output(self): scenario_inst = scenario.Scenario() + self.assertEqual({"additive": [], "complete": []}, + scenario_inst._output) + additive1 = {"title": "Additive 1", "chart": "FooChart", "description": "Foo description", "items": [["foo", 1], ["bar", 2]]}