From 227e02694597e4fa0cb35013579add75be7cbb96 Mon Sep 17 00:00:00 2001 From: Steve Martin Date: Wed, 10 Apr 2024 14:53:34 -0400 Subject: [PATCH] Remove REFID modules in hopes of rebooting the extra service plugins later with improved factoring and documentation. --- PyICe/refid_modules/__init__.py | 0 PyICe/refid_modules/bench_base.py | 188 --- PyICe/refid_modules/bench_identifier.py | 15 - PyICe/refid_modules/p4_traceability.py | 170 --- .../bench_connections_plugin.py | 111 -- .../default_bench_configuration_template.py | 30 - .../visualizer_locations_template.py | 122 -- .../die_traceability.py | 433 ------- .../die_traceability_plugin.py | 132 -- .../p4_traceability_plugin.py | 352 ------ PyICe/refid_modules/plugin_module/plugin.py | 67 - .../template_check_plugin.py | 49 - .../How_to_Limit_Test_Declaration.txt | 37 - .../correlation_test_declaration_plugin.py | 321 ----- .../limit_test_declaration_plugin.py | 537 -------- .../refid_import_plugin.py | 158 --- PyICe/refid_modules/stdf_utils.py | 1016 --------------- PyICe/refid_modules/temptroller.py | 643 ---------- PyICe/refid_modules/test_archive.py | 212 ---- PyICe/refid_modules/test_module.py | 634 ---------- PyICe/refid_modules/test_results.py | 1102 ----------------- 21 files changed, 6329 deletions(-) delete mode 100644 PyICe/refid_modules/__init__.py delete mode 100644 PyICe/refid_modules/bench_base.py delete mode 100644 PyICe/refid_modules/bench_identifier.py delete mode 100644 PyICe/refid_modules/p4_traceability.py delete mode 100644 PyICe/refid_modules/plugin_module/bench_connections_plugin/bench_connections_plugin.py delete mode 100644 PyICe/refid_modules/plugin_module/bench_connections_plugin/default_bench_configuration_template.py delete mode 100644 PyICe/refid_modules/plugin_module/bench_connections_plugin/visualizer_locations_template.py delete mode 100644 PyICe/refid_modules/plugin_module/die_traceability_plugin/die_traceability.py delete mode 100644 PyICe/refid_modules/plugin_module/die_traceability_plugin/die_traceability_plugin.py delete mode 100644 PyICe/refid_modules/plugin_module/p4_traceability_plugin/p4_traceability_plugin.py delete mode 100644 PyICe/refid_modules/plugin_module/plugin.py delete mode 100644 PyICe/refid_modules/plugin_module/template_check_plugin/template_check_plugin.py delete mode 100644 PyICe/refid_modules/plugin_module/test_declaration_plugin/How_to_Limit_Test_Declaration.txt delete mode 100644 PyICe/refid_modules/plugin_module/test_declaration_plugin/correlation_test_declaration_plugin.py delete mode 100644 PyICe/refid_modules/plugin_module/test_declaration_plugin/limit_test_declaration_plugin.py delete mode 100644 PyICe/refid_modules/plugin_module/test_declaration_plugin/refid_import_plugin.py delete mode 100644 PyICe/refid_modules/stdf_utils.py delete mode 100644 PyICe/refid_modules/temptroller.py delete mode 100644 PyICe/refid_modules/test_archive.py delete mode 100644 PyICe/refid_modules/test_module.py delete mode 100644 PyICe/refid_modules/test_results.py diff --git a/PyICe/refid_modules/__init__.py b/PyICe/refid_modules/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/PyICe/refid_modules/bench_base.py b/PyICe/refid_modules/bench_base.py deleted file mode 100644 index f87f661..0000000 --- a/PyICe/refid_modules/bench_base.py +++ /dev/null @@ -1,188 +0,0 @@ -import abc -import atexit -import inspect -import os -from PyICe import lab_core -from PyICe.lab_utils.banners import print_banner - -class bench_base(abc.ABC): - def __init__(self, **kwargs): - atexit.register(self.cleanup) - self._notification_functions = [] - self._cleanup_registry = [] - self._master = lab_core.master() - self._kwargs = kwargs - self._master.add_channel_dummy('comment') - self._master.add_channel_dummy('device_num') - self.init(self._master) - if 'tdegc' not in self._master.get_all_channel_names(): - def dummy_oven_write(value): - if value is not None: - msg = "*** ERROR: Wrote dummy oven temperature.\nThis channel only exists to facilitate plotting before moving a test to the oven.\nTemperatures are meaningless, and you risk corrupting your databse with improperly collected data!\nSet 'temperatures=None' next time.***" - self.notify(msg) - raise Exception(msg) - self._master.add_channel_virtual('tdegc', write_function=dummy_oven_write) - self._master['tdegc'].set_description("Dummy Oven. Don't write to anything other than 'None'.") - if 'tdegc_sense' not in self._master.get_all_channel_names(): - self._master.add_channel_dummy('tdegc_sense') - self._master['tdegc_sense'].write(None) #Doesn't pretend to read temperatures. Just to facilitate plotting. - self._master['tdegc_sense'].set_description('''Dummy Oven Temp Readback. Will only ever read 'None'. Suggest SQL query like: """SELECT ifnull(tdegc_sense, 27) AS tdegc_sense, foo FROM {table_name}"""''') - self._master['tdegc_sense'].set_write_access(False) - self.add_traceability_channels() - self._master.write_html(file_name='project.html', verbose=True, sort_categories=True) # Don't like the general name "project" - ##################################################### - # HACK HACK HACK HACK HACK HACK HACK HACK HACK HACK # - ##################################################### - # PyICe has developed some kind of threading problem - # which crashes 34970 with HTX9011 imeas channels. - # This will slow everything down until resolved!!! - self.get_master().set_allow_threading(False) - ##################################################### - @abc.abstractmethod - def init(master, virtual_oven): - ''''make this gun work''' - def add_traceability_channels(self): - ch_cat = 'eval_traceability' - self._master.add_channel_dummy('bench').write(str(inspect.getmodule(type(self)))) - self._master['bench'].set_category(ch_cat) - self._master['bench'].set_write_access(False) - self._master.add_channel_dummy('bench_operator').write(os.getlogin()) - self._master['bench_operator'].set_category(ch_cat) - self._master['bench_operator'].set_write_access(False) - # module_file = inspect.getsourcefile(type(self)) ### Part of p4_traceability_plugin now. - # fileinfo = p4_traceability.get_fstat(module_file) - # for property in fileinfo: - # self._master.add_channel_dummy(f'bench_{property}').write(fileinfo[property]) - # self._master[f'bench_{property}'].set_category(ch_cat) - # self._master[f'bench_{property}'].set_write_access(False) - # if fileinfo['depotFile'] is None: - # print("********* WARNING *********") - # print("* Lab bench unversioned. *") - # print(f"* {self._master['bench'].read()}") - # print("***************************") - # resp = input('Press "y" to continue: ') - # if resp.lower() != 'y': - # raise Exception('Unversioned bench module.') - # elif fileinfo['action'] is not None: - # print("************* WARNING *************") - # print("* Lab bench uncommitted changes. *") - # print(f"* {self._master['bench'].read()}") - # print("***********************************") - # resp = input('Press "y" to continue: ') - # if resp.lower() != 'y': - # raise Exception('Uncommitted bench module working copy.') - def get_ch_group_info(ch_group, ident_level=0): - ret_str = '' - tabs = '\t' * ident_level - try: - idn = ch_group.identify() - except AttributeError: - idn = "No Information Available." - ret_str = f'{tabs}{ch_group.get_name()}: {idn}\n' - for ch_subgrp in ch_group.get_channel_groups(): - ret_str += get_ch_group_info(ch_subgrp, ident_level+1) - return ret_str - self._master.add_channel_dummy('bench_instruments').write(get_ch_group_info(self._master)) - self._master['bench_instruments'].set_category(ch_cat) - self._master['bench_instruments'].set_write_access(False) - def get_master(self): - return self._master - def add_notification(self, fn): - self._notification_functions.append(fn) - def add_cleanup(self, fn): - self._cleanup_registry.append(fn) - def notify(self, msg, subject=None, attachment_filenames=[], attachment_MIMEParts=[]): - if not len(attachment_filenames) and not len(attachment_MIMEParts): - #Just a plain message - print(msg) - for fn in self._notification_functions: - try: - fn(msg, subject=subject, attachment_filenames=attachment_filenames, attachment_MIMEParts=attachment_MIMEParts) - except TypeError: - # Function probably doesn't accept subject or attachments - try: - fn(msg) - except Exception as e: - # Don't let a notiffication crash a more-important cleanup/shutdown. - print(e) - except Exception as e: - # Don't let a notiffication crash a more-important cleanup/shutdown. - print(e) - def cleanup_oven(self): - try: - if self._master.read('tdegc_enable'): - self._master.write('tdegc_soak', 1) - # self._master.write('tdegc', 25) # Autonics has non-volatile memory - self._master.write('tdegc_enable', False) - else: - # Oven wasn't used or is already cleaned up - pass - except lab_core.ChannelAccessException as e: - # Expected. Channel just might not exist. - pass - except IOError as e: - print('Oven off???', e) - pass - except Exception as e: - # What happened? - print("Something unexpected happened in oven cleanup. Please Contact PyICe Support at PyICe-developers@analog.com.") - breakpoint() - raise e - else: - # Oven successfully disabled. Now for powerdown... - pass - finally: - try: - # Might not have these channels defined?? - self._master.write('tdegc_power_relay', False) - except lab_core.ChannelAccessException as e: - # Expected. Channel just might not exist. - pass - except Exception as e: - print(f'Oven cleanup {type(e)}') - # @atexit.register - def cleanup(self): - for cleanup in self._cleanup_registry: - cleanup() - def close_ports(self): - m = self.get_master() - delegator_list = [ch.resolve_delegator() for ch in self.get_master()] - delegator_list = list(set(delegator_list)) - interfaces = [] - for delegator in delegator_list: - for interface in delegator.get_interfaces(): - interfaces.append(interface) - interfaces = list(set(interfaces)) - for iface in interfaces: - try: - iface.close() - except AttributeError as e: - pass - except Exception as e: - print(e) - def __enter__(self): - return self - def __exit__(self, exc_type, exc_val, exc_tb): - # TODO atexit.register? - # https://docs.python.org/3/library/atexit.html - self.cleanup() - self.cleanup_oven() - self.close_ports() - print_banner('All cleaned up, Outa Here!') - return False - - - - - - - - - - - - - - - - diff --git a/PyICe/refid_modules/bench_identifier.py b/PyICe/refid_modules/bench_identifier.py deleted file mode 100644 index 683c449..0000000 --- a/PyICe/refid_modules/bench_identifier.py +++ /dev/null @@ -1,15 +0,0 @@ -import importlib, socket, os - -def get_bench_instruments(project_folder_name, benchsetup = None): - if benchsetup is None: - thismachine = socket.gethostname().replace("-","_") - thisuser = thisbench = os.getlogin().lower() # Duplicate benches because of case sensitivity! - thisbench = f'{thisuser}_{thismachine}' - else: - thisbench = benchsetup - try: - module = importlib.import_module(name=f"{project_folder_name}.{project_folder_name}_base.benches.{thisbench}", package=None) - except ImportError as e: - print(e) - raise Exception(f"Can't find bench file {thisbench}. Note that dashes must be replaced with underscores.") - return module.bench_instruments diff --git a/PyICe/refid_modules/p4_traceability.py b/PyICe/refid_modules/p4_traceability.py deleted file mode 100644 index d5366f2..0000000 --- a/PyICe/refid_modules/p4_traceability.py +++ /dev/null @@ -1,170 +0,0 @@ -import collections -import os -import re -import subprocess -import sys - -class results_dict(collections.OrderedDict): - '''Ordered dictionary with pretty print addition.''' - def __str__(self): - s = '' - max_key_name_length = 0 - for k,v in self.items(): - max_key_name_length = max(max_key_name_length, len(k)) - s += '{}:\t{}\n'.format(k,v) - s = s.expandtabs(max_key_name_length+2) - return s - -client_pat = re.compile(r'^\.\.\. (?P\w+) (?P.*?)\s*?$', re.MULTILINE) -def get_client_info(): - p4proc = subprocess.run(["p4", "-ztag", "client", "-o"], capture_output=True) - client_fields = results_dict({'Client': None, - 'Owner': None, - 'Description': None, - 'Root': None, - 'Options': None, - }) - if p4proc.returncode: - print(f"Perforce client returned: {p4proc.returncode}!") - else: - for match in client_pat.finditer(p4proc.stdout.decode(sys.stdout.encoding)): - if match.group('f_name') in client_fields: - client_fields[match.group('f_name')] = match.group('f_val') - return client_fields - -fstat_pat = re.compile(r'^\.\.\. (?P\w+) (?P.*?)\s*?$', re.MULTILINE) -def get_fstat(local_file): - # TODO: Consider switching to Helix Python API? https://www.perforce.com/downloads/helix-core-api-python - p4proc = subprocess.run(["p4", "fstat", local_file], capture_output=True) - fstat_fields = {'depotFile': None, - 'clientFile': None, - 'headAction': None, - 'headChange': None, - 'headRev': None, - 'haveRev': None, - 'action': None, - 'diff': None, - 'swarmLink': None, - } - if p4proc.returncode: - print(f"Perforce fstat returned: {p4proc.returncode}!") - elif p4proc.stderr.decode(sys.stdout.encoding).strip() == f'{local_file} - no such file(s).': - pass #not versioned! - elif p4proc.stderr.decode(sys.stdout.encoding).strip() == f'{local_file} - file(s) not in client view.': - pass #not versioned! - elif p4proc.stderr != b'': - print(f"Perforce fstat sent: {p4proc.stderr.decode(sys.stdout.encoding)} to stderr!") - #Match stowe_eval.html - no such file(s)? - else: - for match in fstat_pat.finditer(p4proc.stdout.decode(sys.stdout.encoding)): - if match.group('f_name') in fstat_fields: - fstat_fields[match.group('f_name')] = match.group('f_val') - if fstat_fields['action'] == 'edit': - diffproc = subprocess.run(['p4', 'diff', local_file], capture_output=True, check=True) - fstat_fields['diff'] = diffproc.stdout.decode(sys.stdout.encoding) - if fstat_fields['depotFile'] is not None: - swarm_prefix = 'https://swarm.adsdesign.analog.com/files/' - swarm_fpath = fstat_fields['depotFile'][2:] #remove '//' - swarm_revision = f"?v={fstat_fields['haveRev']}" if fstat_fields['haveRev'] is not None else '' - fstat_fields['swarmLink'] = f'{swarm_prefix}{swarm_fpath}{swarm_revision}' - return fstat_fields - -describe_zpat = re.compile(r'^\.\.\. (?P\w+) (?P.*?)\s*?$', re.MULTILINE) -def get_describe(change_number): - # TODO: Consider switching to Helix Python API? https://www.perforce.com/downloads/helix-core-api-python - p4proc = subprocess.run(["p4", "-ztag", "describe", f"{change_number}"], capture_output=True) - describe_fields = {'change': None, - 'user': None, - 'client': None, - 'time': None, - 'desc': None, - 'status': None, - 'changeType': None, - 'path': None, - 'depotFile0': None, - 'action0': None, - 'type0': None, - 'rev0': None, - 'fileSize0': None, - 'digest0': None, - } -# ... change 1656161 -# ... user dsimmons -# ... client stowe_eval--dsimmons--DSIMMONS-L01 -# ... time 1613009815 -# ... desc mistake with TST_TEMP datatype. According to official spec, this should be a character string. COnverted back to number layer by Python/SQLite. - -# ... status submitted -# ... changeType public -# ... path //adi/stowe/evaluation/TRUNK/modules/* -# ... depotFile0 //adi/stowe/evaluation/TRUNK/modules/stdf_utils.py -# ... action0 edit -# ... type0 text -# ... rev0 14 -# ... fileSize0 33359 -# ... digest0 15AD23C1FD1BF6F4FE5669838FD94449 - - if p4proc.returncode: - print(f"Perforce describe returned: {p4proc.returncode}!") - # elif p4proc.stderr.decode(sys.stdout.encoding).strip() == f'{local_file} - no such file(s).': - # pass #not versioned! - # elif p4proc.stderr.decode(sys.stdout.encoding).strip() == f'{local_file} - file(s) not in client view.': - # pass #not versioned! - elif p4proc.stderr != b'': - print(f"Perforce describe sent: {p4proc.stderr.decode(sys.stdout.encoding)} to stderr!") - #Match stowe_eval.html - no such file(s)? - else: - for match in fstat_pat.finditer(p4proc.stdout.decode(sys.stdout.encoding)): - if match.group('f_name') in describe_fields: - describe_fields[match.group('f_name')] = match.group('f_val') - return describe_fields - -def check_file_writable(fnm): - if os.path.exists(fnm): - # path exists - if os.path.isfile(fnm): # is it a file or a dir? - # also works when file is a link and the target is writable - return os.access(fnm, os.W_OK) - else: - return False # path is a dir, so cannot write as a file - return False - # # target does not exist, check perms on parent dir - # pdir = os.path.dirname(fnm) - # if not pdir: pdir = '.' - # # target is creatable if parent dir is writable - # return os.access(pdir, os.W_OK) - -def sync_workspace(template_workspace): - p4proc = subprocess.run(["p4", "workspace", "-t", template_workspace], capture_output=True) - if p4proc.returncode: - print(f"Perforce fstat returned: {p4proc.returncode}!") - if p4proc.stdout != b'': - print(f'stdout: {p4proc.stdout}') - if p4proc.stderr != b'': - print(f'stderr: {p4proc.stderr}') - -edit_pat = re.compile(r'^\.\.\. (?P\w+) (?P.*?)\s*?$', re.MULTILINE) -def p4_edit(filename): - p4proc = subprocess.run(["p4", "-ztag", "edit", filename], capture_output=True) - edit_fields = results_dict({'depotFile': None, - 'clientFile': None, - 'workRev': None, - 'action': None, - 'type': None, - }) - if p4proc.returncode: - print(f"Perforce client returned: {p4proc.returncode}!") - else: - for match in client_pat.finditer(p4proc.stdout.decode(sys.stdout.encoding)): - if match.group('f_name') in edit_fields: - edit_fields[match.group('f_name')] = match.group('f_val') - if edit_fields['action'] == 'edit': - return True - else: - print(f'Checkout of {filename} failed.') - print(edit_fields) - return False - - - -# stdout: b'Client stowe_eval--dsimmons--CHLM-PELAB-D01 not changed.\r\n' \ No newline at end of file diff --git a/PyICe/refid_modules/plugin_module/bench_connections_plugin/bench_connections_plugin.py b/PyICe/refid_modules/plugin_module/bench_connections_plugin/bench_connections_plugin.py deleted file mode 100644 index bd2a608..0000000 --- a/PyICe/refid_modules/plugin_module/bench_connections_plugin/bench_connections_plugin.py +++ /dev/null @@ -1,111 +0,0 @@ -from PyICe.refid_modules.plugin_module.plugin import plugin -from PyICe.bench_configuration_management import bench_configuration_management, bench_visualizer -from types import MappingProxyType -import importlib, os, inspect, sys, types - -class bench_connections(plugin): - desc = "This plugin compares bench setups between tests being run together to verify there are no conflicts, and produces a diagram of the setup." - def __init__(self, test_mod, default_bench_location, visualizer_locations): - super().__init__(test_mod) - self.tm.interplugs['check_bench_connections_pre_amalgamation']=[] - self.tm.interplugs['check_bench_connections_post_amalgamation']=[] - self.bc = default_bench_location.default_bench_configuration - self.file_locations(default_bench_location, visualizer_locations) - - - def __str__(self): - return "This plugin compares bench setups between tests being run together to verify there are no conflicts, and produces a diagram of the setup." - - def file_locations(self, default_bench_location, visualizer_locations): - ''' - These are files that need to be made per project to make the plugin function. See the templates for an example script of each. - ''' - self.bc = default_bench_location.default_bench_configuration - self.locations = visualizer_locations - - def _set_atts_(self): - self.att_dict = MappingProxyType({ - '_get_components_dict':self._get_components_dict, - '_get_default_connections':self._get_default_connections, - '_get_connection_diagram':self._get_connection_diagram, - 'check_bench_connections':self.check_bench_connections, - 'add_bench_traceability_channel':self.add_bench_traceability_channel, - }) - def get_atts(self): - return self.att_dict - - def get_hooks(self): - plugin_dict={ - 'pre_collect':[self._set_one_and_done], - 'begin_collect':[self.check_bench_connections], - 'tm_logger_setup': [self.add_bench_traceability_channel, self._set_one_and_done], - } - return plugin_dict - def set_interplugs(self): - pass - def execute_interplugs(self, hook_key, *args, **kwargs): - for (k,v) in self.tm.interplugs.items(): - if k is hook_key: - for f in v: - f(*args, **kwargs) - - def _set_one_and_done(self, *args): - self.tm.tt._need_to_benchcheck=True - - def check_bench_connections(self): - ''' - This only needs to be run once per regression. It combines all the connections listed in the test scripts of the regression and makes sure that - there are no conflicts. e.g. a single port with two connections or a blocked port with a connection. - ''' - if self.tm.tt._need_to_benchcheck: - self.components_dict = self._get_components_dict() - all_connections = {} - for test in self.tm.tt: - self.connection_collection = self._get_connection_collection(test=test) - if len(self.tm.tt) > 1 and not test.is_included(): - if input(f'Excluded test {test.get_name()} included in multi-test regression. Reason: {test.get_exclude_reason()} Continue? [y/n]: ').lower() == 'y': - continue - else: - breakpoint() - test.configure_bench(self.components_dict, self.connection_collection) - self.execute_interplugs('check_bench_connections_pre_amalgamation') - all_connections[test] = self.connection_collection - connections = bench_configuration_management.connection_collection.distill(all_connections.values()) - connections.print_connections(exclude = self._get_default_connections()) - if self.locations is not None: - visualizer = bench_visualizer.visualizer(connections=connections.connections, locations=self.locations.component_locations().locations) - visualizer.generate(file_base_name="Bench_Config", prune=True, file_format='svg', engine='neato') - self.tm.tt.connection_diagram = connections.print_connections() - - self.connections = connections - ############################################################################### - # This can be used to dump the terminals list for easy reference. - # Not sure where to put this - Dave - please help if interested. - ############################################################################### - # f = open("terminals_reference.txt", "w") - # print(bench_components.print_terminals_by_component(), file=f) - # f.close() - owners = set() - for x in range(len(self.connections.get_connections())): - owners.add(self.connections.get_connections()[x].terminals[0].get_owner()) - owners.add(self.connections.get_connections()[x].terminals[1].get_owner()) - # modifiers = board_template_modifiers.board_template_modifiers(owners) - self.execute_interplugs('check_bench_connections_post_amalgamation') - self.tm.tt._need_to_benchcheck=False - - def _get_components_dict(self): - return self.bc.component_collection().get_components() - def _get_connection_collection(self, test): - return self.bc.default_connections(self.components_dict, name=type(test).__name__) - def _get_default_connections(self): - return self.bc.default_connections(self.components_dict, name="default").get_connections() - def _get_connection_diagram(self): - return self.connection_diagram - def add_bench_traceability_channel(self, logger): - ''' - Adds the sum connections made for the regression into the sql table. - ''' - logger.add_channel_dummy('bench_configuration') - logger['bench_configuration'].set_category('eval_traceability') - logger.write('bench_configuration', self.tm.tt.connection_diagram) - logger['bench_configuration'].set_write_access(False) diff --git a/PyICe/refid_modules/plugin_module/bench_connections_plugin/default_bench_configuration_template.py b/PyICe/refid_modules/plugin_module/bench_connections_plugin/default_bench_configuration_template.py deleted file mode 100644 index 8e1f2f5..0000000 --- a/PyICe/refid_modules/plugin_module/bench_connections_plugin/default_bench_configuration_template.py +++ /dev/null @@ -1,30 +0,0 @@ -from PyICe.bench_configuration_management import bench_configuration_management, lab_components -import abc - -class default_bench_configuration_template(abc.ABC): - - @abc.abstractmethod - def component_collection(): - ''' Maintain a list of components that will be used on your bench for your various setups. Many can be found in lab_components, but you may need to make a few of your own a la target boards. - - from {project_folder_name}.{project_folder_name}_base.modules import {project_folder_name}_bench_configuration_components # If you are adding in project specific boards or the like - - components = bench_configuration_management.component_collection() - - components.add_component(lab_components.whatuwant('BABY_I_GOT_IT')) - components.add_component(lab_components.whatuneed('BABY_YAKNOW_I_GOT_IT')) - return components - - - ''' - @abc.abstractmethod - def default_connections(components, name): - ''' Here you can have a default bench setup that all others will be compared to. When displaying what connections are made, only the ones that differ from these will be presented. - - project_default_connections = bench_configuration_management.connection_collection(name) - - project_default_connections.add_connection(components["BABY_I_GOT_IT"][a terminal], components["BABY_YAKNOW_I_GOT_IT"][another terminal]) - project_default_connections.add_connection(components["BABY_I_GOT_IT"][a different terminal], components["BABY_YAKNOW_I_GOT_IT"][a fourth terminal]) - - return stowe_default_connections - ''' diff --git a/PyICe/refid_modules/plugin_module/bench_connections_plugin/visualizer_locations_template.py b/PyICe/refid_modules/plugin_module/bench_connections_plugin/visualizer_locations_template.py deleted file mode 100644 index 44f6ab4..0000000 --- a/PyICe/refid_modules/plugin_module/bench_connections_plugin/visualizer_locations_template.py +++ /dev/null @@ -1,122 +0,0 @@ -import pathlib - -class component_locations: - def __init__(self): - ''' - path = pathlib.Path(__file__).parent.resolve().as_posix() + "/visualizer_images/" - self.locations = { - ##################################################### - # # - # Test Equipment # - # # - ##################################################### - "CONFIGURATORXT" : {"position" : {"xpos":0, "ypos":0} , "image" : f"{path}ConfigXT.PNG", "use_label" : False}, - "SIGLENT" : {"position" : {"xpos":500, "ypos":1000} , "image" : f"{path}Siglent.PNG", "use_label" : False}, - "SPAT" : {"position" : {"xpos":-800, "ypos":-200} , "image" : f"{path}SPAT.PNG", "use_label" : False}, - "AGILENT_U2300_DAQ" : {"position" : {"xpos":-800, "ypos":500} , "image" : f"{path}U2331A.PNG", "use_label" : False}, - "U2300_TO_CAT5" : {"position" : {"xpos":-800, "ypos":250} , "image" : f"{path}U2331A_Adapter.PNG", "use_label" : False}, - "HAMEG" : {"position" : {"xpos":-700, "ypos":-1400} , "image" : f"{path}Hameg4040.PNG", "use_label" : False}, - "Rampinator" : {"position" : {"xpos":-800, "ypos":-800} , "image" : f"{path}Rampinator.PNG", "use_label" : False}, - "OSCILLOSCOPE" : {"position" : {"xpos":1350, "ypos":925} , "image" : f"{path}Agilent3034a.PNG", "use_label" : False}, - "AGILENT_3497x" : {"position" : {"xpos":-800, "ypos":1000} , "image" : f"{path}Agilent34970.PNG", "use_label" : False}, - "AGILENT_34908A" : {"position" : {"xpos":-100, "ypos":1075} , "image" : f"{path}Agilent34908a.PNG", "use_label" : False}, - "AGILENT_34901A_2" : {"position" : {"xpos":-100, "ypos":1000} , "image" : f"{path}Agilent34901A.PNG", "use_label" : False}, - "AGILENT_34901A_3" : {"position" : {"xpos":-100, "ypos":925} , "image" : f"{path}Agilent34901A.PNG", "use_label" : False}, - "PSA_RFMUX" : {"position" : {"xpos":1500, "ypos":-500} , "image" : f"{path}HTX9016.PNG", "use_label" : False}, - "PSA" : {"position" : {"xpos":1850, "ypos":-1350} , "image" : f"{path}PSA.png", "use_label" : False}, - ##################################################### - # # - # Terminators and Probes # - # # - ##################################################### - "SCOPEPROBE1" : {"position" : {"xpos":800, "ypos":600} , "image" : f"{path}ScopeProbe.PNG", "use_label" : False}, - "SCOPEPROBE2" : {"position" : {"xpos":1100, "ypos":600} , "image" : f"{path}ScopeProbe.PNG", "use_label" : False}, - "SCOPEPROBE3" : {"position" : {"xpos":1400, "ypos":600} , "image" : f"{path}ScopeProbe.PNG", "use_label" : False}, - "SCOPEPROBE4" : {"position" : {"xpos":1700, "ypos":600} , "image" : f"{path}ScopeProbe.PNG", "use_label" : False}, - "VOUT0_TERMINATOR" : {"position" : {"xpos":950, "ypos":400} , "image" : f"{path}Tekronix50Ohm.PNG", "use_label" : False}, - "VOUT1_TERMINATOR" : {"position" : {"xpos":1100, "ypos":400} , "image" : f"{path}Tekronix50Ohm.PNG", "use_label" : False}, - "VOUT2_TERMINATOR" : {"position" : {"xpos":1250, "ypos":400} , "image" : f"{path}Tekronix50Ohm.PNG", "use_label" : False}, - - - "PSA_DC_BLOCKER" : {"position" : {"xpos":2300, "ypos":-800} , "image" : f"{path}HTX9015_DC_BLOCKER.png", "use_label" : False}, - - - "AVIN_TERMINATOR" : {"position" : {"xpos":1400, "ypos":400} , "image" : f"{path}Tekronix50Ohm.PNG", "use_label" : False}, - "RST_TERMINATOR" : {"position" : {"xpos":1550, "ypos":400} , "image" : f"{path}Tekronix50Ohm.PNG", "use_label" : False}, - "WDD_TERMINATOR" : {"position" : {"xpos":1700, "ypos":400} , "image" : f"{path}Tekronix50Ohm.PNG", "use_label" : False}, - "CURRENTPROBE_A" : {"position" : {"xpos":2000, "ypos":400} , "image" : f"{path}CurrentProbe.PNG", "use_label" : False}, - "CURRENTPROBE_B" : {"position" : {"xpos":2000, "ypos":700} , "image" : f"{path}CurrentProbe.PNG", "use_label" : False}, - ##################################################### - # # - # Power Breakouts and Y's # - # # - ##################################################### - "Y_CONNECTOR_POWER1" : {"position" : {"xpos":-500, "ypos":-500} , "image" : f"{path}YPower.PNG", "use_label" : False}, - "Y_CONNECTOR_POWER5" : {"position" : {"xpos":-400, "ypos":-500} , "image" : f"{path}YPower.PNG", "use_label" : False}, - "Y_CONNECTOR_POWER6" : {"position" : {"xpos":-300, "ypos":-500} , "image" : f"{path}YPower.PNG", "use_label" : False}, - "Y_CONNECTOR_POWER2" : {"position" : {"xpos":-200, "ypos":-500} , "image" : f"{path}YPower.PNG", "use_label" : False}, - "Y_CONNECTOR_POWER7" : {"position" : {"xpos":-100, "ypos":-500} , "image" : f"{path}YPower.PNG", "use_label" : False}, - "Y_CONNECTOR_POWER3" : {"position" : {"xpos":0, "ypos":-500} , "image" : f"{path}YPower.PNG", "use_label" : False}, - "POWER3_BREAKOUT" : {"position" : {"xpos":450, "ypos":500} , "image" : f"{path}PowerBreakout.PNG", "use_label" : False}, - "POWER4_BREAKOUT" : {"position" : {"xpos":450, "ypos":700} , "image" : f"{path}PowerBreakout.PNG", "use_label" : False}, - ##################################################### - # # - # Target Boards # - # # - ##################################################### - "LT3390_BOARD" : {"position" : {"xpos":2000, "ypos":200}, "image" : f"{path}TargetBoard.PNG", "use_label" : True}, - "LT3390_1_BOARD" : {"position" : {"xpos":2000, "ypos":200}, "image" : f"{path}TargetBoard.PNG", "use_label" : True}, - "LT3390_2_BOARD" : {"position" : {"xpos":2000, "ypos":200}, "image" : f"{path}TargetBoard.PNG", "use_label" : True}, - "LT3390_3_BOARD" : {"position" : {"xpos":2000, "ypos":200}, "image" : f"{path}TargetBoard.PNG", "use_label" : True}, - "LT3390_4_BOARD" : {"position" : {"xpos":2000, "ypos":200}, "image" : f"{path}TargetBoard.PNG", "use_label" : True}, - "LT3390_5_BOARD" : {"position" : {"xpos":2000, "ypos":200}, "image" : f"{path}TargetBoard.PNG", "use_label" : True}, - "LT3390_6_BOARD" : {"position" : {"xpos":2000, "ypos":200}, "image" : f"{path}TargetBoard.PNG", "use_label" : True}, - "LT3390_BOARD_OPEN_LOOP" : {"position" : {"xpos":2000, "ypos":200}, "image" : f"{path}TargetBoard.PNG", "use_label" : True}, - "TARGET_BOARD" : {"position" : {"xpos":2000, "ypos":200}, "image" : f"{path}TargetBoard.PNG", "use_label" : True}, - ##################################################### - # # - # Auxilliary Boards # - # # - ##################################################### - "BASE_BOARD" : {"position" : {"xpos":950, "ypos":0} , "image" : f"{path}Baseboard.PNG", "use_label" : False}, - "LT8609_BOARD" : {"position" : {"xpos":1750, "ypos":0} , "image" : f"{path}LT8609Adapter.PNG", "use_label" : False}, - "LT8609_BOARD_VMAINA_PASSTHRU" : {"position" : {"xpos":1750, "ypos":0} , "image" : f"{path}LT8609Adapter.PNG", "use_label" : False}, - "LT3390_1_ADAPTER" : {"position" : {"xpos":1750, "ypos":0} , "image" : f"{path}LT3390-1_Adapter.PNG", "use_label" : False}, - "LT3390_3_ADAPTER" : {"position" : {"xpos":1750, "ypos":0} , "image" : f"{path}LT3390-1_Adapter.PNG", "use_label" : False}, - "LT3390_5_ADAPTER" : {"position" : {"xpos":1750, "ypos":0} , "image" : f"{path}LT3390-1_Adapter.PNG", "use_label" : False}, - "CAP_MASTER_BLASTER_0K_MB" : {"position" : {"xpos":1500, "ypos":0} , "image" : f"{path}CapBoard.PNG", "use_label" : False}, - "CAP_MASTER_BLASTER_2K_MB" : {"position" : {"xpos":1500, "ypos":0} , "image" : f"{path}CapBoard.PNG", "use_label" : False}, - "MEASURE_BUS_ISO_2K" : {"position" : {"xpos":1500, "ypos":0} , "image" : f"{path}CapBoard.PNG", "use_label" : False}, - ##################################################### - # # - # Specific Loads BK8500 # - # # - ##################################################### - "BK8500_ILOAD0" : {"position" : {"xpos":-100, "ypos":-900} , "image" : f"{path}BK8500.PNG", "use_label" : False}, - "BK8500_ILOAD1" : {"position" : {"xpos":-100, "ypos":-1200} , "image" : f"{path}BK8500.PNG", "use_label" : False}, - "BK8500_ILOAD2" : {"position" : {"xpos":-100, "ypos":-1500} , "image" : f"{path}BK8500.PNG", "use_label" : False}, - ##################################################### - # # - # Generic Loads # - # # - ##################################################### - "GENERIC_ILOAD0" : {"position" : {"xpos":400, "ypos":-500} , "image" : f"{path}Missing.PNG", "use_label" : False}, - "GENERIC_ILOAD1" : {"position" : {"xpos":400, "ypos":-700} , "image" : f"{path}Missing.PNG", "use_label" : False}, - "GENERIC_ILOAD2" : {"position" : {"xpos":400, "ypos":-900} , "image" : f"{path}Missing.PNG", "use_label" : False}, - "GENERIC_ILOAD3" : {"position" : {"xpos":400, "ypos":-1100} , "image" : f"{path}Missing.PNG", "use_label" : False}, - "GENERIC_ILOAD4" : {"position" : {"xpos":400, "ypos":-1300} , "image" : f"{path}Missing.PNG", "use_label" : False}, - "single_channel_electronic_load" : {"position" : {"xpos":400, "ypos":-1500} , "image" : f"{path}Missing.PNG", "use_label" : False}, - ##################################################### - # # - # Specific Loads HTX9000 # - # # - ##################################################### - "HTX9000_ILOAD0" : {"position" : {"xpos":900, "ypos":-500} , "image" : f"{path}HTX9000.PNG", "use_label" : False}, - "HTX9000_ILOAD1" : {"position" : {"xpos":900, "ypos":-700} , "image" : f"{path}HTX9000.PNG", "use_label" : False}, - "HTX9000_ILOAD2" : {"position" : {"xpos":900, "ypos":-900} , "image" : f"{path}HTX9000.PNG", "use_label" : False}, - "HTX9000_ILOAD3" : {"position" : {"xpos":900, "ypos":-1100} , "image" : f"{path}HTX9000.PNG", "use_label" : False}, - "HTX9000_ILOAD4" : {"position" : {"xpos":900, "ypos":-1300} , "image" : f"{path}HTX9000.PNG", "use_label" : False}, - "HTX9000_POWER1" : {"position" : {"xpos":900, "ypos":-1500} , "image" : f"{path}HTX9000.PNG", "use_label" : False}, - } - ''' - pass \ No newline at end of file diff --git a/PyICe/refid_modules/plugin_module/die_traceability_plugin/die_traceability.py b/PyICe/refid_modules/plugin_module/die_traceability_plugin/die_traceability.py deleted file mode 100644 index 7a93744..0000000 --- a/PyICe/refid_modules/plugin_module/die_traceability_plugin/die_traceability.py +++ /dev/null @@ -1,433 +0,0 @@ -from PyICe.refid_modules import bench_identifier -from PyICe import lab_core, virtual_instruments -from PyICe.lab_utils.sqlite_data import sqlite_data -try: - from PyICe.data_utils import stdf_utils -except ImportError as e: - print('STDF processing library unavailable. Expected on Linux VDI.') -import collections -import functools -import itertools -import importlib -import hashlib -import sqlite3 -import time -import abc -import os -import re - -class byte_ord_dict(collections.OrderedDict): - '''Ordered dictionary for channel results reporting with pretty print addition.''' - def __str__(self): - s = '' - max_channel_name_length = 0 - for k,v in self.items(): - max_channel_name_length = max(max_channel_name_length, len(k)) - # s += f'{k}:\t{v[0]:02X}\n' - s += f'{k}:\t{v:02X}\n' - s = s.expandtabs(max_channel_name_length+2).replace(' ','.') - return s - -class SQLiteDBError(Exception): - '''parent exception for problems reading from logged sqlite data.''' -class MissingColumnsError(SQLiteDBError): - '''die traceablilty columns weren't logged''' -class EmptyTableError(SQLiteDBError): - '''sqlite table has no rows from which to extract die traceability''' -class InvalidDataError(SQLiteDBError): - '''sqlite table has no rows from which to extract die traceability''' - -class die_traceability(abc.ABC): - '''Shared infrastructure to compute identification values from I2C, SQLite, or STDF''' - - @classmethod - def read_registers_i2c(cls, register_list, channels, powerup_fn=None, powerdown_fn=None): - '''read relevant data directly from DUT. Assumes "channels" PyICe master is properly configured and DUT is powered and able to talk I2C. - powerup_fn and powerdown_fn should take zero arguments. They can be uses as an alternative method to get Stowe talking I2C in other apps/configs.''' - if powerup_fn is None: - try: - cls.powerup_fn(channels) - except: - print('Please define a powerup_fn in your project specific die_traceability script to read the registers.') - breakpoint() - else: - powerup_fn(channels) - results = byte_ord_dict() - for ch in register_list: - if ch in cls.zero_intercept_registers: ### This isn't good. Why are we assuming there is an attribute called zero_intercept_registers? - v = 0 - else: - v = channels.read(ch) - # results[ch] = (bytes((v,))) - results[ch] = v - if powerdown_fn is None: - try: - cls.powerdown_fn(channels) - except: - print('Please define a powerdown_fn in your project specific die_traceability script to put the dut to rest.') - breakpoint() - else: - powerdown_fn(channels) - return results - - @classmethod - def read_registers_sqlite(cls, register_list, db_file, db_table): - '''pull relevant register data from first row of sqlite database file/table''' - conn = sqlite3.connect(db_file, detect_types=sqlite3.PARSE_DECLTYPES|sqlite3.PARSE_COLNAMES) #automatically convert datetime column to Python datetime object - conn.row_factory = sqlite3.Row #index row data tuple by column name - legacy_register_list = [f"0 as {r}" if r in cls.zero_intercept_registers else r for r in register_list] - column_query_str = ', '.join(legacy_register_list) - try: - row = conn.execute(f"SELECT {column_query_str} FROM {db_table}").fetchone() - except sqlite3.OperationalError as e: - if len(e.args) and e.args[0] == 'no such column: f_variant': - legacy_register_list = [f"0 as {r}" if r in cls.zero_intercept_registers or r in ('f_variant',) else r for r in register_list] - column_query_str = ', '.join(legacy_register_list) - row = conn.execute(f"SELECT {column_query_str} FROM {db_table}").fetchone() - elif len(e.args) and e.args[0].startswith('no such column: ') and input('Logger DB table is missing die traceability register(s). Proceed anyway? ').lower.startswith('y'): - return byte_ord_dict() - else: - raise e - # assert row is not None #Empty table! - if row is None: - raise EmptyTableError() - results = byte_ord_dict() - for k in row.keys(): - # results[k] = (bytes((row[k],))) - results[k] = row[k] - if results[k] is None: - raise InvalidDataError() - return results - @classmethod - def read_from_dict(cls, register_list, data_dict): - '''filter and reorder dict data. data_dict must contain superset of required data.''' - results = byte_ord_dict() - for k in register_list: - if k in cls.zero_intercept_registers: - results[k] = 0 - else: - results[k] = data_dict[k] - return results - @classmethod - def read_registers_stdf(cls, test_map, device_number, stdf_reader): - '''read device information from PyICe.data_utils.stdf_utils.stdf_utils record''' - assert isinstance(stdf_reader, stdf_utils.stdf_reader) - return {tname: stdf_reader.get_value(devnum=device_number, testnum=tnum) for tname,tnum in test_map.items()} - @classmethod - def compute_hash_stdf(cls, device_number, stdf_reader): - '''read die traceability hash from PyICe.data_utils.stdf_utils.stdf_utils record - a cls.traceability_test_map dictionary is required.''' - # This method is only compatible with REVID7+ DUTS which have undergone wafer sort. - # Blind built units and units wich predate the correction of the signed x,y loc register mis-allocation won't work correctly. - # Legacy unit calculation is possible, but much more complicated, and removed here for the sake of simplicity. Legacy calculation is intact in stowe_eval.stowe_eval_base.modules.stdf_utils. - - record = cls.read_registers_stdf(cls.traceability_test_map, device_number, stdf_reader) - record = {k: int(v) for k,v in record.items()} - record['f_die_running_7_0_'] = record['f_die_running_7_0_'] & 0xFF - record['f_die_running_9_8_'] = (record['f_die_running_9_8_'] >> 8) & 0x03 - dut_data = cls.read_from_dict(cls.traceability_registers, record) - return f'0x{cls.compute_hash(dut_data).hex().upper()}' - @classmethod - def compute_hash(cls, register_data): - h = hashlib.blake2b(digest_size=4) - assert isinstance(register_data, byte_ord_dict) - for ch in register_data: - # Cast possibly signed data into single unsigned byte - # x and y die coordinates are now signed bytes. - try: - # 0-255 first try - byte_data = register_data[ch].to_bytes(length=1, byteorder='big', signed=False) - except OverflowError as e: - # Something went wrong. Subset of possible outcomes: - # int too big to convert - # can't convert negative int to unsigned - # Without checking too carefully, let's just see if it fits into a signed byte instead. No loss of information here. - byte_data = register_data[ch].to_bytes(length=1, byteorder='big', signed=True) - # If this one doesn't take, let it crash. Not sure what else to do. Incoming data isn't supposed to be bigger than a byte by the time it gets here. - h.update(byte_data) - return h.digest() - @classmethod - def compute_unconfigured_hash(cls, revid): - unconfigured_data = cls.read_from_dict(register_list=cls.nvm_registers, data_dict = {k:0 for k in cls.nvm_registers}) - unconfigured_data['revid'] = revid - return cls.compute_hash(register_data=unconfigured_data) - @classmethod - def compute_untraceable_hash(cls, revid): - unconfigured_data = cls.read_from_dict(register_list=cls.traceability_registers, data_dict = {k:0 for k in cls.traceability_registers}) - unconfigured_data['revid'] = revid - return cls.compute_hash(register_data=unconfigured_data) - @classmethod - def compare_nvm_hash(cls, data1, data2): - '''this is dumb. Why not compare the registers directly? No need to hash!''' - data1_ord_filt = cls.read_from_dict(register_list=cls.nvm_registers, data_dict=data1) - data2_ord_filt = cls.read_from_dict(register_list=cls.nvm_registers, data_dict=data2) - unconfigured_data1 = cls.compute_unconfigured_hash(revid=data1['revid']) - unconfigured_data2 = cls.compute_unconfigured_hash(revid=data2['revid']) - data1_hash = cls.compute_hash(data1_ord_filt) - data2_hash = cls.compute_hash(data2_ord_filt) - assert data1_hash != unconfigured_data1 - assert data2_hash != unconfigured_data2 - return data1_hash == data2_hash - @classmethod - def compare_traceability_hash(cls, data1, data2): - '''this is dumb. Why not compare the registers directly? No need to hash!''' - data1_ord_filt = cls.read_from_dict(register_list=cls.traceability_registers, data_dict=data1) - data2_ord_filt = cls.read_from_dict(register_list=cls.traceability_registers, data_dict=data2) - unconfigured_data1 = cls.compute_untraceable_hash(revid=data1['revid']) - unconfigured_data2 = cls.compute_untraceable_hash(revid=data2['revid']) - data1_hash = cls.compute_hash(data1_ord_filt) - data2_hash = cls.compute_hash(data2_ord_filt) - assert data1_hash != unconfigured_data1 - assert data2_hash != unconfigured_data2 - return data1_hash == data2_hash - @classmethod - def compare_nvm(cls, data1, data2): - data1_ord_filt = cls.read_from_dict(register_list=cls.nvm_registers, data_dict=data1) - data2_ord_filt = cls.read_from_dict(register_list=cls.nvm_registers, data_dict=data2) - unconfigured_data1 = cls.read_from_dict(register_list=cls.nvm_registers, data_dict = {k:0 for k in cls.nvm_registers}) - unconfigured_data2 = cls.read_from_dict(register_list=cls.nvm_registers, data_dict = {k:0 for k in cls.nvm_registers}) - unconfigured_data1['revid'] = data1['revid'] - unconfigured_data2['revid'] = data2['revid'] - assert data1_ord_filt != unconfigured_data1 - assert data2_ord_filt != unconfigured_data2 - return data1_ord_filt == data2_ord_filt - @classmethod - def compare_traceability(cls, data1, data2): - data1_ord_filt = cls.read_from_dict(register_list=cls.traceability_registers, data_dict=data1) - data2_ord_filt = cls.read_from_dict(register_list=cls.traceability_registers, data_dict=data2) - unconfigured_data1 = cls.read_from_dict(register_list=cls.traceability_registers, data_dict = {k:0 for k in cls.traceability_registers}) - unconfigured_data2 = cls.read_from_dict(register_list=cls.traceability_registers, data_dict = {k:0 for k in cls.traceability_registers}) - unconfigured_data1['revid'] = data1['revid'] - unconfigured_data2['revid'] = data2['revid'] - return data1_ord_filt == data2_ord_filt and data1_ord_filt != unconfigured_data1 and data2_ord_filt != unconfigured_data2 - @classmethod - def compare_dut_database(cls, channels, db_file, db_table, die_only=True, powerup_fn=None, powerdown_fn=None): - '''just _die_ registers, not trim registers''' - if die_only: - register_list = cls.traceability_registers - else: - register_list = cls.nvm_registers - i2c_data = cls.read_registers_i2c(register_list=register_list, channels=channels, powerup_fn=powerup_fn, powerdown_fn=powerdown_fn) - db_data = cls.read_registers_sqlite(register_list=register_list, db_file=db_file, db_table=db_table) - return i2c_data == db_data - @classmethod - def compare_dut_database(cls, db_file1, db_table1, db_file2, db_table2, die_only=True): - '''just _die_ registers, not trim registers''' - if die_only: - register_list = cls.traceability_registers - else: - register_list = cls.nvm_registers - db_data1 = cls.read_registers_sqlite(register_list=register_list, db_file=db_file1, db_table=db_table1) - db_data2 = cls.read_registers_sqlite(register_list=register_list, db_file=db_file2, db_table=db_table2) - return db_data1 == db_data2 - @classmethod - def replace_die_traceability_channels(cls, channels, die_only=False, powerup_fn=None, powerdown_fn=None): - if die_only: - register_list = cls.traceability_registers - else: - #register_list = cls.nvm_registers - register_list = cls.nvm_derived_registers - register_data = cls.read_registers_i2c(register_list = register_list, - channels = channels, - powerup_fn=powerup_fn, - powerdown_fn=powerdown_fn, - ) - # c_g_dummy = lab_core.channel_group('die_traceability dummy channels') - c_g_dummy = virtual_instruments.dummy_quantum_twin('die_traceability dummy channels') - c_g_orig = lab_core.channel_group('die_traceability original channels') - for (bf_name, bf_value) in register_data.items(): - print(f'NOTICE: Replacing {bf_name} register channel with dummy channel.') - try: - c_g_orig._add_channel(channels[bf_name]) - channels.remove_channel_by_name(bf_name) - # ch = lab_core.channel(name=bf_name, read_function=None, write_function=None) - c_g_dummy.add_channel(live_channel=c_g_orig[bf_name], skip_read=True, cached_value=bf_value) - except lab_core.ChannelAccessException as e: - if bf_name in cls.zero_intercept_registers: - # Deleted in future Yoda - pass - else: - raise e - channels.add(c_g_dummy) - return {'dummy_replacements': c_g_dummy, - 'originals' : c_g_orig, - } - @classmethod - def compute_variant(cls, data): - # REVID omitted. Might a Si-spin be related to variant programming? - - data_ord_filt = cls.read_from_dict(register_list=cls.variant_regs, data_dict=data) - # Debug to see why nothing matches - # foo = {k: LT3390.LT3390[k.upper()]==v for k,v in data_ord_filt.items()} - # bar = list(itertools.filterfalse(lambda kv: kv[1], foo.items())) - # It's broken enums: (Pdb) !bar [('f_x_ldo_mode_ch3', False), ('f_x_ldo_mode_ch4', False), ('f_mon_ov_en_ch3', False)] - # is_LT3390 = functools.reduce(lambda a,b: a and b, foo.values()) - # print(is_LT3390) - matches = {} - for variant in cls.variants: - reg_matches = {k: variant[k.upper()]==v for k,v in data_ord_filt.items()} # Why did the model change bf names to all uppecase? - is_variant = functools.reduce(lambda a,b: a and b, reg_matches.values()) - why_not = list(itertools.filterfalse(lambda kv: kv[1], reg_matches.items())) # Debug. Not matching anything because of f_x_ldo_mode_chx bogus enums now. - matches[variant['NAME']] = { - 'is_variant': is_variant, - 'reg_matches': reg_matches, - 'why_not': why_not, - } - if not len([m for (m,findings) in matches.items() if findings['is_variant']]): - for variant in cls.variants: - print(f'{variant["NAME"]}: (model vs DUT)') - for (mismatch, is_mismatched) in matches[variant['NAME']]['why_not']: - print(f'\t{mismatch}: {variant[mismatch.upper()]} vs. {data_ord_filt[mismatch]}') - return matches - # Todo: what if these aren't always unique? - # Todo: what if not all variant registers are present in the record? - # Todo: try this against stdf data record instead, matching just on f_die_ registers? - @classmethod - def get_ATE_config(cls, data, given_trace_hash=None): - if given_trace_hash is None: - traceability_data = cls.read_from_dict(register_list=cls.traceability_registers, - data_dict=data, # Should be superset of required data - ) - traceability_hash = cls.compute_hash(register_data=traceability_data) - traceability_hash_table = f'0x{traceability_hash.hex().upper()}' #String version - else: - traceability_hash_table = given_trace_hash - query_str = f'''SELECT START_T, STDF_file, SETUP_ID as config_file, ROM_COD as config_change, ENG_ID as config_date, SPEC_NAM as variant_datasheet, PART_TYP as variant_shell FROM population_data WHERE TRACEABILITY_HASH == '{traceability_hash_table}' AND CAST(TST_TEMP AS NUMERIC) == 25''' - try: - db = sqlite_data(database_file=cls.correlation_db_filename) - row = db.query(query_str).fetchone() - if row is None: - print(f'WARNING: ATE data not found for DUT {traceability_hash_table}.') - return None - elif None in row: - # Legacy data before ATE program stored values into previously unused fields. - print(f'WARNING: (Legacy) ATE found for DUT {traceability_hash_table}, but missing configuration traceability fields.') - return None - return {k: row[k] for k in row.keys()} - except sqlite3.OperationalError as e: - # OperationalError('no such table: 0xC6A857A5') - table_pat = re.compile(r'^no such table: (?P\w+)$', re.MULTILINE) - # OperationalError('no such column: SETUP_ID3') - column_pat = re.compile(r'^no such column: (?P\w+)$', re.MULTILINE) - if table_pat.match(str(e)): - # This shouldn't happen when looking in population_data table instead of individual DUT table. - print(f'WARNING: ATE data not found for DUT {traceability_hash_table}.') - elif column_pat.match(str(e)): - print(f'ERROR: Missing columns from ATE data for DUT {traceability_hash_table}. This should never happen. Please send details to PyICe Support at PyICe-developers@analog.com for investigation.') - raise # Don't need to crash here, but would really like to know if this system isn't working as designed. - else: - # What happened??? - print(f'ERROR: Unknown problem with ATE config traceability data for DUT {traceability_hash_table}. This should never happen. Please send details to PyICe Support at PyICe-developers@analog.com for investigation.') - print(type(e), e) - raise - return None - @classmethod - def get_ATE_variant(cls, data): - try: - cfg_file = cls.get_ATE_config(data)['config_file'] - except TypeError as e: - # missing ATE data - return None - ate_config = os.path.basename(cfg_file).rstrip(' $') - #todo what about non-unique # versioning if moved/renamed. Change number? - return ate_config - -if __name__ == '__main__': - import sys - from stowe_eval.stowe_eval_base.modules import test_module #not used, but fixes import cycle - from stowe_eval.stowe_eval_base.modules import test_results - register_list = stowe_die_traceability.nvm_registers - # register_list = stowe_die_traceability.traceability_registers - if len(sys.argv) == 1: - #no arguments; read from live DUT - with bench_identifier.get_bench_instruments(project_folder_name = 'dummy_project', benchsetup = None)() as bench: #This is increadibly frustrating. If I import a bench from dummy_project, the gui won't have FUSE DATA or FUSE CNTRL, but will if I import from stowe_eval. - channels = bench.get_master() - channels.gui() - # breakpoint() - register_data = stowe_die_traceability.read_registers_i2c(register_list=register_list, - channels=channels, - ) - # channels.write("enable_pin", "AVIN") - # channels.write("vmaina_force", 3.3) - # time.sleep(0.1) - # print(f'msm_state={channels.read("msm_state")}') - # channels.write("enable_pin", "LOW") - # channels.write("vmaina_force", 0) - elif len(sys.argv) == 2: - # filename argument - (script_name, db_filename) = sys.argv[:] - path, file_name = os.path.split(db_filename) - file_base, file_ext = os.path.splitext(file_name) - if file_ext == ".sqlite": - db = sqlite_data(database_file=db_filename) - table_names = db.get_table_names() - print(table_names) - sys.exit(0) - elif file_ext == ".json": - if file_base == "test_results": - trr = test_results.test_results_reload(db_filename) - # print(trr) - register_data = byte_ord_dict({k:trr.get_traceability_info()[k] for k in stowe_die_traceability.traceability_registers}) - elif file_base == "correlation_results": - crr = test_results.correlation_results_reload(db_filename) - # print(crr) - register_data = byte_ord_dict({k:crr.get_traceability_info()[k] for k in stowe_die_traceability.traceability_registers}) - else: - raise Exception(f'Unknown file type: {db_filename}') - else: - raise Exception(f'Unknown file type: {db_filename}') - elif len(sys.argv) == 3: - # filename and database table name argument - (script_name, db_filename, table_name) = sys.argv[:] - register_data = stowe_die_traceability.read_registers_sqlite(register_list=register_list, - db_file=db_filename, - db_table=table_name, - ) - else: - raise Exception('USAGE:' + \ - '\n\tWith no arguments, script will power up default bench and read I2C registers.' + \ - '\n\tWith one argument, script will open the SQLite database file named in argument, print table names, and exit.' + \ - '\n\tWith two arguments, script will open the SQLite database file named in first argument and read from table named in second argument.' + \ - f'\n\t{[arg for arg in sys.argv]}' - ) - # Compute hash over subset of registers specific to die traceability. - # This uniquely (hopefully) identifies a part, even if other NVM registers are changed by test mode - # This will fail if die level traceabiility registers aren't configured properly by ATE. - # Early test programs (limited to 1st Si) didn't do this configuration. - unconfigured_hash = stowe_die_traceability.compute_untraceable_hash(revid=register_data['revid']) - traceability_data = stowe_die_traceability.read_from_dict(register_list=stowe_die_traceability.traceability_registers, - data_dict=register_data, # Should be superset of required data - ) - # Print hash of traceability registers only. Do it early to reduce prominence (and chance of being mistaken for the all-NVM hash - - traceability_hash = stowe_die_traceability.compute_hash(register_data=traceability_data) - print(f"Traceability (only) hash: 0x{traceability_hash.hex().upper()}") - # Print summary of NVM configuration - print() - print(register_data) - nvm_hash = stowe_die_traceability.compute_hash(register_data=register_data) - # Warn if die traceability registers appear empty (last for prominence - this is important!): - if traceability_hash == unconfigured_hash: - print("WARNING! This DUT's die-level traceability registers were improperly programmed (empty).") - try: - # if register_data['f_die_crc_7_5_'] == 0 and \ - # register_data['f_die_crc_4_'] == 0 and \ - # register_data['f_die_crc_3_'] == 0 and \ - # register_data['f_die_crc_2_'] == 0 and \ - # register_data['f_die_crc_1_'] == 0 and \ - # register_data['f_die_crc_0_'] == 0: - if register_data['f_die_crc'] == 0: - print("WARNING! This DUT's die traceability CRC registers were improperly programmed (empty).") - # print(f"NVM (complete) hash: 0x{nvm_hash.hex().upper()}") - matches = stowe_die_traceability.compute_variant(register_data).items() - matches_filt = [m for (m,findings) in matches if findings['is_variant']] - if len(matches_filt) == 1: - print(f'NVM inspection matched variant: {matches_filt[0]}') - else: - for name,variant in matches: - print(f'{name}:\t{variant["is_variant"]}'.expandtabs(10)) # TODO prettier? Necessary? - except KeyError as e: - print("WARNING! This DUT's die traceability CRC missing.") # Not in JSONs?? - print(stowe_die_traceability.get_ATE_config(traceability_data)) - - print(stowe_die_traceability.get_ATE_variant(register_data)) \ No newline at end of file diff --git a/PyICe/refid_modules/plugin_module/die_traceability_plugin/die_traceability_plugin.py b/PyICe/refid_modules/plugin_module/die_traceability_plugin/die_traceability_plugin.py deleted file mode 100644 index b9a16c3..0000000 --- a/PyICe/refid_modules/plugin_module/die_traceability_plugin/die_traceability_plugin.py +++ /dev/null @@ -1,132 +0,0 @@ -123 -from PyICe.refid_modules.plugin_module.plugin import plugin -from types import MappingProxyType -import importlib, os, inspect, sqlite3, sys, abc - -class die_traceability(plugin): - desc = "Creates columns in the datalog to store identifying information of the dut. Right now it's actually reading the traceability hash from within the part, but expect that to be generalized soon." - def __init__(self, test_mod, project_die_traceability): - super().__init__(test_mod) - self.tm.interplugs['die_traceability_begin_collect']=[] - self.tm.interplugs['die_traceability_set_traceability']=[] - self._file_locations(project_die_traceability) - - def __str__(self): - return "Creates columns in the datalog to store identifying information of the dut. Right now it's actually reading the traceability hash from within the part, but expect that to be generalized soon." - - def _file_locations(self,project_die_traceability): - """ - Assigns provided file to an instance attribute. - - Args: - project_die_traceability - project file. - """ - sys.path.append(os.path.dirname(inspect.getsourcefile(type(self.tm)))) - project_file=project_die_traceability - from . import die_traceability as die_tra - self.die_tra=die_tra - self.dt = die_tra.die_traceability - - def _set_atts_(self): - self.att_dict = MappingProxyType({ - 'get_die_traceability_hash_table':self.get_die_traceability_hash_table, - '_get_traceability_info':self._get_traceability_info, - 'establish_traceability':self.establish_traceability, - 'set_traceability':self.set_traceability, - 'initial_check':self.initial_check, - }) - def get_atts(self): - return self.att_dict - - def get_hooks(self): - - plugin_dict={ - 'begin_collect':[self.initial_check], - 'tm_plot_from_table':[self._get_traceability_info], - 'post_collect':[self.set_traceability,self.get_die_traceability_hash_table], - } - return plugin_dict - - def set_interplugs(self): - try: - self.tm.interplugs['register_test__test_from_table'].extend([self.establish_traceability]) - self.tm.interplugs['register_test__compile_test_results'].extend([self.get_die_traceability_hash_table]) - self.tm.interplugs['register_test_tt_compile_test_results'].extend([self.set_traceability]) - except KeyError: ## Not using the optional other plug - pass - try: - self.tm.interplugs['register_correlation_test_get_correlation_data'].extend([self.get_die_traceability_hash_table]) - except KeyError: ## Not using the optional other plug - pass - - def execute_interplugs(self, hook_key, *args, **kwargs): - for (k,v) in self.tm.interplugs.items(): - if k is hook_key: - for f in v: - f(*args, **kwargs) - -#### #### #### - - - def get_db_table_name(self): - """ - Returns the table name used by the script. - - Returns - string - name of table from database. - """ - return self.tm.get_name() - - @abc.abstractmethod - def initial_check(self): - ''' - This will check to make sure that the dut can be identified before the data collection begins. Can also be used to pass identification info into the database. - ''' - pass - - def establish_traceability(self, table_name, db_file): - self.tm._test_results._set_traceability_info(**self._get_traceability_info(table_name=table_name, db_file=db_file)) - self.tm._correlation_results._set_traceability_info(**self._get_traceability_info(table_name=table_name, db_file=db_file)) - - def _get_traceability_info(self, table_name, db_file): - """ - Reads channels assigned in a project specific folder from the given table from the given database. If a table name and database file are not provided, the script will check self.tm's attributes to draw upon. - - Args: - table_name - Name of the table to look for. - db_file - File of the database. - Returns: - Dictionary of the channels listed in the project specific die_traceability file and their values. - """ - resp = {} - ######################### - # Calculate from sqlite # - ######################### - if table_name is None: - table_name = self.tm.get_db_table_name() - if db_file is None: - db_file=self.tm._db_file - - die_data = self.dt.read_registers_sqlite( register_list = self.dt.traceability_registers, - db_file = db_file, - db_table = table_name - ) - for k,v in die_data.items(): - resp[k] = v - return resp - - def traceability_json_addon(self, res_dict): - """ - Add traceability info to the produced json. - """ - trace_data = self.get_traceability_info() - res_dict['collection_date'] = trace_data['datetime'] - res_dict['traceability'] = {k:v for k,v in trace_data.items() if k not in ['datetime']} - - def set_traceability(self,test=None): - self._set_traceability(test=test) - self.execute_interplugs('die_traceability_set_traceability', test) - - @abc.abstractmethod - def _set_traceability(self,test): - pass \ No newline at end of file diff --git a/PyICe/refid_modules/plugin_module/p4_traceability_plugin/p4_traceability_plugin.py b/PyICe/refid_modules/plugin_module/p4_traceability_plugin/p4_traceability_plugin.py deleted file mode 100644 index 880ecaf..0000000 --- a/PyICe/refid_modules/plugin_module/p4_traceability_plugin/p4_traceability_plugin.py +++ /dev/null @@ -1,352 +0,0 @@ -from PyICe.refid_modules.plugin_module.plugin import plugin -from PyICe.lab_utils.banners import print_banner -from types import MappingProxyType -import inspect -import collections -import re -import subprocess -import sys - -class results_dict(collections.OrderedDict): - '''Ordered dictionary with pretty print addition.''' - def __str__(self): - s = '' - max_key_name_length = 0 - for k,v in self.items(): - max_key_name_length = max(max_key_name_length, len(k)) - s += '{}:\t{}\n'.format(k,v) - s = s.expandtabs(max_key_name_length+2) - return s - -fstat_pat = re.compile(r'^\.\.\. (?P\w+) (?P.*?)\s*?$', re.MULTILINE) -edit_pat = re.compile(r'^\.\.\. (?P\w+) (?P.*?)\s*?$', re.MULTILINE) -client_pat = re.compile(r'^\.\.\. (?P\w+) (?P.*?)\s*?$', re.MULTILINE) -describe_zpat = re.compile(r'^\.\.\. (?P\w+) (?P.*?)\s*?$', re.MULTILINE) -class p4_traceability_plugin(plugin): - def __init__(self, test_mod): - super().__init__(test_mod) - from .p4_traceability_plugin import results_dict - - def __str__(self): - return "Adds columns to the database that records revision info on the test script and allows for checking out files before editting them via the script." - - def _set_atts_(self): - self.att_dict = MappingProxyType({ - 'DB_unversioned_check':self.DB_unversioned_check, - 'add_p4_traceability_channels':self.add_p4_traceability_channels, - 'get_client_info':self.get_client_info, - 'get_describe':self.get_describe, - 'p4_edit':self.p4_edit, - }) - def get_atts(self): - return self.att_dict - - def get_hooks(self): - plugin_dict={ - 'tm_set':[self._set_variables], - 'tm_logger_setup': [self.DB_unversioned_check, self.add_p4_traceability_channels, self.add_bench_p4_traceability_channels], - 'tm_plot':[self._p4_plot_armor], - } - return plugin_dict - def set_interplugs(self): - pass - def execute_interplugs(self, hook_key, *args, **kwargs): - for (k,v) in self.tm.interplugs.items(): - if k is hook_key: - for f in v: - f(*args, **kwargs) - - def _p4_plot_armor(self, *args, **kwargs): - """ - Makes a copy of the script's plot method, then replaces it with the plugin's own, which is just the script's plot method but with a decorator. - """ - self.tm.regular_plot = self.tm.plot - bound_method = p4_traceability_plugin.plot.__get__(self.tm, self.tm.__class__) ## Hokay, this takes an unbound version of the plugin method and binds a particular instance to the test_module. - setattr(self.tm,'plot',bound_method) - - def _set_variables(self, *args, **kwargs): - """ - Simply cookies to keep repeat calls to function under control. - """ - self.auto_checkout=False - self.already_asked=False - self.tm.tt._once_is_enough=True - - def add_p4_traceability_channels(self, logger): - """ - Adds the perforce information of the script to the logger. - - Args: - logger - The logger that will get the perforce channels. - """ - ch_cat = 'eval_traceability' - module_file = inspect.getsourcefile(type(self.tm)) - fileinfo = self.get_fstat(module_file) - if fileinfo['depotFile'] is None: - print_banner(f"{self.tm.get_name()} is not checked in.") - for property in fileinfo: - logger.add_channel_dummy(f'test_{property}').write(fileinfo[property]) - logger[f'test_{property}'].set_category(ch_cat) - logger[f'test_{property}'].set_write_access(False) - if type(self.tm)._archive_enabled == False: #Class variable - # Don't nag. Asked and answered. - pass - elif not self.tm._debug and fileinfo['depotFile'] is None: - print_banner(f"Test module unversioned: {self.tm.get_name()}") - resp = input('You will not be able to archive results. Press "y" to continue: ') - if resp.lower() != 'y': - raise Exception('Unversioned test module.') - else: - type(self.tm)._archive_enabled = False # Added strangely late?! 2020/09/01 DJS. - elif not self.tm._debug and fileinfo['action'] is not None: - print_banner("*** WARNING ***", f"Test module uncommitted changes: {self.tm.get_name()}") - resp = input('You will not be able to archive results. Press "y" to continue: ') - if resp.lower() != 'y': - raise Exception('Uncommitted test module working copy.') - else: - type(self)._archive_enabled = False - try: - for child_module in self.tm._multitest_units: - multitest_module_file = inspect.getsourcefile(type(child_module)) - fileinfo = self.get_fstat(multitest_module_file) - if fileinfo['depotFile'] is None: - print_banner(f"{child_module.get_name()} is not checked in.") - # print(fileinfo) - # Todo: Actual checks and datalogging???? - except AttributeError as e: - # Not a multitest parent module - pass - - def add_bench_p4_traceability_channels(self, logger): - """ - Adds the perforce information about the bench file to the logger. - - Args: - logger - The logger that will get the perforce channels. - """ - ch_cat = 'eval_traceability' - module_file = inspect.getsourcefile(self.tm.get_lab_bench().get_bench_file()) - fileinfo = self.get_fstat(module_file) - module_name=module_file[-1*module_file[::-1].index('\\'):] - for property in fileinfo: - logger.add_channel_dummy(f'bench_{property}').write(fileinfo[property]) - logger[f'bench_{property}'].set_category(ch_cat) - logger[f'bench_{property}'].set_write_access(False) - if fileinfo['depotFile'] is None: - print_banner(f"Lab bench {module_name} unversioned.") - resp = input('Press "y" to continue: ') - if resp.lower() != 'y': - raise Exception('Unversioned bench module.') - elif fileinfo['action'] is not None and self.tm.tt._once_is_enough: - print_banner(f"WARNING: Lab bench {module_name} has uncommitted changes.") - resp = input('Press "y" to continue: ') - if resp.lower() != 'y': - raise Exception('Uncommitted bench module working copy.') - self.tm.tt._once_is_enough = False - - def DB_unversioned_check(self, logger): - """ - Raises an error if a database is checked in. - """ - log_info = self.get_fstat(logger.get_database()) - if log_info['depotFile'] is not None and log_info['action'] is None and log_info['headAction'] not in ['delete','move/delete']: - # TODO: Ask to p4 check out - raise Exception(f'DB File is checked in!!! ({logger.get_database()})') - - def perforce_checkout_check(func): - """ - Decorator used on a script's plot function to allow for a perforce checkout if a plot is attempting to be rewritten. - - Args: - func - The plot function created in the plugin. - Returns: - The plots generated by the script. - """ - def wrapper(self, database, table_name, plot_filepath, skip_output=False): - try: - plts = func(self, database, table_name, plot_filepath, skip_output) ### Houston, we have a problem with those that fell behind. - return plts - except PermissionError as e: - if table_name is not None and database is not None: - #re-plot. Files might be checked in. - if not self.auto_checkout and not self.already_asked: - self.auto_checkout = input(f'{e.filename} is not writeable. Attempt p4 checkout of this and all files that follow? [y/n]: ').lower() in ['y', 'yes'] - self.already_asked=True - if not self.auto_checkout: - do_it = input(f'{e.filename} is not writeable. Attempt p4 checkout of just this file? [y/n]: ').lower() in ['y', 'yes'] - if self.auto_checkout or do_it: - if self.p4_edit(e.filename): - plts = self.plot(database, table_name, plot_filepath, skip_output) - return plts - else: - raise - else: - raise - else: - raise - return wrapper - @perforce_checkout_check - def plot(self, database=None, table_name=None, plot_filepath=None, skip_output=False): - """ - This plot replaces the script's plot method. It has a decorator that will allow a user to perforce checkout a file (plot) being modified instead of crashing immediately. - """ - try: - plts = self.regular_plot(database, table_name, plot_filepath, skip_output) - except TypeError as e: - print(f'WARNING: Test {self.get_name()} plot method does not support skip_output argument.') - plts = self.regular_plot(database, table_name, plot_filepath) - return plts - - - def get_fstat(self, local_file): - """ - Returns perforce infomation of a given file. - - Args: - local_file - String or binary. Name of the local file to be read. - Returns: - Dictionary of fstat fields and values. - """ - # TODO: Consider switching to Helix Python API? https://www.perforce.com/downloads/helix-core-api-python - p4proc = subprocess.run(["p4", "fstat", local_file], capture_output=True) - fstat_fields = {'depotFile': None, - 'clientFile': None, - 'headAction': None, - 'headChange': None, - 'headRev': None, - 'haveRev': None, - 'action': None, - 'diff': None, - 'swarmLink': None, - } - if p4proc.returncode: - pass - # print(f"Perforce fstat for {local_file} returned: {p4proc.returncode}!") ## CHECK WITH DAVE! RHM 4/20/2023 - elif p4proc.stderr.decode(sys.stdout.encoding).strip() == f'{local_file} - no such file(s).': - pass #not versioned! - elif p4proc.stderr.decode(sys.stdout.encoding).strip() == f'{local_file} - file(s) not in client view.': - pass #not versioned! - elif p4proc.stderr != b'': - print(f"Perforce fstat sent: {p4proc.stderr.decode(sys.stdout.encoding)} to stderr!") - #Match stowe_eval.html - no such file(s)? - else: - for match in fstat_pat.finditer(p4proc.stdout.decode(sys.stdout.encoding)): - if match.group('f_name') in fstat_fields: - fstat_fields[match.group('f_name')] = match.group('f_val') - if fstat_fields['action'] == 'edit': - diffproc = subprocess.run(['p4', 'diff', local_file], capture_output=True, check=True) - fstat_fields['diff'] = diffproc.stdout.decode(sys.stdout.encoding) - if fstat_fields['depotFile'] is not None: - swarm_prefix = 'https://swarm.adsdesign.analog.com/files/' - swarm_fpath = fstat_fields['depotFile'][2:] #remove '//' - swarm_revision = f"?v={fstat_fields['haveRev']}" if fstat_fields['haveRev'] is not None else '' - fstat_fields['swarmLink'] = f'{swarm_prefix}{swarm_fpath}{swarm_revision}' - return fstat_fields - - def p4_edit(self,filename): - """ - Checks out a file from perforce for editting. - - Args: - filename - String. The name of the local file to be checked out. - Returns: - Boolean. True if checkout was successful, else False. - """ - p4proc = subprocess.run(["p4", "-ztag", "edit", filename], capture_output=True) - edit_fields = results_dict({'depotFile': None, - 'clientFile': None, - 'workRev': None, - 'action': None, - 'type': None, - }) - if p4proc.returncode: - print(f"Perforce client returned: {p4proc.returncode}!") - else: - for match in client_pat.finditer(p4proc.stdout.decode(sys.stdout.encoding)): - if match.group('f_name') in edit_fields: - edit_fields[match.group('f_name')] = match.group('f_val') - if edit_fields['action'] == 'edit': - return True - else: - print(f'Checkout of {filename} failed.') - print(edit_fields) - return False - - def get_client_info(self): - """ - Returns a dictionary containing information on the current perforce workspace. - - Returns: - A dictionary containing information on the current perforce workspace. - """ - p4proc = subprocess.run(["p4", "-ztag", "client", "-o"], capture_output=True) - client_fields = results_dict({'Client': None, - 'Owner': None, - 'Description': None, - 'Root': None, - 'Options': None, - }) - if p4proc.returncode: - print(f"Perforce client returned: {p4proc.returncode}!") - else: - for match in client_pat.finditer(p4proc.stdout.decode(sys.stdout.encoding)): - if match.group('f_name') in client_fields: - client_fields[match.group('f_name')] = match.group('f_val') - return client_fields - - def get_describe(self,change_number): - """ - Returns a dictionary containing information regarding a particualr change in perforce. - - Args: - change_number - Int. The perforce ID number of the submission in question. - Returns: - A dictionary with info pertaining to a submission in perforce. E.g. user, client, and time. - """ - # TODO: Consider switching to Helix Python API? https://www.perforce.com/downloads/helix-core-api-python - p4proc = subprocess.run(["p4", "-ztag", "describe", f"{change_number}"], capture_output=True) - describe_fields = {'change': None, - 'user': None, - 'client': None, - 'time': None, - 'desc': None, - 'status': None, - 'changeType': None, - 'path': None, - 'depotFile0': None, - 'action0': None, - 'type0': None, - 'rev0': None, - 'fileSize0': None, - 'digest0': None, - } - # ... change 1656161 - # ... user dsimmons - # ... client stowe_eval--dsimmons--DSIMMONS-L01 - # ... time 1613009815 - # ... desc mistake with TST_TEMP datatype. According to official spec, this should be a character string. COnverted back to number layer by Python/SQLite. - - # ... status submitted - # ... changeType public - # ... path //adi/stowe/evaluation/TRUNK/modules/* - # ... depotFile0 //adi/stowe/evaluation/TRUNK/modules/stdf_utils.py - # ... action0 edit - # ... type0 text - # ... rev0 14 - # ... fileSize0 33359 - # ... digest0 15AD23C1FD1BF6F4FE5669838FD94449 - - if p4proc.returncode: - print(f"Perforce describe returned: {p4proc.returncode}!") - # elif p4proc.stderr.decode(sys.stdout.encoding).strip() == f'{local_file} - no such file(s).': - # pass #not versioned! - # elif p4proc.stderr.decode(sys.stdout.encoding).strip() == f'{local_file} - file(s) not in client view.': - # pass #not versioned! - elif p4proc.stderr != b'': - print(f"Perforce describe sent: {p4proc.stderr.decode(sys.stdout.encoding)} to stderr!") - #Match stowe_eval.html - no such file(s)? - else: - for match in fstat_pat.finditer(p4proc.stdout.decode(sys.stdout.encoding)): - if match.group('f_name') in describe_fields: - describe_fields[match.group('f_name')] = match.group('f_val') - return describe_fields \ No newline at end of file diff --git a/PyICe/refid_modules/plugin_module/plugin.py b/PyICe/refid_modules/plugin_module/plugin.py deleted file mode 100644 index da30c7f..0000000 --- a/PyICe/refid_modules/plugin_module/plugin.py +++ /dev/null @@ -1,67 +0,0 @@ -import abc - -class plugin(abc.ABC): - def __init__(self, test_mod, **kwargs): - self.tm = test_mod - if not hasattr(self.tm, 'interplugs'): - self.tm.interplugs={} - self._set_atts_() - - @abc.abstractmethod - def get_atts(self): - ''' - This will add attributes to the test module so it will be able to call certain values without invoking the plugins in the future. - - att_dict = { - 'attribute_name':self.corresponding_method, - 'same_deal':self.pattern_recognition, - ... - } - return att_dict - ''' - - @abc.abstractmethod - def __str__(self): - """Provide a description of the plugin""" - - @abc.abstractmethod - def get_hooks(self): - '''Here you assign what methods you want to include in your temptroller or test_module in the style of - - plugin_dict={ - "where these methods go":[method1, method2], - "where other methods go": [method3, method4], - ... - } - return plugin_dict - - the list of possible locations for the temptroller and test_module is as follows: - 'begin_collect' - 'tm_add_attr' - 'tm_logger_setup' - 'tm_set' - 'tm_plot' - 'pre_collect' - 'post_collect' - 'post_repeatability' - 'tm_plot_from_table' - 'begin_archive' - ''' - - @abc.abstractmethod - def set_interplugs(self): - ''' - If you have another plug and you know the name of one of their interplug spots, you can add in functions here in the style of - - self.tm.interplugs[example_other_plugin_spot] = [self.example, self.functions, self.to, self.add] - self.tm.interplugs[_its__another_plugin_spot] = [self.even, self.more, self.stuff] - ''' - - def execute_interplugs(self, hook_key, *args, **kwargs): - ''' And here you go through all the listed interplugs from all plugins and see if anything in there matters to you. In the style of ''' - - for (k,v) in self.tm.interplugs.items(): - if k is hook_key: - for f in v: - f(*args, **kwargs) - diff --git a/PyICe/refid_modules/plugin_module/template_check_plugin/template_check_plugin.py b/PyICe/refid_modules/plugin_module/template_check_plugin/template_check_plugin.py deleted file mode 100644 index 15f20cc..0000000 --- a/PyICe/refid_modules/plugin_module/template_check_plugin/template_check_plugin.py +++ /dev/null @@ -1,49 +0,0 @@ -from PyICe.refid_modules.plugin_module.plugin import plugin -import importlib - -class template_checker(plugin): - def __init__(self, test_mod,template_check): - super().__init__(test_mod) - self.template_check=template_check - - def __str__(self): - return "Compares a board's checked in blueprint to that of a given template, reporting discrepancies." - - def get_atts(self): - att_dict = { - } - return att_dict - - def _set_atts_(self): - pass - - def get_hooks(self): - plugin_dict={ - 'tm_set':[self._temche_set_variables], - } - return plugin_dict - - def set_interplugs(self): - try: - self.tm.interplugs['die_traceability_begin_collect'].extend([self.tem_check_itself]) - except KeyError: - print('\nTemplate_check_plugin requires die_traceability_plugin to be able to function.\n') - - def execute_interplugs(self, hook_key, *args, **kwargs): - for (k,v) in self.tm.interplugs.items(): - if k is hook_key: - for f in v: - f(*args, **kwargs) - - def _temche_set_variables(self, *args, **kwargs): - self.tm.tt._need_to_temche = True - - def tem_check_itself(self): - if self.tm.tt._need_to_temche: - prob_msg = self.template_check.template_check(self.tm.tb_data, self.tm.variant) - if len(prob_msg[0]): - print(f'\nBOM discrepancies between target board and variant template') - [print(f' {msg.expandtabs(prob_msg[1]+5)}') for msg in prob_msg[0]] - if input('Continue? [y/n] ').upper() not in ['Y', 'YES']: - raise Exception('Too many BOM discrepancies') - self.tm.tt._need_to_temche = False \ No newline at end of file diff --git a/PyICe/refid_modules/plugin_module/test_declaration_plugin/How_to_Limit_Test_Declaration.txt b/PyICe/refid_modules/plugin_module/test_declaration_plugin/How_to_Limit_Test_Declaration.txt deleted file mode 100644 index 24c0bca..0000000 --- a/PyICe/refid_modules/plugin_module/test_declaration_plugin/How_to_Limit_Test_Declaration.txt +++ /dev/null @@ -1,37 +0,0 @@ -How to use the limit_test_declaration plugin. - -This plugin allows for the naming of goals and what values are needed to pass. - -In a test class, define a method register_tests(self). Within this method, for each named goal, select which type of test to register based on how the limits are defined. - - self.register_test_functional(name, description=None, notes=None, requirement_reference=None): - If the expected result is simply a Pass/Fail. - self.register_test_exact(name, expect, description=None, notes=None, requirement_reference=None): - To pass, all results are an exact value. - self.register_test_abs_limits(name, min_abs=None, max_abs=None, description=None, notes=None, requirement_reference=None): - To pass, all results are within two given values. - self.register_test_abs_tol(name, expect, abs_tolerance, description=None, notes=None, requirement_reference=None) - To pass, all results are within a given absolute distance from an expected value. (e.g. expect=5, abs_tolerance=2, pass window of 3 to 7) - self.register_test_rel_tol(name, expect, rel_tolerance, description=None, notes=None, requirement_reference=None): - To pass, all results are within a given relative distance from an expected value. (e.g. expect=8, rel_tolerance=0.5, pass window of 4 to 12) - self.register_test_abs_tol_asym(name, expect, min_abs_tolerance=None, max_abs_tolerance=None, description=None, notes=None, requirement_reference=None): - To pass, all results are within a given absolute distance from an expected value. (e.g. expect=5, min_abs_tolerance=2, max_abs_tolerance=3, pass window of 3 to 8) - self.register_test_rel_tol_asym(name, expect, min_rel_tolerance=None, max_rel_tolerance=None, description=None, notes=None, requirement_reference=None): - To pass, all results are within a given relative distance from an expected value. (e.g. expect=8, min_rel_tolerance=0.5, max_rel_tolerance=0.25, pass window of 4 to 10) - -Also in the test class, define a method compile_test_results(self, database, table_name). Within this method, prepare the data from the database that will be submitted to a named goal and submit the data using the function: - - self.register_test_result(name, data) - -For example, say for a dut the output voltage of channel 2 needs to stay within 3% of 5V and the output current needs to stay under 500mA. In the test class, it would look something like this: - - def register_tests(self): - self.register_test_rel_tol(name='DUT_CH2_VOUT', expect=5, rel_tolerance=0.03) - self.register_test_abs_limits(name='DUT_CH2_IOUT', max_abs=0.5) - - def compile_test_results(self, database, table_name): - vout_data = database.query(f'SELECT vout2_meas FROM {table_name}) - self.register_test_result(name='DUT_CH2_VOUT', data=vout_data) - - iout_data = database.query(f'SELECT iout2_meas FROM {table_name}) - self.register_test_result(name='DUT_CH2_IOUT', data=iout_data) \ No newline at end of file diff --git a/PyICe/refid_modules/plugin_module/test_declaration_plugin/correlation_test_declaration_plugin.py b/PyICe/refid_modules/plugin_module/test_declaration_plugin/correlation_test_declaration_plugin.py deleted file mode 100644 index 6ec136a..0000000 --- a/PyICe/refid_modules/plugin_module/test_declaration_plugin/correlation_test_declaration_plugin.py +++ /dev/null @@ -1,321 +0,0 @@ -from PyICe.lab_utils.sqlite_data import sqlite_data -from PyICe.refid_modules import stdf_utils -from PyICe.refid_modules.plugin_module.plugin import plugin -from PyICe.refid_modules.test_results import correlation_results -from types import MappingProxyType -import abc, traceback, pdb, math, os, sqlite3, importlib - -def isnan(value): - try: - return math.isnan(float(value)) - except (TypeError,ValueError): - return False - -class correlation_test_declaration(plugin): - desc = "Associates collected data with named test and compares to a different set of data to assign a pass/fail status." - def __init__(self, test_mod, correlation_data_location): - super().__init__(test_mod) - self.tm.interplugs['register_correlation_test__test_from_table']=[] - self.tm.interplugs['register_correlation_test_get_correlation_data']=[] - self.correlation_data_location=correlation_data_location - self._one_result_print = True - # self.supplemental_thing = importlib.import_module(name=self.tm.plugins[self.__class__.__name__]['dependencies']['correlation_supplements'], package=None).correlation_plugin_supp(self.tm) - # self.tm=[] - - def __str__(self): - return "Associates collected data with named test and compares to a different set of data to assign a pass/fail status." - - def _set_atts_(self): - self.att_dict = MappingProxyType({ - 'get_correlation_declarations':self.get_correlation_declarations, - 'register_correlation_test':self.register_correlation_test, - 'register_correlation_result':self.register_correlation_result, - 'register_correlation_failure':self.register_correlation_failure, - '_get_ate_population_data':self._get_ate_population_data, - '_get_correlation_data':self._get_correlation_data, - 'get_correlation_data_scalar':self.get_correlation_data_scalar, - 'get_correlation_data':self.get_correlation_data, - 'get_ate_population_data':self.get_ate_population_data, - 'reg_ate_result':self.reg_ate_result, - 'display_correlation_results':self.display_correlation_results, - 'register_test_get_pass_fail':self.register_test_get_pass_fail, - 'register_test_get_test_results':self.register_test_get_test_results, - 'register_test_run_repeatability_results':self.register_test_run_repeatability_results, - 'register_test_set_tm':self.register_test_set_tm, - 'register_test__test_from_table_2':self.register_test__test_from_table_2, - 'set_corr_traceability':self.set_corr_traceability, - }) - - def get_atts(self): - return self.att_dict - - def get_hooks(self): - plugin_dict={ - } - return plugin_dict - - def set_interplugs(self): - try: - self.tm.interplugs['register_test__compile_test_results'].extend([self.reg_ate_result]) - self.tm.interplugs['register_test_tt_compile_test_results_2'].extend([self.display_correlation_results]) - self.tm.interplugs['register_test_get_test_results'].extend([self.register_test_get_test_results]) - self.tm.interplugs['register_test_get_pass_fail'].extend([self.register_test_get_pass_fail]) - self.tm.interplugs['register_test_run_repeatability_results'].extend([self.register_test_run_repeatability_results]) - self.tm.interplugs['register_test_set_tm'].extend([self.register_test_set_tm]) - self.tm.interplugs['register_test__test_from_table'].extend([self.set_table_name, self.set_db_filepath]) - self.tm.interplugs['register_test__test_from_table_2'].extend([self.display_correlation_results,self.register_test__test_from_table_2]) - except KeyError: - raise Exception('This plugin requires the register_test_plugin to be included as well.') - try: - self.tm.interplugs['die_traceability_set_traceability'].extend([self.set_corr_traceability]) - except KeyError: - raise Exception('This plugin requires the die_traceability_plugin to be included as well.') - - def get_correlation_declarations(self): - return self._correlation_results.get_correlation_declarations() - def register_correlation_test(self, name): - - ## They data needs to be provided somehow. We look at our imported refids, but the next group might not. So, import supplemental project specific script. - ## corr_reg_inputs = self._get_corr_inputs(self, name, test_module) - ## This will return a dictionary of the arguments for the self.tm._correlation_results._register_correlation_test, which will just be called with (**corr_reg_inputs) - ## self.supplemental_thing. - - if name not in self.tm.REFIDs.keys(): - raise Exception(f'ERROR: Correlation test {name} registered from {self.tm.get_name()} not found in REFID master spec.') - if self.tm.REFIDs[name]['CORRELATION SPEC'] != 'Δ' and self.tm.REFIDs[name]['CORRELATION SPEC'] != '%': - raise Exception(f"ERROR: Correlation test {name} registered from {self.tm.get_name()} appears to have invalid correlation column type {self.tm.REFIDs[name]['CORRELATION SPEC']}. Contact support.") - self.tm._correlation_results._register_correlation_test(refid_name=name, - ATE_test=self.tm.REFIDs[name]['ATE TEST #'], - ATE_subtest=self.tm.REFIDs[name]['ATE SUBTEST #'], - owner=self.tm.REFIDs[name]['OWNER'], - assignee=self.tm.REFIDs[name]['ASSIGNEE'], - lower_limit=self.tm.REFIDs[name]['MIN'] if not isnan(self.tm.REFIDs[name]['MIN']) else None, - upper_limit=self.tm.REFIDs[name]['MAX'] if not isnan(self.tm.REFIDs[name]['MAX']) else None, - unit=self.tm.REFIDs[name]['UNIT'], - description=self.tm.REFIDs[name]['DESCRIPTION'], - notes=f"{self.tm.REFIDs[name]['CONDITIONS']}\n{self.tm.REFIDs[name]['COMMENTS / METHOD']}", - limits_units_percentage=(self.tm.REFIDs[name]['CORRELATION SPEC'] == '%') - ) - def register_correlation_result(self, name, data_temp_pairs, conditions=None): - '''register correlation results against corr REFIDs. Each data point must also contain a temperature setpoint for matching to ATE data.''' - self.tm._correlation_results._register_correlation_result(refid_name=name, iter_data=data_temp_pairs, conditions=conditions) - def register_correlation_failure(self, name, reason, temperature, conditions=None): - '''mark test failed, without submitting test data''' - self.tm._correlation_results._register_correlation_failure(name, reason, temperature, conditions) - - # def get_correlation_data(self, REFID, *args, **kwargs): - # return supplemental_thing._get_correlation_data(REFID, *args, **kwargs) - def get_correlation_data(self, REFID, temperature=None, strict=False, extra_columns=[]): - self.execute_interplugs('register_correlation_test_get_correlation_data', REFID, temperature, strict, extra_columns) - try: - self.tm.REFIDs[REFID] - except KeyError: - # TODO more than printed warning???? Exception? - if strict: - raise ATETestException(f'{REFID} not found in REFID worksheet.') - else: - print(f'''WARNING: {REFID} lookup from worksheet failed. This is either a "fake" REFID or there's a typo. Correlation data won't be found.''') - return [] # Fake REFID - try: - if temperature.upper() == 'NULL': - temperature = 25 #correlate bench data without oven; tdegc=NULL. TODO 25 vs "25". - except AttributeError: - pass #can't upper() a number. Better to ask for forgiveness.... - try: - if math.isnan(self.tm.REFIDs[REFID]['ATE TEST #']) or math.isnan(self.tm.REFIDs[REFID]['ATE SUBTEST #']): - # Blank cells in worksheet - if strict: - raise ATEBlankException(f'{REFID} ATE number lookup from worksheet failed. Found blank cell(s) for test number.') - else: - # print(f'''WARNING: {REFID} ATE number lookup from worksheet failed. Correlation data won't be found.''') # DJS remove? Probably too naggy... - return [] - except TypeError as e: - # It's not blank, and it's not a number either. Probably a string - if not strict: - return [] - elif self.tm.REFIDs[REFID]['ATE TEST #'] == 'NA' or self.tm.REFIDs[REFID]['ATE SUBTEST #'] == 'NA': - raise ATENAException(f'{REFID}') - elif self.REFIDs[REFID]['ATE TEST #'] == 'TBD' or self.tm.REFIDs[REFID]['ATE SUBTEST #'] == 'TBD': - raise ATETBDException(f'{REFID}') - else: - raise ATELinkageException(f'{REFID}') - - try: - return self.tm._get_correlation_data(ate_test_number=(self.tm.REFIDs[REFID]['ATE TEST #'],self.tm.REFIDs[REFID]['ATE SUBTEST #']), temperature=temperature, extra_columns=extra_columns) - except stdf_utils.testNumberException as e: - if strict: - raise ATETestException(f'{REFID}') from e - else: - print(e) - return [] - def get_correlation_data_scalar(self, REFID, temperature): - if temperature is None: temperature = 'NULL'#Database queries from ambient data forced to 25C correlation. - result = self.get_correlation_data(REFID, temperature, strict=True, extra_columns=['FLOW_ID','STDF_file']) - # assert len(result) <= 1, 'Temperature not available or mutiple rows found in ATE data at the temperature requested' - if len(result) > 2: - raise ATEDataException(f'Multiple rows found in ATE data at temperature {temperature} for DuT {self._traceability_hash_table}. Contact PyICe Support at PyICe-developers@analog.com.\n{[row["STDF_file"] for row in result]}') - elif len(result) == 2: - flows = set([row['FLOW_ID'] for row in result]) - if flows == set(('FT TRIM', 'QA ROOM')): - # Specific exemption to favor QAR data over FTR data when both exist. Other cases not handled (deliberately, for now). - result = [row for row in result if row['FLOW_ID'] == 'QA ROOM'] - assert len(result) == 1 - return result[0]['ate_data'] - raise ATEDataException(f'Multiple rows found in ATE data at temperature {temperature} for DuT {self._traceability_hash_table}. Contact PyICe Support at PyICe-developers@analog.com.\n{[row["STDF_file"] for row in result]}') - elif len(result) == 0: - # Old: - ######################## - # Differentiate between missing data (maybe ok) and missing link to data (less ok). - # This method is less tolerant of the missing link than get_correlation_data() because it's called manually and therefore deliberately. The other one is called in the normal flow of every test even when correlation is not planned. - #try: - # if isnan(self.REFIDs[REFID]['ATE TEST #']) or isnan(self.REFIDs[REFID]['ATE SUBTEST #']): - # raise Exception(f'Missing REFID worksheet link between {REFID} and ATE test number.') - #except TypeError as e: - # raise Exception(f'Invalid REFID worksheet link between {REFID} and ATE test number.') - ######################## - # Data is missing; ok. - return None #If there is no ATE Data at the requested temperature, return None - elif len(result) == 1: - return result[0]['ate_data'] #Unpack the scalar data in the 'ate_data' entry of the dictionary in the list returned by get_correlation_data - else: - raise ATEDataException('How did I get here? Contact support at PyICe-developers@analog.com.') - def _get_correlation_data(self, ate_test_number, temperature=None, extra_columns=[]): - if not hasattr(self.tm, '_traceability_hash_table'): - raise Exception(f'WARNING: get_correlation_data() called before _traceability_hash_table computed from database. Contact support. ({self.tm.get_name()})') - elif self.tm._traceability_hash_table is None: - # Problem getting traceability data from database. Perhaps it was never logged? - raise Exception(f'WARNING: get_correlation_data() unable to determine uniqe DUT ID. Were f_die_<> registers logged?. ({self.tm.get_name()})') - else: - if temperature is None: - where_clause = '' - temp_message = '' - else: - where_clause = f'AND CAST(TST_TEMP AS NUMERIC) == {temperature}' - temp_message = f' at temperature {temperature}C' - if len(extra_columns): - extra_columns_clause = f", {', '.join(extra_columns)}" - else: - extra_columns_clause = '' - ate_test_number = stdf_utils.test_number(ate_test_number).to_string() - query_str = f'SELECT "{ate_test_number}" as ate_data, CAST(TST_TEMP AS INTEGER) AS tdegc{extra_columns_clause} FROM population_data WHERE TRACEABILITY_HASH == "{self.tm._traceability_hash_table}" {where_clause}' - correlation_db_filename = os.path.join(os.path.dirname(__file__), f'../../../../../{self.correlation_data_location}') - - try: - db = sqlite_data(database_file=correlation_db_filename) - db.query(query_str) - rows = db.to_list() - if not len(rows): - # print(f'WARNING: Correlation data rows not found for DUT {self._traceability_hash_table}{temp_message}.') #TODO too many warnings!!! - pass - return [{k: r[k] for k in r.keys()} for r in db.to_list()] - except sqlite3.OperationalError as e: - # OperationalError('no such table: 0xC6A857A5') - # TODO: check error more carefully with regular expression or examination of exception object?? - print(f'WARNING: Correlation database not found for DUT {self.tm._traceability_hash_table}.{e}') - # raise e - return [] - - def get_ate_population_data(self, REFID, temperature=None): - try: - self.REFIDs[REFID] - except KeyError: - # TODO print warning???? - return [] # Fake REFID - try: - if isnan(self.REFIDs[REFID]['ATE TEST #']) or isnan(self.REFIDs[REFID]['ATE SUBTEST #']): - return [] # None #no test number reference. skip. Todo: exception instead of None?? - except TypeError as e: - return [] # probably a string - try: - return self._get_ate_population_data(ate_test_number=(self.REFIDs[REFID]['ATE TEST #'],self.REFIDs[REFID]['ATE SUBTEST #']), temperature=temperature) - except stdf_utils.testNumberException as e: - print(e) - return [] - except sqlite3.OperationalError as e: - # OperationalError('no such table: 0xC6A857A5') - # TODO: check error more carefully with regular expression or examination of exception object?? - print(f'WARNING: ATE data not found for REFID: {REFID}, ATE test {(self.REFIDs[REFID]["ATE TEST #"],self.REFIDs[REFID]["ATE SUBTEST #"])}.') - return [] - - def _get_ate_population_data(self, ate_test_number, temperature=None): - if not hasattr(self, '_traceability_hash_table'): - raise Exception(f'WARNING: get_correlation_data() called before _traceability_hash_table computed from database. Contact support. ({self.get_name()})') - elif self._traceability_hash_table is None: - # Problem getting traceability data from database. Perhaps it was never logged? - raise Exception(f'WARNING: get_correlation_data() unable to determine uniqe DUT ID. Were f_die_<> registers logged?. ({self.get_name()})') - else: - if temperature is None: - where_clause = f'' - else: - where_clause = f'WHERE CAST(TST_TEMP AS NUMERIC) == "{temperature}"' - ate_test_number = stdf_utils.test_number(ate_test_number).to_string() - # Double quotes are SQL for "identifier". Unfortunately, SQLite implementation allowes them to revert to strong literal if unmatched, which breaks the query. Use non-standard square braces (MS Access) compatibility, which has no such fallback - query_str = f'SELECT min([{ate_test_number}]) as ate_min, avg([{ate_test_number}]) as ate_mean, max([{ate_test_number}]) as ate_max, CAST(TST_TEMP AS INTEGER) AS tdegc FROM population_data {where_clause} GROUP BY tdegc' - # query_str = f'SELECT min("{ate_test_number}") as ate_min, avg("{ate_test_number}") as ate_mean, max("{ate_test_number}") as ate_max, CAST(TST_TEMP AS INTEGER) AS tdegc FROM population_data {where_clause} GROUP BY tdegc' - # query_str = f"SELECT '{ate_test_number}', TST_TEMP AS tdegc FROM '{self._traceability_hash_table}' {where_clause}" #No good. A keyword in single quotes is a string literal. A keyword in double-quotes is an identifier. - #Magic number alert: - correlation_db_filename = os.path.join(os.path.dirname(__file__), '../../correlation/stdf_data.sqlite') - try: - db = sqlite_data(database_file=correlation_db_filename) - db.query(query_str) - return [{k: r[k] for k in r.keys()} for r in db.to_list()] - except sqlite3.OperationalError as e: - raise - - def reg_ate_result(self): - for test in self.tm.get_test_declarations(): - c_d = self.tm.get_correlation_data(test) - for ate_record in c_d: - self.tm._test_results._register_ate_result(name=test, result=ate_record['ate_data'], temperature=ate_record['tdegc']) - - def set_corr_traceability(self,test=None): - self.tm._correlation_results._set_traceability_info(**self.tm._get_traceability_info(table_name=None, db_file=None)) - - def register_test_get_test_limits(self, test_name): - return self.tm._correlation_results._correlation_declarations[test_name] - - def register_test_get_test_results(self, res_str): - pass - # res_str+= self._correlation_results - - def register_test_get_pass_fail(self): - return bool(self.tm._correlation_results) - - def register_test_run_repeatability_results(self, test_run): - for corr_name in test_run._correlation_results: - try: - # TODO shared results dict??? - results[corr_name].append(test_run._correlation_results[corr_name]) - except KeyError as e: - results[corr_name] = [] - results[corr_name].append(test_run._correlation_results[corr_name]) - # TODO THEN WHAT with CORR?? - def register_test__test_from_table_2(self, table_name, db_file): - if db_file==None: - db_file=self._db_file - c_r = self.tm._correlation_results.json_report() - dest_abs_filepath = os.path.join(os.path.dirname(db_file),f"correlation_results.json") - try: - if c_r is not None: - with open(dest_abs_filepath, 'wb') as f: - f.write(c_r.encode('utf-8')) - f.close() - except PermissionError as e: - print(f'ERROR: Unable to write {dest_abs_filepath}') - def display_correlation_results(self, table_name, db_file=None): - if len(self.tm._correlation_results): - if self._one_result_print: - res_str = f'{self.tm._correlation_results}' - passes = self.tm._correlation_results - res_str += f'*** Module {self.tm.get_name()} Correlation Summary {"PASS" if passes else "FAIL"}. ***\n\n' - print(res_str) - self._one_result_print = False - def register_test_set_tm(self,test_module, *args, **kwargs): - self.tm = test_module - self.tm._correlation_results = correlation_results(self.tm.get_name(), module=self.tm) - for (key,value) in self.get_atts().items(): - self.tm._add_attr(key,value) - def set_table_name(self, table_name, *args,**kwargs): - self.tm._correlation_results.set_table_name(table_name) - def set_db_filepath(self, db_file, *args, **kwargs): - self.tm._correlation_results.set_db_filepath(os.path.abspath(db_file)) \ No newline at end of file diff --git a/PyICe/refid_modules/plugin_module/test_declaration_plugin/limit_test_declaration_plugin.py b/PyICe/refid_modules/plugin_module/test_declaration_plugin/limit_test_declaration_plugin.py deleted file mode 100644 index 697689e..0000000 --- a/PyICe/refid_modules/plugin_module/test_declaration_plugin/limit_test_declaration_plugin.py +++ /dev/null @@ -1,537 +0,0 @@ -from PyICe.lab_utils.sqlite_data import sqlite_data -from PyICe.lab_utils.banners import build_banner -from PyICe.refid_modules import stdf_utils -from PyICe.refid_modules.test_results import test_results -from PyICe.refid_modules.plugin_module.plugin import plugin -from types import MappingProxyType -import traceback, pdb, os, sys, collections - -class ATEDataException(Exception): - '''Superclass of errors related to fetching ATE data.''' -class ATETraceabilityException(ATEDataException): - '''Can't uniquely identify DUT to find data.''' -class ATELinkageException(ATEDataException): - '''Superclass of problems matching a REFID to an ATE test.''' -class ATENAException(ATELinkageException): - '''Can't fetch ATE data because the test number is listed "NA".''' -class ATETBDException(ATELinkageException): - '''Can't fetch ATE data because the test number is listed "TBD".''' -class ATEBlankException(ATELinkageException): - '''Can't fetch ATE data because the test number is unlisted.''' -class ATETestException(ATELinkageException): - '''Can't fetch ATE data because the test number is invalid.''' - -class limit_test_declaration(plugin): - desc = "Associates data collected with a named test and a compares to set limits to assign a pass/fail status." - def __init__(self, test_mod): - super().__init__(test_mod) - self.registered_tests = set() - self.tm.interplugs['register_test__test_from_table']=[] - self.tm.interplugs['register_test__test_from_table_2']=[] - self.tm.interplugs['register_test__compile_test_results']=[] - self.tm.interplugs['register_test_tt_compile_test_results']=[] - self.tm.interplugs['register_test_tt_compile_test_results_2']=[] - self.tm.interplugs['register_test_get_test_results']=[] - self.tm.interplugs['register_test_get_pass_fail']=[] - self.tm.interplugs['register_test_run_repeatability_results']=[] - self.tm.interplugs['register_test_set_tm']=[] - self.tm._test_results = test_results(self.tm.get_name(), module=self.tm) - self._single_results_print = True - - def __str__(self): - return "Associates data collected with a named test and a compares to set limits to assign a pass/fail status." - - def _set_atts_(self): - self.att_dict = MappingProxyType({ - 'get_test_limits':self.get_test_limits, - 'get_test_upper_limit':self.get_test_upper_limit, - 'get_test_lower_limit':self.get_test_lower_limit, - 'get_test_declarations':self.get_test_declarations, - 'get_exclude_reason':self.get_exclude_reason, - 'set_exclude':self.set_exclude, - 'is_included':self.is_included, - '_register_test':self._register_test, - 'register_test_functional':self.register_test_functional, - 'register_test_exact':self.register_test_exact, - 'register_test_abs_limits':self.register_test_abs_limits, - 'register_test_abs_tol':self.register_test_abs_tol, - 'register_test_rel_tol':self.register_test_rel_tol, - 'register_test_abs_tol_asym':self.register_test_abs_tol_asym, - 'register_test_rel_tol_asym':self.register_test_rel_tol_asym, - 'register_test_result':self.register_test_result, - 'register_test_failure':self.register_test_failure, - '_compile_test_results':self._compile_test_results, - 'get_test_results':self.get_test_results, - 'get_pass_fail':self.get_pass_fail, - 'test_from_table':self.test_from_table, - '_test_from_table':self._test_from_table, - 'tt_compile_test_results':self.tt_compile_test_results, - 'add_registration':self.add_registration, - 'run_repeatability_results':self.run_repeatability_results, - 'register_refids':self.register_refids, - }) - - def get_atts(self): - return self.att_dict - - def get_hooks(self): - plugin_dict={ - 'pre_collect':[self.register_refids, self.add_registration], - 'post_collect':[self.tt_compile_test_results], - 'tm_plot_from_table':[self.register_refids,self.set_table_name, self.set_db_filepath, self._test_from_table], - 'post_repeatability':[self.run_repeatability_results], - } - return plugin_dict - - def set_interplugs(self): - pass - - def register_refids(self, *args): - """ - Executes the register_tests method of the test script after providing an opening for interplugs to interact. - - Args: - *args - completely ignored. Only included to make it easy for whatever is calling this method. - """ - self.tm._compile_crashed = None - self.execute_interplugs('register_test_set_tm', self.tm) - if self.tm._is_multitest_module: - for multitest_unit in self.tm._multitest_units: - multitest_unit.register_tests() - else: - self.tm.register_tests() - - def set_table_name(self, table_name, *args, **kwargs): - """ - Establishes the database table to be used for the test. - - Args: - table_name - String. Name to be given to the table. - *args - Unused. Provided for ease of method call. - **kwargs - Unused. Provided for ease of method call. - """ - self.tm._test_results.set_table_name(table_name) - def set_db_filepath(self, db_file, *args,**kwargs): - """ - Establishes the database location to be used for the test. - - Args: - db_file - Any identifying method of the database location that can be recognized by os.path.abspath. - *args - Unused. Provided for ease of method call. - **kwargs - Unused. Provided for ease of method call. - """ - self.tm._test_results.set_db_filepath(os.path.abspath(db_file)) - - def add_registration(self): - """ - Adds to the set of declared test names. Each name must be unique, or an Error will occur. - """ - for decl in self.tm.get_test_declarations(): - if decl in self.registered_tests: - raise Exception(f'Duplicated test name {decl} across modules {self.tm.tt._tests[-1].get_name()}!') - else: - self.registered_tests.add(decl) - - def tt_compile_test_results(self): - """ - Produces a pass/fail/crash string for all results. Will pass the string to those affected by temptroller's notify method. - - Returns: - A string that contains a pass/fail/crash verdict on registered tests. - """ - res_str = '' - all_pass = True - try: - if self.tm._crashed is None: - self.execute_interplugs('register_test_tt_compile_test_results', self) - self._compile_test_results() - if self.tm._compile_crashed is not None: - (typ, value, trace_bk) = self.tm._compile_crashed - notify_message = f'{self.tm.get_name()} compile test result crash! Moving on.\n' - notify_message += f'{"".join(traceback.format_exception(typ, value, trace_bk))}\n' - self.tm.tt.notify(f'{self.tm.tt.get_bench_identity()["user"]} on {self.tm.tt.get_bench_identity()["host"]}\n'+notify_message) - res_str = f'{res_str}\n{self.get_test_results()}' - all_pass &= self.get_pass_fail() - - res_str += '***********************\n' - res_str += f'* Summary: {"PASS" if all_pass else "FAIL"} *\n' - res_str += '***********************\n' - self.tm.tt.notify(res_str, subject='Test Results') - self._single_results_print = False - self.execute_interplugs('register_test_tt_compile_test_results_2', self) - except AttributeError as e: - print(e) - return res_str - #TODO check for duplicated results reporting. By REFID? - - def get_test_limits(self, test_name): - """ - Returns a tuple of the upper and lower limits of the given test_name. - - Args: - test_name - String. The name of the test which has the limits requested. - - Returns - Tuple. Lower limit first, then upper limit. - """ - try: - tst_decl = self.tm._test_results._test_declarations[test_name] - except KeyError: - tst_decl = self.execute_interplugs('register_test_get_test_limits', test_name) - #test_declaration(test_name='CH0_VDROOP', requirement_reference=None, lower_limit=-0.15, upper_limit=0.15, description='CH0 droop due to load step', notes=None) - return (tst_decl.lower_limit, tst_decl.upper_limit) - def get_test_upper_limit(self, test_name): - """ - Returns just the upper limit of the given test_name. - - Args: - test_name - String. The name of the test which has the limit requested. - - Returns - The value of the upper limit. Type is the same as what was initially provided at registration. - """ - return self.get_test_limits(test_name)[1] - def get_test_lower_limit(self, test_name): - """ - Returns just the lower limit of the given test_name. - - Args: - test_name - String. The name of the test which has the limit requested. - - Returns - The value of the lower limit. Type is the same as what was initially provided at registration. - """ - return self.get_test_limits(test_name)[0] - def get_test_declarations(self): - """ - Returns a list of the registered test_names. - - Returns - List. All the tests registered during register_tests(). - """ - return self.tm._test_results.get_test_declarations() - def set_exclude(self, reason): - '''call from inside register_tests or register_multitest_units to exclude from audit and regressions. ie, debug, WIP, boneyard, alternate implementation, etc.''' - self._exclude_reason = reason - def get_exclude_reason(self): - """ - Returns the scripts provided excuse for being excluded from audits, if any. - - Returns - String or NoneType. Reason provided when test was declared excluded. Returns NoneType if it is not excluded. - """ - try: - return self._exclude_reason - except AttributeError: - return - def is_included(self): - """ - A check to see if a script is included in audits. - - Returns - Bool. True if no reason for exclusion was provided, else False. - """ - try: - self._exclude_reason - return False - except AttributeError: - return True - - #@typing.final - def _register_test(self, name, lower_limit, upper_limit, **kwargs): - #need some consitent vocabulary. What is a test? What is a sweep? - #should tests and test results be linked back to their collection sweep more explicitly through this data model? - ''' - The basic form of registering tests. Should not be called itself. All arguments are passed on to the test_results and made into attributes. - - Args: - name - String. Name to be used for this test. - lower_limit - Lower limit against which all result data will be compared. - upper_limit - Upper limit against which all result data will be compared. - **kwargs - Attributes that will be linked to the test. - ''' - if self.is_included(): - if name not in self.tm.REFIDs.keys(): - print(f'WARNING! Test {name} registered from {self.tm.get_name()} not found in REFID master spec.') - self.tm._test_results._register_test(name=name, lower_limit=lower_limit, upper_limit=upper_limit, **kwargs) - - #@typing.final - def register_test_functional(self, name, **kwargs): - """ - Registers a test which will only be looking at boolean results. - - Args: - name - String. Name to be used for this test. - **kwargs - Attributes that will be linked to the test. - """ - self._register_test(name=name, lower_limit=True, upper_limit=True, **kwargs) - #@typing.final - def register_test_exact(self, name, expect, **kwargs): - """ - Registers a test which does not allow for a window. - - Args: - name - String. Name to be used for this test. - expect - Int, Float, String, Bool. Value against which all result data will be compared. - **kwargs - Attributes that will be linked to the test. - """ - self._register_test(name=name, lower_limit=expect, upper_limit=expect, **kwargs) - #@typing.final - def register_test_abs_limits(self, name, min_abs=None, max_abs=None, **kwargs): - """ - Registers a test that will pass if all submitted result values are within the provided limits. - - Args: - name - String. Name to be used for this test. - min_abs - Int, Float, None. Minimum Value against which all result data will be compared. If None, there is no lower limit. - max_abs - Int, Float, None. Maximum Value against which all result data will be compared. If None, there is no upper limit. - **kwargs - Attributes that will be linked to the test. - """ - self._register_test(name=name, lower_limit=min_abs, upper_limit=max_abs, **kwargs) - #@typing.final - def register_test_abs_tol(self, name, expect, abs_tolerance, **kwargs): - """ - Registers a test that will pass if all submitted result values are within the given tolerance of the given expected value. - - Args: - name - String. Name to be used for this test. - expect - Int, Float. Value against which all result data will be compared. - abs_tolerance- Int, Float. Maximum distance from which all result data can be from expected value. - **kwargs - Attributes that will be linked to the test. - """ - self._register_test(name=name, lower_limit=expect-abs_tolerance, upper_limit=expect+abs_tolerance, **kwargs) - #@typing.final - def register_test_rel_tol(self, name, expect, rel_tolerance, **kwargs): - """ - Registers a test that will pass if all submitted result values are within the given percentage of the given expected value. - - Args: - name - String. Name to be used for this test. - expect - Int, Float. Value against which all result data will be compared. - rel_tolerance- Int, Float. Percent off a passing result can be from the expected value. - **kwargs - Attributes that will be linked to the test. - """ - self._register_test(name=name, lower_limit=expect*(1-rel_tolerance), upper_limit=expect*(1+rel_tolerance), **kwargs) - #@typing.final - def register_test_abs_tol_asym(self, name, expect, min_abs_tolerance=None, max_abs_tolerance=None, **kwargs): - """ - Registers a test that will pass if all submitted result values are within the given tolerance of the given expected value. - - Args: - name - String. Name to be used for this test. - expect - Int, Float. Value against which all result data will be compared. - min_abs_tolerance- Int, Float. Maximum Value from which all result data can be below expected value. - max_abs_tolerance- Int, Float. Maximum Value from which all result data can be above expected value. - **kwargs - Attributes that will be linked to the test. - """ - self._register_test(name=name, lower_limit=expect-min_abs_tolerance, upper_limit=expect+max_abs_tolerance, **kwargs) - #@typing.final - def register_test_rel_tol_asym(self, name, expect, min_rel_tolerance=None, max_rel_tolerance=None, **kwargs): - """ - Registers a test that will pass if all submitted result values are within the given percentage of the given expected value. - - Args: - name - String. Name to be used for this test. - expect - Int, Float. Value against which all result data will be compared. - min_rel_tolerance- Int, Float. Percent off a passing result can be below the expected value. If None, there will be no lower limit. - max_rel_tolerance- Int, Float. Percent off a passing result can be above the expected value. If None, there will be no upper limit. - **kwargs - Attributes that will be linked to the test. - """ - self._register_test(name=name, lower_limit=expect*(1-min_rel_tolerance), upper_limit=expect*(1+max_rel_tolerance), **kwargs) - ###store limit type for each test, or discard? - #@typing.final - def register_test_result(self, name, data, conditions=None): - ''' - Submit data to be considered. - - Args: - name - String. Name of the registered test the data is for. - data - Boolean, or any iterable. Data to be compared to the upper and lower limits of the named test. - conditions - Dictionary. Only used if the iterable is a sqlite database. Groups the data provided by the channel names provided in the values. - ''' - #data should be True/False or iterable. - #Handle True/False here for functional tests, or pass through? - self.tm._test_results._register_test_result(name=name, iter_data=data, conditions=conditions) - def register_test_failure(self, name, reason='', conditions=None): - '''mark test failed, without submitting test data - - Args: - name - String. Name of the test that failed. - reason - Optional. String. Reason for the failure. Will be logged in the database. - conditions - Optional. None, String. Conditions under which the test reached the failure. - ''' - self.tm._test_results._register_test_failure(name, reason, conditions) - - def _compile_test_results(self, table_name=None, db_file=None): - '''get database loaded up first to streamline test module''' - if self.tm._crashed is not None: - print(f'Skipping test result compilation for crashed test {self.tm.get_name()}.') - else: - if table_name is None: - table_name = self.tm.get_db_table_name() - if db_file is None: - # db = self.tm.get_db() - db = sqlite_data(database_file=self.tm._db_file, table_name=table_name) - else: - db = sqlite_data(database_file=db_file, table_name=table_name) - if f'{table_name}_all' in db.get_table_names(): - table_name = f"{table_name}_all" #Redirect to presets-joined table - db.set_table(table_name) - # print(table_name) - if self.tm.bench_name == None: - my_bench=db.query(f'SELECT bench FROM {table_name}').fetchone()['bench'] - self.tm.bench_name=my_bench[my_bench.find('benches')+8:my_bench.find("' from")] - try: - self.tm.compile_test_results(database=db, table_name=table_name) - self.execute_interplugs('register_test__compile_test_results') - self.tm._test_results._plot() - except Exception as e: - # Something else, NOS, has gone wrong with REFID result plotting. Let's try to muddle through it to avoid interrupting the other plots and still give a chance of archiving. - if self.tm._debug: - traceback.print_exc() - if input('Debug [y/n]? ').lower() in ('y', 'yes'): - pdb.post_mortem() - else: - # Just crash. No sense in carrying on with other tests in the regression. - raise e - self.tm._compile_crashed = sys.exc_info() - for t in self.get_test_declarations(): - self.register_test_failure(t) - - def get_test_results(self): - # moved crashing bench name stuff up to _compile_test_results where it has access to database table name. - res_str = f'\nTested on {self.tm.bench_name}\n' - res_str += f'*** Module {self.tm.get_name()} ***\n' - res_str += f'{self.tm._test_results}{self.tm.crash_info()}' - passes = self.tm._test_results - res_str += f'*** Module {self.tm.get_name()} Summary {"PASS" if passes else "FAIL"}. ***\n\n' - self.execute_interplugs('register_test_get_test_results', res_str) - return res_str - def get_pass_fail(self): - # self.execute_interplugs('register_test_get_pass_fail') - return not self.tm.is_crashed() and bool(self.tm._test_results) - - def run_repeatability_results(self): - results = collections.OrderedDict() #De-tangle runs - for test_run in self.tm.tt: - for test_name in test_run._test_results: - try: - results[test_name].append(test_run._test_results[test_name]) - except KeyError as e: - results[test_name] = [] - results[test_name].append(test_run._test_results[test_name]) - self.execute_interplugs('register_test_run_repeatability_results', test_run) - for test_name in results: - print('TODO FIXME to factor trials') - -# -# -# for condition_hash, condition_orig in results[t_d].get_conditions().items(): -# filter_results = results[t_d].filter(condition_hash) -# cond_dict = {'conditions': condition_orig, #TODO put back to dictionary! -# 'case_results': [{k:v for k,v in t_r._asdict().items() if k not in ['test_name', 'conditions', 'plot']} for t_r in filter_results], -# -# -# for temperature in results[t_d].get_temperatures(): -# temperature_dict = {'temperature': temperature, -# 'cases': [], -# # 'summary': {}, -# } -# res_dict['tests'][t_d]['results']['temperatures'].append(temperature_dict) -# temp_group = results[t_d].filter_temperature(temperature) -# for condition_hash, condition_orig in temp_group.get_conditions().items(): -# cond_group = temp_group.filter_conditions(condition_hash) -# cond_dict = {'conditions': condition_orig, -# 'case_results': [{k:v for k,v in cond._asdict().items() if k not in ['refid_name', 'temperature', 'conditions']} for cond in cond_group], -# 'summary': {'min_error': cond_group._min_error(), -# 'max_error': cond_group._max_error(), -# 'passes': bool(cond_group), -# }, -# } -# temperature_dict['cases'].append(cond_dict) -# -# -# -# -# print(f'{test_name} meanmean:{statistics.mean([trial.mean for trial in results[test_name]])}, meansigma:{statistics.stdev([trial.mean for trial in results[test_name]])}') -# for trial in results[test_name]: -# print(f'\tmean:{trial.mean}, min:{trial.min_data}, max:{trial.max_data}') - - @classmethod - def test_from_table(cls, table_name=None, db_file=None, debug=False): - """ - Creates an instance of the script test, then calls and returns _test_from_table. - - Returns - The return of _test_from_table. - """ - self.tm = cls(debug=debug) - return self._test_from_table(table_name, db_file) - def _test_from_table(self, table_name=None, db_file=None): - """ - Drawing upon a database, produces plots, compiles results, and creates a json. - - Args: - table_name -Optional. String. Name of the table from which data is drawn. - db_file -Optional. Any identifying method of the database location that can be recognized by os.path.abspath. - """ - if not hasattr(self.tm, '_test_results'): - self.tm._test_results = test_results(self.tm.get_name(), module=self.tm) - self.execute_interplugs('register_test_set_tm', self.tm) - self.tm._test_results.set_table_name(table_name) - self.tm._test_results.set_db_filepath(os.path.abspath(db_file)) - self.execute_interplugs('register_test__test_from_table', table_name=table_name, db_file=db_file) - self._compile_test_results(table_name, db_file) - if self.tm._compile_crashed is not None: - (typ, value, trace_bk) = self.tm._compile_crashed - msg = f'{self.tm.get_name()} compile test results crash!.\n' - msg += f'{"".join(traceback.format_exception(typ, value, trace_bk))}\n' - # Skipping the "moving on" here on replotting (vs after collection) since it doesn't affect archive, etc. - # print(notify_message) - raise Exception(msg) - res_str = self.tm.get_test_results() - banner_str = [f'Summary: {"PASS" if self.tm.get_pass_fail() else "FAIL"}'] - if table_name is not None: - banner_str.append(f' {table_name}{" " * (38-len(table_name))}') - res_str += build_banner(*banner_str) - if self._single_results_print: - print(res_str) - self._single_results_print = False - self.execute_interplugs('register_test__test_from_table_2', table_name, db_file) - # JSON OUTPUT WIP - if db_file==None: - db_file=self.tm._db_file - t_r = self.tm._test_results.json_report() - dest_abs_filepath = os.path.join(os.path.dirname(db_file),f"test_results.json") - try: - if t_r is not None: - with open(dest_abs_filepath, 'wb') as f: - f.write(t_r.encode('utf-8')) - f.close() - except PermissionError as e: - print(f'ERROR: Unable to write {dest_abs_filepath}') - # ###################### - return res_str - - -### From here on in, it's the test_result.py stuff. - - - - - - - - - - - - - - - - - - - - diff --git a/PyICe/refid_modules/plugin_module/test_declaration_plugin/refid_import_plugin.py b/PyICe/refid_modules/plugin_module/test_declaration_plugin/refid_import_plugin.py deleted file mode 100644 index df445dc..0000000 --- a/PyICe/refid_modules/plugin_module/test_declaration_plugin/refid_import_plugin.py +++ /dev/null @@ -1,158 +0,0 @@ -from PyICe.refid_modules.plugin_module.test_declaration_plugin.limit_test_declaration_plugin import limit_test_declaration -from PyICe.refid_modules.test_results import correlation_results -import importlib, math, os, sqlite3 - -def isnan(value): - try: - return math.isnan(float(value)) - except (TypeError,ValueError): - return False - -class refid_import_plugin(limit_test_declaration): - def __init__(self, test_mod): - super().__init__(test_mod) - self.reg_inputs={} - self.tm.interplugs['register_correlation_test__test_from_table']=[] - self.tm.interplugs['register_correlation_test_get_correlation_data']=[] - self._one_result_print = True - - def get_atts(self): - att_dict = { - 'register_test_abs':self.register_test_abs, - 'get_correlation_declarations':self.get_correlation_declarations, - 'register_correlation_test':self.register_correlation_test, - 'register_correlation_result':self.register_correlation_result, - 'register_correlation_failure':self.register_correlation_failure, - 'get_correlation_data_scalar':self.get_correlation_data_scalar, - 'get_correlation_data':self.get_correlation_data, - 'reg_ate_result':self.reg_ate_result, - 'display_correlation_results':self.display_correlation_results, - 'register_test_get_pass_fail':self.register_test_get_pass_fail, - 'register_test_run_repeatability_results':self.register_test_run_repeatability_results, - 'register_test_set_tm':self.register_test_set_tm, - 'register_test__test_from_table_2':self.register_test__test_from_table_2, - 'set_corr_traceability':self.set_corr_traceability, - } - att_dict.update(super().get_atts()) - return att_dict - - def get_hooks(self): - plugin_dict = super().get_hooks() - plugin_dict['pre_collect'].insert(0,self.import_refids) - plugin_dict['tm_plot_from_table'].insert(0,self.import_refids) - plugin_dict['tm_set'] = [self._set_refids_once] - return plugin_dict - - def set_interplugs(self): - self.tm.interplugs['register_test__compile_test_results'].extend([self.reg_ate_result]) - self.tm.interplugs['register_test_tt_compile_test_results_2'].extend([self.display_correlation_results]) - self.tm.interplugs['register_test_get_pass_fail'].extend([self.register_test_get_pass_fail]) - self.tm.interplugs['register_test_run_repeatability_results'].extend([self.register_test_run_repeatability_results]) - self.tm.interplugs['register_test_set_tm'].extend([self.register_test_set_tm]) - self.tm.interplugs['register_test__test_from_table'].extend([self.set_table_name, self.set_db_filepath]) - self.tm.interplugs['register_test__test_from_table_2'].extend([self.display_correlation_results,self.register_test__test_from_table_2]) - try: - self.tm.interplugs['die_traceability_set_traceability'].extend([self.set_corr_traceability]) - except Exception: - pass # This feature requires the die_traceability_plugin to be imported in the test_module.') - - def _set_refids_once(self, *args): - self.tm.tt._need_to_get_refids=True - - def get_refids(self): - '''Return a panda database containing the names and details of the project's refids.''' - pass - - def import_refids(self, *args): - if not self.tm.tt: - self.tm.REFIDs = self.get_refids() - return - elif self.tm.tt._need_to_get_refids: - self.tm.tt.refids = self.get_refids() - self.tm.tt._need_to_get_refids = False - self.tm.REFIDs = self.tm.tt.refids - - def get_reg_inputs(self, name): - return self.reg_inputs[name] - - def register_test_abs(self, name): - '''Pull test limits directly from REFID document automagically.''' - if not hasattr(self.tm, 'REFIDs'): - self.import_refids() - self._set_reg_inputs(name) - self._register_test(**self.get_reg_inputs(name)) - def register_correlation_test(self, name): - """With any luck, this should be redundant sometime soon, and we will just be able to 'register_test_ab' for everything imported.""" - if not hasattr(self.tm, 'REFIDs'): - self.import_refids() - self._set_reg_inputs(name) - self.tm._correlation_results._register_correlation_test(**self.get_reg_inputs(name)) - - def get_correlation_declarations(self): - return self._correlation_results.get_correlation_declarations() - - def register_correlation_result(self, name, data_temp_pairs, conditions=None): - '''register correlation results against corr REFIDs. Each data point must also contain a temperature setpoint for matching to ATE data.''' - self.tm._correlation_results._register_correlation_result(refid_name=name, iter_data=data_temp_pairs, conditions=conditions) - - def register_correlation_failure(self, name, reason, temperature, conditions=None): - '''mark test failed, without submitting test data''' - self.tm._correlation_results._register_correlation_failure(name, reason, temperature, conditions) - - def get_correlation_data(self, REFID, *args, **kwargs): - self.execute_interplugs('register_correlation_test_get_correlation_data', REFID, *args, **kwargs) - return self.get_correlation_data(REFID, *args, **kwargs) - - def get_correlation_data_scalar(self, REFID, *args, **kwargs): - return self.get_correlation_data_scalar(REFID, *args, **kwargs) - - def reg_ate_result(self): - for test in self.tm.get_test_declarations(): - c_d = self.tm.get_correlation_data(test) - for ate_record in c_d: - self.tm._test_results._register_ate_result(name=test, result=ate_record['ate_data'], temperature=ate_record['tdegc']) - - def set_corr_traceability(self,test=None): - self.tm._correlation_results._set_traceability_info(**self.tm._get_traceability_info(table_name=None, db_file=None)) - - def register_test_get_test_limits(self, test_name): - return self.tm._correlation_results._correlation_declarations[test_name] - - def register_test_get_pass_fail(self): - return bool(self.tm._correlation_results) - - def register_test_run_repeatability_results(self, test_run): - for corr_name in test_run._correlation_results: - try: - # TODO shared results dict??? - results[corr_name].append(test_run._correlation_results[corr_name]) - except KeyError as e: - results[corr_name] = [] - results[corr_name].append(test_run._correlation_results[corr_name]) - # TODO THEN WHAT with CORR?? - def register_test__test_from_table_2(self, table_name, db_file): - if db_file==None: - db_file=self._db_file - c_r = self.tm._correlation_results.json_report() - dest_abs_filepath = os.path.join(os.path.dirname(db_file),f"correlation_results.json") - try: - if c_r is not None: - with open(dest_abs_filepath, 'wb') as f: - f.write(c_r.encode('utf-8')) - f.close() - except PermissionError as e: - print(f'ERROR: Unable to write {dest_abs_filepath}') - def display_correlation_results(self, table_name, db_file=None): - if len(self.tm._correlation_results): - if self._one_result_print: - res_str = f'{self.tm._correlation_results}' - passes = self.tm._correlation_results - res_str += f'*** Module {self.tm.get_name()} Correlation Summary {"PASS" if passes else "FAIL"}. ***\n\n' - print(res_str) - self._one_result_print = False - def register_test_set_tm(self,test_module, *args, **kwargs): - self.tm._correlation_results = correlation_results(self.tm.get_name(), module=self.tm) - def set_table_name(self, table_name, *args,**kwargs): - self.tm._correlation_results.set_table_name(table_name) - def set_db_filepath(self, db_file, *args, **kwargs): - self.tm._correlation_results.set_db_filepath(os.path.abspath(db_file)) diff --git a/PyICe/refid_modules/stdf_utils.py b/PyICe/refid_modules/stdf_utils.py deleted file mode 100644 index 4279b27..0000000 --- a/PyICe/refid_modules/stdf_utils.py +++ /dev/null @@ -1,1016 +0,0 @@ - -import collections -import functools -import math -import os -import re -import sqlite3 -import sys - -try: - from pystdf.IO import Parser - import pystdf.V4 - from pystdf.Writers import format_by_type -except ModuleNotFoundError as e: - # pystdf is needed for processing files, but not needed for audits, IVY and other Linux PyICe usage. - print(f'WARNING: Import error with pystdf library. This is ok if not processing STDF dlogs directly, and temporarily expected in Linux because of Anaconda package issues.\n{type(e)}: {e.args}') -import time -from stowe_eval.stowe_eval_base.modules.stowe_die_traceability import stowe_die_traceability -from PyICe.data_utils import units_parser - - -class results_ord_dict(collections.OrderedDict): - '''Ordered dictionary with pretty print addition.''' - def __str__(self): - s = '' - max_name_length = 0 - for k,v in self.items(): - max_name_length = max(max_name_length, len(str(k))) - s += f'{k}:\t{v}\n' - s = s.expandtabs(max_name_length+2) - return s - - -#hash table name - # dlog file - # PART_ID - # 9900065: RevID - # 899900000: f_die_year (Since 2000) - # 899900001: f_die_week (eng=month) - # 899900002: f_die_wafer_number (eng=day) - # 899900003: f_die_loc_x (eng=hour) - # 899900004: f_die_loc_y (eng=minute) - # 899900005: f_die_running (eng=serial #) - # - # 'revid', - # 'f_die_fab_2_0_', - # 'f_die_fab_4_3_', - # 'f_die_parent_child_9_8_', - # 'f_die_loc_y', - # 'f_die_loc_x', - # 'f_die_running_7_0_', - # 'f_die_parent_child_7_0_', - # 'f_die_fab_6_5_', - # 'f_die_wafer_number', - # 'f_die_running_9_8_', - # 'f_die_week', - # 'f_die_year', - -class testNumberException(Exception): - '''class of exceptions for unable to parse test number input successfully.''' - -class test_number: - def __init__(self, test_number): - if isinstance(test_number, int): - (f_subtest, f_test) = math.modf(test_number/1e5) - self._test = int(round(f_test)) - self._subtest = int(round(f_subtest*1e5)) - elif isinstance(test_number, str): - assert len(test_number) == 10, 'String format is "tttttsssss".' - self._test = int(test_number[0:5]) - self._subtest = int(test_number[5:10]) - elif isinstance(test_number, float): - (f_subtest, f_test) = math.modf(test_number) - self._test = int(round(f_test)) - self._subtest = int(round(f_subtest*1e5)) - elif isinstance(test_number, (tuple, list)): - assert len(test_number) == 2, 'Tuple format is (test, subtest)' - self._test = int(test_number[0]) - self._subtest = int(test_number[1]) - else: - raise testNumberException('Unknown ATE test number format type: {type(test_number)}.') - def __str__(self): - ret_str = '' - ret_str += f'Test: {self._test} Subtest: {self._subtest}\n' - ret_str += f'INT: {self.to_int()}\n' - ret_str += f'STR: {self.to_string()}\n' - ret_str += f'DEC: {self.to_decimal()}\n' - ret_str += f'TUP: {self.to_pair()}\n' - return ret_str - def to_int(self): - return int(self._test*1e5 + self._subtest) - def to_string(self): - return f'{self.to_int():010d}' - def to_decimal(self): - return self._test + self._subtest/1e5 - def to_pair(self): - return (self._test, self._subtest) - - -# print(test_number(12300045)) -# print(test_number(123.00045)) -# print(test_number('0012300045')) -# print(test_number((123,45))) - - -def x_loc_getter(dut_record): - try: - return dut_record['f_die_loc_x (eng=hour)'] #8999.3 - except KeyError as e: - try: - return dut_record['f_die_loc_x'] #8010.4 - except KeyError as f: - print(e) - print(f) - raise - -def y_loc_getter(dut_record): - try: - return dut_record['f_die_loc_y (eng=minute)'] #8999.4 - except KeyError as e: - try: - return dut_record['f_die_loc_y'] #8010.5 - except KeyError as f: - print(e) - print(f) - raise - -def die_running_getter(dut_record): - try: - return dut_record['f_die_running (eng=serial #)'] #8999.5 - except KeyError as e: - try: - return dut_record['f_die_running'] #8010.3 - except KeyError as f: - print(e) - print(f) - raise - -def die_wafer_getter(dut_record): - try: - return dut_record['f_die_wafer_number (eng=day)'] #8999.2 - except KeyError as e: - try: - return dut_record['f_die_wafer_number'] #8010.2 - except KeyError as f: - print(e) - print(f) - raise - -def die_week_getter(dut_record): - try: - return dut_record['f_die_week (eng=month)'] #8999.1 - except KeyError as e: - try: - return dut_record['f_die_week'] #8010.1 - except KeyError as f: - print(e) - print(f) - raise - - -class ETS_units_scaler: - ''' - From: Keefer, Mark - Sent: Thursday, April 29, 2021 1:29 PM - To: Simmons, David ; Arnold, Jerimy - Subject: FW: ETS stdf writer, results and limit scaling - - HI Dave, - - See below the list of recognized and scaled base units. It’s small. - - Another oddity is that if a test has no limits, it will not apply scaling regardless of the base unit. - - The good news is that no data is misrepresented if your stdf parser reads the UNITS field and recognizes metric prefixes, right? - - Mark - - From: Randy Williams - Sent: Wednesday, April 28, 2021 8:04 PM - To: Keefer, Mark ; Cristian Jimenez - Cc: Arnold, Jerimy ; Carboaro, Elijah ; Andrew Westall - Subject: RE: ETS stdf writer, results and limit scaling - - Mark, - - Currently Ohms are not normalized to the base units. The only units that are currently normalized are volts amps seconds hertz and percentages. - - Regards, - Randy Williams - Field Apps. Engineer - - 7124 LANTANA TERRACE - CARLSBAD, CA 92011 - randy.williams@teradyne.com - - ''' - ''' - % - %/V - %/mA - %/uA - A - A/V - A/uA - A/us - AMPS - BITS - Bits - C - DUT/Hr - FAIL - HERTZ - Hr - KOhms - MHZ - Mhos - NUM - OHM - Ohms - SECONDS - Sec - V - V/V - VOLTS - Wafer - Week - Year - kOHM - mA - mOhm - mS - mV - mV/V - nS - ns - nsec - pF - uA - uMHO - usec - - - ENG adds: - Day - Dnum - MHz - Month - ''' - ets_normalized = {'%', #Skip units parser - 'A', - 'AMPS', - 'BITS', - 'Bits', - 'C', - 'DUT/Hr', - 'FAIL', - 'HERTZ', - 'MHZ', - 'NUM', - 'SECONDS', - 'Sec', - 'V', - 'VOLTS', - 'Wafer', - 'Week', - 'Year', - 'mA', - 'mS', - 'mV', - 'nS', - 'ns', - 'nsec', - 'uA', - 'usec', - 'P/F', - 'T/F', - 'Day', - 'Dnum', - 'MHz', - 'Month', - 'Ohm' - } - un_normalized = {'%/V', #Parse units - '%/mA', - '%/uA', - 'A/V', - 'A/uA', - 'A/us', - 'Hr', - 'KOhms', - 'kOhm', - 'Mhos', - 'OHM', - 'Ohms', - 'V/V', - 'kOHM', - 'mOhm', - 'mV/V', - 'pF', - 'uMHO', - 'LSB', - '', - } - unconfirmed = ( - ) - def __init__(self): - self.unrecognized_units = set() - cls = type(self) - assert len(cls.ets_normalized.intersection(cls.un_normalized)) == 0 - def print_unkown_units(self): - if len(self.unrecognized_units): - raise Exception(f"ERROR: Unrecognized units: '{sorted(list(self.unrecognized_units))}' in dlog. I don't know if these need to be renormalized.") - def normalize(self, units): - '''fix the units that ETS didn't normalized before logging. - leave the ones that were already scaled by the ETS shell alone. - The only way to know is via the support email info above. - ''' - if units in type(self).ets_normalized: - return self.fake_parser(units) - elif units in type(self).un_normalized: - return units_parser.parser(units) - else: - self.unrecognized_units.add(units) - return self.fake_parser(units) - def fake_parser(self, units): - return {'MULT': 1, 'UNITS': f"[{units}]/[]"} -class master_parser: - traceability_regs = {'revid': lambda self: self._dut_record['RevID'], - # 'f_die_fab_2_0_': lambda self: self._dut_record['f_die_fab'] & 0x7, - 'f_die_fab_2_0_': lambda self: 0, #removed from memory map, but hash order preserved. - # 'f_die_fab_4_3_': lambda self: (self._dut_record['f_die_fab'] >> 3) & 0x3, - 'f_die_fab_4_3_': lambda self: 0, - # 'f_die_parent_child_9_8_': lambda self: (self._dut_record['f_die_parent_child'] >> 8) & 0x3, - 'f_die_parent_child_9_8_': lambda self: 0, - # 'f_die_loc_y': lambda self: self._dut_record['f_die_loc_y (eng=minute)'], - 'f_die_loc_y': lambda self: y_loc_getter(self._dut_record), - # 'f_die_loc_x': lambda self: self._dut_record['f_die_loc_x (eng=hour)'], - 'f_die_loc_x': lambda self: x_loc_getter(self._dut_record), - # 'f_die_running_7_0_': lambda self: self._dut_record['f_die_running (eng=serial #)'] & 0xFF, - 'f_die_running_7_0_': lambda self: die_running_getter(self._dut_record) & 0xFF, - # 'f_die_parent_child_7_0_': lambda self: self._dut_record['f_die_parent_child'] & 0xFF, - 'f_die_parent_child_7_0_': lambda self: 0, - # 'f_die_fab_6_5_': lambda self: (self._dut_record['f_die_fab'] >> 5) & 0x3, - 'f_die_fab_6_5_': lambda self: 0, - # 'f_die_wafer_number': lambda self: self._dut_record['f_die_wafer_number (eng=day)'], - 'f_die_wafer_number': lambda self: die_wafer_getter(self._dut_record), - # 'f_die_running_9_8_': lambda self: (self._dut_record['f_die_running (eng=serial #)'] >> 8) & 0x3, - 'f_die_running_9_8_': lambda self: (die_running_getter(self._dut_record) >> 8) & 0x3, - 'f_die_week': lambda self: die_week_getter(self._dut_record), - 'f_die_year': lambda self: self._dut_record['f_die_year (Since 2000)'], - } - _db = None #Shared db conn. - def __init__(self): - pass - def _debug_units_scaling(self, test_result_record): - # TODO debug units scaling. STDF storage of values in base units vs metric prefix units inconsistent. Need to audit dlog for offenders.... - stdf_scale = 10**(-1*test_result_record["RES_SCAL"]) - steve_scale = self._units[test_result_record["TEST_NUM"]]["MULT"] - # if self._units[test_result_record["TEST_NUM"]]["MULT"] != 1: - # if stdf_scale != steve_scale: - if test_result_record["TEST_NUM"] == 1000000 or test_result_record["TEST_NUM"] == 2000000: - print(f'{test_result_record["TEST_TXT"]} {test_result_record["TEST_NUM"]} {test_result_record["RESULT"]} {test_result_record["UNITS"]} {self._units[test_result_record["TEST_NUM"]]} {test_result_record["RES_SCAL"]}') - def map_data(self, record_type, data): - return {k: v for (k,v) in zip(record_type.fieldNames, data)} - def _compute_traceability_registers(self): - for reg, f in type(self).traceability_regs.items(): - try: - assert reg not in self._dut_record, f'Traceablity reg {reg} stepping on ATE dlist test name.' #could still be a problem with case sensitivity - except AssertionError as e: - if reg not in ['f_die_loc_y', 'f_die_loc_x', 'f_die_wafer_number', 'f_die_week']: #allow overwrite of a few that are unchanged - raise e - try: - self._dut_record[reg] = f(self) - except KeyError as e: - raise # No more workarounds for missing data post revid 2. - if reg == 'revid': - print(f'{reg} not dlogged. Assumed 1 (2nd Si).') #Ugh! - self._dut_record[reg] = 1 - else: - print(f'{reg} not dlogged. Assumed zero.') - self._dut_record[reg] = 0 - try: - del self._dut_record['RevID'] #duplicate case insensitive column name - except KeyError: - pass # Not logged in early ATE programs - traceability_data = stowe_die_traceability.read_from_dict(register_list=stowe_die_traceability.traceability_registers, - data_dict=self._dut_record, - ) - # if traceability_data['f_die_running_9_8_'] == 0 and traceability_data['f_die_running_7_0_'] == 142: - # # 0xAC87AAA4 - # print(traceability_data) - # breakpoint() - # if traceability_data['f_die_running_9_8_'] == 0 and traceability_data['f_die_running_7_0_'] == 0x30: - # # 0xBE305BAD - # print(traceability_data) - # breakpoint() - traceability_hash = stowe_die_traceability.compute_hash(register_data=traceability_data) - # self._dut_record['traceability_hash'] = f'0x{traceability_hash.hex().upper()}' - # print(f"Traceability (only) hash: 0x{traceability_hash.hex().upper()}") - return f'0x{traceability_hash.hex().upper()}' - def after_begin(self, dataSrc): - self.dataSrc = dataSrc - self.inp_file = self.dataSrc.inp.name - print(f'Processing {self.inp_file}') - self._dut_count = 0 - self._die_traceability_tests = results_ord_dict() - - self.ets_units_scaler = ETS_units_scaler() - - self._units = results_ord_dict() # Fix inconsitent ETS units scaling with Python parser - # if type(self)._db is None: - # type(self)._db = sqlite3.connect('stdf_data.sqlite') #share conn aross concurrent parsers. Bad idea?!?! - # type(self)._cur = type(self)._db.cursor() - # self._db = type(self)._db - # self._cur = type(self)._cur - self._db = sqlite3.connect('stdf_data.sqlite') - self._cur = self._db.cursor() - def after_send(self, dataSrc, data): - if data is None: - breakpoint() - record_type = type(data[0]) - record_data = data[1] - if record_type is pystdf.V4.Far: - # File Attributes Record (FAR) - # Function: Contains the information necessary to determine how to decode the STDF data - # contained in the file. - # CPU_TYPE U*1 CPU type that wrote this file - # STDF_VER U*1 STDF version number - # (, [2, 4]) - pass - elif record_type is pystdf.V4.Mir: - # Master Information Record (MIR) - # Function: The MIR and the MRR (Master Results Record) contain all the global information that - # is to be stored for a tested lot of parts. Each data stream must have exactly one MIR, - # immediately after the FAR (and the ATRs, if they are used). This will allow any data - # reporting or analysis programs access to this information in the shortest possible - # amount of time. - # SETUP_T U*4 Date and time of job setup - # START_T U*4 Date and time first part tested - # STAT_NUM U*1 Tester station number - # MODE_COD C*1 Test mode code (e.g. prod, dev) space - # RTST_COD C*1 Lot retest code space - # PROT_COD C*1 Data protection code space - # BURN_TIM U*2 Burn-in time (in minutes) 65,535 - # CMOD_COD C*1 Command mode code space - # LOT_ID C*n Lot ID (customer specified) - # PART_TYP C*n Part Type (or product ID) - # NODE_NAM C*n Name of node that generated data - # TSTR_TYP C*n Tester type - # JOB_NAM C*n Job name (test program name) - # JOB_REV C*n Job (test program) revision number length byte = 0 - # SBLOT_ID C*n Sublot ID length byte = 0 - # OPER_NAM C*n Operator name or ID (at setup time) length byte = 0 - # EXEC_TYP C*n Tester executive software type length byte = 0 - # EXEC_VER C*n Tester exec software version number length byte = 0 - # TEST_COD C*n Test phase or step code length byte = 0 - # TST_TEMP C*n Test temperature length byte = 0 - # USER_TXT C*n Generic user text length byte = 0 - # AUX_FILE C*n Name of auxiliary data file length byte = 0 - # PKG_TYP C*n Package type length byte = 0 - # FAMLY_ID C*n Product family ID length byte = 0 - # DATE_COD C*n Date code length byte = 0 - # FACIL_ID C*n Test facility ID length byte = 0 - # FLOOR_ID C*n Test floor ID length byte = 0 - # PROC_ID C*n Fabrication process ID length byte = 0 - # OPER_FRQ C*n Operation frequency or step length byte = 0 - # SPEC_NAM C*n Test specification name length byte = 0 - # SPEC_VER C*n Test specification version number length byte = 0 - # FLOW_ID C*n Test flow ID length byte = 0 - # SETUP_ID C*n Test setup ID length byte = 0 - # DSGN_REV C*n Device design revision length byte = 0 - # ENG_ID C*n Engineering lot ID length byte = 0 - # ROM_COD C*n ROM code ID length byte = 0 - # SERL_NUM C*n Tester serial number length byte = 0 - # SUPR_NAM C*n Supervisor name or ID length byte = 0 - # (, [1600092154, 1600092154, 1, 'D', 'E', ' ', 65535, ' ', 'ENG', '1', 'ETS-364-', 'ETS364B', 'LT3390', '0.00', ' ', 'Engineer', 'ETS Test Executive', '2018A [2018.1.2.4]', 'ENG', '25', 'UNUSED', 'C:\\ETS\\APPS\\LT3390\\LT3390.pds', 'Package', 'Power', '08/23/2018', '', 'Boston', '', '', 'LT3390', '0.00', 'ENG1', None, None, None, None, None, None]) - self._master_info = self.map_data(*data) - while True: - corrected_temp = input(f"{self.inp_file} {self._master_info['FLOW_ID']} reports {self._master_info['TST_TEMP']}C. Corrected temperature? [{self._master_info['TST_TEMP']}] ") - if not len(corrected_temp): - break #accept STDF unchanged - try: - float(corrected_temp) # make sure response is a number, but database stores as string according to STDF spec. - self._master_info['TST_TEMP'] = corrected_temp - break - except ValueError: - pass - elif record_type is pystdf.V4.Sdr: - # Site Description Record (SDR) - # Function: Contains the configuration information for one or more test sites, connected to one test - # head, that compose a site group. - # HEAD_NUM U*1 Test head number - # SITE_GRP U*1 Site group number - # SITE_CNT U*1 Number (k) of test sites in site group - # SITE_NUM kxU*1 Array of test site numbers - # HAND_TYP C*n Handler or prober type length byte = 0 - # HAND_ID C*n Handler or prober ID length byte = 0 - # CARD_TYP C*n Probe card type length byte = 0 - # CARD_ID C*n Probe card ID length byte = 0 - # LOAD_TYP C*n Load board type length byte = 0 - # LOAD_ID C*n Load board ID length byte = 0 - # DIB_TYP C*n DIB board type length byte = 0 - # DIB_ID C*n DIB board ID length byte = 0 - # CABL_TYP C*n Interface cable type length byte = 0 - # CABL_ID C*n Interface cable ID length byte = 0 - # CONT_TYP C*n Handler contactor type length byte = 0 - # CONT_ID C*n Handler contactor ID length byte = 0 - # LASR_TYP C*n Laser type length byte = 0 - # LASR_ID C*n Laser ID length byte = 0 - # EXTR_TYP C*n Extra equipment type field length byte = 0 - # EXTR_ID C*n Extra equipment ID length byte = 0 - # (, [1, 255, 4, [1, 2, 3, 4], None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None]) - pass - elif record_type is pystdf.V4.Pir: - # Part Information Record (PIR) - # Function: Acts as a marker to indicate where testing of a particular part begins for each part - # tested by the test program. The PIR and the Part Results Record (PRR) bracket all the - # stored information pertaining to one tested part. - # HEAD_NUM U*1 Test head number - # SITE_NUM U*1 Test site number - # (, [1, 2]) - self._test_results = results_ord_dict() - self._dut_record = results_ord_dict() - self._dut_record['STDF_file'] = os.path.basename(self.inp_file) - self._dut_record.update(self._master_info) - elif record_type is pystdf.V4.Ptr: - # Parametric Test Record (PTR) - # Function: Contains the results of a single execution of a parametric test in the test program. The - # first occurrence of this record also establishes the default values for all semi-static - # information about the test, such as limits, units, and scaling. The PTR is related to the - # Test Synopsis Record (TSR) by test number, head number, and site number. - # TEST_NUM U*4 Test number - # HEAD_NUM U*1 Test head number - # SITE_NUM U*1 Test site number - # TEST_FLG B*1 Test flags (fail, alarm, etc.) - # PARM_FLG B*1 Parametric test flags (drift, etc.) - # RESULT R*4 Test result TEST_FLG bit 1 = 1 - # TEST_TXT C*n Test description text or label length byte = 0 - # ALARM_ID C*n Name of alarm length byte = 0 - # OPT_FLAG B*1 Optional data flag See note - # RES_SCAL I*1 Test results scaling exponent OPT_FLAG bit 0 = 1 - # LLM_SCAL I*1 Low limit scaling exponent OPT_FLAG bit 4 or 6 = 1 - # HLM_SCAL I*1 High limit scaling exponent OPT_FLAG bit 5 or 7 = 1 - # LO_LIMIT R*4 Low test limit value OPT_FLAG bit 4 or 6 = 1 - # HI_LIMIT R*4 High test limit value OPT_FLAG bit 5 or 7 = 1 - # UNITS C*n Test units length byte = 0 - # C_RESFMT C*n ANSI C result format string length byte = 0 - # C_LLMFMT C*n ANSI C low limit format string length byte = 0 - # C_HLMFMT C*n ANSI C high limit format string length byte = 0 - # LO_SPEC R*4 Low specification limit value OPT_FLAG bit 2 = 1 - # HI_SPEC R*4 High specification limit value OPT_FLAG bit 3 = 1 - - # (, [2999900097, 1, 2, 129, 192, 0.0, 'Timeline', 'ALARM', 206, 0, 0, 0, -inf, inf, 'Sec', '%9.0f', '%9.0f', '%9.0f', None, None]) - # (, [2999900098, 1, 2, 0, 192, 0.0, 'UPH', ' ', 206, 0, 0, 0, -inf, inf, 'DUT/Hr', '%9.1f', '%9.1f', '%9.1f', None, None]) - test_result_record = self.map_data(*data) - - - # RESULT The RESULT value is considered useful only if all the following bits from TEST_FLG and PARM_FLG are 0: - # TEST_FLG - # bit 0 = 0 no alarm - # bit 1 = 0 value in result field is valid - # bit 2 = 0 test result is reliable - # bit 3 = 0 no timeout - # bit 4 = 0 test was executed - # bit 5 = 0 no abort - # PARM_FLG - # bit 0 = 0 no scale error - # bit 1 = 0 no drift error - # bit 2 = 0 no oscillation - # If any one of these bits is 1, then the PTR result should not be used. - if ((test_result_record['TEST_FLG'] & 0b111111) | (test_result_record['PARM_FLG'] & 0b111)): - print(f'Invalid test data flags for test {test_result_record["TEST_NUM"]}') - test_result_record['RESULT'] = None - # breakpoint() - ########################## - # STOWE SPECIFIC!!!!!!!! # - ########################## - if test_result_record['TEST_NUM'] >= 901000000 and test_result_record['TEST_NUM'] <= 901099999: - #skip all test 9010s. Fuse resistances are only in FTT, not QA runs. They make the correlation database have too many columns for Sqlite. - return - ########################## - ########################## - # if test_result_record['TEST_NUM'] == 102000022: - # if test_result_record['TEST_NUM'] == 10000001 or \ - # test_result_record['TEST_NUM'] == 14000000 or \ - # test_result_record['TEST_NUM'] == 51000011: - # if test_result_record['RES_SCAL'] != 0: - # print(test_result_record) - # breakpoint() - if test_result_record['TEST_TXT'] != '': - #first record - if re.search('f_die', test_result_record['TEST_TXT']): - self._die_traceability_tests[test_result_record['TEST_NUM']] = test_result_record['TEST_TXT'] - elif re.match('RevID', test_result_record['TEST_TXT']): - self._die_traceability_tests[test_result_record['TEST_NUM']] = test_result_record['TEST_TXT'] - if test_result_record['TEST_NUM'] in self._die_traceability_tests.keys(): - # Keyed by test name. Consider switch to test number??? - try: - self._dut_record[self._die_traceability_tests[test_result_record['TEST_NUM']]] = int(test_result_record['RESULT']) #traceability regs all ints! Why not dlog'd as such?? - except TypeError as e: - self._dut_record[self._die_traceability_tests[test_result_record['TEST_NUM']]] = None #invalid data - if test_result_record['UNITS'] is None: - # Not first record - pass - # elif test_result_record['UNITS'] == '': - # breakpoint() - # elif test_result_record['UNITS'] == 'P/F' or \ - # test_result_record['UNITS'] == 'T/F': - # # Parser can't deal with apparent quotient. Manual bypass hack. - # self._units[test_result_record['TEST_NUM']] = {'UNITS': test_result_record['UNITS'], - # 'MULT' : 1.0, - # 'UNITS_ORIG': test_result_record['UNITS'] - # } - else: - # self._units[test_result_record['TEST_NUM']] = units_parser.parser(test_result_record['UNITS']) #Units info only in first record of each file. - self._units[test_result_record['TEST_NUM']] = self.ets_units_scaler.normalize(test_result_record['UNITS']) #Units info only in first record of each file. - self._units[test_result_record['TEST_NUM']]['UNITS_ORIG'] = test_result_record['UNITS'] - # if True: - # self._debug_units_scaling(test_result_record) - # Fix units scaling with Python Parser. - try: - test_result_record['UNITS'] = self._units[test_result_record['TEST_NUM']]['UNITS'] # Nobody really cares though... - test_result_record['RESULT'] = self._units[test_result_record['TEST_NUM']]['MULT'] * test_result_record['RESULT'] - except TypeError as e: - #Probably missing/invalid flagged data - test_result_record['UNITS'] = None #eh... is this ok??? - test_result_record['RESULT'] = None - except KeyError as e: - # Tests 99.67, 99.68, 99.69 have no test name, and therefore never stored any units. Jerimy said it's ok. It's for MPW sorting. - print(e) - breakpoint() - self._test_results[f"{test_result_record['TEST_NUM']:010d}"] = test_result_record['RESULT'] - elif record_type is pystdf.V4.Prr: - # Part Results Record (PRR) - # Function: Contains the result information relating to each part tested by the test program. The - # PRR and the Part Information Record (PIR) bracket all the stored information - # pertaining to one tested part. - # HEAD_NUM U*1 Test head number - # SITE_NUM U*1 Test site number - # PART_FLG B*1 Part information flag - # NUM_TEST U*2 Number of tests executed - # HARD_BIN U*2 Hardware bin number - # SOFT_BIN U*2 Software bin number 65535 - # X_COORD I*2 (Wafer) X coordinate -32768 - # Y_COORD I*2 (Wafer) Y coordinate -32768 - # TEST_T U*4 Elapsed test time in milliseconds 0 - # PART_ID C*n Part identification length byte = 0 - # PART_TXT C*n Part description text length byte = 0 - # PART_FIX B*n Part repair information length byte = 0 - #(, [1, 2, 0, 1915, 1, 1, -32768, -32768, 1802, '1', None, None]) - self._dut_record.update(self.map_data(*data)) - if self._dut_record['HARD_BIN'] in (65535,) or self._dut_record['SOFT_BIN'] in (65535,): - print(f'Omitting DUT {self._dut_record["FLOW_ID"]} Head {self._dut_record["HEAD_NUM"]} Site {self._dut_record["SITE_NUM"]} Num {self._dut_record["NUM_TEST"]}. HARD_BIN={self._dut_record["HARD_BIN"]} SOFT_BIN={self._dut_record["SOFT_BIN"]}.') - elif self._dut_record['HARD_BIN'] not in [1,2]: #Bin 2 seems to be REVID3 passes for MPW (pizza) sorting purpose. - try: - hash_str = self._compute_traceability_registers() - except KeyError as e: - print(f'Omitting DUT with missing traceability data. HARD_BIN={self._dut_record["HARD_BIN"]}') - else: - if input(f'Allow this part anyway {self._dut_record} [y/n]? ').lower() in ['y', 'yes']: - self._dut_record.update(self._test_results) - self._log(hash_str) - else: - print(f'Omitting DUT {self._dut_record["FLOW_ID"]} Head {self._dut_record["HEAD_NUM"]} Site {self._dut_record["SITE_NUM"]} Num {self._dut_record["NUM_TEST"]}. HARD_BIN={self._dut_record["HARD_BIN"]}.') - elif self._dut_record['SOFT_BIN'] not in [1,2]: - # print(f'Omitting DUT {hash_str}. SOFT_BIN={self._dut_record["SOFT_BIN"]}') - try: - hash_str = self._compute_traceability_registers() - except KeyError as e: - print(f'Omitting DUT with missing traceability data. SOFT_BIN={self._dut_record["SOFT_BIN"]}') - else: - if input(f'Allow this part anyway {self._dut_record} [y/n]? ').lower() in ['y', 'yes']: - self._dut_record.update(self._test_results) - self._log(hash_str) - else: - print(f'Omitting DUT {self._dut_record["FLOW_ID"]} Head {self._dut_record["HEAD_NUM"]} Site {self._dut_record["SITE_NUM"]} Num {self._dut_record["NUM_TEST"]}. SOFT_BIN={self._dut_record["SOFT_BIN"]}.') - else: - hash_str = self._compute_traceability_registers() - self._dut_record.update(self._test_results) - self._log(hash_str) - # print(self._dut_record) - self._dut_record = None - self._test_results = None - elif record_type is pystdf.V4.Tsr: - # Test Synopsis Record (TSR) - # Function: Contains the test execution and failure counts for one parametric or functional test in - # the test program. Also contains static information, such as test name. The TSR is - # related to the Functional Test Record (FTR), the Parametric Test Record (PTR), and the - # Multiple Parametric Test Record (MPR) by test number, head number, and site - # number. - # HEAD_NUM U*1 Test head number See note - # SITE_NUM U*1 Test site number - # TEST_TYP C*1 Test type space - # TEST_NUM U*4 Test number - # EXEC_CNT U*4 Number of test executions 4,294,967,295 - # FAIL_CNT U*4 Number of test failures 4,294,967,295 - # ALRM_CNT U*4 Number of alarmed tests 4,294,967,295 - # TEST_NAM C*n Test name length byte = 0 - # SEQ_NAME C*n Sequencer (program segment/flow) name length byte = 0 - # TEST_LBL C*n Test label or text length byte = 0 - # OPT_FLAG B*1 Optional data flag See note - # TEST_TIM R*4 Average test execution time in seconds OPT_FLAG bit 2 = 1 - # TEST_MIN R*4 Lowest test result value OPT_FLAG bit 0 = 1 - # TEST_MAX R*4 Highest test result value OPT_FLAG bit 1 = 1 - # TST_SUMS R*4 Sumof test result values OPT_FLAG bit 4 = 1 - # TST_SQRS R*4 Sum of squares of test result values OPT_FLAG bit 5 = 1 - # (, [255, 255, 'P', 9900063, 73, 0, 4294967295, 'Fuse Readings - Reg 191', '', '', 203, 1.6989434957504272, 0.0, 0.0, 0.0, 0.0]) - # (, [255, 255, 'P', 9900064, 73, 0, 4294967295, 'Lockout Set', '', '', 203, 1.6989434957504272, 0.0, 0.0, 0.0, 0.0]) - # (, [255, 255, 'P', 10000000, 73, 0, 4294967295, 'Vref Regs - Pre Trim', '', '', 203, 1.6989434957504272, 1.1398190259933472, 1.1470963954925537, 83.49664306640625, 95.50273132324219]) - pass - elif record_type is pystdf.V4.Hbr: - # (, [255, 255, 16, 0, 'F', 'Continuity']) - # (, [255, 255, 17, 0, 'F', 'AbsMax']) - # (, [255, 255, 18, 0, 'F', 'Parametric']) - # (, [255, 255, 19, 0, 'F', 'Leakage']) - # (, [255, 255, 20, 0, 'F', 'Oxide Stress']) - # (, [255, 255, 21, 0, 'F', 'Buck']) - # (, [255, 255, 22, 0, 'F', 'Boost']) - # (, [255, 255, 23, 0, 'F', 'LDO']) - pass - elif record_type is pystdf.V4.Sbr: - # (, [255, 255, 16, 0, 'F', 'Continuity']) - # (, [255, 255, 17, 0, 'F', 'AbsMax']) - # (, [255, 255, 18, 0, 'F', 'Parametric']) - # (, [255, 255, 19, 0, 'F', 'Leakage']) - # (, [255, 255, 20, 0, 'F', 'Oxide Stress']) - # (, [255, 255, 21, 0, 'F', 'Buck']) - # (, [255, 255, 22, 0, 'F', 'Boost']) - # (, [255, 255, 23, 0, 'F', 'LDO']) - pass - elif record_type is pystdf.V4.Pcr: - # (, [1, 1, 0, 0, 4294967295, 0, 4294967295]) - # (, [1, 2, 73, 0, 4294967295, 64, 4294967295]) - # (, [1, 3, 0, 0, 4294967295, 0, 4294967295]) - # (, [1, 4, 0, 0, 4294967295, 0, 4294967295]) - pass - elif record_type is pystdf.V4.Mrr: - # (, [1600092457, None, None, None]) - pass - elif record_type is pystdf.V4.Bps: - # (, ['TouchDownID=0']) - pass - elif record_type is pystdf.V4.Eps: - # (, []) - pass - elif record_type is pystdf.V4.Wcr: - # Wafer Configuration Record (WCR) - # Function: Contains the configuration information for the wafers tested by the job plan. The - # WCR provides the dimensions and orientation information for all wafers and dice - # in the lot. This record is used only when testing at wafer probe time. - # Data Fields: - # REC_LEN U*2 Bytes of data following header - # REC_TYP U*1 Record type (2) - # REC_SUB U*1 Record sub-type (30) - # WAFR_SIZ R*4 Diameter of wafer in WF_UNITS 0 - # DIE_HT R*4 Height of die in WF_UNITS 0 - # DIE_WID R*4 Width of die in WF_UNITS 0 - # WF_UNITS U*1 Units for wafer and die dimensions 0 - # WF_FLAT C*1 Orientation of wafer flat space - # CENTER_X I*2 X coordinate of center die on wafer -32768 - # CENTER_Y I*2 Y coordinate of center die on wafer -32768 - # POS_X C*1 Positive X direction of wafer space - # POS_Y C*1 Positive Y direction of wafer space - # Notes on Specific Fields: - # WF_UNITS Has these valid values: - # 0 = Unknown units - # 1 = Units are in inches - # 2 = Units are in centimeters - # 3 = Units are in millimeters - # 4 = Units are in mils - # WF_FLAT Has these valid values: - # U = Up - # D = Down - # L = Left - # R = Right - # space = Unknown - # CENTER_X, - # CENTER_Y - # Use the value -32768 to indicate that the field is invalid. - # POS_X Has these valid values: - # L = Left - # R = Right - # space = Unknown - # POS_Y Has these valid values: - # U = Up - # D = Down - # space = Unknown - # Frequency: One per STDF file (used only if wafer testing). - # Location: Anywhere in the data stream after the initial sequence (see page 14), and before the MRR. - # Possbile Use: Wafer Map - - # (, [0.0, 0.0, 0.0, 0, ' ', -32768, -32768, ' ', ' ']) - # Doesn't appear to be filled out properly in our ETS probe setup. - pass - elif record_type is pystdf.V4.Wir: - # Wafer Information Record (WIR) - # Function: Acts mainly as a marker to indicate where testing of a particular wafer begins for each - # wafer tested by the job plan. The WIR and the Wafer Results Record (WRR) bracket all - # the stored information pertaining to one tested wafer. This record is used only when - # testing at wafer probe. A WIR/WRR pair will have the same HEAD_NUM and SITE_GRP - # values. - # Data Fields: - # REC_LEN U*2 Bytes of data following header - # REC_TYP U*1 Record type (2) - # REC_SUB U*1 Record sub-type (10) - # HEAD_NUM U*1 Test head number - # SITE_GRP U*1 Site group number 255 - # START_T U*4 Date and time first part tested - # WAFER_ID C*n Wafer ID length byte = 0 - # Notes on Specific Fields: - # SITE_GRP Refers to the site group in the SDR. This is ameans of relating the wafer information to the configuration of the equipment used to test it. If this information is not known, or the tester does not support the concept of site groups, this field should be set to 255. - # WAFER_ID Is optional, but is strongly recommended in order to make the resultant data files as useful as possible. - # Frequency: One per wafer tested. - # Location: Anywhere in the data stream after the initial sequence (see page 14) and before the MRR. - # Sent before testing each wafer. - - # (, [1, 255, 1620737799, 'W01']) - # DJS: WAFER_ID might be useful to disambiguate coordinate data when multiple wafer lots are mixed into a single production lot. - pass - else: - # DJS: Wrr looks useful, but haven't seen it in an ETS probe dlog yet. - print('Unknown record type!') - print(data) - breakpoint() - def after_complete(self, dataSrc): - # print(self._die_traceability_tests) #Use to fine f_die registers! - # print(self._units) # Check no crazy parser mistakes. - - self.ets_units_scaler.print_unkown_units() - - print(f'Processed {self._dut_count} DUTs.') - self._db.commit() - self._db.close() - # print('end!') - # def after_cancel(self, exc): - # print('die!') - # raise exc - - - - -class population_data(master_parser): - '''log single table with traceability column''' - # (Pdb) self._dut_record results_ord_dict([('STDF_file', '.\\LT3390H_ENG_QA_COLD_m50C_ENG_01142021_132648.std_1'), ('SETUP_T', 1610630929), ('START_T', 1610630929), ('STAT_NUM', 1), ('MODE_COD', 'D'), ('RTST_COD', 'E'), ('PROT_COD', ' '), ('BURN_TIM', 65535), ('CMOD_COD', ' '), ('LOT_ID', 'ENG'), ('PART_TYP', 'LT3390H'), ('NODE_NAM', 'BOS-EAGLE2'), ('TSTR_TYP', 'ETS364B'), ('JOB_NAM', 'LT3390'), ('JOB_REV', '0.00'), ('SBLOT_ID', ' '), ('OPER_NAM', 'Engineer'), ('EXEC_TYP', 'ETS Test Executive'), ('EXEC_VER', '2018A [2018.1.2.4]'), ('TEST_COD', 'ENG'), ('TST_TEMP', -50.0), ('USER_TXT', 'UNUSED'), ('AUX_FILE', 'C:\\ETS\\APPS\\LT3390\\LT3390.pds'), ('PKG_TYP', 'Package'), ('FAMLY_ID', 'Power'), ('DATE_COD', '08/23/2018'), ('FACIL_ID', ''), ('FLOOR_ID', 'Boston'), ('PROC_ID', ''), ('OPER_FRQ', ''), ('SPEC_NAM', 'LT3390'), ('SPEC_VER', '0.00'), ('FLOW_ID', 'QA COLD'), ('SETUP_ID', None), ('DSGN_REV', None), ('ENG_ID', None), ('ROM_COD', None), ('SERL_NUM', None), ('SUPR_NAM', None), ('f_die_year (Since 2000)', 21), ('f_die_week (eng=month)', 1), ('f_die_wafer_number (eng=day)', 14), ('f_die_loc_x (eng=hour)', 10), ('f_die_loc_y (eng=minute)', 48), ('f_die_running (eng=serial #)', 6), ('f_die_fab', 0), ('f_die_parent_child', 0), ('f_die_crc', 0), ('HEAD_NUM', 1), ('SITE_NUM', 2), ('PART_FLG', 0), ('NUM_TEST', 609), ('HARD_BIN', 1), ('SOFT_BIN', 1), ('X_COORD', -32768), ('Y_COORD', -32768), ('TEST_T', 1669), ('PART_ID', '1'), ('PART_TXT', None), ('PART_FIX', None), ('revid', 3), ('f_die_fab_2_0_', 0), ('f_die_fab_4_3_', 0), ('f_die_parent_child_9_8_', 0), ('f_die_loc_y', 48), ('f_die_loc_x', 10), ('f_die_running_7_0_', 6), ('f_die_parent_child_7_0_', 0), ('f_die_fab_6_5_', 0), ('f_die_wafer_number', 14), ('f_die_running_9_8_', 0), ('f_die_week', 1), ('f_die_year', 21), ('2999900097', 0.0), ('2999900098', 0.0), ('2999900099', 0.017000000923871994), ('2999900100', 0.08299999684095383), ('2999900101', 2.305555608472787e-05), ('0000100000', -0.5450356602668762), ('0000100001', -0.5449007153511047), ('0000100002', -0.5442603826522827), ('0000100003', -0.5431643128395081), ('0000100004', -0.5447741150856018), ('0000100005', -0.5448459386825562), ('0000100006', -0.5448157787322998), ('0000100007', -0.5452605485916138), ('0000100008', -0.928746223449707), ('0000100009', -0.6678857207298279), ('0000100010', -0.8823137283325195), ('0000100011', -0.679685115814209), ('0000100012 - def _log(self, traceability_hash): - table_name = "population_data" - self._dut_record['TRACEABILITY_HASH'] = traceability_hash - missing_column_exc_str = f'^table {table_name} has no column named (?P.+$)' - unique_exc_str = f'^UNIQUE constraint failed: (?P.+$)' - def _add_column(column_name): - print(f'Column {column_name} is missing from table {table_name}. Attempting to add it. {self.inp_file}') - self._cur.execute(f'ALTER TABLE "{table_name}" ADD COLUMN "{column_name}"') - self._db.commit() - col_str = ', '.join([f'"{k}"' for k in self._dut_record.keys()]) - try: - ## RHM - In conference with DJS, we really don't know why PART_ID is in the primary key. It allows through retests of the same dut. - # self._cur.execute(f'CREATE TABLE IF NOT EXISTS "{table_name}" ({col_str}, PRIMARY KEY (TRACEABILITY_HASH, STDF_file, PART_ID))') - self._cur.execute(f'CREATE TABLE IF NOT EXISTS "{table_name}" ({col_str}, PRIMARY KEY (TRACEABILITY_HASH, STDF_file))') - self._db.commit() - except sqlite3.OperationalError as e: - print(e) - print(len(self._dut_record.keys())) - print('Too many columns???') - raise e - try: - # self._cur.execute(f'INSERT OR IGNORE INTO "{table_name}" ({col_str}) VALUES ({", ".join(["?" for i in range(len(self._dut_record))])})', tuple(self._dut_record.values())) - self._cur.execute(f'INSERT INTO "{table_name}" ({col_str}) VALUES ({", ".join(["?" for i in range(len(self._dut_record))])})', tuple(self._dut_record.values())) - self._db.commit() #thread locking issue. Will slow down script! - self._dut_count += 1 - except sqlite3.OperationalError as e: - # if re.match(r'table 0xB0DEA1B1 has no column named 103000055', str(e)): - # r'Bus: {netlist_name}\s+State = (?P0x[0-9a-f]+)' - missing_col = re.match(missing_column_exc_str, str(e)) - if missing_col: - _add_column(missing_col.group("test_number")) - #Try again recursively - self._log(traceability_hash) - # Column 103000055 is missinng from table 0xB0DEA1B1. Attempting to add it. - else: - print(e) - breakpoint() - raise e - except sqlite3.IntegrityError as e: - # sqlite3.IntegrityError: UNIQUE constraint failed: population_data.TRACEABILITY_HASH, population_data.STDF_file - # 2022-10-26 db is good test case - unique_violation = re.match(unique_exc_str, str(e)) - if unique_violation: - col_violations = unique_violation.group('constraint_cols').split(', ') - stdf_file = self._dut_record['STDF_file'] - print("UNIQUE constraint failure. Either you are re-running this script on an existing database, or the STDF data contains duplicated re-test data for the same DUT.") - print("If re-running, please try deleting existing database and trying again.") - print(f"DUT:{traceability_hash} in {stdf_file}") - if input(f'Replace previous DUT record with latest (re-test) record ({self._dut_record["PART_ID"]}) [y/n]? ') in ['y', 'yes']: - #Note, this would allow replacement of a bin1 with a bin!1 if data existed in the STDF file in that test ordering. - self._cur.execute(f'DELETE FROM {table_name} WHERE TRACEABILITY_HASH == "{traceability_hash}" AND STDF_file == "{stdf_file}"') - self._dut_count -= 1 - self._log(traceability_hash) - else: - raise e - else: - #unknown problem - raise e - -def copy_ATE_data(source_db=None, dest_db=None): - if source_db is None: - source_db = './stdf_data.sqlite' - source_conn = sqlite3.connect(source_db) - if dest_db is None: - dest_db = os.path.join(os.path.dirname(__file__), '../../correlation/stdf_data.sqlite') - # p4_dest_info = p4_traceability.get_fstat(dest_db) - # if p4_dest_info['depotFile'] is not None and p4_info['action'] is None: - # # TODO: Ask to p4 check out - # print('Skipping copy. Destination database checked in') - # return False - source_dest_table = 'population_data' - conn = sqlite3.connect(dest_db) - conn.row_factory = sqlite3.Row - attach_schema = '__source_db__' - conn.execute(f"ATTACH DATABASE '{source_db}' AS {attach_schema}") - source_cols = set(conn.execute(f'SELECT * FROM {attach_schema}.{source_dest_table} LIMIT 1').fetchone().keys()) - dest_cols = set(conn.execute(f'SELECT * FROM {source_dest_table} LIMIT 1').fetchone().keys()) - extra_source_cols = source_cols - dest_cols - if len(extra_source_cols): - for col in extra_source_cols: - print(col) - if input(f'Modify destination table columns to accommodate extra source columns [y/n]? ') in ['y', 'yes']: - for col in extra_source_cols: - sql_alter = f'ALTER TABLE {source_dest_table} ADD COLUMN "{col}"' - print(sql_alter) - conn.execute(sql_alter) - else: - #Crash coming no matter what!!! - raise Exception('Column mismatch uncorrected.') - insert_str = f'''INSERT INTO {source_dest_table} ("{'","'.join(source_cols)}")\n SELECT\n"{'","'.join(source_cols)}"\nFROM\n{attach_schema}.{source_dest_table}''' - # print(insert_str) - try: - conn.execute(insert_str) - conn.commit() - except sqlite3.IntegrityError as e: - # assumed! Could copy regex from above to be certain. - print("UNIQUE constraint failure. Most likely you are re-inserting existing data. Try pruning first.") - raise e - finally: - conn.close() - -def process_file(filename): - with open(filename, 'rb') as f: - p = Parser(inp=f, reopen_fn=None) - p.addSink(population_data()) - p.parse() - f.close() - -def process_dir(top_dir): - if os.path.splitext(top_dir)[1] in ['.std_1', '.stdf']: - # Single file - process_file(top_dir) - else: - # Directory tree - for (dirpath, dirnames, filenames) in os.walk(top_dir, topdown=True, onerror=None, followlinks=False): - for filename in filenames: - filebase, file_extension = os.path.splitext(filename) - if file_extension in ['.std_1', '.stdf']: - process_file(os.path.join(dirpath, filename)) - else: - print(f'rejected file extension: {filename}, {file_extension}') - -def clean_dir(top_dir): - if os.path.splitext(top_dir)[1] in ['.std_1', '.stdf']: - # Single file - dest_db = os.path.join(os.path.dirname(__file__), '../../correlation/stdf_data.sqlite') - dest_table = 'population_data' - conn = sqlite3.connect(dest_db) - cur = conn.cursor() - sql = f'DELETE FROM {dest_table} WHERE STDF_file == "{top_dir}" OR STDF_file == "{os.path.basename(top_dir)}"' - print(sql) - cur.execute(sql) - print(f'{cur.rowcount} rows deleted from {top_dir}.') - conn.commit() - conn.close() - else: - # Directory tree - for (dirpath, dirnames, filenames) in os.walk(top_dir, topdown=True, onerror=None, followlinks=False): - for filename in filenames: - filebase, file_extension = os.path.splitext(filename) - if file_extension in ['.std_1', '.stdf']: - # process_file(os.path.relpath(os.path.abspath(os.path.join(dirpath, filename)), start=os.path.abspath(top_dir)), filepath=top_dir) - # process_file(os.path.abspath(os.path.join(dirpath, filename))) - clean_dir(os.path.join(dirpath, filename)) - else: - print(f'rejected file extension: {filename}, {file_extension}') - -if __name__ == "__main__": - # process_dir(r'../../correlation') - if input(f'Prune STDF data from present working directory from correlation lookup database [y/n]? ').lower() in ['y', 'yes']: - clean_dir(r'.') - if input(f'Process STDF files from present working directory [y/n]? ').lower() in ['y', 'yes']: - process_dir(r'.') - if input(f'Merge database from present working directory with corrlation lookup database [y/n]? ') in ['y', 'yes']: - copy_ATE_data() - - -''' -Helpful queries. TODO: Functionalize someday? -Duplicate search: -SELECT a.stdf_file, b.stdf_file, a.revid, traceability_hash, a.oper_frq, b.oper_frq -FROM population_data as a join population_data as b -USING (traceability_hash) -WHERE -a.flow_id == b.flow_id AND -a.stdf_file != b.stdf_file AND -a.rowid != b.rowid - -Duplicate removal: -DELETE FROM population_data WHERE STDF_file == '.\5305699_LT33903_25C_QA100PCT_PRI_QA_ROOM_LT3390_BOS-EAGLE2_20210519_151447.std_1' -''' diff --git a/PyICe/refid_modules/temptroller.py b/PyICe/refid_modules/temptroller.py deleted file mode 100644 index 6c3cb21..0000000 --- a/PyICe/refid_modules/temptroller.py +++ /dev/null @@ -1,643 +0,0 @@ -from PyICe.refid_modules import bench_identifier, test_archive -from PyICe import virtual_instruments -from PyICe.lab_utils.banners import print_banner -from PyICe.lab_utils.sqlite_data import sqlite_data -import abc -import collections -import datetime -import functools -import numbers -import os -import inspect -# os.environ['FOR_IGNORE_EXCEPTIONS'] = '1' -# os.environ['FOR_DISABLE_CONSOLE_CTRL_HANDLER'] = '1' -import socket -import sqlite3 -import subprocess -import urllib -import traceback -from email.mime.image import MIMEImage - -try: - import cairosvg - cairo_ok = True -except Exception as e: - print(e) - traceback.print_exc() - cairo_ok = False -#TODO rebuild sweep finding infrastructure? - - -#Intercept Fortran ctrl-c handling: -# https://stackoverflow.com/questions/15457786/ctrl-c-crashes-python-after-importing-scipy-stats -try: - import win32api - import _thread - def sig_handler(dwCtrlType, hook_sigint=_thread.interrupt_main): - if dwCtrlType == 0: # CTRL_C_EVENT - print("Here I am") - hook_sigint() - return 1 # don't chain to the next handler - return 0 # chain to the next handler - win32api.SetConsoleCtrlHandler(sig_handler, 1) -except Exception as e: - #os error on Linux?!?! - print(type(e)) - print(e) - traceback.print_exc() - -def ctrlc(sig, frame): - raise KeyboardInterrupt("CTRL-C!") - -import signal -# # signal.signal(signal.SIGINT, signal.default_int_handler) -signal.signal(signal.SIGINT, ctrlc) - -# import win32api -# def doSaneThing(sig, func=None): - # print("Here I am") - # raise KeyboardInterrupt -# win32api.SetConsoleCtrlHandler(doSaneThing, 1) - - - - - -class temptroller(): - def __init__(self, temperatures=None, debug=False): # lab_bench=None, temperatures=[25]): - self._tests = [] - self._debug = debug - self.plug_instances=[] - if hasattr(self, 'plugins'): ## Hokay. Slight issue here. If there's a regression going on, how do we make each test_module get a personal instance of a plugin?. - self.interplugs={} - for plugin in self.plugins: - plug = plugin(temptroller=self) - self.plug_instances.append(plug) - if temperatures is None: - temperatures = (None,) - else: - temperatures = tuple(temperatures) - assert None not in temperatures - for temp in temperatures: - assert isinstance(temp, numbers.Number) - assert temp >= -60 - assert temp <= 171 #What's safe? - assert len(temperatures) - self._temperatures = temperatures - self._plots = [] - # self.set_lab_bench_constructor(self._get_bench_instruments()) - # self.set_lab_bench_constructor(self._get_bench_instruments()) - self._lab_bench_constructor = None - def get_bench_identity(self): - '''just used to differentiate notifications. Not necessarily related to acutal bench in use if overridden.''' - thismachine = socket.gethostname() - thisuser = thisbench = os.getlogin() - return {'host': thismachine, - 'user': thisuser, - } - def notify(self, msg, subject=None, attachment_filenames=[], attachment_MIMEParts=[]): - if not self._debug: - try: - self._lab_bench.notify(msg, subject=subject, attachment_filenames=attachment_filenames, attachment_MIMEParts=attachment_MIMEParts) - except AttributeError as e: - if not len(attachment_filenames) and not len(attachment_MIMEParts): - print(msg) - else: - if not len(attachment_filenames) and not len(attachment_MIMEParts): - print(msg) - def set_lab_bench_constructor(self, lab_bench_constructor): - '''override automatic bench lookup mechanism for special equipment setup. - Usually not necessary - constructure should be executable with no arguments and return an object with (minimally) channel master and notification function attributes''' - self._lab_bench_constructor = lab_bench_constructor - - def add_test(self, test_module, repeat_count=1): - assert repeat_count > 0 - if repeat_count > 1: - for c in range(repeat_count): - iter_test = test_module(debug=self._debug) - iter_test.tt=self - iter_test.hook_functions('tm_set', iter_test) - db_filename = f"repeatability_{c:03d}.sqlite" #TODO: common file, different tables? - iter_test._db_file = os.path.join(iter_test._module_path, db_filename) - self._tests.append(iter_test) - else: - solo_test = test_module(debug=self._debug) - solo_test.tt=self - solo_test.hook_functions('tm_set', solo_test) - self._tests.append(solo_test) - if self._lab_bench_constructor is None: - self.set_lab_bench_constructor(bench_identifier.get_bench_instruments(test_module.project_folder_name)) - def __iter__(self): - return iter(self._tests) - def __len__(self): - return len(self._tests) - @abc.abstractmethod - def _get_bench_instruments(self): - '''Returns instruments used on the test bench''' - def hook_functions(self, hook_key, *args): - for test in self._tests: - for plugin in test.plug_instances: - for (k,v) in plugin.get_hooks().items(): - if k is hook_key: - for f in v: - f(*args) - def collect(self): - oven_timer = virtual_instruments.timer() - oven_timer.add_channel_total_minutes('oven_time_tot_min') - oven_timer.add_channel_delta_minutes('oven_time_delta_min') - troller_timer = virtual_instruments.timer() - troller_timer.add_channel_total_minutes('temptroller_time_min') - troller_timer.resume_timer() - try: - # self._lab_bench = self._lab_bench_constructor()() - self._lab_bench = self._lab_bench_constructor() - # self._lab_bench.master.background_gui() #TODO dumps core on VDI/Py38! - if self._debug: - #Make tests easier to debug by preventing crashes from happening in worker threads. - self._lab_bench.get_master().set_allow_threading(False) - self.hook_functions('begin_collect') - summary_msg = '{user} on {host}\n'.format(**self.get_bench_identity()) - door_heater_exists = True if ('door_heater' in self._lab_bench.get_master().get_all_channel_names() and None not in self._temperatures) else False - for test in self: - test._setup(lab_bench=self._lab_bench) - summary_msg += f'\t* {test.get_name()}*\n' - for (temp_idx, temperature) in enumerate(self._temperatures): - test_count = 0 - for test in self: - if test.temp_is_included(temperature): - test_count += 1 - if test_count == 0: - continue - if temperature is not None: - if self._debug: - self._lab_bench.get_master()['tdegc_soak'].write(30) - print(f'#########WARNING!!!!!This test is running in debug mode. Soak time will be 30s#########') - if input('Continue? [y/n] ').lower() not in ['y', 'yes']: - raise Exception('Soak time is insufficient') - summary_msg += f"\nSetting temperature to {temperature}°C\n" - self.notify(summary_msg, subject='Next Temperature') - summary_msg = '{user} on {host}\n'.format(**self.get_bench_identity()) - while True: - try: - oven_timer.resume_timer() - if door_heater_exists: - self._lab_bench.get_master()['door_heater'].write('ON' if temperature <=20 else 'OFF') - self._lab_bench.get_master()['tdegc_enable'].write(True) - self._lab_bench.get_master()['tdegc'].write(temperature) - break - except Exception: - oven_timer.pause_timer() - self._lab_bench.get_master()['tdegc_enable'].write(False) - if door_heater_exists: - self._lab_bench.get_master()['door_heater'].write('OFF') - input(f'Oven failed to settle to {temperature}. Replace the tank and hit Enter to try again.') - oven_timer_data = oven_timer.read_all_channels() - oven_timer.pause_timer() - summary_msg += f'*** {temperature}°C Summary ***\n' - summary_msg += f'\t* Oven slew and settle took {oven_timer_data["oven_time_delta_min"]:3.1f} minutes. *\n' - cumulative_tests_time = oven_timer_data["oven_time_tot_min"] - loop_time = oven_timer_data["oven_time_delta_min"] - else: - cumulative_tests_time = 0 #no oven; no previous temperatures or tests - loop_time=0 - test_time_remaining=0 - script_time={} - # def delete_soon_to_be_rewritten_data(test,temp): - # table_name = test.get_db_table_name() - # db_file = test._db_file - # db = sqlite_data(database_file=db_file, table_name=table_name) - # cur=db.conn.cursor() - # cur.execute(f'DELETE FROM {table_name} WHERE tdegc is {temp}') - # cur.close() - # db.conn.commit() - # test._crashed=None - for (test_idx, test) in enumerate(self): - if (not test.in_range(temperature)) or (temperature in test.excluded_temperatures): - temp_message=f"Skipping {test.get_name()}. " + (f"Temp out of range." if (not test.in_range(temperature)) else f"Excluded temp.") - print_banner(temp_message) - summary_msg += f'\t* {test.get_name()} skipped due to temperature. 0 minutes at {temperature}°C"' - try: - summary_msg += f', {script_time[test]:3.1f} minutes total. *\n' - test_time_remaining+=len([x for x in self._temperatures[temp_idx+1:] if test.is_included(x)]) * script_time[test] - except: - summary_msg += f'.\n' - continue - test.test_timer.resume_timer() - timer_data = test.test_timer.read_all_channels() - if not test.is_crashed(): - while True: - if door_heater_exists: - self._lab_bench.get_master()['door_heater'].write('ON' if temperature <=20 else 'OFF') - test._collect() - if test.is_crashed(): - self.notify(test.crash_info(), subject='CRASHED!!!') #First crash - if temperature is not None and abs(self._lab_bench.get_master()['tdegc_sense'].read()-temperature)>5: # Implies the tank ran out of gas. - self.notify(f'{self.get_bench_identity()["user"]} on {self.get_bench_identity()["host"]}\n'+f'While running {test.get_name()}, the oven failed to maintain its set temperature of {temperature}C. Pausing the regression.', subject='Temperature Walked!') - self._lab_bench.get_master()['tdegc_enable'].write(False) - if door_heater_exists: - self._lab_bench.get_master()['door_heater'].write('OFF') - once_more= input('Oven failed to maintain its temperature. Try again? [y/[n]]: ').lower() - if once_more == 'y': - input(f'Replace the tank then hit Enter to try again.') - # delete_soon_to_be_rewritten_data - table_name = test.get_db_table_name() - db_file = test._db_file - db = sqlite_data(database_file=db_file, table_name=table_name) - cur = db.conn.cursor() - cur.execute(f'DELETE FROM {table_name} WHERE tdegc is {temperature}') - cur.close() - db.conn.commit() - test._crashed=None - self.notify(f'{self.get_bench_identity()["user"]} on {self.get_bench_identity()["host"]}\n'+f'Rerunning {test.get_name()} at {temperature}C, and resuming the regression.', subject='Regression Resumed') - if door_heater_exists: - self._lab_bench.get_master()['door_heater'].write('ON' if temperature <=20 else 'OFF') - self._lab_bench.get_master()['tdegc_enable'].write(True) - self._lab_bench.get_master()['tdegc'].write(temperature) - else: - break - else: - break - timer_data = test.test_timer.read_all_channels() - test.test_timer.pause_timer() - cumulative_tests_time += timer_data["test_time_min"] - if test.is_crashed(): - summary_msg += f'\t* {test.get_name()} crashed/skipped. {timer_data["loop_time_min"]:3.1f} minutes at {temperature}°C, {timer_data["test_time_min"]:3.1f} minutes total. *\n' - else: - summary_msg += f'\t* {test.get_name()} completed. {timer_data["loop_time_min"]:3.1f} minutes at {temperature}°C, {timer_data["test_time_min"]:3.1f} minutes total. *\n' - loop_time+=timer_data["loop_time_min"] - script_time[test]=timer_data["loop_time_min"] - test_time_remaining+=sum(map(lambda x: test.temp_is_included(x),self._temperatures[temp_idx+1:])) * timer_data["loop_time_min"] - frac_complete = 1.*(temp_idx+1)/len(self._temperatures) - frac_incomplete = 1-frac_complete - # etr = datetime.timedelta(minutes=cumulative_tests_time * frac_incomplete / frac_complete) - etr = datetime.timedelta(minutes=test_time_remaining + 10*len(self._temperatures[temp_idx+1:])) - summary_msg += f'{temp_idx+1} of {len(self._temperatures)} temperatures complete in {cumulative_tests_time:.1f} minutes.\n' - if temp_idx+1 < len(self._temperatures): - summary_msg += f'ETR: {etr.total_seconds()/60.:.0f} minutes.\n' - summary_msg += f'ETC: {(datetime.datetime.now()+etr).strftime("%a %b %d %H:%M")}.\n' - troller_time_data = troller_timer.read_all_channels() - troller_timer.pause_timer() - finish_msg = summary_msg - finish_msg += f'\n*** All tests completed. Total time: {troller_time_data["temptroller_time_min"]:3.1f} minutes. ***\n' - if temperature is not None: - finish_msg += f'\t* Oven slewing and settling took {oven_timer_data["oven_time_tot_min"]:3.1f} minutes total. (Average {oven_timer_data["oven_time_tot_min"]/len(self._temperatures):3.1f} minutes per temperature.) *\n' - for test in self: - test._teardown() - test._add_db_indices(table_name=None, db_file=None) - if test.is_crashed(): - finish_msg += f'\t* {test.crash_info()} *\n' - else: - timer_data = test.test_timer.read_all_channels() - finish_msg += f'\t* Test {test.get_name()} completed. {timer_data["test_time_min"]:3.1f} minutes total. *\n' - except Exception as e: - # Oven crash, for example - try: - self.notify(f'{self.get_bench_identity()["user"]} on {self.get_bench_identity()["host"]}\n'+str(e), subject='Temptroller crash!') - except: - pass - raise e - else: - self.notify(finish_msg, subject='Collection Complete') # Finish_msg might not be defined in the event of a crash. - finally: - try: - self._lab_bench.cleanup() - except AttributeError as e: - # Didn't get far enough to create a _lab_bench before crashing. - pass - except Exception as e: - # Don't let bench problem prevent oven cleanup - try: - self.notify(f'{self.get_bench_identity()["user"]} on {self.get_bench_identity()["host"]}\n'+str(e), subject='Bench cleanup crash!') - except: - pass - print(e) #remove? - try: - self._lab_bench.cleanup_oven() - except AttributeError as e: - # Didn't get far enough to create a _lab_bench before crashing. - pass - except Exception as e: - # Don't let bench problem prevent port close - try: - self.notify(f'{self.get_bench_identity()["user"]} on {self.get_bench_identity()["host"]}\n'+str(e), subject='Oven cleanup crash!') - except: - pass - print(e) #remove? - try: - self._lab_bench.close_ports() - except AttributeError as e: - # Didn't get far enough to create a _lab_bench before crashing. - pass - except Exception as e: - # Not sure if this should crash now or not, or how it might happen - try: - self.notify(f'{self.get_bench_identity()["user"]} on {self.get_bench_identity()["host"]}\n'+str(e), subject='Port cleanup crash!') - except: - pass - traceback.print_exc() - # raise e - def plot(self): - for test in self: - plts = test._plot() - self._plots.extend(plts) - if test._plot_crashed is not None: - (typ, value, trace_bk) = test._plot_crashed - notify_message = f'{test.get_name()} plot crash! Moving on.\n' - notify_message += f'{"".join(traceback.format_exception(typ, value, trace_bk))}\n' - self.notify(f'{self.get_bench_identity()["user"]} on {self.get_bench_identity()["host"]}\n'+notify_message) - if len(self._plots): #Don't send empty emails - self.email_plots(self._plots) - return self._plots - def email_plots(self, plot_svg_source): - msg_body = '' - attachment_MIMEParts=[] - for (i,plot_src) in enumerate(plot_svg_source): - if cairo_ok: - plot_png = cairosvg.svg2png(bytestring=plot_src) - plot_mime = MIMEImage(plot_png, 'image/png') - plot_mime.add_header('Content-Disposition', 'inline') - plot_mime.add_header('Content-ID', f'') - msg_body += f'' - else: - plot_mime = MIMEImage(plot_src, 'image/svg+xml') - plot_mime.add_header('Content-Disposition', 'attachment', filename=f'G{i:03d}.svg') - plot_mime.add_header('Content-ID', f'') - attachment_MIMEParts.append(plot_mime) - self.notify(msg_body, subject='Plot Results', attachment_MIMEParts=attachment_MIMEParts) - - def _archive(self, interactive=True): - # This method depends on some state collected by successfully completing the collect() method. - if functools.reduce(lambda a,b: a or b, [True if test._archive_table_name is not None else False for test in self]): - if interactive: - archive = input('Manage collected data? [[y]/n]: ').lower() not in ["n", "no"] - else: - archive = True - if archive: - self.folder_suggestion = datetime.datetime.utcnow().strftime("%Y_%m_%d_%H_%M") - self.hook_functions('begin_archive') - for test in self: - if test._archive_table_name is None: - continue - if test._plot_crashed is not None: - # Going to solicit input no matter what the interactive setting is. There's not obviously right answer when things have gone this unexpectedly wrong. - print(test._plot_crashed) - while True: - arch_resp = input('Archive data with crashed plotting? [y/[n]]: ').lower() - if len(arch_resp): - break - if arch_resp not in ['y', 'yes']: - continue - try: - tdegc_conn = sqlite3.connect(test._db_file, detect_types=sqlite3.PARSE_DECLTYPES|sqlite3.PARSE_COLNAMES) - tdegc_conn.row_factory = sqlite3.Row #index row data tuple by column name - row = tdegc_conn.execute(f"SELECT tdegc FROM {test._archive_table_name}").fetchone() - assert row is not None #Empty table! - if row['tdegc'] is None: - self.folder_suggestion = f'{self.folder_suggestion}__AMBIENT' - else: - self.folder_suggestion = f'{self.folder_suggestion}__TEMPERATURE' - break - except sqlite3.OperationalError as e: - print(f'tdegc information not available from {test.get_name()} results database.') - continue - except Exception as e: - print(test.get_name()) - print(type(e)) - print(e) - traceback.print_exc() - print('This is unexpected. Please contact PyICe Support at PyICe-developers@analog.com with this stack trace.') - # For whatever reason, this data doesn't seem to be suitable for collecting traceability info. To be revisited later. - continue - if interactive: - archive_folder = test_archive.database_archive.ask_archive_folder(suggestion=self.folder_suggestion) - else: - archive_folder = self.folder_suggestion - archived_tables = [] - for test in self: - if test._archive_table_name is not None: - # Test completed - archiver = test_archive.database_archive(db_source_file=test._db_file) - db_dest_file = archiver.compute_db_destination(archive_folder) - if interactive: - resp = archiver.disposition_table(table_name=test._archive_table_name, db_dest_file=db_dest_file, db_indices=test._column_indices) - if resp is not None: - archived_tables.append((test, resp[0], resp[1])) #return ((db_dest_table, db_dest_file)) - test._add_db_indices(table_name=resp[0], db_file=resp[1]) - else: - archiver.copy_table(db_source_table=test._archive_table_name, db_dest_table=test._archive_table_name, db_dest_file=db_dest_file, db_indices=test._column_indices) - archived_tables.append((test, test._archive_table_name, db_dest_file)) - test._add_db_indices(table_name=test._archive_table_name, db_file=db_dest_file) - if len(archived_tables): - arch_plot_scripts = [] - if interactive: - waps = input('Write archive plot script(s)? [[y]/n]: ').lower() not in ["n", "no"] - else: - waps = True - if waps: - for (test, db_table, db_file) in archived_tables: - arch_plot_script = test_archive.database_archive.write_plot_script(test_module=test.get_import_str(), test_class=test.get_name(), db_table=db_table, db_file=db_file) - arch_plot_scripts.append(arch_plot_script) - if len(arch_plot_scripts) and (not interactive or input('Generate archive plot(s)? [[y]/n]: ').lower() not in ['n', 'no']): - for (test, db_table, db_file) in archived_tables: - db = sqlite_data(database_file=db_file, table_name=db_table) - plot_filepath = os.path.join(os.path.dirname(db_file), db_table) - try: - test.plot(database=db, table_name=test._archive_table_name, plot_filepath=plot_filepath, skip_output=False) - except TypeError: - test.plot(database=db, table_name=test._archive_table_name, plot_filepath=plot_filepath) - if len(arch_plot_scripts) > 1 and (not interactive or input('Write summary plots script? [[y]/n]: ').lower() not in ['n', 'no']): - os.mkdir(self.folder_suggestion) - dest_abs_filepath = os.path.join(self.folder_suggestion,"replot_data.py") - summary_str = f"from PyICe.refid_modules.temptroller import replotter\n" - for (test, db_table, db_file) in archived_tables: - x=inspect.getfile(type(test)) - x=x.replace("\\" , '.') - x=x.replace('.py',"") - x= x[x.find(test.project_folder_name):] - summary_str += f"from {x} import {test.get_name()}\n" - summary_str += f"\nif __name__ == '__main__':\n" - summary_str += f'\n rplt = replotter()\n' - for (test, db_table, db_file) in archived_tables: - rel_db_file=os.path.relpath(db_file, start =test.project_folder_name) - rel_db_file=rel_db_file.replace('\\', '/') - summary_str += f' rplt.add_test_run(test_class={test.get_name()},table_name=r"{db_table}",db_file="{rel_db_file}")\n' - summary_str += f'\n rplt.write_html("summary_plots.html")' - try: - with open(dest_abs_filepath, 'w') as f: #overwrites existing - f.write(summary_str) - f.close() - except Exception as e: - print(type(e)) - print(e) - traceback.print_exc() - breakpoint() - pass - if len(arch_plot_scripts) > 1 and (not interactive or input('Generate summary plots? [[y]/n]: ').lower() not in ['n', 'no']): - #Write out summary plot, or write out summary plot maker?!?! TODO! - html_plotter = replotter() - for (test, db_table, db_file) in archived_tables: - html_plotter.add_test_run(test_class=type(test), table_name=db_table, db_file=db_file) - try: - html_plotter.write_html('regression_summary.html') - except PermissionError as e: - if input(f'{e.filename} is not writeable. Attempt p4 checkout? [y/[n]]: ').lower() in ['y', 'yes']: - if p4_traceability.p4_edit(e.filename): - html_plotter.write_html('regression_summary.html') - else: - raise e - else: - raise e - def manual_archive(self): - archive_folder = test_archive.database_archive.ask_archive_folder() - for test in self: - archiver = test_archive.database_archive(db_source_file=test._db_file) - copied_tables_files = archiver.copy_interactive(archive_folder=archive_folder, project_folder=test.project_folder_name) - for (table, file) in copied_tables_files: - test._add_db_indices(table_name=table, db_file=file) - def run(self, collect_data, skip_plot=False, force_archive=False): - if not len(self): - raise Exception('ABORT: No tests added to test suite!') - self.hook_functions('pre_collect') - if collect_data: - self.collect() - self.hook_functions('post_collect') - if not skip_plot: #an not skip_compile_test_results??? Is there an interdependence? - self.plot() #Don't want to suppress test results with plot garbage, but might need test results stored data to produce plots! - self.hook_functions('post_plot') - self._archive(interactive=not force_archive) - self.hook_functions('post_archive') - -class replotter: - def __init__(self): - # self.test_run = collections.namedtuple('test_run', ['test_class', 'table_name', 'db_file']) - self.test_result = collections.namedtuple('test_result', ['test_class', 'table_name', 'db_file', 'test_name', 'plots', 'results_array', 'corr_results_array', 'bench_setup_image', 'bench_setup_list', 'bench_instruments', 'refid']) - # self._test_runs = [] - self._test_results = [] - def add_test_run(self, test_class, table_name, db_file, bench_setup_image=None, bench_setup_list= None, bench_instruments=None, refid=None, skip_plots=False): - # self._test_runs.append(self.test_run(test_class=test_class, table_name=table_name, db_file=db_file)) - test_inst = test_class() - if not hasattr(test_inst, '_test_result'): - test_inst.register_refids() - test_inst._test_results._set_traceability_info(**test_inst._get_traceability_info(table_name=table_name, db_file=db_file)) - test_inst._correlation_results._set_traceability_info(**test_inst._get_traceability_info(table_name=table_name, db_file=db_file)) - try: - # breakpoint() ## Delete the results_str and instead look into test_inst.test_results and look into [test][ch2_vout or whatever]. Ask in a pythonic way for the results. - results_str = test_inst._test_from_table(table_name=table_name, db_file=db_file) - results_array = test_inst._test_results - corr_results_array = test_inst._correlation_results - except Exception as e: - # Don't kill whole replot over one broken test, missing column, etc. Log exception in results and move on... - results_str = f'{e}' - if not skip_plots: - plots = test_inst._plot(table_name=table_name, db_file=db_file, skip_output=True) # TODO: try/except?? - if plots is None: - plots = [] - else: - assert isinstance(plots, list), "Plot method should return a list of plot objects. Contact PyICe Support at PyICe-developers@analog.com." - else: - plots = [] - # breakpoint() - for tst_name in test_inst._test_results: #These are the keys / refid names now. TODO provide public getter methods to the test results modules. - for tst_result in test_inst._test_results[tst_name]: #change 2021/12. Now a list of independently tracked results. - for tst_plt in tst_result.plot: #Today, there's just plots in the first result, because of a hack over in test_results module. That might change, but this should work either way. - plots.append(tst_plt) - # self._test_results.append(self.test_result(test_class=test_class, table_name=table_name, db_file=db_file, test_name=test_inst.get_name(), plots=plots, results_str=results_str, bench_setup=bench_setup, bench_instruments=bench_instruments, refid=refid)) - self._test_results.append(self.test_result(test_class=test_class, table_name=table_name, db_file=db_file, test_name=test_inst.get_name(), plots=plots, results_array=results_array, corr_results_array=corr_results_array, bench_setup_image=bench_setup_image, bench_setup_list=bench_setup_list, bench_instruments=bench_instruments, refid=refid)) - # def plot(self, html_file=None): - # assert len(self._test_runs), 'ABORT: No test runs added to replotter suite!' - def write_html(self, html_file): - with open(html_file, 'wb') as f: - f.write(self._html().encode("UTF-8")) - f.close() - def _min(self, data): - if not len(data): - return None - return min(data) - def _max(self, data): - if not len(data): - return None - return max(data) - def _html(self): - ret_str = '' - ret_str += '\n' - ret_str += '\n' - ret_str += '\n' - ret_str += 'PyICe Test Result Replotter\n' - ret_str += '\n' - ret_str += '\n' - for test_result in self._test_results: - ret_str += f'

{test_result.test_name}

\n' - ret_str += f'

{test_result.db_file} {test_result.table_name}

\n' - ret_str += f'

\n' - for refid in test_result.results_array._test_results: - ret_str += f'

{refid}

' - ret_str += f'  RESULTS
' - for data in test_result.results_array._test_results[refid]: - ret_str += f'    {data.conditions}  TRIALS:{len(data.collected_data)}  VERDICT:'+(f'PASSES
' if data.passes == True else f'FAILS
') - this_min = self._min(data.collected_data) - if data.failure_reason != '': - ret_str += f'    FORCED_FAIL: {data.failure_reason}
' - elif len(data) > 1: - ret_str += f'    MIN:{f"{this_min:g}" if type(this_min) is int else f"{this_min}" if this_min is not None else "None"}
' if not data else '' - this_max = self._max(data.collected_data) - ret_str += f'    MAX:{f"{this_max:g}" if type(this_max) is int else f"{this_max}" if this_max is not None else "None"}
' if not data else '' - else: - ret_str += f'    DATA:{f"{min:g}" if type(min) is int else f"{min}" if min is not None else "None"}
' if not data else '' - ret_str += f'***
* {refid} ' + ('PASSES!
' if all([x.passes for x in test_result.results_array._test_results[refid]]) else 'FAILS!
') + '***
' - for refid in test_result.corr_results_array._correlation_results: - ret_str += f'

{refid}

' - ret_str += f'  RESULTS
' - for data in test_result.corr_results_array._correlation_results[refid]: - ret_str += f'    {data.conditions}  ATE DATA:{data.ate_data}  ERROR:{data.error}  VERDICT:'+(f'PASSES
' if data.passes == True else f'FAILS
') - ret_str += f'    {data.conditions}  ATE DATA:{data.ate_data}  ERROR:{data.error}  VERDICT:'+(f'PASSES
' if data.passes == True else f'FAILS
') - if data.failure_reason != '': - ret_str += f'    FORCED_FAIL: {data.failure_reason}
' - elif len(data) > 1: - this_min = self._min(data.bench_data) - ret_str += f'    ATE DATA:{data.ate_data}  MIN:{f"{this_min:g}" if type(this_min) is int else f"{this_min}" if this_min is not None else "None"}
' if not data.passes else '' - this_max = self._max(data.bench_data) - ret_str += f'    ATE DATA:{data.ate_data}  MAX:{f"{this_max:g}" if type(this_max) is int else f"{this_max}" if this_max is not None else "None"}
' if not data else '' - else: - ret_str += f'    ATE DATA:{data.ate_data}  BENCH DATA:{f"{this_min:g}" if type(this_min) is int else f"{this_min}" if this_min is not None else "None"}
' if not data else '' - ret_str += f'***
* {refid} ' + ('PASSES!
' if all([x.passes for x in test_result.corr_results_array._correlation_results[refid]]) else 'FAILS!
') + '***
' - ret_str += f'

\n' - for plt in test_result.plots: - # https://css-tricks.com/lodge/svg/09-svg-data-uris/ - # https://css-tricks.com/using-svg/ - ret_str += f'\n' - ret_str += '
\n' - ret_str += '\n' - ret_str += '\n' - return ret_str - - def add_to_markdown(self, refid, indent_level=None): - ret_str = '' - for test_result in self._test_results: - if test_result.refid[0] == refid: - ret_str += f'

File Location

{test_result.db_file} \n' - ret_str += f'

Table Name

{test_result.table_name}' - try: - ret_str += f'

Bench Instruments

{test_result.bench_instruments}' - ret_str += f'

Bench Setup

' - ret_str += f'{test_result.bench_setup_list}
' - ret_str += f'  ' - except Exception as e: - print(e) - breakpoint() - ret_str += f'

RESULTS

' - for rslt in test_result.results_array[refid]: - ret_str += f'  {rslt.conditions}    VERDICT:' + ('PASSES
' if rslt.passes else 'FAILS
') - ret_str += f'' - ret_str += f'

PLOTS

' - for index,plt in enumerate(test_result.plots,1): - # https://css-tricks.com/lodge/svg/09-svg-data-uris/ - # https://css-tricks.com/using-svg/ - ret_str += f' ' - ret_str += '
\n' - ret_str += '\n' - return ret_str \ No newline at end of file diff --git a/PyICe/refid_modules/test_archive.py b/PyICe/refid_modules/test_archive.py deleted file mode 100644 index 302c61c..0000000 --- a/PyICe/refid_modules/test_archive.py +++ /dev/null @@ -1,212 +0,0 @@ -#!/usr/bin/env python - -import os -import sqlite3 -import re -# from . import p4_traceability - -class database_archive(): - def __init__(self, db_source_file): #, db_dest_file=None, db_source_table=None, db_dest_table=None): - self.db_source_file = os.path.abspath(db_source_file) - (self.db_source_abspath, self.db_source_filename) = os.path.split(self.db_source_file) - self.db_source_folder = os.path.basename(self.db_source_abspath) - self.source_conn = sqlite3.connect(self.db_source_file) - def copy_table(self, db_source_table, db_dest_table, db_dest_file, db_indices): - # p4_info = p4_traceability.get_fstat(db_dest_file) - # # Basic OS writability check??? - if False: - pass - # if p4_info['depotFile'] is not None and p4_info['action'] is None: - # # TODO: Ask to p4 check out - # print('Skipping copy. Destination database checked in') - # return False - else: - # attach_schema = '__dest_db__' - - conn = sqlite3.connect(db_dest_file) - attach_schema = '__source_db__' - - conn.execute(f"ATTACH DATABASE '{self.db_source_file}' AS {attach_schema}") - ############## - # Main table # - ############## - orig_create_statement = conn.execute(f"SELECT sql FROM {attach_schema}.sqlite_master WHERE name == '{db_source_table}'").fetchone()[0] - (new_create_statement, sub_count) = re.subn(pattern = f'^CREATE TABLE {db_source_table} \( rowid INTEGER PRIMARY KEY, datetime DATETIME, (.*)$', - repl = f'CREATE TABLE {db_dest_table} ( rowid INTEGER PRIMARY KEY, datetime DATETIME, \\1', - string=orig_create_statement, - count=1, - flags=re.MULTILINE - ) - assert sub_count == 1 - try: - conn.execute(new_create_statement) - except sqlite3.OperationalError as e: - print(e) # table already exists? - # Abort - conn.rollback() - conn.execute(f'DETACH DATABASE {attach_schema}') - return - conn.execute(f'INSERT INTO {db_dest_table} SELECT * FROM {attach_schema}.{db_source_table}') - - ############### - # Format view # - ############### - # Rename this view - row = conn.execute(f"SELECT sql FROM {attach_schema}.sqlite_master WHERE name == '{db_source_table}_formatted'").fetchone() - if row is not None: - # Some tables don't have _formatted and _all views if there are no presets/formats in the source data. - orig_create_statement = row[0] - (new_create_statement, sub_count) = re.subn(pattern = f'^CREATE VIEW {db_source_table}_formatted AS SELECT(.*)$', - repl = f'CREATE VIEW {db_dest_table}_formatted AS SELECT\\1', - string=orig_create_statement, - count=1, - flags=re.MULTILINE - ) - assert sub_count == 1 - # Rename source table - (new_create_statement, sub_count) = re.subn(pattern = f'FROM {db_source_table}', - repl = f'FROM {db_dest_table}', - string=new_create_statement, - count=4, - flags=re.MULTILINE - ) - assert sub_count == 4 - conn.execute(new_create_statement) - - ############### - # Joined view # - ############### - conn.execute(f'CREATE VIEW {db_dest_table}_all AS SELECT * FROM {db_dest_table} JOIN {db_dest_table}_formatted USING (rowid)') - - for column_list in db_indices: - columns_str = f'({",".join(column_list)})' - idx_name = f'{db_dest_table}_{"_".join(column_list)}_idx' - try: - conn.execute(f'CREATE INDEX IF NOT EXISTS {idx_name} ON {db_dest_table} {columns_str}') - except sqlite3.OperationalError as e: - missing_col_mat = re.match('no such column: (?P\w+)', str(e)) #alphanumeric column names only? prob ok. - if missing_col_mat is not None: - print(f"You done goofed. One or more of {columns_str} do not exist. Now think about what you've done.") - breakpoint() - else: - print(e) - print('This is unexpected. Please email trace above to PyICe-developers@analog.com.') - breakpoint() - pass - ########### - # Wrap up # - ########### - conn.commit() - conn.execute(f'DETACH DATABASE {attach_schema}') - # print('copy') - # print(db_source_table) - # print(db_dest_table) - # print(db_dest_file) - return True #Probably ok, if we made it this far - def delete_table(self, db_source_table, commit=True): - # p4_info = p4_traceability.get_fstat(db_source_table) - # #Basic OS writability check??? - if False: - pass - # if p4_info['depotFile'] is not None and p4_info['action'] is None: - # # TODO: Ask to p4 check out - # print('Skipping delete. Source database checked in') - else: - self.source_conn.execute(f'DROP TABLE {db_source_table}') - self.source_conn.execute(f'DROP VIEW IF EXISTS {db_source_table}_formatted') - self.source_conn.execute(f'DROP VIEW IF EXISTS {db_source_table}_all') - if commit: - self.source_conn.commit() - # print("delete", db_source_table) - def get_table_names(self): - table_query = "SELECT name FROM sqlite_master WHERE type ='table'" - return [row[0] for row in self.source_conn.execute(table_query)] - @classmethod - def ask_archive_folder(cls, suggestion=None): - while True: - suggestion_str = '' if suggestion is None else f'[{suggestion}]' - archive_folder = input(f'Destination archive folder? {suggestion_str}: ') - if len(archive_folder): - break - elif suggestion is not None: - return suggestion - return archive_folder - def compute_db_destination(self, archive_folder): - db_dest_folder = os.path.join(self.db_source_abspath, 'archive', archive_folder) - db_dest_file = os.path.join(db_dest_folder, self.db_source_filename) - os.makedirs(db_dest_folder, exist_ok=True) - return db_dest_file - def copy_interactive(self, archive_folder=None, project_folder_name=None): - copied_tables_files = [] - table_names = self.get_table_names() - if len(table_names): - print("Database tables:") - for table in table_names: - # Give wold view before start of inquisition. - print(f"\t{table}") - if archive_folder is None: - archive_folder = self.ask_archive_folder() - db_dest_file = self.compute_db_destination(archive_folder) - for table in table_names: - resp = self.disposition_table(table_name=table, db_dest_file=db_dest_file) - if resp is not None: - copied_tables_files.append((resp[0], resp[1])) - if input('Write archive plot script(s)? [y/[n]]: ').lower() in ['y', 'yes']: - import_file=os.path.realpath(os.path.relpath(os.getcwd(), start = os.environ['PYTHONPATH'])) - import_file=import_file[import_file.find(project_folder_name):] - import_file += '\\' - import_file=import_file.replace('\\',".") - import_file += os.path.basename(os.getcwd()) - self.write_plot_script(test_module= import_file, test_class=os.path.basename(os.getcwd()), db_table=resp[0], db_file=resp[1]) - self.source_conn.commit() - return copied_tables_files - def disposition_table(self, table_name, db_dest_file, db_indices): - while True: - action = input(f'{table_name} action? (s[kip], c[opy], m[ove], d[elete]) : ') - if action.lower() == 's' or action.lower() == 'skip': - return None - elif action.lower() == 'c' or action.lower() == 'copy': - db_dest_table = input(f'\tDestination table? [{table_name}]: ') - if not len(db_dest_table): - db_dest_table = table_name - if self.copy_table(db_source_table=table_name, db_dest_table=db_dest_table, db_dest_file=db_dest_file, db_indices=db_indices): - return ((db_dest_table, db_dest_file)) - else: - return None - elif action.lower() == 'm' or action.lower() == 'move': - db_dest_table = input(f'\tDestination table? [{table_name}]: ') - if not len(db_dest_table): - db_dest_table = table_name - if self.copy_table(db_source_table=table_name, db_dest_table=db_dest_table, db_dest_file=db_dest_file, db_indices=db_indices): - self.delete_table(table_name, commit=False) - return ((db_dest_table, db_dest_file)) - else: - return None - elif action.lower() == 'd' or action.lower() == 'delete': - conf_resp = input(f'\tConfirm delete? [(yes/[no]]: ') - if conf_resp.lower() == 'yes': - self.delete_table(table_name, commit=False) - return None - @classmethod - def write_plot_script(cls, test_module, test_class, db_table, db_file): - # TODO: allow list of modules/databases to plot? How? - (dest_folder, f) = os.path.split(os.path.abspath(db_file)) - dest_file = os.path.join(dest_folder, f"plot_{test_class}.py") - db_rel = os.path.relpath(db_file, start=os.path.commonpath((dest_file, db_file))) - plot_script_src = "if __name__ == '__main__':\n" - plot_script_src += f" from {test_module} import {test_class}\n" - plot_script_src += f" {test_class}.plot_from_table(table_name=r'{db_table}', db_file=r'{db_rel}')\n" - try: - with open(dest_file, 'a') as f: #exists, overwrite, append? - f.write(plot_script_src) - except Exception as e: - #write locked? exists? - print(type(e)) - print(e) - else: - return dest_file - - -if __name__ == '__main__': - db_arch = database_archive('./data_log.sqlite') - db_arch.copy_interactive() diff --git a/PyICe/refid_modules/test_module.py b/PyICe/refid_modules/test_module.py deleted file mode 100644 index 4bca616..0000000 --- a/PyICe/refid_modules/test_module.py +++ /dev/null @@ -1,634 +0,0 @@ -from PyICe import lab_core, lab_instruments, virtual_instruments, LTC_plot -from PyICe.refid_modules.temptroller import temptroller -from PyICe.bench_configuration_management.bench_configuration_management import bench_configuration_error -from PyICe.lab_utils.banners import print_banner -from PyICe.lab_utils.sqlite_data import sqlite_data - -import abc -import datetime -import inspect -import math -import os.path -import pdb -import re -import sqlite3 #Catch exceptions -import sys -import traceback -# import typing - - -def isnan(value): - try: - return math.isnan(float(value)) - except (TypeError,ValueError): - return False - -class regressionException(Exception): - '''special exception class to crash not just a single test module, but the whole enchilada.''' - -#TODO: Missing data (rows, not columns) is not treated as an exception now. TBD. - -class test_module(abc.ABC): - '''class template - all test modules shall inherit from this class and implement these abstract methods. - ''' - - _is_test_module = True - _is_multitest_module = False - _archive_enabled = None #class variable to prevent repeatability nagging - #@typing.final - def __init__(self, debug=False): #, lab_bench): - '''TODO''' - self.name = type(self).__name__ - self.bench_name = None - self._is_setup = False - self._logger = None - self._archive_table_name = None #re-set by copy_table() - (self._module_path, file) = os.path.split(inspect.getsourcefile(type(self))) - db_filename = 'data_log.sqlite' - self._db_file = os.path.join(self._module_path, db_filename) - self._channel_reconfiguration_settings = [] - self._debug = debug - type(self)._archive_enabled = not self._debug #Class variable - - self.test_timer = virtual_instruments.timer() - self.test_timer.add_channel_total_minutes('test_time_min') - self.test_timer.add_channel_delta_minutes('loop_time_min') - self._crashed = None - self._plot_crashed = None - self._column_indices = [] - self.max_test_temp=None - self.min_test_temp=None - self.excluded_temperatures=[] - self.tt=None - if hasattr(self, 'plugins'): ## Hokay. Slight issue here. If there's a regression going on, how do we make each test_module get a personal instance of a plugin?. - for plugin in self.plugins.keys(): - plug = self.plugins[plugin]['instance'] - self.plug_instances.append(plug) - for (k,v) in plug.get_atts().items(): - self._add_attr(k,v) ## Meaning one test_module calling the method will not affect another test_module's values. Sweet! - for (k,v) in plug.get_hooks().items(): - try: - self.plugin_hooks[k].extend(v) - except KeyError: - self.plugin_hooks[k]=v - [ploog.set_interplugs() for ploog in self.plug_instances] - @classmethod - def __subclasshook__(cls, C): - if cls is test_module: - return True - return NotImplemented - def abort_regression(self, reason=None): - '''force crash not just this test, but all of them.''' - raise regressionException(reason) - def debug_mode(self): - '''query to see if module debug flag is set. useful where otherwise not provided as method argument, ex in setup().''' - return bool(self._debug) - @classmethod - def get_name(cls): - return cls.__name__ - def get_max_test_temp(self): - return self.max_test_temp - def set_max_test_temp(self,temperature): - self.max_test_temp=temperature - def get_min_test_temp(self): - return self.min_test_temp - def set_min_test_temp(self,temperature): - self.min_test_temp=temperature - def in_range(self,tdegc): - if self.get_min_test_temp() is not None: - if self.get_min_test_temp() > tdegc: - return False - if self.get_max_test_temp() is not None: - if self.get_max_test_temp() < tdegc: - return False - return True - def temp_is_included(self,tdegc): - if self.is_crashed(): - return False - elif tdegc in self.excluded_temperatures: - return False - elif not self.in_range(tdegc): - return False - return True - def register_plugin(self, plugin): - if not hasattr(self, 'plugins'): - self.plugins={} - self.plug_instances=[] - self.plugin_hooks={} - key = plugin.__class__.__name__ - self.plugins[key] = {} - self.plugins[key]['instance'] = plugin - self.plugins[key]['description'] = str(plugin) - self.plugins[key]['data'] = {} - def hook_functions(self, hook_key, *args): - for (k,v) in self.plugin_hooks.items(): - if k is hook_key: - for f in v: - f(*args) - def _add_attr(self,key,value): - setattr(self,key,value) - def _execute_plugin_fns(self, func): - func(self) - @classmethod - # @abc.abstractmethod #Make abstract eventually - def configure_bench(cls, components): - # print_banner(f'{cls.__name__}: No bench configuration declared.','The AI neural net will attempt to discern all adhoc bench connections and', 'make the declarations on your behalf.') - raise bench_configuration_error(f'Test {cls.get_name()} failed to implement configure_bench(components). This is required for inclusion into the regression system.') - def get_import_str(self): - abspath = os.path.abspath(inspect.getsourcefile(type(self))) - root_path = os.path.join(inspect.getsourcefile(test_module), '../../../..') - relpath = os.path.relpath(abspath, start=root_path) - modpath, ext = os.path.splitext(relpath) - dirs = [] - while True: - (head, tail) = os.path.split(modpath) - dirs.insert(0, tail) - if head == '': - break - else: - modpath = head - return '.'.join(dirs) - def _add_timer_channels(self, logger): - ch_cat = 'eval_traceability' #is this right? - cumulative_timer = virtual_instruments.timer() - cumulative_timer.add_channel_total_minutes('test_cumulative_time').set_category(ch_cat) - cumulative_timer.add_channel_delta_minutes('test_incremental_time').set_category(ch_cat) - temperature_timer = virtual_instruments.timer() - temperature_timer.add_channel_total_minutes('test_temperature_incremental_time').set_category(ch_cat) - temperature_timer.stop_and_reset_timer() - logger.add(cumulative_timer) - logger.add(temperature_timer) - return (cumulative_timer, temperature_timer) - - @abc.abstractmethod - def setup(self, channels): - '''customize logger channels and virtual instruments for test here''' - #@typing.final - def _setup(self, lab_bench): - '''entrance from temptroller''' - if not self._is_setup: - self._is_setup = True - self._start_time = datetime.datetime.now() - assert self._logger is None - self._lab_bench = lab_bench #TODO should this be stored? Should it get set to None in __init__? - self._logger = lab_core.logger(database=self._db_file, use_threads=not self._debug) #TODO Threading!! - self.hook_functions('tm_logger_setup', self.get_logger()) - self._logger.merge_in_channel_group(lab_bench.get_master().get_flat_channel_group()) - (self._cumulative_timer, self._temperature_timer) = self._add_timer_channels(self.get_logger()) - ret = self.setup(self.get_logger()) - # Make sure traceability channels not removed - # for register in stowe_die_traceability.stowe_die_traceability.nvm_derived_registers: - # if register in stowe_die_traceability.stowe_die_traceability.zero_intercept_registers: - # continue # Legacy unused f_die_fab and f_die_parent_child not in future Yoda map. - # assert register in self._logger.get_all_channel_names(), f'ERROR: Die traceability register {register} removed from test {self.get_name()} logger.\n\nIf I2C communications must be disabled, consider leveraging stowe_die_traceability:\n\nfrom stowe_eval.stowe_eval_base.modules.test_module import stowe_die_traceability\ndef setup(self, channels):\n stowe_die_traceability.stowe_die_traceability.replace_die_traceability_channels(channels, powerup_fn=powerup, powerdown_fn=powerdown)\n\n' - # for register in stowe_die_traceability.stowe_die_traceability.traceability_registers: - # if register not in stowe_die_traceability.stowe_die_traceability.zero_intercept_registers: - # self._logger[register].set_write_access(False) - # ---- - self._logger.new_table(table_name=self.get_name(), replace_table=True) - #@abc.abstractmethod - def teardown(self): - '''Any test cleanup required after all temps?''' - # Optional in test module. Probably almost never required. - pass - #@typing.final - def _teardown(self): - '''cleanup required (module, not per-collect)''' - ret = self.teardown() - if self._crashed is None: - if not self._debug: - table_name = self.copy_table() #TODO better timestamp? beginning of test? - print(f"Copied test results table to {table_name}") - if not self._archive_enabled: - # Copy table to timestamp version, but don't allow copy to new database. Perhaps test module unversioned. - self._archive_table_name = None # Un-do work done by self.copy_table() - else: - print(f'Skipping data archive for debug test {self.get_name()}.') - else: - print(f'Archiving crashed test results {self.get_name()} to disposable table.') - self.get_logger().execute(f'DROP TABLE IF EXISTS {self.get_name()}_crash') #Copy will fail if destination exists. - self.get_logger().execute(f'DROP VIEW IF EXISTS {self.get_name()}_crash_formatted') #Copy will fail if destination exists. - self.get_logger().execute(f'DROP VIEW IF EXISTS {self.get_name()}_crash_all') #Copy will fail if destination exists. - self.get_logger().copy_table(old_table=self.get_name(), new_table=f"{self.get_name()}_crash") - self.get_logger().new_table(table_name=self.get_name(), replace_table=True) #Empty table to prevent accidental recording of bad data later. - # if self._crashed is None: #this doesn't seem to fix anything if another test has crashed logger. Not sure why. They must share the backend thread somehow. - # #print(self.get_name()) - # #self._logger.stop() #Unsafe to mess with logger if backed thread has crashed. - # #Maybe shouldn't stop logger. It seems to go down with bench destruction. - # pass - return ret - #@typing.final - def _collect(self): - if self._crashed is None: - try: - self._temperature_timer.resume_timer() - self._temperature_timer.reset_timer() - self._cumulative_timer.resume_timer() - self._reconfigure() - print_banner(f"Running Test: {self.get_name()}") - self.collect(channels=self.get_logger(), debug=self._debug) - self._temperature_timer.pause_timer() - self._cumulative_timer.pause_timer() - except regressionException as e: - # raised by self.abort_regression(reason) - # for example, if it's determined that the wrong DUT is inserted, no point in spending a whole day running inappropriate tests destined for certain failure. - # prevent local handling of this one so that the temptroller can handle the shutdown and cleanup of all the other tests too. - raise - except (Exception, KeyboardInterrupt) as e: - if self._debug or isinstance(e, KeyboardInterrupt): - #TODO - ctrl-c events will drop here, but leave PyICe locks in an inconsistent state. - #Need to figure out how to unwind the stack or move forward to a sensible place in the main script loop before entering debugger. - - traceback.print_exc() - # Optionally drop into debugger or gui to give a chance to examine test conditions and results. - # DANGER: This hangs the script, powered, forever! debug=True isn't for unattended use. - # Would be nice to have a timeout on the prompt, but that's complicated with threads and queues. Maybe a lab_utils project for a rainy day. - if input('Debug [y/n]? ').lower() in ('y', 'yes'): - # pdb.set_trace() - pdb.post_mortem() - else: - if input('GUI [y/n]? ').lower() in ('y','yes'): - self.get_logger().gui() - else: - # Just crash. No sense in carrying on with other tests in the regression. - raise e - self._crashed = sys.exc_info() - else: - #test ran! - pass - finally: - try: - self._restore() - except Exception as e: - # Not much to be done if this crashes. - # Press on trying to clean up! - print(e) - self._lab_bench.cleanup() # This might crash. If so, best effort (inside temptroller) to clean up oven and stop all tests - no sense running other tests with broken bench. - else: - print(f"Skipping previously crashed test: {self.get_name()}") - @abc.abstractmethod - def collect(self, channels, debug): - '''perform data collection at a single temperature.''' - def _plot(self, table_name=None, db_file=None, skip_output=False): - '''get database loaded up first to streamline test module''' - '''skip output to facilitate replotting to different location wihtout having to p4 check out everything. requires argument support inside each test, which no legacy tests have yet''' - if self._crashed is not None: - print(f'Skipping plotting for crashed test: {self.get_name()}.') - return [] - else: - if table_name is None: - tn = self.get_db_table_name() - plot_filepath = self._module_path - else: - tn = table_name - plot_filepath = os.path.join(self._module_path, table_name) - tn_base = tn - if db_file is None: - # db = self.get_db() - db = sqlite_data(database_file=self._db_file, table_name=tn) - else: - db = sqlite_data(database_file=db_file, table_name=tn) - (db_root, db_filename) = os.path.split(os.path.abspath(db_file)) - if table_name is None: - plot_filepath = db_root - else: - plot_filepath = os.path.join(db_root, table_name) - if f'{tn}_all' in db.get_table_names(): - #If there aren't any presets or formats, the _all view never gets created by PyICe - tn = f'{tn}_all' #Redirect to presets-joined table - db.set_table(tn) - self.hook_functions('tm_plot', tn, db, plot_filepath) - try: - plts = self.plot(database=db, table_name=tn, plot_filepath=plot_filepath, skip_output=skip_output) - except TypeError as e: - #skip_output argument not supported by legacy script - print(f'WARNING: Test {self.get_name()} plot method does not support skip_output argument.') - try: - plts = self.plot(database=db, table_name=tn, plot_filepath=plot_filepath) - except Exception as e: - # Something else, NOS, has gone wrong with plotting. Let's try to muddle through it to avoid interrupting the other plots and still give a chance of archiving. - if self._debug: - traceback.print_exc() - if input('Debug [y/n]? ').lower() in ('y', 'yes'): - pdb.post_mortem() - else: - # Just crash. No sense in carrying on with other tests in the regression. - raise e - self._plot_crashed = sys.exc_info() - plts = [] - except Exception as e: - # Something else, NOS, has gone wrong with plotting. Let's try to muddle through it to avoid interrupting the other plots and still give a chance of archiving. - if self._debug: - traceback.print_exc() - if input('Debug [y/n]? ').lower() in ('y', 'yes'): - pdb.post_mortem() - else: - # Just crash. No sense in carrying on with other tests in the regression. - raise e - self._plot_crashed = sys.exc_info() - plts = [] - def convert_svg(plot): - if isinstance(plot, LTC_plot.plot): - page = LTC_plot.Page(rows_x_cols = None, page_size = None, plot_count = 1) - page.add_plot(plot=plot) - return page.create_svg(file_basename=None, filepath=None) - elif isinstance(plot, LTC_plot.Page): - return plot.create_svg(file_basename=None, filepath=None) - elif isinstance(plot, (str,bytes)): - # Assume this is already SVG source. - # TODO: further type checking? - return plot - else: - raise Exception(f'Not sure what this plot is:\n{type(plot)}\n{plot}') - if plts is None: - print(f'WARNING: {self.get_name()} failed to return plots.') - return [] - elif isinstance(plts, (LTC_plot.plot, LTC_plot.Page)): - return [convert_svg(plts)] - elif isinstance(plts, (str,bytes)): - return [plts] - else: - assert isinstance(plts, list) - return [convert_svg(plt) for plt in plts] - @abc.abstractmethod - def plot(self, database, table_name, plot_filepath, skip_output): - '''output plot from previously stored data. Static method prevents logger/instrument creation. what about self.name??? - return list of svg sources???''' - - #@typing.final - def reconfigure(self, channel, value): - '''save channel settings specific to a particular test. Unwound after test at each temperature.''' - self._channel_reconfiguration_settings.append((channel, channel.read(), value)) - #@typing.final - def _reconfigure(self): - '''save channel setting before writing to value''' - for (ch, old, new) in self._channel_reconfiguration_settings: - ch.write(new) - #@typing.final - def _restore(self): - '''undo any changes made by reconfigure''' - for (ch, old, new) in self._channel_reconfiguration_settings: - ch.write(old) - #@typing.final - def copy_table(self): - #self.get_logger().copy_table(old_table=self.get_name(), new_table=self.get_name() + datetime.datetime.now().strftime("_%Y_%m_%d_%H_%M")) - new_table_name = self.get_name() + self._start_time.strftime("_%Y_%m_%d_%H_%M_%S") - self.get_logger().copy_table(old_table=self.get_name(), new_table=new_table_name) #Needs to have been through a _setup()! - self._archive_table_name = new_table_name - # Remove Excel output, per Steve fear of encouraging Excel usage. - # self.export_table_xlsx(filename=f'{new_table_name}.xlsx') - return new_table_name - #@typing.final - def export_table_xlsx(self, filename): - self.get_db().xlsx(output_file=filename, elapsed_time_columns=True) - #@typing.final - def get_logger(self): - if self._logger is None: - raise Exception('No Logger setUp.') - return self._logger - #@typing.final - def get_db(self): - return sqlite_data(database_file=self._db_file, table_name=self.get_name()) - def get_db_table_name(self): - return self.get_name() - def get_lab_bench(self): - # raise Exception('Is this really needed?') - #TODO What if the lab_bench doesn't exist yet? Exception? - return self._lab_bench - - @classmethod - def run(cls, collect_data, temperatures=None, debug=False, lab_bench_constructor=None): - tt = temptroller(temperatures=temperatures, debug=debug) - # tt = cls._get_project_temptroller(cls, temperatures=temperatures, debug=debug) - if lab_bench_constructor is not None: - tt.set_lab_bench_constructor(lab_bench_constructor) - tt.add_test(cls) - tt.run(collect_data) - @classmethod - def run_repeatability(cls, repeat_count, temperatures=None, debug=False, lab_bench_constructor=None): - tt = temptroller(temperatures=temperatures, debug=debug) - if lab_bench_constructor is not None: - tt.set_lab_bench_constructor(lab_bench_constructor) - tt.add_test(cls, repeat_count=repeat_count) - tt.run(collect_data=True) - self.hook_functions('tm_post_repeatability') - html_plotter = cls.replotter() - for test_run in tt: - html_plotter.add_test_run(test_class=type(test_run), table_name=test_run.get_db_table_name(), db_file=test_run._db_file) - html_plotter.write_html('repeatability_summary.html') #TODO check permissions? - # TODO collect up data, present repeatability summary statistics, differentiate plotting, ... - # As a fist step, this just runs the test multiple times to see if it crashes, but not more. - #TODO write out special JSON file to preserve results???? - def is_crashed(self): - return bool(self._crashed) - def crash_info(self): - if self._crashed is None: - return '' - else: - # (type, value, trace_bk) = self._crashed - (typ, value, trace_bk) = self._crashed - crash_str = f'Test: {self.get_name()} crashed: {typ},{value}\n' - crash_sep = '==================================================\n' - crash_str += crash_sep - # crash_str += f'{traceback.print_tb(trace_bk)}\n' - crash_str += f'{"".join(traceback.format_exception(typ, value, trace_bk))}\n' - crash_str += crash_sep - return crash_str - def _add_db_indices(self, table_name=None, db_file=None): - if table_name is None: - table_name = self.get_db_table_name() - if db_file is None: - db_file = self._db_file - for column_list in self._column_indices: - self.__add_db_indices(column_list=column_list, table_name=table_name, db_file=db_file) - def __add_db_indices(self, column_list, table_name, db_file): - '''recursively callable on db errors''' - db = sqlite_data(database_file=db_file, table_name=table_name) - columns_str = f'({",".join(column_list)})' - idx_name = f'{table_name}_{"_".join(column_list)}_idx' - try: - db.conn.execute(f'CREATE INDEX IF NOT EXISTS {idx_name} ON {table_name} {columns_str}') - db.conn.commit() - except sqlite3.OperationalError as e: - # "no such column: foo" - - # sqlite3.OperationalError: no such column: foo - - - missing_col_mat = re.match('no such column: (?P\w+)', str(e)) #alphanumeric column names only? prob ok. - if missing_col_mat is not None: - missing_col = missing_col_mat.group("missing_col") - column_list = list(column_list) #could be tuple! - column_list.remove(missing_col) - print(f'WARNING: {idx_name} database index specifies missing column {missing_col}. Trying again without it.') - if len(column_list): - #try again with fewer columns... - self.__add_db_indices(column_list=column_list, table_name=table_name, db_file=db_file) - else: - print(f'{idx_name} empty residual column list. Aborting.') - else: - print(e) - print('This is unexpected. Please email trace above to Dave') - breakpoint() - #Not sure what might go wrong here without more testing. Write locks? TBD... - pass - def register_db_index(self, column_list): - self._column_indices.append(column_list) - @classmethod - def plot_from_table(cls, table_name=None, db_file=None, debug=False): - self = cls(debug=debug) - with sqlite_data(database_file=db_file, table_name=table_name) as db: - my_bench = db.query(f'SELECT bench FROM {table_name}').fetchone()['bench'] - self.bench_name=my_bench[my_bench.find('benches')+8:my_bench.find("' from")] - self.hook_functions('tm_plot_from_table', table_name, db_file) - plts = self._plot(table_name=table_name, db_file=db_file) - if self._plot_crashed is not None: - (typ, value, trace_bk) = self._plot_crashed - msg = f'{self.get_name()} plot crash!.\n' - msg += f'{"".join(traceback.format_exception(typ, value, trace_bk))}\n' - # Skipping the "moving on" here on replotting (vs after collection) since it doesn't affect archive, etc. - # print(notify_message) - raise Exception(msg) - if plts is None: - plts = [] - else: - assert isinstance(plts, list), "Plot method should return a list of plot objects. See Dave" - # for tst in self._test_results: ### Hey Dave, what is this for? --RHM - # for data_group in self._test_results._test_results[tst]: - # for tst_plt in data_group.plot: - # # TODO, there's only one plot inside the first entry now. - # # Structure this whole mess better eventually to account for the iterable results data groups. - # plts.append(tst_plt) - return plts - - -class multitest_module(test_module): - '''class template - all test modules shall inherit from this class and implement these abstract methods. - ''' - _is_multitest_module = True - def __init__(self, debug=False): - self._multitest_units = [] - self.add_multitest_units() - super().__init__(debug) - @abc.abstractmethod - def add_multitest_units(self): - '''Implement with repeated calls to self.add_multitest_unit(multitest_unit_class)''' - def add_multitest_unit(self, multitest_unit_class): - self._multitest_units.append(multitest_unit_class(parent_module=self)) - # Perforce checks?? Here or in setup? - def register_tests(self): - '''todo docs''' - for multitest_unit in self._multitest_units: - multitest_unit.register_tests() - #TODO collect return value? - def _add_attr(self,key,value): - setattr(self,key,value) - for multitest_unit in self._multitest_units: - setattr(multitest_unit,key,value) - def _execute_plugin_fns(self, func): - for multitest_unit in self._multitest_units: - func(multitest_unit) - # def plot(self, database, table_name, plot_filepath, db_file=None, skip_output=False): - def plot(self, database, table_name, plot_filepath, skip_output=False): - '''output plot from previously stored data. Static method prevents logger/instrument creation. what about self.get_name()??? - return list of svg sources???''' - plts = [] - for multitest_unit in self._multitest_units: - if plot_filepath == self._module_path: - pfp = multitest_unit._module_path - elif self._archive_enabled: - pfp = plot_filepath - else: - print("Russell failed to forsee this possibility. Please tell him what you did to get here.") - breakpoint() - # if db_file is not None: #Archive! - # # Plotting non-standard db file - # # Just accept filepath that came in, for lack of a better behavior spec - # # This puts plots together with database/table/module name rather than in specific multitest locations - # # ...which might just be the right thing to do? - # # TODO revisit later! - # pfp = plot_filepath - # elif table_name == self.get_name(): - # # Normal - # pfp = multitest_unit._module_path - # else: #elif db_file is None: ## RUSSELL ISN'T SURE WHAT THIS WAS TO CATCH, AND SO DOESN'T KNOW HOW TO REPLACE IT IN THIS SIMPLIFIED MODEL! 04/11/2023 - # # Plotting non-standard table - # pfp = os.path.join(multitest_unit._module_path, table_name) - try: - unit_plts = multitest_unit.plot(database, table_name, plot_filepath=pfp, skip_output=skip_output) #Ignore multitest_module location. Place plot in multitest_unit location. - except TypeError as e: - #skip_output argument not supported by legacy script - print(f'WARNING: Multitest {self.get_name()} unit {multitest_unit.get_name()} plot method does not support skip_output argument.') - unit_plts = multitest_unit.plot(database, table_name, plot_filepath=pfp) #Ignore multitest_module location. Place plot in multitest_unit location. - if isinstance(unit_plts, (str,bytes)): - unit_plts = [unit_plts] - elif unit_plts is None: - print(f'WARNING: {self.get_name()} module returned no plots.') - continue - assert isinstance(unit_plts, list), f'Multitest module {self.get_name()} expected plot list, but got {type(unit_plts)}.' - plts.extend(unit_plts) - return plts - def compile_test_results(self, database, table_name): - '''inspect previously collected data. Check against parametic limits or functional specification to return Pass/Fail aggregated result.''' - for multitest_unit in self._multitest_units: - multitest_unit.compile_test_results(database, table_name) - #TODO collect return value? - # need to create and return an instance of sweep_results() - -class multitest_unit(abc.ABC): - '''class template - all test modules shall inherit from this class and implement these abstract methods. - ''' - def __init__(self, parent_module): - self._parent_module = parent_module - (self._module_path, file) = os.path.split(inspect.getsourcefile(type(self))) - # self._plot_crashed = None Maybe some day make multitest unit crashed independent. TBD! - # For now, the crash recovery and reporting works, but the first unit to crash will prevent execution of the others, resulting in a possibly iterative debug. - def get_name(self): - return type(self).__name__ - def get_revid(self): - try: - return self._parent_module._revid # only set after self._get_die_traceability_hash - except AttributeError as e: - print('Problem retrieving revid information. Ask Dave for help') - return -1 #??? - def get_variant_id(self): - try: - return self._parent_module._variant_id # only set after self._get_die_traceability_hash - except AttributeError as e: - print('Problem retrieving variant id information. Ask Dave for help') - return -1 #??? - def debug_mode(self): - '''query to see if module debug flag is set. useful where otherwise not provided as method argument, ex in setup().''' - return bool(self._parent_module._debug) - def get_die_traceability_hash_table(self): - return self._parent_module._die_traceability_hash - def get_correlation_data(self, REFID, temperature=None): - return self._parent_module.get_correlation_data(REFID, temperature) - def get_correlation_data_scalar(self, REFID, temperature): - return self._parent_module.get_correlation_data_scalar(REFID, temperature) - def get_test_limits(self, test_name): - try: - tst_decl = self._parent_module._test_results._test_declarations[test_name] - except KeyError: - tst_decl = self._parent_module._correlation_results._correlation_declarations[test_name] - #test_declaration(test_name='CH0_VDROOP', requirement_reference=None, lower_limit=-0.15, upper_limit=0.15, description='CH0 droop due to load step', notes=None) - return (tst_decl.lower_limit, tst_decl.upper_limit) - @abc.abstractmethod - def register_tests(self): - '''enumerate measurments to be taken, limits, etc before writing test or collcting results - make repeated calls to self.register_test_*()''' - @abc.abstractmethod - def plot(self, database, table_name, plot_filepath, skip_output): - '''output plot from previously stored data. Static method prevents logger/instrument creation. what about self.name??? - return list of svg sources??? - if skip_output, return svg source without writing to files''' - @abc.abstractmethod - def compile_test_results(self, database, table_name): - '''inspect previously collected data. Check against parametic limits or functional specification to return Pass/Fail aggregated result.''' - - def _add_attr(self,key,value): - setattr(self,key,value) - diff --git a/PyICe/refid_modules/test_results.py b/PyICe/refid_modules/test_results.py deleted file mode 100644 index c967152..0000000 --- a/PyICe/refid_modules/test_results.py +++ /dev/null @@ -1,1102 +0,0 @@ -from PyICe import LTC_plot -from PyICe.lab_utils.sqlite_data import sqlite_data -from PyICe.refid_modules import test_module -import collections -import datetime -import functools -import json -import numbers -import pprint -from numpy import bool_, ndarray - -pp = pprint.PrettyPrinter(indent=4) -# pp.pformat(obj) -# pp.pprint(obj) -# print = pp.pprint - -# 2021/12/14 Dave Simmons -# Notes collected below w.r.t deficiencies identified in the output format, and tests results module more generally -# Gathered through discussions with Sauparna and Steve. -# Intend to fix them incrementally and remove comment line here as-addressed. - -# eeprom contents logging -# change stowe logger to directly capture collect() method? -# add compile() to report? Logging not necessary because it's re-run at report gen time. -# https://json-schema.org/understanding-json-schema/structuring.html#structuring -# test results schema checker -# -# https://jama.analog.com/perspective.req#/items/4382084?projectId=597 -# plot hyperlink -# data hyperlink -# conditions -# scope of measurement -# test method -# jira link - -# https://docs.python.org/3/library/dataclasses.html - - -# https://stackoverflow.com/questions/5884066/hashing-a-dictionary/22003440#22003440 -def freeze(o): - if isinstance(o,dict): - return frozenset({ k:freeze(v) for k,v in o.items()}.items()) #sorted?? - if isinstance(o,set): - return frozenset(o) - if isinstance(o,list): - return tuple(freeze(v) for v in o) - if isinstance(o,tuple): - # Tuples are immutable, but their members might not be. - return tuple(freeze(v) for v in o) - try: - hash(o) - except TypeError as e: - raise TypeError("Something slipped through the freeze function. Contact pyice-developers@analog.com. Or file a bug report at jira.analog.com/pyice") from e - else: - return o - -def make_hash(o): - """ - makes a hash out of anything that contains only list,dict and hashable types including string and numeric types - """ - return hash(freeze(o)) - -def none_min(a, b): - if a is None and b is None: - return None - if a is None: - return b - if b is None: - return a - return min(a,b) -def none_max(a, b): - if a is None and b is None: - return None - if a is None: - return b - if b is None: - return a - return max(a,b) -def none_abs(a): - if a is None: - return None - return abs(a) - - -class generic_results(): - '''Parent of test_results and correlation_results and keeper of any commonalities.''' - def __init__(self): - raise Exception("This class isn't supposed to be instantiated directly.") - # TODO https://docs.python.org/3/library/abc.html ? - def _init(self, name, module): - self._name = name - self._traceability_info = collections.OrderedDict() - #{'TRACEABILITY_HASH': None, - # 'REVID': None, - # 'BENCH_CONFIGURATION': None, - # 'EQUIPMENT_INFO': None, - # 'OPERATOR': None, - # 'AUTHOR': None, - # 'OWNER': None, - # 'VARIANT': None, - # 'VARIANT_REVISION': None, - # 'STDF_FILE': None, - # # SWARM LINKS? - #} - # datetime - self._module = module - self.table_name=None - self.db_filepath=None - def get_name(self): - return self._name - def get_traceability_info(self): - return self._traceability_info - def set_table_name(self,table_name): - self.table_name=table_name - def set_db_filepath(self,db_filepath): - self.db_filepath=db_filepath - def _set_traceability_info(self, **kwargs): - for k,v in kwargs.items(): - if True: #k in self._traceability_info.keys(): - self._traceability_info[k] = v - else: - raise Exception(f'Received unexpected traceability field {k} with value {v}. Contact pyice-developers@analog.com. Or file a bug report at jira.analog.com/pyice.') - for k in ["datetime", - "TRACEABILITY_HASH", - "bench_operator", - "bench_instruments", - "comment", - "test_swarmlink", - "test_cumulative_time", - "bench_configuration", - "revid", - "f_die_fab_2_0_", - "f_die_fab_4_3_", - "f_die_parent_child_9_8_", - "f_die_loc_y", - "f_die_loc_x", - "f_die_running_7_0_", - "f_die_parent_child_7_0_", - "f_die_fab_6_5_", - "f_die_wafer_number", - "f_die_running_9_8_", - "f_die_week", - "f_die_year", - "START_T", - "STDF_file", - "config_file", - "config_change", - "config_date", - "variant_datasheet", - "variant_shell", - ]: - try: - assert k in self._traceability_info, f'ERROR: {k} key missing from {self.get_name()} traceability information.' - except AssertionError as e: - if input (f'{e} Continue anyway? ').lower() not in ("y", "yes"): - raise e - else: - self._traceability_info[k] = None - try: - self._traceability_info['ATE_date'] = datetime.datetime.utcfromtimestamp(self._traceability_info['START_T']).strftime('%Y-%m-%dT%H:%M:%S.%fZ') - except TypeError as e: - self._traceability_info['ATE_date'] = None # "app_configuration" expected with EEPROM hardware (Schema 1.0), but not required (Schema 0.2) - # "cap_mb_isolator_serialnum", - # "cap_mb_isolatorboard_component_purpose", - # "cap_mb_isolatorboard_data", - # "targetboard_serialnum", - # "targetboard_component_purpose", - # "targetboard_data", - # "skew" expected Schema 1.1 onward. - - - - def _json_report(self, declarations, results, ate_results=[]): - # def _json_report(self, declarations, results, ate_results=[], *args): - # if not len(declarations.keys()): - if not len(self): - # No tests (or no correlations) in this module - # Omit whole report, since there's nothing to report. - return None - class CustomJSONizer(json.JSONEncoder): - def default(self, obj): - if isinstance(obj, bool_): - return bool(obj) - elif isinstance(obj, datetime.datetime): - return obj.strftime('%Y-%m-%dT%H:%M:%S.%fZ') - elif isinstance(obj, ndarray): - return obj.tolist() - # elif isinstance(obj, np.integer): - # return int(obj) - # elif isinstance(obj, np.floating): - # return float(obj) - else: - try: - return super().default(obj) - except TypeError as e: - print(f'JSON Serialization error with object of type {type(obj)}:') - print(obj) - breakpoint() - raise e - res_dict = {} - # res_dict['schema_version'] = 0.2 # Increment after release - # res_dict['schema_version'] = 1.0 # Traceable eval hardware via eeprom - # res_dict['schema_version'] = 1.1 # Add skew map (will provide workaround for legacy format) and 5% (of window) autocorrelation hint (output only - remade from raw data). - # res_dict['schema_version'] = 1.2 # Add a human readable version of the ATE test datetime to traceability. - res_dict['schema_version'] = 1.3 # Add table_name and dirpath for easy eval_report generation. - res_dict['test_module'] = self.get_name() - res_dict['report_date'] = datetime.datetime.utcnow().strftime('%Y-%m-%dT%H:%M:%S.%fZ') - trace_data = self.get_traceability_info() - res_dict['collection_date'] = trace_data['datetime'] - res_dict['table_name'] = self.table_name - res_dict['filepath'] = self.db_filepath - res_dict['traceability'] = {k:v for k,v in trace_data.items() if k not in ['datetime']} - # for f in *args: - # f(res_dict) - res_dict['tests'] = {} - for t_d in declarations: - res_dict['tests'][t_d] = {} - res_dict['tests'][t_d]['declaration'] = {k:v for k,v in declarations[t_d]._asdict().items() if k not in ['test_name', 'refid_name']} - try: - results[t_d] - except KeyError as e: - res_dict['tests'][t_d]['passes'] = False - else: - res_dict['tests'][t_d]['results'] = {} - - if isinstance(self, test_results): - res_dict['tests'][t_d]['results']['cases'] = [] - res_dict['tests'][t_d]['results']['summary'] = {} - for condition_hash, condition_orig in results[t_d].get_conditions().items(): - filter_results = results[t_d].filter(condition_hash) - cond_dict = {'conditions': condition_orig, #TODO put back to dictionary! - 'case_results': [{k:v for k,v in t_r._asdict().items() if k not in ['test_name', 'conditions', 'plot']} for t_r in filter_results], - 'summary': {'min_data': filter_results._min(), - 'max_data': filter_results._max(), - 'passes': bool(filter_results), - }, - } - res_dict['tests'][t_d]['results']['cases'].append(cond_dict) - res_dict['tests'][t_d]['results']['summary'] = {'min_data': results[t_d]._min(), - 'max_data': results[t_d]._max(), - 'passes': bool(results[t_d]), - } - res_dict['tests'][t_d]['ate_results'] = [] - for ate_r in ate_results[t_d]: - res_dict['tests'][t_d]['ate_results'].append({k:v for k,v in ate_r._asdict().items() if k not in ['test_name']}) - #TODO tdegc alignment! - try: - min_error = ate_r.result - results[t_d]._min() - except TypeError as e: - min_error = None - try: - max_error = ate_r.result - results[t_d]._max() - except TypeError as e: - max_error = None - res_dict['tests'][t_d]['ate_results'][-1]['min_error'] = min_error - res_dict['tests'][t_d]['ate_results'][-1]['max_error'] = max_error - max_abs_error = none_max(none_abs(min_error), none_abs(max_error)) - if max_abs_error is not None and res_dict['tests'][t_d]['declaration']['correlation_autolimit'] is not None: - passes = max_abs_error <= res_dict['tests'][t_d]['declaration']['correlation_autolimit'] - else: - # can't compare something - passes = True - res_dict['tests'][t_d]['ate_results'][-1]['passes'] = passes - elif isinstance(self, correlation_results): - res_dict['tests'][t_d]['results']['temperatures'] = [] - for temperature in results[t_d].get_temperatures(): - temperature_dict = {'temperature': temperature, - 'cases': [], - # 'summary': {}, - } - res_dict['tests'][t_d]['results']['temperatures'].append(temperature_dict) - temp_group = results[t_d].filter_temperature(temperature) - for condition_hash, condition_orig in temp_group.get_conditions().items(): - cond_group = temp_group.filter_conditions(condition_hash) - cond_dict = {'conditions': condition_orig, - 'case_results': [{k:v for k,v in cond._asdict().items() if k not in ['refid_name', 'temperature', 'conditions']} for cond in cond_group], - 'summary': {'min_error': cond_group._min_error(), - 'max_error': cond_group._max_error(), - 'passes': bool(cond_group), - }, - } - temperature_dict['cases'].append(cond_dict) - temperature_dict['summary'] = {'min_error': temp_group._min_error(), - 'max_error': temp_group._max_error(), - 'passes': bool(temp_group), - } - res_dict['tests'][t_d]['results']['summary'] = {'min_error': results[t_d]._min_error(), - 'max_error': results[t_d]._max_error(), - 'passes': bool(results[t_d]), - } - else: - raise Exception("I'm lost.") - res_dict['summary'] = {'passes': bool(self)} - # TODO Signature/CRC? - return json.dumps(res_dict, indent=2, ensure_ascii=False, cls=CustomJSONizer) - -class test_results(generic_results): - _ate_result = collections.namedtuple('ate_result', ['test_name', 'result', 'temperature']) # 'lower_limit', 'upper_limit', FROM DLOG?? Prob discarded.... - class _test_result(collections.namedtuple('test_result', ['test_name', 'conditions', 'min_data', 'max_data', 'passes', 'failure_reason', 'collected_data', 'plot', 'query'])): - '''add some helper moethods for easy summary''' - def __new__(cls, **kwargs): - '''fix (allowed) missing fields. FOr instance, original JSON didn't retain SQL query string.''' - if 'query' not in kwargs: - kwargs['query'] = None - return super().__new__(cls, **kwargs) - def __bool__(self): - return bool(self.passes) - def _min(self): - if not len(self.collected_data): - return None - return min(self.collected_data) - def _max(self): - if not len(self.collected_data): - return None - return max(self.collected_data) - def __str__(self): - summary_str = '' - summary_str += f'{self.conditions}\tTRIALS:{len(self)}\tVERDICT:{"PASS" if self else "FAIL"}\n'.expandtabs() - min = self._min() - if self.failure_reason != '': - summary_str += f'\tFORCED_FAIL: {self.failure_reason}\n' - elif len(self) > 1: - summary_str += f'\tMIN:{f"{min:g}" if type(min) is int else f"{min}" if min is not None else "None"}\n' if not self else '' - max = self._max() - summary_str += f'\tMAX:{f"{max:g}" if type(max) is int else f"{max}" if max is not None else "None"}\n' if not self else '' - else: - summary_str += f'\tDATA:{f"{min:g}" if type(min) is int else f"{min}" if min is not None else "None"}\n' if not self else '' - return summary_str - def __len__(self): - return len(self.collected_data) - def __add__(self, other): - assert isinstance(other, type(self)) - assert self.test_name == other.test_name - assert self.conditions == other.conditions - assert self.query == other.query, f"ERROR {self.test_name} grouping mismatch. Grouped results have unexpectedly disparate SQL queries. Consider adding conditions by selecting addtional columns or by keyword argument. If you think you've received this message in error, see Dave." - return type(self)(test_name=self.test_name, - conditions=self.conditions, - min_data=none_min(self.min_data, other.min_data), #None creeps in from register_test_failure() - max_data=none_max(self.max_data, other.max_data), #None creeps in from register_test_failure() - passes=self.passes and other.passes, - failure_reason=f'{self.failure_reason}{other.failure_reason}', #TODO cleanup format - collected_data=self.collected_data + other.collected_data, - plot=self.plot + other.plot, - query=self.query - ) - class _test_results_list(list): - '''add some helper methods for easy filtering and summary''' - def __init__(self, declaration): - self._declaration = declaration - super().__init__() - def __bool__(self): - if not len(self): - return False - return bool(functools.reduce(lambda a,b: a and b, [item.passes for item in self])) - def __str__(self): - resp = '' - resp += f'{self._declaration.test_name}\n' - resp += f'\tLIMITS:' - if self._declaration.upper_limit == self._declaration.lower_limit: #DOES THIS WORK? WRONG CLASS????? - #Exact test - try: - resp += f'\t SL:{self._declaration.upper_limit:g}' - except TypeError as e: - resp += f'\t SL:{self._declaration.upper_limit}' - else: - if self._declaration.lower_limit is not None: - resp += f'\tLSL:{self._declaration.lower_limit:g}' - if self._declaration.upper_limit is not None: - resp += f'\tUSL:{self._declaration.upper_limit:g}' - resp += '\n' - resp += '\tRESULTS:\n' - for cond_res in self.factored(): - for line in str(cond_res).splitlines(): - resp += f'\t\t{line}\n' - resp += f'{self._declaration.test_name} summary {"PASS" if self else "FAIL"}.\n' - return resp - def _min(self): - if not len(self): - return None - return functools.reduce(none_min, (r._min() for r in self)) #None creeps in from failures - def _max(self): - if not len(self): - return None - return functools.reduce(none_max, (r._max() for r in self)) #None creeps in from failures - def get_conditions(self): - #TODO fix hash collisions??? - return {make_hash(data_group.conditions): data_group.conditions for data_group in self} - def filter(self, condition_hash): - ret = type(self)(declaration=self._declaration.test_name) - for data_group in self: - if make_hash(data_group.conditions) == condition_hash: - ret.append(data_group) - return ret - def factored(self): - '''returns new object; doesn't modifiy existing one in place - merges all resutls from like conditions''' - ret = type(self)(declaration=self._declaration.test_name) - for cond_hash in self.get_conditions(): - our_list = [data_group for data_group in self if make_hash(data_group.conditions)==cond_hash] - if cond_hash == make_hash(None) and not sum([len(x) for x in our_list]): - ## We got here because no rows were returned and conditions were lost. Red Flag. - [ret.append(x) for x in our_list] - elif cond_hash == make_hash(None) and not min([len(x) for x in our_list]): - raise Exception("There is a mix in the group of no conditions of no results and some results. For want of an example or test case, how to handle that is presently unclear. Please contact support.") - else: - data_group = functools.reduce(lambda a,b: a+b, our_list) - ret.append(data_group) - return ret - def __init__(self, name, module): - '''TODO''' - self._test_declarations = collections.OrderedDict() - self._test_results = collections.OrderedDict() - self._ate_results = collections.OrderedDict() - self._init(name, module) - def json_report(self): - return self._json_report(declarations=self._test_declarations, results=self._test_results, ate_results=self._ate_results) - # return self._json_report(declarations=self._test_declarations, results=self._test_results, *args) - def _set_additional_registration_arguments(self, *args): - self._test_declaration = collections.namedtuple('test_declaration', ['test_name', 'lower_limit', 'upper_limit', 'correlation_autolimit', *args]) - def get_test_declarations(self): - return list(self._test_declarations.keys()) - def get_test_declaration(self, key): - return self._test_declarations[key] - def __str__(self): - '''printable regression results''' - #TODO more concise summary when passing, grouped results, etc. - resp = '' - passes = bool(len(self._test_declarations)) - for test in self._test_declarations: - for line in str(self[test]).splitlines(): - resp += f'\t{line}\n' - passes &= bool(self[test]) - return resp.expandtabs(3) - def __bool__(self): - if not len(self): - #No tests declared - return True - return bool(functools.reduce(lambda a,b: a and b, [bool(self[k]) for k in self])) - def __iter__(self): - '''test declaration names''' - return iter(self._test_declarations.keys()) - def __len__(self): - return len(self.get_test_declarations()) - def __getitem__(self, key): - return self._test_results[key] - def _plot(self, key=None): - '''how should this work? one at a time or all together?''' - # TODO keep or remove plotting? - unit_axis_conversion = { - 'µA':"CURRENT (µA)", - 'mA':"CURRENT (mA)", - 'A':"CURRENT (A)", - 'mV':"VOLTAGE (mV)", - 'V':"VOLTAGE (V)", - '%':"PERCENT (%)", - 'µs':"TIME (µs)", - 'ns':"TIME (ns)", - 's':"TIME (s)", - 'P|F':"Pass/Fail", - 'W':"Power (W)", - 'MHz':"FREQUENCY (MHz)", - 'Ω':"RESISTANCE (Ω)", - 'kΩ':"RESISTANCE (kΩ)", - 'pF':"CAPACITANCE (pF)", - } - if key is not None: - tsts = [key] - else: - tsts = [k for k in self] - for tst in tsts: - tst_decl = self._test_declarations[tst] - tst_rslts = self._test_results[tst] - passes = functools.reduce(lambda a,b: a and b, [data_group.passes for data_group in tst_rslts]) if len(tst_rslts) else False # No data submitted. - plt = LTC_plot.plot(plot_title=f'{tst_decl.test_name}\n{"PASS" if passes else "FAIL"}', - plot_name=tst_decl.test_name, - # xaxis_label=unit_axis_conversion[tst_decl.unit] if tst_decl.unit in unit_axis_conversion else "don't know units anymore", - # yaxis_label='COUNT', - xaxis_label="TRIAL", - yaxis_label='VALUE', - # xlims=(tst_rslts.min_data, tst_rslts.max_data), - # xlims=(foo, bar), - xlims=None, - ylims=None, - xminor=None, - xdivs=4, - yminor=None, - ydivs=10, - logx=False, - logy=False - ) - scatter_data = [] - xdata=0 - for data_group in tst_rslts: - for ydata in data_group.collected_data: - scatter_data.append((xdata,ydata)) - xdata+=1 - plt.add_scatter(axis=1, - data=scatter_data, - color=LTC_plot.LT_RED_2, - marker='*', - legend=str(data_group.conditions), #TODO Notes field? - ) - # This autoranging stuff has problems when empty/Null/None data gets in this far. It crashes matplotlib limit scaling functions. Noted here, but hopefully that type of test result is alwasy bogus and shold be caught and dealt with before we get this far along. - if tst_decl.lower_limit is not None: - plt.add_horizontal_line(value=tst_decl.lower_limit, xrange=None, note=None, color=[1,0,0]) - if tst_decl.upper_limit is not None: - plt.add_horizontal_line(value=tst_decl.upper_limit, xrange=None, note=None, color=[1,0,0]) - try: - tst_rslts[0].plot.append(plt.create_svg(file_basename=None)) #Todo - where should the plot really go??? - except IndexError as e: - # No data submitted! - pass - except ValueError as e: - print(e) #TODO debug later. Crashing when list is submitted for population data (sequence up order). No idea what relaly happening. DJS 2021/04/05 - - def _register_test(self, name, lower_limit, upper_limit, **kwargs): - self._set_additional_registration_arguments(*list(kwargs.keys())) - # TODO bring jira link through from REFID where possible? - if name in self._test_declarations: - raise Exception(f'Duplicated test name {name} within module {self.get_name()}!') - try: - correlation_autotolerance = 0.05 #Magic number alert. Maxim standard correlation to 5% of spec window size. - correlation_autolimit = correlation_autotolerance*(upper_limit-lower_limit) - if correlation_autolimit < 0: - correlation_autolimit = None - except TypeError as e: - correlation_autolimit = None - - self._test_declarations[name] = self._test_declaration(test_name=name, - lower_limit=lower_limit, - upper_limit=upper_limit, - correlation_autolimit=correlation_autolimit, - **kwargs - ) - self._test_results[name] = self._test_results_list(self._test_declarations[name]) - self._ate_results[name] = [] - return self._test_declarations[name] - def _register_ate_result(self, name, result, temperature): - self._ate_results[name].append(self._ate_result(test_name=name, - result=result, - temperature=temperature, - ) - ) - def _register_test_failure(self, name, reason, conditions, query=None): - if name not in self._test_declarations: - raise Exception(f'Undeclared test results: {name}') - failure_result = self._test_result(test_name=name, - conditions=conditions, - min_data=None, - max_data=None, - passes=False, - failure_reason=reason, - collected_data=[], #Give a chance to re-compute summary statistics if more data comes in later. - plot=[], #Mutable; add later - query=query, - ) - self._test_results[name].append(failure_result) - return failure_result - def _register_test_result(self, name, iter_data, conditions, query=None): - if name not in self._test_declarations: - raise Exception(f'Undeclared test results: {name}') - ############################################################# - # TODO deal with functional test pass/fail non-numeric data # - ############################################################# - if type(iter_data) == sqlite_data: - assert query is None - query = (iter_data.sql_query, iter_data.params) - if iter_data.get_column_names() is None: - print(f'\nWARNING FROM {__name__}!\n The sql query {query} for {name} returned nothing. Please double check the query parameters.\n') - return self._register_test_failure(name=name, reason="No submitted data.", conditions=conditions, query=query) - if len(iter_data.get_column_names()) > 1: - conditions_columns = iter_data.get_column_names()[1:] - nt_type = collections.namedtuple('distincts',conditions_columns) - # distincts = iter_data.get_distinct(conditions_columns, force_tuple=True) - iter_data = iter_data.to_list() - distincts = {nt_type._make(freeze(row[1:])) for row in iter_data} - try: - distincts = sorted(distincts) - except TypeError: - pass - rowcount = len(iter_data) - match_count = 0 - assert conditions is None, "TODO: This isn't a permanent error, but it hasn't been impllemented yet. What to do about explicit conditions???? Append???" - for condition in distincts: - data = [row[0] for row in iter_data if freeze(row[1:]) == condition] - self._register_test_result(name=name, iter_data=data, conditions=condition._asdict(), query=query) #todo, consider reimplementing __str__ instead of dict conversion. - match_count += len(data) - assert match_count == rowcount - return - else: - iter_data = [row[0] for row in iter_data] - elif isinstance(iter_data, numbers.Number): - iter_data = [iter_data] - elif isinstance(iter_data, (list, tuple)): - # Steve passing in an ordered list for sequence order. Needs to be double-listed to avoid iterating the sequence itself! - pass - if iter_data is None: - return self._register_test_failure(name=name, reason="None encountered in submitted data.", conditions=conditions, query=query) - if None in iter_data: - t_f = self._register_test_failure(name=name, reason="None encountered in submitted data.", conditions=conditions, query=query) - iter_data = [item for item in iter_data if item is not None] - if not len(iter_data): - return t_f - assert (self._test_declarations[name].upper_limit is not None or self._test_declarations[name].lower_limit is not None), f'Something is wrong with test limits for {name}. See Dave.' - if self._test_declarations[name].upper_limit != self._test_declarations[name].lower_limit: - passes = functools.reduce(lambda x,y: x and y, [data_pt is not None \ - and (self._test_declarations[name].upper_limit is None or data_pt <= self._test_declarations[name].upper_limit) \ - and (self._test_declarations[name].lower_limit is None or data_pt >= self._test_declarations[name].lower_limit) \ - for data_pt in iter_data - ] - ) - else: - # I think this is an exact test. Avoid arithmetic and ordering comparisons in case data isn't scalar. - passes = functools.reduce(lambda x,y: x and y, [data_pt is not None \ - and (data_pt == self._test_declarations[name].upper_limit) #upper==lower \ - for data_pt in iter_data - ] - ) - min_data = min(iter_data) - max_data = max(iter_data) - new_result_record = self._test_result(test_name=name, - conditions=conditions, - min_data=min_data, - max_data=max_data, - passes=passes, - failure_reason='', - collected_data=iter_data, #Give a chance to re-compute summary statistics if more data comes in later. - plot=[], #Mutable; add later - query=query, - # TODO Notes?? - ) - self._test_results[name].append(new_result_record) - return new_result_record - -class ResultsSchemaMismatchException(Exception): - '''Mismatch between writer and reader formats''' - -class test_results_reload(test_results): - def __init__(self, results_json='test_results.json'): - self._schema_version = 1.3 - self._test_declarations = collections.OrderedDict() - self._test_results = collections.OrderedDict() - self._ate_results = collections.OrderedDict() - with open(results_json, mode='r', encoding='utf-8') as f: - self.__results = json.load(f) - f.close() - # if self.__results['schema_version'] != self._schema_version: - if self.__results['schema_version'] not in (0.2, 1.0, 1.1, 1.2, 1.3): - raise ResultsSchemaMismatchException(f'Results file {results_json} written with schema version {self.__results["schema_version"]}, but reader expecting {self._schema_version}.') - self._init(name=self.__results['test_module'], module=None) - # print(f'INFO Loading test {self.get_name()} record produced on {self.__results["report_date"]} from data collected on {self.__results["collection_date"]}.\n\t({results_json})') #TODO too loud for logs? - self._set_traceability_info(datetime=self.__results["collection_date"], **self.__results["traceability"]) - # TODO flag json re-output as derivative???? - for test in self.__results['tests']: - try: - # Not used on read-in. Recreated from limts each time. - del self.__results['tests'][test]['declaration']['correlation_autolimit'] - except KeyError: - # Not present before schema 1.1 - pass - self._register_test(name=test, **self.__results['tests'][test]['declaration']) - for case in self.__results['tests'][test]['results']['cases']: - for trial in case['case_results']: - self._test_results[test].append(self._test_result(test_name=test, - conditions=case['conditions'], - plot=[], - **trial - ) - ) - for ate_data in self.__results['tests'][test]['ate_results']: - try: - # Not used on read-in. Recreated from limts and raw data each time. - del ate_data['min_error'] - del ate_data['max_error'] - del ate_data['passes'] - except KeyError: - # Not present before schema 1.1 - pass - self._register_ate_result(name=test, **ate_data) - def json_report(self, filename='test_results_rewrite.json'): - with open(filename, 'wb') as f: - try: - f.write(super().json_report().encode('utf-8')) - except: - f.write(super().json_report().encode('utf-8')) - f.close() - - -class correlation_results(generic_results): - _correlation_declaration = collections.namedtuple('correlation_declaration', ['refid_name', 'ATE_test', 'ATE_subtest', 'owner', 'assignee', 'lower_limit', 'upper_limit', 'unit', 'description', 'notes', 'limits_units_percentage', 'requirement_reference', 'safety_critical', 'correlation_spec']) - # _correlation_declaration = collections.namedtuple('correlation_declaration', ['refid_name', 'lower_limit', 'upper_limit', 'auxiliary']) - class _correlation_result(collections.namedtuple('correlation_result', ['refid_name', 'temperature', 'conditions', 'bench_data', 'ate_data', 'error', 'failure_reason', 'passes', 'query'])): - # class _correlation_result(collections.namedtuple('correlation_result', ['refid_name', 'error', 'passes', 'auxiliary'])): - '''add some helper moethods for easy summary''' - def __new__(cls, **kwargs): - '''fix (allowed) missing fields. FOr instance, original JSON didn't retain SQL query string.''' - if 'query' not in kwargs: - kwargs['query'] = None - return super().__new__(cls, **kwargs) - def __bool__(self): - return bool(self.passes) #numpy _bool infiltration - def __str__(self): - summary_str = '' - summary_str += f'\tERROR: {self.error:g}' if self.error is not None else '' - summary_str += f'\t{self.failure_reason}' if self.failure_reason is not '' else '' - summary_str += f'\tVERDICT:{"PASS" if self else "FAIL"}\n' - return summary_str - - class _correlation_results_list(list): - '''helper methods''' - def __init__(self, declaration): - self._declaration = declaration - def __bool__(self): - if not len(self): - return False - return bool(functools.reduce(lambda a,b: a and b, [item.passes for item in self])) - def __str__(self): - resp = f'{self._declaration.refid_name}\n' - resp += f'\tLIMITS:\t' - resp += f'LSL: {self._declaration.lower_limit:g}\t' if self._declaration.lower_limit is not None else '' - resp += f'USL: {self._declaration.upper_limit:g}\t' if self._declaration.upper_limit is not None else '' - resp += '\n' - resp += '\tRESULTS:\n' - for temperature in self.get_temperatures(): - resp += f'\t\tTemperature {temperature}\n' - temp_group = self.filter_temperature(temperature) #sorted?? - if all('ATE data missing.' in result.failure_reason for result in temp_group) and not functools.reduce(lambda a,b: a or b, [res.passes for res in temp_group]): - #Each and every one failed, and because temp data was missing on ATE's side. - resp += '\t\t\tATE data missing.\n' - resp += f'\t\tTemperature {temperature} summary FAIL ({len(temp_group)} case{"s" if len(temp_group) != 1 else ""}).\n' - continue - elif {result.failure_reason for result in temp_group} == {f'{temperature}C is not a QAC, QAR, QAH, CORR temperature.'}: - #Each and every one passed, because temp data isn't correlated. - resp += f'\t\t\t{temperature}C is not a QAC, QAR, QAH, CORR temperature.\n' - resp += f'\t\tTemperature {temperature} summary PASS ({len(temp_group)} case{"s" if len(temp_group) != 1 else ""}).\n' - continue - for cond_hash, cond_orig in temp_group.get_conditions().items(): - resp_cond = f'{cond_orig}' - cond_group = temp_group.filter_conditions(cond_hash) - try: - if len(cond_group) > 1: - resp += f'\t\t\t{resp_cond}\n' - if all([res.passes for res in cond_group]) and all([res.error is not None for res in cond_group]): - resp += f'\t\t\t\tMIN ERR: {min([res.error for res in cond_group]):.3e}\t MAX ERR: {max([res.error for res in cond_group]):.3e}\n' - else: - for result in cond_group: - res_line = str(result).lstrip('\t').expandtabs() - resp += f'\t\t\t\t{res_line}' - else: - for result in cond_group: - resp_cond = f'{resp_cond}{result}'.expandtabs() - resp += f'\t\t\t{resp_cond}' - except Exception as e: - print(f'Partial resp: {resp}') - - print(f'resp_cond: {resp_cond}; cond_group: {cond_group}') - raise e - resp += f'\t\tTemperature {temperature} summary {"PASS" if temp_group else "FAIL"} ({len(temp_group)} case{"s" if len(temp_group) != 1 else ""}).\n' - resp += f'{self._declaration.refid_name} Summary {"PASS" if self else "FAIL"}.\n' - return resp - def _min_error(self): - if not len(self): - return None - return functools.reduce(none_min, [res.error for res in self]) - def _max_error(self): - if not len(self): - return None - return functools.reduce(none_max, [res.error for res in self]) - def get_conditions(self): - #TODO fix hash collisions??? - return {make_hash(data_group.conditions): data_group.conditions for data_group in self} - def get_temperatures(self): - return sorted({data_group.temperature for data_group in self}) - def filter_conditions(self, condition_hash): - ret = type(self)(declaration=self._declaration) - for data_group in self: - if make_hash(data_group.conditions) == condition_hash: - ret.append(data_group) - return ret - def filter_temperature(self, temperature): - '''single temp or list''' - try: - iter(temperature) - except TypeError as e: - temperatures = (temperature,) - else: - temperatures = temperature - ret = type(self)(declaration=self._declaration) - for data_group in self: - if data_group.temperature in temperatures: - ret.append(data_group) - return ret - # Doesn't make sense yet, since results aren't stored as a list; no way to aggregate without data model change. - # def factored(self): - # '''returns new object; doesn't modifiy existing one in place - # merges all resutls from like temperature and conditions''' - # ret = type(self)(declaration=self._declaration) - # for temperature in self.get_temperatures(): - # for cond_hash in self.get_conditions(): - # data_group = functools.reduce(lambda a,b: a+b, [data_group for data_group in self if make_hash(data_group.conditions)==cond_hash and data_group.temperature==temperature]) - # ret.append(data_group) - # return ret - - def __init__(self, name, module): - '''TODO''' - self._correlation_declarations = collections.OrderedDict() - self._correlation_results = collections.OrderedDict() - self._init(name, module) - def __str__(self): - #try: - # resp = lab_utils.build_banner(f'Module {self.get_name()} Correlation Report Stub {self._traceability_info["TRACEABILITY_HASH"]}') - #except KeyError: # This will happen if a test crashes and skips the result compilation. - # resp = lab_utils.build_banner(f'Module {self.get_name()} Correlation Report Stub Unknown: Hash Value Error') - resp = '' - for test in self._correlation_declarations: - for line in str(self[test]).splitlines(): - resp += f'\t{line}\n' - # resp += f'Correlation Summary {"PASS" if self else "FAIL"}.\n\n' - return resp.expandtabs(3) - return resp - def __getitem__(self, key): - return self._correlation_results[key] - def __iter__(self): - return iter(self._correlation_declarations.keys()) - def json_report(self): - return self._json_report(declarations=self._correlation_declarations, results=self._correlation_results) - def get_correlation_declarations(self): - return list(self._correlation_declarations.keys()) - def get_correlation_declaration(self, key): - return self._correlation_declarations[key] - def _set_additional_correlation_attributes(self, *args): - self._correlation_declaration = collections.namedtuple('correlation_declaration', ['refid_name', 'lower_limit', 'upper_limit', *args]) - def _is_in_spec(self, name, error): - declaration = self._correlation_declarations[name] - if error is None: - return False - if declaration.upper_limit is not None and error > declaration.upper_limit: - return False - if declaration.lower_limit is not None and error < declaration.lower_limit: - return False - return True - def _error(self, refid_name,temperature,bench_data): - # Won't work when module doesn't exist, ex json reload. - declaration = self._correlation_declarations[refid_name] - # Massage hot and cold a bit to force alignment with ATE data - # Per concern raised by Ashish and decided with Sauparna 2022/01/13 - if temperature is None: - t_ish = None # Ambient - elif isinstance(temperature, str) and temperature.upper() == 'NULL': - t_ish = None # Ambient with sqlite replacement in test script - elif -55 <= temperature <= -40: - t_ish = -40 - elif 120 <= temperature <= 160: - t_ish = 150 - elif 15 <= temperature <= 35: - t_ish = 25 - else: - t_ish = temperature - self.ate_result = self._module.get_correlation_data_scalar(REFID=refid_name, temperature=t_ish) - if declaration.limits_units_percentage: - return (self.ate_result - bench_data) / bench_data - else: - return self.ate_result - bench_data - - def __len__(self): - return len(self._correlation_declarations.keys()) - def __bool__(self): - if not len(self): - #No declarations - return True - return bool(functools.reduce(lambda a,b: a and b, (self[k] for k in self))) - - def _register_correlation_test(self, name, lower_limit, upper_limit, **kwargs): - if name in self._correlation_declarations: - raise Exception(f'Duplicated correlation refid name {name} within module {self.get_name()}!') - self._set_additional_correlation_attributes(*list(kwargs.keys())) - self._correlation_declarations[name] = self._correlation_declaration(refid_name= name, - lower_limit= lower_limit, - upper_limit= upper_limit, - **kwargs - ) - self._correlation_results[name] = self._correlation_results_list(declaration=self._correlation_declarations[name]) - return self._correlation_declarations[name] - def _register_correlation_result(self, refid_name, iter_data, conditions, query=None): # TODO conditions - if refid_name not in self._correlation_declarations: - raise Exception(f'Undeclared correlation results: {refid_name}') - if type(iter_data) == sqlite_data: - query = (iter_data.sql_query, iter_data.params) - if type(iter_data) == sqlite_data and iter_data.get_column_names() is not None and len(iter_data.get_column_names()) > 2: - conditions_columns = iter_data.get_column_names()[2:] - nt_type = collections.namedtuple('distincts',conditions_columns) - #distincts = iter_data.get_distinct(conditions_columns, force_tuple=True) - iter_data = iter_data.to_list() - distincts = {nt_type._make(freeze(row[2:])) for row in iter_data} - try: - distincts = sorted(distincts) - except TypeError: - pass - rowcount = len(iter_data) - match_count = 0 - assert conditions is None, "TODO: This isn't a permanent error, but it hasn't been impllemented yet. What to do about explicit conditions???? Append???" - for condition in distincts: - data = [(row[0], row[1]) for row in iter_data if freeze(row[2:]) == condition] - self._register_correlation_result(refid_name=refid_name, iter_data=data, conditions=condition._asdict(), query=query) #todo, consider reimplementing __str__ instead of dict conversion. - match_count += len(data) - assert match_count == rowcount - return - for (result, temperature) in iter_data: - #todo dimensional error handling? - if result is None: - # There's no bench data. Something has gone pretty wrong already, and this correlation is doomed to failure. - # Just to be extra-nice, we'll make a Hail Mary attempt to fetch ATE data, for the permanent record. It might be useful for debugging. - # If there were bench data, this work would be done inside self._error. - if temperature is None: - t_ish = None # Ambient - elif isinstance(temperature, str) and temperature.upper() == 'NULL': - t_ish = None # Ambient with sqlite replacement in test script - elif -55 <= temperature <= -40: - t_ish = -40 - elif 120 <= temperature <= 160: - t_ish = 150 - else: - # Let it through. 25 will match. Others won't - t_ish = temperature - try: - ate_result = self._module.get_correlation_data_scalar(REFID=refid_name, temperature=t_ish) - except ATEDataException as e: - ate_result = None - self._register_correlation_failure(name=refid_name, reason='Bench data missing.', temperature=temperature, conditions=conditions, ate_data=ate_result, query=query) - continue - try: - if temperature is 'NULL': - temperature = 25 - err=self._error(refid_name,temperature,result) - except TypeError: - # Magic number alert. Matches ATE temp massaging in _error() - if temperature is None: - # FTT, QAR - expected_missing = False - failure_reason = 'QAR/FTT ATE data missing.' - elif -55 <= temperature <= -40: - # QAC - expected_missing = False - failure_reason = 'QAC ATE data missing.' - # elif temperature == 25: - elif 15 <= temperature <= 35: - # FTT, QAR - expected_missing = False - failure_reason = 'QAR/FTT ATE data missing.' - elif 120 <= temperature <= 160: - # QAH - expected_missing = False - failure_reason = 'QAH ATE data missing.' - else: - expected_missing = True - failure_reason = f'{temperature}C is not a QAC, QAR, QAH, CORR temperature.' - self._correlation_results[refid_name].append(self._correlation_result(refid_name=refid_name, - temperature=temperature, - conditions=conditions, - bench_data=result, - ate_data=None, - error=None, - failure_reason=failure_reason, - passes=expected_missing, - query=query, - ) - ) - - continue - else: - passes = self._is_in_spec(refid_name, err) - self._correlation_results[refid_name].append(self._correlation_result(refid_name = refid_name, - temperature = temperature, - conditions=conditions, - bench_data = result, - ate_data=self.ate_result, - error=err, - failure_reason='', #TODO None (new schema?) - passes=passes, - query=query, - )) - def _register_correlation_failure(self, name, reason, temperature, conditions, bench_data=None, ate_data=None, query=None): - if name not in self._correlation_declarations: - raise Exception(f'Undeclared correlation results: {name}') - self._correlation_results[name].append(self._correlation_result(refid_name=name, - temperature=temperature, - conditions=conditions, - bench_data=bench_data, - error=None, - ate_data=ate_data, - failure_reason=reason, - passes=False, - query=query, - )) - -class correlation_results_reload(correlation_results): - def __init__(self, results_json='correlation_results.json'): - self._schema_version = 1.3 - self._correlation_declarations = collections.OrderedDict() - self._correlation_results = collections.OrderedDict() - with open(results_json, mode='r', encoding='utf-8') as f: - self.__results = json.load(f) - f.close() - # if self.__results['schema_version'] != self._schema_version: - if self.__results['schema_version'] not in (0.2, 1.0, 1.1, 1.2, 1.3): - raise ResultsSchemaMismatchException(f'Results file {results_json} written with schema version {self.__results["schema_version"]}, but reader expecting {self._schema_version}.') - self._init(name=self.__results['test_module'], module=None) - # print(f'INFO Loading correlation {self.get_name()} record produced on {self.__results["report_date"]} from data collected on {self.__results["collection_date"]}.\n\t({results_json})') #TODO too loud for logs? - self._set_traceability_info(datetime=self.__results["collection_date"], **self.__results["traceability"]) - # TODO flag json re-output as derivative???? - for test in self.__results['tests']: - self._register_correlation_test(name=test, **self.__results['tests'][test]['declaration']) - for temperature_group in self.__results['tests'][test]['results']['temperatures']: - temperature = temperature_group['temperature'] - for condition_group in temperature_group['cases']: - for result in condition_group['case_results']: - self._correlation_results[test].append(self._correlation_result(refid_name = test, - temperature = temperature, - conditions=condition_group['conditions'], - **result, - ) - ) - def json_report(self, filename='correlation_results_rewrite.json'): - with open(filename, 'wb') as f: - f.write(super().json_report().encode('utf-8')) - f.close() - - -if __name__ == '__main__': - from stowe_eval.stowe_eval_base.modules import refid_importer - #TODO command line filter/group args like stowe_die_traceability? - refids_plan = refid_importer.refid_importer() - try: - trr = test_results_reload() - except FileNotFoundError as e: - print("test_results.json not found in working directory.") - else: - print(trr) - # Copied from jira refid crosscheck - for refid in trr: - try: - spec_lower_limit = refids_plan[refid]['MIN'] if not refid_importer.isnan(refids_plan[refid]['MIN']) else None - spec_upper_limit = refids_plan[refid]['MAX'] if not refid_importer.isnan(refids_plan[refid]['MAX']) else None - spec_units = refids_plan[refid]['UNIT'] #if not refid_importer.isnan(refids_plan[refid]['UNIT']) else None - spec_ate_test = refids_plan[refid]['ATE TEST #'] if not refid_importer.isnan(refids_plan[refid]['ATE TEST #']) else None - spec_ate_subtest = refids_plan[refid]['ATE SUBTEST #'] if not refid_importer.isnan(refids_plan[refid]['ATE SUBTEST #']) else None - #todo collect script and multitest unit revisions check here against json???? - except KeyError as e: - print(f'ERROR REFID {refid} missing from eval plan.') - continue - - result_lower_limit = trr.get_test_declaration(refid).lower_limit - result_upper_limit = trr.get_test_declaration(refid).upper_limit - result_units = trr.get_test_declaration(refid).unit - result_ate_test = trr.get_test_declaration(refid).ATE_test if not refid_importer.isnan(trr.get_test_declaration(refid).ATE_test) else None - result_ate_subtest = trr.get_test_declaration(refid).ATE_subtest if not refid_importer.isnan(trr.get_test_declaration(refid).ATE_subtest) else None - if result_lower_limit != spec_lower_limit: - print(f'WARNING {refid} lower limit mismatches REFID spec. ({result_lower_limit} vs {spec_lower_limit})') - elif result_upper_limit != spec_upper_limit: - print(f'WARNING {refid} upper limit mismatches REFID spec. ({result_upper_limit} vs {spec_upper_limit})') - elif result_units != spec_units: - print(f'WARNING {refid} units mismatch REFID spec. ({result_units} vs {spec_units})') - elif result_ate_test != spec_ate_test: - print(f'WARNING {refid} ATE test mismatch REFID spec. ({result_ate_test} vs {spec_ate_test})') - elif result_ate_subtest != spec_ate_subtest: - print(f'WARNING {refid} ATE subtest mismatch REFID spec. ({result_ate_subtest} vs {spec_ate_subtest})') - - try: - crr = correlation_results_reload() - except FileNotFoundError as e: - print("correlation_results.json not found in working directory.") - else: - print(crr) - # Copied from jira refid crosscheck - for refid in crr: - try: - spec_lower_limit = refids_plan[refid]['MIN'] if not refid_importer.isnan(refids_plan[refid]['MIN']) else None - spec_upper_limit = refids_plan[refid]['MAX'] if not refid_importer.isnan(refids_plan[refid]['MAX']) else None - spec_units = refids_plan[refid]['UNIT'] #if not refid_importer.isnan(refids_plan[refid]['UNIT']) else None - spec_ate_test = refids_plan[refid]['ATE TEST #'] if not refid_importer.isnan(refids_plan[refid]['ATE TEST #']) else None - spec_ate_subtest = refids_plan[refid]['ATE SUBTEST #'] if not refid_importer.isnan(refids_plan[refid]['ATE SUBTEST #']) else None - except KeyError as e: - print(f'ERROR REFID {refid} missing from eval plan.') - continue - result_lower_limit = crr.get_correlation_declaration(refid).lower_limit - result_upper_limit = crr.get_correlation_declaration(refid).upper_limit - result_units = crr.get_correlation_declaration(refid).unit - result_ate_test = crr.get_correlation_declaration(refid).ATE_test if not refid_importer.isnan(crr.get_correlation_declaration(refid).ATE_test) else None - result_ate_subtest = crr.get_correlation_declaration(refid).ATE_subtest if not refid_importer.isnan(crr.get_correlation_declaration(refid).ATE_subtest) else None - if result_lower_limit != spec_lower_limit: - print(f'WARNING {refid} lower limit mismatches REFID spec. ({f"{result_lower_limit:g}" if result_lower_limit is not None else None} vs {f"{spec_lower_limit:g}" if spec_lower_limit is not None else None})') - elif result_upper_limit != spec_upper_limit: - print(f'WARNING {refid} upper limit mismatches REFID spec. ({f"{result_upper_limit:g}" if result_upper_limit is not None else None} vs {f"{spec_upper_limit:g}" if spec_upper_limit is not None else None})') - elif result_units != spec_units: - print(f'WARNING {refid} units mismatch REFID spec. ({result_units} vs {spec_units})') - elif result_ate_test != spec_ate_test: - print(f'WARNING {refid} ATE test mismatch REFID spec. ({result_ate_test} vs {spec_ate_test})') - elif result_ate_subtest != spec_ate_subtest: - print(f'WARNING {refid} ATE subtest mismatch REFID spec. ({result_ate_subtest} vs {spec_ate_subtest})') - - -