Mercurial > repos > fubar > tool_factory_2
comparison toolfactory/galaxy-tool-test @ 99:d4d88d393285 draft
Uploaded
author | fubar |
---|---|
date | Mon, 23 Nov 2020 02:22:01 +0000 |
parents | |
children |
comparison
equal
deleted
inserted
replaced
98:67628c7dc9f3 | 99:d4d88d393285 |
---|---|
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() |