class ServiceRunner(ServiceTemplate): def __init__(self, **kwargs): self.client = OperetoClient() ServiceTemplate.__init__(self, **kwargs) self.sflow_id = self.input['opereto_source_flow_id'] self.remove_test_results_dir = False self.op_state = self._get_state() or {} def validate_input(self): input_scheme = { "type": "object", "properties": { "test_results_path": { "type": "string", "minLength": 1 }, "parent_pid": { "type": "string" }, "listener_frequency": { "type": "integer", "minValue": 1 }, "debug_mode": { "type": "boolean" } }, "required": ['listener_frequency', 'test_results_path'], "additionalProperties": True } validator = JsonSchemeValidator(self.input, input_scheme) validator.validate() self.parent_pid = self.input['parent_pid'] or self.input['pid'] self.test_results_dir = self.input['test_results_path'] self.debug_mode = self.input['debug_mode'] self.result_keys = process_result_keys self.status_keys = process_status_keys self.tests_json_scheme = { "type": "object", "properties": { "test_suite": { "type": "object", "properties": { "links": { "type": "array" }, "status": { "enum": self.status_keys } } }, "test_records": { "type": "array", "items": [{ "type": "object", "properties": { "testname": default_entity_name_scheme, "status": { "enum": self.status_keys }, "title": default_entity_name_scheme, "links": { "type": "array", "items": [{ "type": "object", "properties": { "url": { "type": "string" }, "name": { "type": "string" } } }] } }, "required": ['testname', 'status'], "additionalProperties": True }] } }, "additionalProperties": True } self.end_of_test_suite = None self.test_data = {} self.suite_links = [] self._state = {} def _print_test_link(self, link): print( '[OPERETO_HTML]<br><a href="{}"><font style="color: #222; font-weight: 600; font-size: 13px;">{}</font></a>' .format(link['url'], link['name'])) def _append_to_process_log(self, pid, ppid, loglines, log_level='info'): log_request_data = { 'sflow_id': self.sflow_id, 'pflow_id': ppid, 'agent_id': self.input['opereto_agent'], 'product_id': self.input['opereto_product_id'], 'data': [] } count = 1 for line in loglines: try: millis = int(round(time.time() * 1000)) + count log_request_data['data'].append({ 'level': log_level, 'text': line.strip(), 'timestamp': millis }) except Exception as e: print(e) count += 1 self.client._call_rest_api( 'post', '/processes/{}/log'.format(pid), data=log_request_data, error='Failed to update test log (test pid = {})'.format(pid)) def _modify_record(self, test_record): testname = test_record['testname'] test_input = test_record.get('test_input') or {} title = test_record.get('title') or testname status = test_record['status'] test_links = test_record.get('links') or [] test_ppid = test_record.get('ppid') or self.parent_pid test_pid = test_record.get('pid') if testname not in self._state: if not test_pid: test_pid = self.client.create_process( 'opereto_test_listener_record', testname=testname, title=title, test_input=test_input, test_runner_id=test_ppid, pflow_id=test_ppid) self.client.wait_to_start(test_pid) self._state[testname] = { 'ppid': test_ppid, 'pid': test_pid, 'status': 'in_process', 'title': title, 'test_output_md5': '', 'summary_md5': '', 'last_log_line': 1 } else: test_pid = self._state[testname]['pid'] if self._state[testname]['status'] not in self.result_keys: if title != self._state[testname]['title']: ### TBD: add title change API call self._state[testname]['title'] = title results_dir = os.path.join(self.test_results_dir, testname) if os.path.exists(results_dir): output_json_file = os.path.join(results_dir, 'output.json') log_file = os.path.join(results_dir, 'stdout.log') summary_file = os.path.join(results_dir, 'summary.txt') if os.path.exists(output_json_file): output_json_md5 = get_file_md5sum(output_json_file) if output_json_md5 != self._state[testname][ 'test_output_md5']: with open(output_json_file, 'r') as of: output_json = json.load(of) self.client.modify_process_property('test_output', output_json, pid=test_pid) self._state[testname][ 'test_output_md5'] = output_json_md5 if os.path.exists(summary_file): summary_md5 = get_file_md5sum(summary_file) if summary_md5 != self._state[testname]['summary_md5']: with open(summary_file, 'r') as sf: summary = sf.read() self.client.modify_process_summary( test_pid, summary) self._state[testname]['summary_md5'] = summary_md5 if os.path.exists(log_file): with open(log_file, 'r') as lf: count = 1 loglines = [] for line in lf.readlines(): if count >= self._state[testname]['last_log_line']: if count > MAX_LOG_LINES_PER_PROCESS: message = 'Test log is too long. Please save test log in remote storage and add a link to it in Opereto log. See service info to learn how to add links to your tests.json file.' loglines.append( '[OPERETO_HTML]<br><br><font style="width: 800px; padding: 15px; color: #222; font-weight: 400; border:2px solid red; background-color: #f8f8f8;">{}</font><br><br>' .format(message)) break loglines.append(line.strip()) count += 1 self._append_to_process_log(test_pid, test_ppid, loglines) self._state[testname]['last_log_line'] = count if status in self.result_keys: links = [] for link in test_links: html_link = '[OPERETO_HTML]<br><a href="{}"><font style="color: #222; font-weight: 600; font-size: 13px;">{}</font></a>'.format( link['url'], link['name']) links.append(html_link) self._append_to_process_log(test_pid, test_ppid, links) self.client.stop_process(test_pid, status=status) self._state[testname]['status'] = status def process(self): def process_results(): try: tests_json = os.path.join(self.test_results_dir, 'tests.json') if os.path.exists(tests_json): with open(tests_json, 'r') as tf: try: self.test_data = json.load(tf) self.op_state['test_data'] = self.test_data try: validator = JsonSchemeValidator( self.test_data, self.tests_json_scheme) validator.validate() except Exception as e: print('Invalid tests json file: {}'.format(e)) return if 'test_records' in self.test_data: for test_record in self.test_data[ 'test_records']: self._modify_record(test_record) if 'test_suite' in self.test_data: if 'status' in self.test_data['test_suite']: self.op_state[ 'test_suite_final_status'] = self.test_data[ 'test_suite']['status'] if 'links' in self.test_data['test_suite']: self.suite_links = self.test_data[ 'test_suite']['links'] self.op_state[ 'test_suite'] = self.suite_links finally: try: self._save_state(self.op_state) except Exception as e: print(e) finally: self.end_of_test_suite = self.client.get_process_property( name='stop_listener_code') if self.debug_mode: print('[DEBUG] content of tests.json: {}'.format( json.dumps(self.test_data))) while (True): process_results() time.sleep(self.client.input['listener_frequency']) if self.end_of_test_suite: print('Stopping listener process..') break def setup(self): self._print_step_title('Start opereto test listener..') if not os.path.exists(self.input['test_results_path']): make_directory(self.input['test_results_path']) self.op_state = { 'test_suite_final_status': 'success', 'test_results_path': self.input['test_results_path'], 'test_data': {}, 'suite_links': [] } self._save_state(self.op_state) def teardown(self): if 'test_results_path' in self.op_state: remove_directory_if_exists(self.input['test_results_path']) self._print_step_title('Opereto test listener stopped.') print('Final content of tests_json: {}'.format(json.dumps( self.op_state['test_data']), indent=4)) suite_links = self.op_state.get('suite_links') or [] for link in suite_links: self._print_test_link(link) print('Final listener status is {}'.format( self.op_state['test_suite_final_status'])) if self.op_state['test_suite_final_status'] == 'success': return self.client.SUCCESS elif self.op_state['test_suite_final_status'] == 'failure': return self.client.FAILURE elif self.op_state['test_suite_final_status'] == 'warning': return self.client.WARNING else: return self.client.ERROR