Mercurial > repos > fubar > tool_factory_docker
comparison toolfactory/galaxy-tool-test @ 0:83f8bb78781e draft
Uploaded
| author | fubar |
|---|---|
| date | Fri, 11 Dec 2020 02:51:15 +0000 |
| parents | |
| children |
comparison
equal
deleted
inserted
replaced
| -1:000000000000 | 0:83f8bb78781e |
|---|---|
| 1 #!/usr/bin/env python | |
| 2 | |
| 3 import argparse | |
| 4 import datetime as dt | |
| 5 import json | |
| 6 import logging | |
| 7 import os | |
| 8 import sys | |
| 9 import tempfile | |
| 10 from collections import namedtuple | |
| 11 from concurrent.futures import thread, ThreadPoolExecutor | |
| 12 | |
| 13 import yaml | |
| 14 | |
| 15 from galaxy.tool_util.verify.interactor import ( | |
| 16 DictClientTestConfig, | |
| 17 GalaxyInteractorApi, | |
| 18 verify_tool, | |
| 19 ) | |
| 20 | |
| 21 DESCRIPTION = """Script to quickly run a tool test against a running Galaxy instance.""" | |
| 22 DEFAULT_SUITE_NAME = "Galaxy Tool Tests" | |
| 23 ALL_TESTS = -1 | |
| 24 ALL_TOOLS = "*" | |
| 25 ALL_VERSION = "*" | |
| 26 LATEST_VERSION = None | |
| 27 | |
| 28 | |
| 29 TestReference = namedtuple("TestReference", ["tool_id", "tool_version", "test_index"]) | |
| 30 TestException = namedtuple("TestException", ["tool_id", "exception", "was_recorded"]) | |
| 31 | |
| 32 | |
| 33 class Results: | |
| 34 | |
| 35 def __init__(self, default_suitename, test_json, append=False): | |
| 36 self.test_json = test_json or "-" | |
| 37 test_results = [] | |
| 38 test_exceptions = [] | |
| 39 suitename = default_suitename | |
| 40 if append: | |
| 41 assert test_json != "-" | |
| 42 with open(test_json) as f: | |
| 43 previous_results = json.load(f) | |
| 44 test_results = previous_results["tests"] | |
| 45 if "suitename" in previous_results: | |
| 46 suitename = previous_results["suitename"] | |
| 47 self.test_results = test_results | |
| 48 self.test_exceptions = test_exceptions | |
| 49 self.suitename = suitename | |
| 50 | |
| 51 def register_result(self, result): | |
| 52 self.test_results.append(result) | |
| 53 | |
| 54 def register_exception(self, test_exception): | |
| 55 self.test_exceptions.append(test_exception) | |
| 56 | |
| 57 def already_successful(self, test_reference): | |
| 58 test_id = _test_id_for_reference(test_reference) | |
| 59 for test_result in self.test_results: | |
| 60 if test_result.get('id') != test_id: | |
| 61 continue | |
| 62 | |
| 63 has_data = test_result.get('has_data', False) | |
| 64 if has_data: | |
| 65 test_data = test_result.get("data", {}) | |
| 66 if 'status' in test_data and test_data['status'] == 'success': | |
| 67 return True | |
| 68 | |
| 69 return False | |
| 70 | |
| 71 def write(self): | |
| 72 tests = sorted(self.test_results, key=lambda el: el['id']) | |
| 73 n_passed, n_failures, n_skips = 0, 0, 0 | |
| 74 n_errors = len([e for e in self.test_exceptions if not e.was_recorded]) | |
| 75 for test in tests: | |
| 76 has_data = test.get('has_data', False) | |
| 77 if has_data: | |
| 78 test_data = test.get("data", {}) | |
| 79 if 'status' not in test_data: | |
| 80 raise Exception(f"Test result data {test_data} doesn't contain a status key.") | |
| 81 status = test_data['status'] | |
| 82 if status == "success": | |
| 83 n_passed += 1 | |
| 84 elif status == "error": | |
| 85 n_errors += 1 | |
| 86 elif status == "skip": | |
| 87 n_skips += 1 | |
| 88 elif status == "failure": | |
| 89 n_failures += 1 | |
| 90 report_obj = { | |
| 91 'version': '0.1', | |
| 92 'suitename': self.suitename, | |
| 93 'results': { | |
| 94 'total': n_passed + n_failures + n_skips + n_errors, | |
| 95 'errors': n_errors, | |
| 96 'failures': n_failures, | |
| 97 'skips': n_skips, | |
| 98 }, | |
| 99 'tests': tests, | |
| 100 } | |
| 101 if self.test_json == "-": | |
| 102 print(json.dumps(report_obj)) | |
| 103 else: | |
| 104 with open(self.test_json, "w") as f: | |
| 105 json.dump(report_obj, f) | |
| 106 | |
| 107 def info_message(self): | |
| 108 messages = [] | |
| 109 passed_tests = self._tests_with_status('success') | |
| 110 messages.append("Passed tool tests ({}): {}".format( | |
| 111 len(passed_tests), | |
| 112 [t["id"] for t in passed_tests] | |
| 113 )) | |
| 114 failed_tests = self._tests_with_status('failure') | |
| 115 messages.append("Failed tool tests ({}): {}".format( | |
| 116 len(failed_tests), | |
| 117 [t["id"] for t in failed_tests] | |
| 118 )) | |
| 119 skiped_tests = self._tests_with_status('skip') | |
| 120 messages.append("Skipped tool tests ({}): {}".format( | |
| 121 len(skiped_tests), | |
| 122 [t["id"] for t in skiped_tests] | |
| 123 )) | |
| 124 errored_tests = self._tests_with_status('error') | |
| 125 messages.append("Errored tool tests ({}): {}".format( | |
| 126 len(errored_tests), | |
| 127 [t["id"] for t in errored_tests] | |
| 128 )) | |
| 129 return "\n".join(messages) | |
| 130 | |
| 131 @property | |
| 132 def success_count(self): | |
| 133 self._tests_with_status('success') | |
| 134 | |
| 135 @property | |
| 136 def skip_count(self): | |
| 137 self._tests_with_status('skip') | |
| 138 | |
| 139 @property | |
| 140 def error_count(self): | |
| 141 return self._tests_with_status('error') + len(self.test_exceptions) | |
| 142 | |
| 143 @property | |
| 144 def failure_count(self): | |
| 145 return self._tests_with_status('failure') | |
| 146 | |
| 147 def _tests_with_status(self, status): | |
| 148 return [t for t in self.test_results if t.get("data", {}).get("status") == status] | |
| 149 | |
| 150 | |
| 151 def test_tools( | |
| 152 galaxy_interactor, | |
| 153 test_references, | |
| 154 results, | |
| 155 log=None, | |
| 156 parallel_tests=1, | |
| 157 history_per_test_case=False, | |
| 158 no_history_cleanup=False, | |
| 159 retries=0, | |
| 160 verify_kwds=None, | |
| 161 ): | |
| 162 """Run through tool tests and write report. | |
| 163 | |
| 164 Refactor this into Galaxy in 21.01. | |
| 165 """ | |
| 166 verify_kwds = (verify_kwds or {}).copy() | |
| 167 tool_test_start = dt.datetime.now() | |
| 168 history_created = False | |
| 169 if history_per_test_case: | |
| 170 test_history = None | |
| 171 else: | |
| 172 history_created = True | |
| 173 test_history = galaxy_interactor.new_history(history_name=f"History for {results.suitename}") | |
| 174 verify_kwds.update({ | |
| 175 "no_history_cleanup": no_history_cleanup, | |
| 176 "test_history": test_history, | |
| 177 }) | |
| 178 with ThreadPoolExecutor(max_workers=parallel_tests) as executor: | |
| 179 try: | |
| 180 for test_reference in test_references: | |
| 181 _test_tool( | |
| 182 executor=executor, | |
| 183 test_reference=test_reference, | |
| 184 results=results, | |
| 185 galaxy_interactor=galaxy_interactor, | |
| 186 log=log, | |
| 187 retries=retries, | |
| 188 verify_kwds=verify_kwds, | |
| 189 ) | |
| 190 finally: | |
| 191 # Always write report, even if test was cancelled. | |
| 192 try: | |
| 193 executor.shutdown(wait=True) | |
| 194 except KeyboardInterrupt: | |
| 195 executor._threads.clear() | |
| 196 thread._threads_queues.clear() | |
| 197 results.write() | |
| 198 if log: | |
| 199 log.info("Report written to '%s'", os.path.abspath(results.test_json)) | |
| 200 log.info(results.info_message()) | |
| 201 log.info("Total tool test time: {}".format(dt.datetime.now() - tool_test_start)) | |
| 202 if history_created and not no_history_cleanup: | |
| 203 galaxy_interactor.delete_history(test_history) | |
| 204 | |
| 205 | |
| 206 def _test_id_for_reference(test_reference): | |
| 207 tool_id = test_reference.tool_id | |
| 208 tool_version = test_reference.tool_version | |
| 209 test_index = test_reference.test_index | |
| 210 | |
| 211 if tool_version and tool_id.endswith("/" + tool_version): | |
| 212 tool_id = tool_id[:-len("/" + tool_version)] | |
| 213 | |
| 214 label_base = tool_id | |
| 215 if tool_version: | |
| 216 label_base += "/" + str(tool_version) | |
| 217 | |
| 218 test_id = label_base + "-" + str(test_index) | |
| 219 return test_id | |
| 220 | |
| 221 | |
| 222 def _test_tool( | |
| 223 executor, | |
| 224 test_reference, | |
| 225 results, | |
| 226 galaxy_interactor, | |
| 227 log, | |
| 228 retries, | |
| 229 verify_kwds, | |
| 230 ): | |
| 231 tool_id = test_reference.tool_id | |
| 232 tool_version = test_reference.tool_version | |
| 233 test_index = test_reference.test_index | |
| 234 # If given a tool_id with a version suffix, strip it off so we can treat tool_version | |
| 235 # correctly at least in client_test_config. | |
| 236 if tool_version and tool_id.endswith("/" + tool_version): | |
| 237 tool_id = tool_id[:-len("/" + tool_version)] | |
| 238 | |
| 239 test_id = _test_id_for_reference(test_reference) | |
| 240 | |
| 241 def run_test(): | |
| 242 run_retries = retries | |
| 243 job_data = None | |
| 244 job_exception = None | |
| 245 | |
| 246 def register(job_data_): | |
| 247 nonlocal job_data | |
| 248 job_data = job_data_ | |
| 249 | |
| 250 try: | |
| 251 while run_retries >= 0: | |
| 252 job_exception = None | |
| 253 try: | |
| 254 if log: | |
| 255 log.info("Executing test '%s'", test_id) | |
| 256 verify_tool( | |
| 257 tool_id, galaxy_interactor, test_index=test_index, tool_version=tool_version, | |
| 258 register_job_data=register, **verify_kwds | |
| 259 ) | |
| 260 if log: | |
| 261 log.info("Test '%s' passed", test_id) | |
| 262 break | |
| 263 except Exception as e: | |
| 264 if log: | |
| 265 log.warning("Test '%s' failed", test_id, exc_info=True) | |
| 266 | |
| 267 job_exception = e | |
| 268 run_retries -= 1 | |
| 269 finally: | |
| 270 if job_data is not None: | |
| 271 results.register_result({ | |
| 272 "id": test_id, | |
| 273 "has_data": True, | |
| 274 "data": job_data, | |
| 275 }) | |
| 276 if job_exception is not None: | |
| 277 was_recorded = job_data is not None | |
| 278 test_exception = TestException(tool_id, job_exception, was_recorded) | |
| 279 results.register_exception(test_exception) | |
| 280 | |
| 281 executor.submit(run_test) | |
| 282 | |
| 283 | |
| 284 def build_case_references( | |
| 285 galaxy_interactor, | |
| 286 tool_id=ALL_TOOLS, | |
| 287 tool_version=LATEST_VERSION, | |
| 288 test_index=ALL_TESTS, | |
| 289 page_size=0, | |
| 290 page_number=0, | |
| 291 check_against=None, | |
| 292 log=None, | |
| 293 ): | |
| 294 test_references = [] | |
| 295 if tool_id == ALL_TOOLS: | |
| 296 tests_summary = galaxy_interactor.get_tests_summary() | |
| 297 for tool_id, tool_versions_dict in tests_summary.items(): | |
| 298 for tool_version, summary in tool_versions_dict.items(): | |
| 299 for test_index in range(summary["count"]): | |
| 300 test_reference = TestReference(tool_id, tool_version, test_index) | |
| 301 test_references.append(test_reference) | |
| 302 else: | |
| 303 assert tool_id | |
| 304 tool_test_dicts = galaxy_interactor.get_tool_tests(tool_id, tool_version=tool_version) or {} | |
| 305 for i, tool_test_dict in enumerate(tool_test_dicts): | |
| 306 this_tool_version = tool_test_dict.get("tool_version", tool_version) | |
| 307 this_test_index = i | |
| 308 if test_index == ALL_TESTS or i == test_index: | |
| 309 test_reference = TestReference(tool_id, this_tool_version, this_test_index) | |
| 310 test_references.append(test_reference) | |
| 311 | |
| 312 if check_against: | |
| 313 filtered_test_references = [] | |
| 314 for test_reference in test_references: | |
| 315 if check_against.already_successful(test_reference): | |
| 316 if log is not None: | |
| 317 log.debug(f"Found successful test for {test_reference}, skipping") | |
| 318 continue | |
| 319 filtered_test_references.append(test_reference) | |
| 320 log.info(f"Skipping {len(test_references)-len(filtered_test_references)} out of {len(test_references)} tests.") | |
| 321 test_references = filtered_test_references | |
| 322 | |
| 323 if page_size > 0: | |
| 324 slice_start = page_size * page_number | |
| 325 slice_end = page_size * (page_number + 1) | |
| 326 test_references = test_references[slice_start:slice_end] | |
| 327 | |
| 328 return test_references | |
| 329 | |
| 330 | |
| 331 def main(argv=None): | |
| 332 if argv is None: | |
| 333 argv = sys.argv[1:] | |
| 334 | |
| 335 args = _arg_parser().parse_args(argv) | |
| 336 log = setup_global_logger(__name__, verbose=args.verbose) | |
| 337 client_test_config_path = args.client_test_config | |
| 338 if client_test_config_path is not None: | |
| 339 log.debug(f"Reading client config path {client_test_config_path}") | |
| 340 with open(client_test_config_path) as f: | |
| 341 client_test_config = yaml.full_load(f) | |
| 342 else: | |
| 343 client_test_config = {} | |
| 344 | |
| 345 def get_option(key): | |
| 346 arg_val = getattr(args, key, None) | |
| 347 if arg_val is None and key in client_test_config: | |
| 348 val = client_test_config.get(key) | |
| 349 else: | |
| 350 val = arg_val | |
| 351 return val | |
| 352 | |
| 353 output_json_path = get_option("output_json") | |
| 354 galaxy_interactor_kwds = { | |
| 355 "galaxy_url": get_option("galaxy_url"), | |
| 356 "master_api_key": get_option("admin_key"), | |
| 357 "api_key": get_option("key"), | |
| 358 "keep_outputs_dir": args.output, | |
| 359 "download_attempts": get_option("download_attempts"), | |
| 360 "download_sleep": get_option("download_sleep"), | |
| 361 } | |
| 362 tool_id = args.tool_id | |
| 363 tool_version = args.tool_version | |
| 364 tools_client_test_config = DictClientTestConfig(client_test_config.get("tools")) | |
| 365 verbose = args.verbose | |
| 366 | |
| 367 galaxy_interactor = GalaxyInteractorApi(**galaxy_interactor_kwds) | |
| 368 results = Results(args.suite_name, output_json_path, append=args.append) | |
| 369 check_against = None if not args.skip_successful else results | |
| 370 test_references = build_case_references( | |
| 371 galaxy_interactor, | |
| 372 tool_id=tool_id, | |
| 373 tool_version=tool_version, | |
| 374 test_index=args.test_index, | |
| 375 page_size=args.page_size, | |
| 376 page_number=args.page_number, | |
| 377 check_against=check_against, | |
| 378 log=log, | |
| 379 ) | |
| 380 log.debug(f"Built {len(test_references)} test references to executed.") | |
| 381 verify_kwds = dict( | |
| 382 client_test_config=tools_client_test_config, | |
| 383 force_path_paste=args.force_path_paste, | |
| 384 skip_with_reference_data=not args.with_reference_data, | |
| 385 quiet=not verbose, | |
| 386 ) | |
| 387 test_tools( | |
| 388 galaxy_interactor, | |
| 389 test_references, | |
| 390 results, | |
| 391 log=log, | |
| 392 parallel_tests=args.parallel_tests, | |
| 393 history_per_test_case=args.history_per_test_case, | |
| 394 no_history_cleanup=args.no_history_cleanup, | |
| 395 verify_kwds=verify_kwds, | |
| 396 ) | |
| 397 exceptions = results.test_exceptions | |
| 398 if exceptions: | |
| 399 exception = exceptions[0] | |
| 400 if hasattr(exception, "exception"): | |
| 401 exception = exception.exception | |
| 402 raise exception | |
| 403 | |
| 404 | |
| 405 def setup_global_logger(name, log_file=None, verbose=False): | |
| 406 formatter = logging.Formatter('%(asctime)s %(levelname)-5s - %(message)s') | |
| 407 console = logging.StreamHandler() | |
| 408 console.setFormatter(formatter) | |
| 409 | |
| 410 logger = logging.getLogger(name) | |
| 411 logger.setLevel(logging.DEBUG if verbose else logging.INFO) | |
| 412 logger.addHandler(console) | |
| 413 | |
| 414 if not log_file: | |
| 415 # delete = false is chosen here because it is always nice to have a log file | |
| 416 # ready if you need to debug. Not having the "if only I had set a log file" | |
| 417 # moment after the fact. | |
| 418 temp = tempfile.NamedTemporaryFile(prefix="ephemeris_", delete=False) | |
| 419 log_file = temp.name | |
| 420 file_handler = logging.FileHandler(log_file) | |
| 421 logger.addHandler(file_handler) | |
| 422 logger.info(f"Storing log file in: {log_file}") | |
| 423 return logger | |
| 424 | |
| 425 | |
| 426 def _arg_parser(): | |
| 427 parser = argparse.ArgumentParser(description=DESCRIPTION) | |
| 428 parser.add_argument('-u', '--galaxy-url', default="http://localhost:8080", help='Galaxy URL') | |
| 429 parser.add_argument('-k', '--key', default=None, help='Galaxy User API Key') | |
| 430 parser.add_argument('-a', '--admin-key', default=None, help='Galaxy Admin API Key') | |
| 431 parser.add_argument('--force_path_paste', default=False, action="store_true", help='This requires Galaxy-side config option "allow_path_paste" enabled. Allows for fetching test data locally. Only for admins.') | |
| 432 parser.add_argument('-t', '--tool-id', default=ALL_TOOLS, help='Tool ID') | |
| 433 parser.add_argument('--tool-version', default=None, help='Tool Version (if tool id supplied). Defaults to just latest version, use * to test all versions') | |
| 434 parser.add_argument('-i', '--test-index', default=ALL_TESTS, type=int, help='Tool Test Index (starting at 0) - by default all tests will run.') | |
| 435 parser.add_argument('-o', '--output', default=None, help='directory to dump outputs to') | |
| 436 parser.add_argument('--append', default=False, action="store_true", help="Extend a test record json (created with --output-json) with additional tests.") | |
| 437 parser.add_argument('--skip-successful', default=False, action="store_true", help="When used with --append, skip previously run successful tests.") | |
| 438 parser.add_argument('-j', '--output-json', default=None, help='output metadata json') | |
| 439 parser.add_argument('--verbose', default=False, action="store_true", help="Verbose logging.") | |
| 440 parser.add_argument('-c', '--client-test-config', default=None, help="Test config YAML to help with client testing") | |
| 441 parser.add_argument('--suite-name', default=DEFAULT_SUITE_NAME, help="Suite name for tool test output") | |
| 442 parser.add_argument('--with-reference-data', dest="with_reference_data", default=False, action="store_true") | |
| 443 parser.add_argument('--skip-with-reference-data', dest="with_reference_data", action="store_false", help="Skip tests the Galaxy server believes use data tables or loc files.") | |
| 444 parser.add_argument('--history-per-suite', dest="history_per_test_case", default=False, action="store_false", help="Create new history per test suite (all tests in same history).") | |
| 445 parser.add_argument('--history-per-test-case', dest="history_per_test_case", action="store_true", help="Create new history per test case.") | |
| 446 parser.add_argument('--no-history-cleanup', default=False, action="store_true", help="Perserve histories created for testing.") | |
| 447 parser.add_argument('--parallel-tests', default=1, type=int, help="Parallel tests.") | |
| 448 parser.add_argument('--retries', default=0, type=int, help="Retry failed tests.") | |
| 449 parser.add_argument('--page-size', default=0, type=int, help="If positive, use pagination and just run one 'page' to tool tests.") | |
| 450 parser.add_argument('--page-number', default=0, type=int, help="If page size is used, run this 'page' of tests - starts with 0.") | |
| 451 parser.add_argument('--download-attempts', default=1, type=int, help="Galaxy may return a transient 500 status code for download if test results are written but not yet accessible.") | |
| 452 parser.add_argument('--download-sleep', default=1, type=int, help="If download attempts is greater than 1, the amount to sleep between download attempts.") | |
| 453 return parser | |
| 454 | |
| 455 | |
| 456 if __name__ == "__main__": | |
| 457 main() |
