diff --git a/install_rally.sh b/install_rally.sh index 595d4014e2..d4e3e26b42 100755 --- a/install_rally.sh +++ b/install_rally.sh @@ -7,7 +7,7 @@ # # NOTE: The script assumes that you have the following # programs already installed: -# -> Python 2.6 or Python 2.7 +# -> Python 2.6, Python 2.7 or Python 3.4 set -e diff --git a/rally/benchmark/scenarios/base.py b/rally/benchmark/scenarios/base.py index 798468c482..a30ffc83eb 100644 --- a/rally/benchmark/scenarios/base.py +++ b/rally/benchmark/scenarios/base.py @@ -19,6 +19,7 @@ import itertools import random import time +from rally.common import costilius from rally.common import log as logging from rally.common import utils from rally import consts @@ -58,7 +59,7 @@ class Scenario(object): self._admin_clients = admin_clients self._clients = clients self._idle_duration = 0 - self._atomic_actions = {} + self._atomic_actions = costilius.OrderedDict() # TODO(amaretskiy): consider about prefix part of benchmark uuid @classmethod diff --git a/rally/cmd/cliutils.py b/rally/cmd/cliutils.py index 6f645bc646..237b7f2d02 100644 --- a/rally/cmd/cliutils.py +++ b/rally/cmd/cliutils.py @@ -323,12 +323,12 @@ def _add_command_parsers(categories, subparsers): def validate_deprecated_args(argv, fn): if (len(argv) > 3 - and (argv[2] == fn.func_name) + and (argv[2] == fn.__name__) and getattr(fn, "deprecated_args", None)): for item in fn.deprecated_args: if item in argv[3:]: LOG.warning("Deprecated argument %s for %s." % (item, - fn.func_name)) + fn.__name__)) def run(argv, categories): @@ -379,14 +379,15 @@ def run(argv, categories): return(0) fn = CONF.category.action_fn - fn_args = [arg.decode("utf-8") for arg in CONF.category.action_args] + fn_args = [encodeutils.safe_decode(arg) + for arg in CONF.category.action_args] fn_kwargs = {} for k in CONF.category.action_kwargs: v = getattr(CONF.category, "action_kwarg_" + k) if v is None: continue if isinstance(v, six.string_types): - v = v.decode("utf-8") + v = encodeutils.safe_decode(v) fn_kwargs[k] = v # call the action with the remaining arguments diff --git a/rally/cmd/commands/task.py b/rally/cmd/commands/task.py index 6fe07d66cd..0068c905a0 100644 --- a/rally/cmd/commands/task.py +++ b/rally/cmd/commands/task.py @@ -608,7 +608,8 @@ class TaskCommands(object): STATUS_FAIL = "FAIL" for result in results: key = result["key"] - for sla in result["data"]["sla"]: + for sla in sorted(result["data"]["sla"], + key=lambda x: x["criterion"]): success = sla.pop("success") sla["status"] = success and STATUS_PASS or STATUS_FAIL sla["benchmark"] = key["name"] @@ -616,7 +617,7 @@ class TaskCommands(object): failed_criteria += int(not success) data.append(sla if tojson else rutils.Struct(**sla)) if tojson: - print(json.dumps(data)) + print(json.dumps(data, sort_keys=False)) else: cliutils.print_list(data, ("benchmark", "pos", "criterion", "status", "detail")) diff --git a/rally/common/costilius.py b/rally/common/costilius.py new file mode 100644 index 0000000000..4a19eb16fa --- /dev/null +++ b/rally/common/costilius.py @@ -0,0 +1,51 @@ +# +# 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. +# + +""" +This module is a storage for different types of workarounds. +""" + +import sys + + +try: + from collections import OrderedDict # noqa +except ImportError: + # NOTE(andreykurilin): Python 2.6 issue. OrderedDict is not + # present in `collections` library. + from ordereddict import OrderedDict # noqa + + +def is_py26(): + return sys.version_info[:2] == (2, 6) + + +if is_py26(): + import simplejson as json +else: + import json + + +def json_loads(*args, **kwargs): + """Deserialize a str or unicode instance to a Python object. + + 'simplejson' is used in Python 2.6 environment, because standard 'json' + library not include several important features(for example + 'object_pairs_hook', which allows to deserialize input object to + OrderedDict) + """ + + return json.loads(*args, **kwargs) diff --git a/rally/db/sqlalchemy/types.py b/rally/db/sqlalchemy/types.py index d78e52397c..de581bde3d 100644 --- a/rally/db/sqlalchemy/types.py +++ b/rally/db/sqlalchemy/types.py @@ -19,6 +19,8 @@ from sqlalchemy.dialects import mysql as mysql_types from sqlalchemy.ext import mutable from sqlalchemy import types as sa_types +from rally.common import costilius + class JSONEncodedDict(sa_types.TypeDecorator): """Represents an immutable structure as a json-encoded string.""" @@ -27,12 +29,13 @@ class JSONEncodedDict(sa_types.TypeDecorator): def process_bind_param(self, value, dialect): if value is not None: - value = json.dumps(value) + value = json.dumps(value, sort_keys=False) return value def process_result_value(self, value, dialect): if value is not None: - value = json.loads(value) + value = costilius.json_loads( + value, object_pairs_hook=costilius.OrderedDict) return value diff --git a/requirements.txt b/requirements.txt index 3b07c4a752..7d22725943 100644 --- a/requirements.txt +++ b/requirements.txt @@ -37,3 +37,7 @@ requests>=2.2.0,!=2.4.0 SQLAlchemy>=0.9.7,<=0.9.99 sphinx>=1.1.2,!=1.2.0,!=1.3b1,<1.3 six>=1.9.0 + +# Python 2.6 related packages(see rally.common.costilius for more details) +ordereddict +simplejson>=2.2.0 diff --git a/setup.cfg b/setup.cfg index 1aaa7da213..e39b0f6a8d 100644 --- a/setup.cfg +++ b/setup.cfg @@ -17,6 +17,8 @@ classifier = Programming Language :: Python :: 2 Programming Language :: Python :: 2.6 Programming Language :: Python :: 2.7 + Programming Language :: Python :: 3 + Programming Language :: Python :: 3.4 [files] packages = diff --git a/tests/functional/test_cli_task.py b/tests/functional/test_cli_task.py index fa6261bd3c..2bd1feb3f7 100644 --- a/tests/functional/test_cli_task.py +++ b/tests/functional/test_cli_task.py @@ -535,11 +535,11 @@ class SLATestCase(unittest.TestCase): rally("task sla_check") expected = [ {"benchmark": "KeystoneBasic.create_and_list_users", - "criterion": "max_seconds_per_iteration", + "criterion": "failure_rate", "detail": mock.ANY, "pos": 0, "status": "PASS"}, {"benchmark": "KeystoneBasic.create_and_list_users", - "criterion": "failure_rate", + "criterion": "max_seconds_per_iteration", "detail": mock.ANY, "pos": 0, "status": "PASS"} ] diff --git a/tests/functional/utils.py b/tests/functional/utils.py index 0229ab54cb..8f23320603 100644 --- a/tests/functional/utils.py +++ b/tests/functional/utils.py @@ -13,6 +13,7 @@ # License for the specific language governing permissions and limitations # under the License. +from oslo_utils import encodeutils from six.moves import configparser import inspect @@ -37,7 +38,7 @@ class RallyCmdError(Exception): def __init__(self, code, output): self.code = code - self.output = output + self.output = encodeutils.safe_decode(output) def __str__(self): return "Code: %d Output: %s\n" % (self.code, self.output) @@ -50,7 +51,7 @@ class TaskConfig(object): def __init__(self, config): config_file = tempfile.NamedTemporaryFile(delete=False) - config_file.write(json.dumps(config).encode("utf-8")) + config_file.write(encodeutils.safe_encode(json.dumps(config))) config_file.close() self.filename = config_file.name @@ -159,7 +160,7 @@ class Rally(object): :param getjson: in cases, when rally prints JSON, you can catch output deserialized :param report_path: if present, rally command and its output will be - wretten to file with passed file name + written to file with passed file name :param raw: don't write command itself to report file. Only output will be written """ @@ -167,8 +168,8 @@ class Rally(object): if not isinstance(cmd, list): cmd = cmd.split(" ") try: - output = subprocess.check_output(self.args + cmd, - stderr=subprocess.STDOUT) + output = encodeutils.safe_decode(subprocess.check_output( + self.args + cmd, stderr=subprocess.STDOUT)) if write_report: if not report_path: @@ -181,6 +182,6 @@ class Rally(object): if getjson: return json.loads(output) - return output.decode("utf-8") + return output except subprocess.CalledProcessError as e: raise RallyCmdError(e.returncode, e.output)