Matthew Treinish | a051c22 | 2016-05-23 15:48:22 -0400 | [diff] [blame] | 1 | # Licensed under the Apache License, Version 2.0 (the "License"); you may |
| 2 | # not use this file except in compliance with the License. You may obtain |
| 3 | # a copy of the License at |
| 4 | # |
| 5 | # http://www.apache.org/licenses/LICENSE-2.0 |
| 6 | # |
| 7 | # Unless required by applicable law or agreed to in writing, software |
| 8 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT |
| 9 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the |
| 10 | # License for the specific language governing permissions and limitations |
| 11 | # under the License. |
| 12 | |
| 13 | """ |
| 14 | Runs tempest tests |
| 15 | |
| 16 | This command is used for running the tempest tests |
| 17 | |
| 18 | Test Selection |
| 19 | ============== |
| 20 | Tempest run has several options: |
| 21 | |
| 22 | * **--regex/-r**: This is a selection regex like what testr uses. It will run |
| 23 | any tests that match on re.match() with the regex |
Nicolas Bock | ff27d3b | 2017-01-11 13:30:32 -0700 | [diff] [blame] | 24 | * **--smoke/-s**: Run all the tests tagged as smoke |
Matthew Treinish | a051c22 | 2016-05-23 15:48:22 -0400 | [diff] [blame] | 25 | |
Masayuki Igawa | 0dcc606 | 2016-08-24 17:06:11 +0900 | [diff] [blame] | 26 | There are also the **--blacklist-file** and **--whitelist-file** options that |
Matthew Treinish | a6b4da9 | 2016-05-23 17:24:12 -0400 | [diff] [blame] | 27 | let you pass a filepath to tempest run with the file format being a line |
zhangyanxian | 68d31b8 | 2016-07-13 01:48:33 +0000 | [diff] [blame] | 28 | separated regex, with '#' used to signify the start of a comment on a line. |
Matthew Treinish | a6b4da9 | 2016-05-23 17:24:12 -0400 | [diff] [blame] | 29 | For example:: |
| 30 | |
| 31 | # Regex file |
| 32 | ^regex1 # Match these tests |
| 33 | .*regex2 # Match those tests |
| 34 | |
| 35 | The blacklist file will be used to construct a negative lookahead regex and |
| 36 | the whitelist file will simply OR all the regexes in the file. The whitelist |
| 37 | and blacklist file options are mutually exclusive so you can't use them |
| 38 | together. However, you can combine either with a normal regex or the *--smoke* |
| 39 | flag. When used with a blacklist file the generated regex will be combined to |
| 40 | something like:: |
| 41 | |
| 42 | ^((?!black_regex1|black_regex2).)*$cli_regex1 |
| 43 | |
| 44 | When combined with a whitelist file all the regexes from the file and the CLI |
| 45 | regexes will be ORed. |
| 46 | |
Matthew Treinish | a051c22 | 2016-05-23 15:48:22 -0400 | [diff] [blame] | 47 | You can also use the **--list-tests** option in conjunction with selection |
| 48 | arguments to list which tests will be run. |
| 49 | |
| 50 | Test Execution |
| 51 | ============== |
| 52 | There are several options to control how the tests are executed. By default |
| 53 | tempest will run in parallel with a worker for each CPU present on the machine. |
| 54 | If you want to adjust the number of workers use the **--concurrency** option |
Nicolas Bock | ff27d3b | 2017-01-11 13:30:32 -0700 | [diff] [blame] | 55 | and if you want to run tests serially use **--serial/-t** |
Matthew Treinish | a051c22 | 2016-05-23 15:48:22 -0400 | [diff] [blame] | 56 | |
Matthew Treinish | c89a951 | 2016-06-09 17:43:35 -0400 | [diff] [blame] | 57 | Running with Workspaces |
| 58 | ----------------------- |
| 59 | Tempest run enables you to run your tempest tests from any setup tempest |
| 60 | workspace it relies on you having setup a tempest workspace with either the |
| 61 | ``tempest init`` or ``tempest workspace`` commands. Then using the |
| 62 | ``--workspace`` CLI option you can specify which one of your workspaces you |
| 63 | want to run tempest from. Using this option you don't have to run Tempest |
| 64 | directly with you current working directory being the workspace, Tempest will |
| 65 | take care of managing everything to be executed from there. |
| 66 | |
Matthew Treinish | 30c9ee5 | 2016-06-09 17:58:47 -0400 | [diff] [blame] | 67 | Running from Anywhere |
| 68 | --------------------- |
| 69 | Tempest run provides you with an option to execute tempest from anywhere on |
| 70 | your system. You are required to provide a config file in this case with the |
| 71 | ``--config-file`` option. When run tempest will create a .testrepository |
| 72 | directory and a .testr.conf file in your current working directory. This way |
| 73 | you can use testr commands directly to inspect the state of the previous run. |
| 74 | |
Matthew Treinish | a051c22 | 2016-05-23 15:48:22 -0400 | [diff] [blame] | 75 | Test Output |
| 76 | =========== |
| 77 | By default tempest run's output to STDOUT will be generated using the |
| 78 | subunit-trace output filter. But, if you would prefer a subunit v2 stream be |
| 79 | output to STDOUT use the **--subunit** flag |
| 80 | |
Matthew Treinish | 7d6e48c | 2017-03-03 12:44:50 -0500 | [diff] [blame] | 81 | Combining Runs |
| 82 | ============== |
| 83 | |
| 84 | There are certain situations in which you want to split a single run of tempest |
| 85 | across 2 executions of tempest run. (for example to run part of the tests |
| 86 | serially and others in parallel) To accomplish this but still treat the results |
| 87 | as a single run you can leverage the **--combine** option which will append |
| 88 | the current run's results with the previous runs. |
Matthew Treinish | a051c22 | 2016-05-23 15:48:22 -0400 | [diff] [blame] | 89 | """ |
| 90 | |
| 91 | import io |
| 92 | import os |
| 93 | import sys |
Matthew Treinish | 7d6e48c | 2017-03-03 12:44:50 -0500 | [diff] [blame] | 94 | import tempfile |
Matthew Treinish | a051c22 | 2016-05-23 15:48:22 -0400 | [diff] [blame] | 95 | import threading |
| 96 | |
| 97 | from cliff import command |
Matthew Treinish | a6b4da9 | 2016-05-23 17:24:12 -0400 | [diff] [blame] | 98 | from os_testr import regex_builder |
Matthew Treinish | a051c22 | 2016-05-23 15:48:22 -0400 | [diff] [blame] | 99 | from os_testr import subunit_trace |
Prateek Arora | a028de1 | 2017-03-14 09:01:03 -0400 | [diff] [blame] | 100 | from oslo_serialization import jsonutils as json |
Davanum Srinivas | 00e3f45 | 2017-01-05 12:40:45 -0500 | [diff] [blame] | 101 | import six |
Matthew Treinish | a051c22 | 2016-05-23 15:48:22 -0400 | [diff] [blame] | 102 | from testrepository.commands import run_argv |
| 103 | |
ghanshyam | 009a1f6 | 2017-08-08 10:22:57 +0300 | [diff] [blame^] | 104 | from tempest import clients |
Prateek Arora | a028de1 | 2017-03-14 09:01:03 -0400 | [diff] [blame] | 105 | from tempest.cmd import cleanup_service |
Matthew Treinish | 30c9ee5 | 2016-06-09 17:58:47 -0400 | [diff] [blame] | 106 | from tempest.cmd import init |
Matthew Treinish | c89a951 | 2016-06-09 17:43:35 -0400 | [diff] [blame] | 107 | from tempest.cmd import workspace |
Prateek Arora | a028de1 | 2017-03-14 09:01:03 -0400 | [diff] [blame] | 108 | from tempest.common import credentials_factory as credentials |
Matthew Treinish | a051c22 | 2016-05-23 15:48:22 -0400 | [diff] [blame] | 109 | from tempest import config |
| 110 | |
| 111 | |
Matthew Treinish | a051c22 | 2016-05-23 15:48:22 -0400 | [diff] [blame] | 112 | CONF = config.CONF |
Prateek Arora | a028de1 | 2017-03-14 09:01:03 -0400 | [diff] [blame] | 113 | SAVED_STATE_JSON = "saved_state.json" |
Matthew Treinish | a051c22 | 2016-05-23 15:48:22 -0400 | [diff] [blame] | 114 | |
| 115 | |
| 116 | class TempestRun(command.Command): |
| 117 | |
Matthew Treinish | 30c9ee5 | 2016-06-09 17:58:47 -0400 | [diff] [blame] | 118 | def _set_env(self, config_file=None): |
| 119 | if config_file: |
| 120 | CONF.set_config_path(os.path.abspath(config_file)) |
Matthew Treinish | a051c22 | 2016-05-23 15:48:22 -0400 | [diff] [blame] | 121 | # NOTE(mtreinish): This is needed so that testr doesn't gobble up any |
| 122 | # stacktraces on failure. |
| 123 | if 'TESTR_PDB' in os.environ: |
| 124 | return |
| 125 | else: |
| 126 | os.environ["TESTR_PDB"] = "" |
Davanum Srinivas | 00e3f45 | 2017-01-05 12:40:45 -0500 | [diff] [blame] | 127 | # NOTE(dims): most of our .testr.conf try to test for PYTHON |
| 128 | # environment variable and fall back to "python", under python3 |
| 129 | # if it does not exist. we should set it to the python3 executable |
| 130 | # to deal with this situation better for now. |
| 131 | if six.PY3 and 'PYTHON' not in os.environ: |
| 132 | os.environ['PYTHON'] = sys.executable |
Matthew Treinish | a051c22 | 2016-05-23 15:48:22 -0400 | [diff] [blame] | 133 | |
Matthew Treinish | c89a951 | 2016-06-09 17:43:35 -0400 | [diff] [blame] | 134 | def _create_testrepository(self): |
| 135 | if not os.path.isdir('.testrepository'): |
| 136 | returncode = run_argv(['testr', 'init'], sys.stdin, sys.stdout, |
| 137 | sys.stderr) |
| 138 | if returncode: |
| 139 | sys.exit(returncode) |
| 140 | |
Matthew Treinish | 30c9ee5 | 2016-06-09 17:58:47 -0400 | [diff] [blame] | 141 | def _create_testr_conf(self): |
| 142 | top_level_path = os.path.dirname(os.path.dirname(__file__)) |
| 143 | discover_path = os.path.join(top_level_path, 'test_discover') |
| 144 | file_contents = init.TESTR_CONF % (top_level_path, discover_path) |
| 145 | with open('.testr.conf', 'w+') as testr_conf_file: |
| 146 | testr_conf_file.write(file_contents) |
| 147 | |
Matthew Treinish | a051c22 | 2016-05-23 15:48:22 -0400 | [diff] [blame] | 148 | def take_action(self, parsed_args): |
Masayuki Igawa | fe2fa00 | 2016-06-22 12:58:34 +0900 | [diff] [blame] | 149 | returncode = 0 |
Matthew Treinish | 30c9ee5 | 2016-06-09 17:58:47 -0400 | [diff] [blame] | 150 | if parsed_args.config_file: |
| 151 | self._set_env(parsed_args.config_file) |
| 152 | else: |
| 153 | self._set_env() |
Matthew Treinish | c89a951 | 2016-06-09 17:43:35 -0400 | [diff] [blame] | 154 | # Workspace execution mode |
| 155 | if parsed_args.workspace: |
| 156 | workspace_mgr = workspace.WorkspaceManager( |
| 157 | parsed_args.workspace_path) |
| 158 | path = workspace_mgr.get_workspace(parsed_args.workspace) |
Brant Knudson | 6a090f4 | 2016-10-13 12:51:49 -0500 | [diff] [blame] | 159 | if not path: |
| 160 | sys.exit( |
| 161 | "The %r workspace isn't registered in " |
| 162 | "%r. Use 'tempest init' to " |
| 163 | "register the workspace." % |
| 164 | (parsed_args.workspace, workspace_mgr.path)) |
Matthew Treinish | c89a951 | 2016-06-09 17:43:35 -0400 | [diff] [blame] | 165 | os.chdir(path) |
| 166 | # NOTE(mtreinish): tempest init should create a .testrepository dir |
| 167 | # but since workspaces can be imported let's sanity check and |
| 168 | # ensure that one is created |
| 169 | self._create_testrepository() |
zhufl | bedb2ad | 2016-06-20 11:39:01 +0800 | [diff] [blame] | 170 | # Local execution mode |
Matthew Treinish | c89a951 | 2016-06-09 17:43:35 -0400 | [diff] [blame] | 171 | elif os.path.isfile('.testr.conf'): |
Matthew Treinish | a051c22 | 2016-05-23 15:48:22 -0400 | [diff] [blame] | 172 | # If you're running in local execution mode and there is not a |
| 173 | # testrepository dir create one |
Matthew Treinish | c89a951 | 2016-06-09 17:43:35 -0400 | [diff] [blame] | 174 | self._create_testrepository() |
Matthew Treinish | 30c9ee5 | 2016-06-09 17:58:47 -0400 | [diff] [blame] | 175 | # local execution with config file mode |
| 176 | elif parsed_args.config_file: |
| 177 | self._create_testr_conf() |
| 178 | self._create_testrepository() |
Matthew Treinish | a051c22 | 2016-05-23 15:48:22 -0400 | [diff] [blame] | 179 | else: |
zhufl | bedb2ad | 2016-06-20 11:39:01 +0800 | [diff] [blame] | 180 | print("No .testr.conf file was found for local execution") |
Matthew Treinish | a051c22 | 2016-05-23 15:48:22 -0400 | [diff] [blame] | 181 | sys.exit(2) |
Prateek Arora | a028de1 | 2017-03-14 09:01:03 -0400 | [diff] [blame] | 182 | if parsed_args.state: |
| 183 | self._init_state() |
| 184 | else: |
| 185 | pass |
| 186 | |
Matthew Treinish | 7d6e48c | 2017-03-03 12:44:50 -0500 | [diff] [blame] | 187 | if parsed_args.combine: |
| 188 | temp_stream = tempfile.NamedTemporaryFile() |
| 189 | return_code = run_argv(['tempest', 'last', '--subunit'], sys.stdin, |
| 190 | temp_stream, sys.stderr) |
| 191 | if return_code > 0: |
| 192 | sys.exit(return_code) |
Matthew Treinish | a051c22 | 2016-05-23 15:48:22 -0400 | [diff] [blame] | 193 | |
| 194 | regex = self._build_regex(parsed_args) |
| 195 | if parsed_args.list_tests: |
| 196 | argv = ['tempest', 'list-tests', regex] |
| 197 | returncode = run_argv(argv, sys.stdin, sys.stdout, sys.stderr) |
| 198 | else: |
| 199 | options = self._build_options(parsed_args) |
| 200 | returncode = self._run(regex, options) |
Matthew Treinish | 7d6e48c | 2017-03-03 12:44:50 -0500 | [diff] [blame] | 201 | if returncode > 0: |
| 202 | sys.exit(returncode) |
| 203 | |
| 204 | if parsed_args.combine: |
| 205 | return_code = run_argv(['tempest', 'last', '--subunit'], sys.stdin, |
| 206 | temp_stream, sys.stderr) |
| 207 | if return_code > 0: |
| 208 | sys.exit(return_code) |
| 209 | returncode = run_argv(['tempest', 'load', temp_stream.name], |
| 210 | sys.stdin, sys.stdout, sys.stderr) |
Matthew Treinish | a051c22 | 2016-05-23 15:48:22 -0400 | [diff] [blame] | 211 | sys.exit(returncode) |
| 212 | |
| 213 | def get_description(self): |
| 214 | return 'Run tempest' |
| 215 | |
Prateek Arora | a028de1 | 2017-03-14 09:01:03 -0400 | [diff] [blame] | 216 | def _init_state(self): |
| 217 | print("Initializing saved state.") |
| 218 | data = {} |
| 219 | self.global_services = cleanup_service.get_global_cleanup_services() |
ghanshyam | 009a1f6 | 2017-08-08 10:22:57 +0300 | [diff] [blame^] | 220 | self.admin_mgr = clients.Manager( |
| 221 | credentials.get_configured_admin_credentials()) |
Prateek Arora | a028de1 | 2017-03-14 09:01:03 -0400 | [diff] [blame] | 222 | admin_mgr = self.admin_mgr |
| 223 | kwargs = {'data': data, |
| 224 | 'is_dry_run': False, |
| 225 | 'saved_state_json': data, |
| 226 | 'is_preserve': False, |
| 227 | 'is_save_state': True} |
| 228 | for service in self.global_services: |
| 229 | svc = service(admin_mgr, **kwargs) |
| 230 | svc.run() |
| 231 | |
| 232 | with open(SAVED_STATE_JSON, 'w+') as f: |
| 233 | f.write(json.dumps(data, |
| 234 | sort_keys=True, indent=2, separators=(',', ': '))) |
| 235 | |
Matthew Treinish | a051c22 | 2016-05-23 15:48:22 -0400 | [diff] [blame] | 236 | def get_parser(self, prog_name): |
| 237 | parser = super(TempestRun, self).get_parser(prog_name) |
| 238 | parser = self._add_args(parser) |
| 239 | return parser |
| 240 | |
| 241 | def _add_args(self, parser): |
Matthew Treinish | c89a951 | 2016-06-09 17:43:35 -0400 | [diff] [blame] | 242 | # workspace args |
| 243 | parser.add_argument('--workspace', default=None, |
| 244 | help='Name of tempest workspace to use for running' |
| 245 | ' tests. You can see a list of workspaces ' |
| 246 | 'with tempest workspace list') |
| 247 | parser.add_argument('--workspace-path', default=None, |
| 248 | dest='workspace_path', |
| 249 | help="The path to the workspace file, the default " |
| 250 | "is ~/.tempest/workspace.yaml") |
Matthew Treinish | 30c9ee5 | 2016-06-09 17:58:47 -0400 | [diff] [blame] | 251 | # Configuration flags |
| 252 | parser.add_argument('--config-file', default=None, dest='config_file', |
| 253 | help='Configuration file to run tempest with') |
Matthew Treinish | a051c22 | 2016-05-23 15:48:22 -0400 | [diff] [blame] | 254 | # test selection args |
| 255 | regex = parser.add_mutually_exclusive_group() |
Nicolas Bock | ff27d3b | 2017-01-11 13:30:32 -0700 | [diff] [blame] | 256 | regex.add_argument('--smoke', '-s', action='store_true', |
Matthew Treinish | a051c22 | 2016-05-23 15:48:22 -0400 | [diff] [blame] | 257 | help="Run the smoke tests only") |
| 258 | regex.add_argument('--regex', '-r', default='', |
| 259 | help='A normal testr selection regex used to ' |
| 260 | 'specify a subset of tests to run') |
Matthew Treinish | a6b4da9 | 2016-05-23 17:24:12 -0400 | [diff] [blame] | 261 | list_selector = parser.add_mutually_exclusive_group() |
Masayuki Igawa | 0dcc606 | 2016-08-24 17:06:11 +0900 | [diff] [blame] | 262 | list_selector.add_argument('--whitelist-file', '--whitelist_file', |
Matthew Treinish | a6b4da9 | 2016-05-23 17:24:12 -0400 | [diff] [blame] | 263 | help="Path to a whitelist file, this file " |
zhangyanxian | 68d31b8 | 2016-07-13 01:48:33 +0000 | [diff] [blame] | 264 | "contains a separate regex on each " |
Matthew Treinish | a6b4da9 | 2016-05-23 17:24:12 -0400 | [diff] [blame] | 265 | "newline.") |
Masayuki Igawa | 0dcc606 | 2016-08-24 17:06:11 +0900 | [diff] [blame] | 266 | list_selector.add_argument('--blacklist-file', '--blacklist_file', |
Matthew Treinish | a6b4da9 | 2016-05-23 17:24:12 -0400 | [diff] [blame] | 267 | help='Path to a blacklist file, this file ' |
| 268 | 'contains a separate regex exclude on ' |
| 269 | 'each newline') |
Matthew Treinish | a051c22 | 2016-05-23 15:48:22 -0400 | [diff] [blame] | 270 | # list only args |
| 271 | parser.add_argument('--list-tests', '-l', action='store_true', |
| 272 | help='List tests', |
| 273 | default=False) |
Puneet Arora | 9ed4104 | 2016-07-05 19:46:06 +0000 | [diff] [blame] | 274 | # execution args |
Matthew Treinish | a051c22 | 2016-05-23 15:48:22 -0400 | [diff] [blame] | 275 | parser.add_argument('--concurrency', '-w', |
| 276 | help="The number of workers to use, defaults to " |
| 277 | "the number of cpus") |
| 278 | parallel = parser.add_mutually_exclusive_group() |
| 279 | parallel.add_argument('--parallel', dest='parallel', |
| 280 | action='store_true', |
| 281 | help='Run tests in parallel (this is the' |
| 282 | ' default)') |
Nicolas Bock | ff27d3b | 2017-01-11 13:30:32 -0700 | [diff] [blame] | 283 | parallel.add_argument('--serial', '-t', dest='parallel', |
Matthew Treinish | a051c22 | 2016-05-23 15:48:22 -0400 | [diff] [blame] | 284 | action='store_false', |
| 285 | help='Run tests serially') |
Prateek Arora | a028de1 | 2017-03-14 09:01:03 -0400 | [diff] [blame] | 286 | parser.add_argument('--save-state', dest='state', |
| 287 | action='store_true', |
| 288 | help="To save the state of the cloud before " |
| 289 | "running tempest.") |
Matthew Treinish | a051c22 | 2016-05-23 15:48:22 -0400 | [diff] [blame] | 290 | # output args |
| 291 | parser.add_argument("--subunit", action='store_true', |
| 292 | help='Enable subunit v2 output') |
Matthew Treinish | 7d6e48c | 2017-03-03 12:44:50 -0500 | [diff] [blame] | 293 | parser.add_argument("--combine", action='store_true', |
| 294 | help='Combine the output of this run with the ' |
| 295 | "previous run's as a combined stream in the " |
| 296 | "testr repository after it finish") |
Matthew Treinish | a051c22 | 2016-05-23 15:48:22 -0400 | [diff] [blame] | 297 | |
| 298 | parser.set_defaults(parallel=True) |
| 299 | return parser |
| 300 | |
| 301 | def _build_regex(self, parsed_args): |
| 302 | regex = '' |
| 303 | if parsed_args.smoke: |
| 304 | regex = 'smoke' |
| 305 | elif parsed_args.regex: |
| 306 | regex = parsed_args.regex |
Matthew Treinish | a6b4da9 | 2016-05-23 17:24:12 -0400 | [diff] [blame] | 307 | if parsed_args.whitelist_file or parsed_args.blacklist_file: |
| 308 | regex = regex_builder.construct_regex(parsed_args.blacklist_file, |
| 309 | parsed_args.whitelist_file, |
| 310 | regex, False) |
Matthew Treinish | a051c22 | 2016-05-23 15:48:22 -0400 | [diff] [blame] | 311 | return regex |
| 312 | |
| 313 | def _build_options(self, parsed_args): |
| 314 | options = [] |
| 315 | if parsed_args.subunit: |
| 316 | options.append("--subunit") |
| 317 | if parsed_args.parallel: |
| 318 | options.append("--parallel") |
| 319 | if parsed_args.concurrency: |
| 320 | options.append("--concurrency=%s" % parsed_args.concurrency) |
| 321 | return options |
| 322 | |
| 323 | def _run(self, regex, options): |
| 324 | returncode = 0 |
| 325 | argv = ['tempest', 'run', regex] + options |
| 326 | if '--subunit' in options: |
| 327 | returncode = run_argv(argv, sys.stdin, sys.stdout, sys.stderr) |
| 328 | else: |
| 329 | argv.append('--subunit') |
| 330 | stdin = io.StringIO() |
| 331 | stdout_r, stdout_w = os.pipe() |
| 332 | subunit_w = os.fdopen(stdout_w, 'wt') |
| 333 | subunit_r = os.fdopen(stdout_r) |
| 334 | returncodes = {} |
| 335 | |
| 336 | def run_argv_thread(): |
| 337 | returncodes['testr'] = run_argv(argv, stdin, subunit_w, |
| 338 | sys.stderr) |
| 339 | subunit_w.close() |
| 340 | |
| 341 | run_thread = threading.Thread(target=run_argv_thread) |
| 342 | run_thread.start() |
Matthew Treinish | 18d2d67 | 2016-09-20 08:30:34 -0400 | [diff] [blame] | 343 | returncodes['subunit-trace'] = subunit_trace.trace( |
| 344 | subunit_r, sys.stdout, post_fails=True, print_failures=True) |
Matthew Treinish | a051c22 | 2016-05-23 15:48:22 -0400 | [diff] [blame] | 345 | run_thread.join() |
| 346 | subunit_r.close() |
| 347 | # python version of pipefail |
| 348 | if returncodes['testr']: |
| 349 | returncode = returncodes['testr'] |
| 350 | elif returncodes['subunit-trace']: |
| 351 | returncode = returncodes['subunit-trace'] |
| 352 | return returncode |