13ad7092fSWeilin Wang#SPDX-License-Identifier: GPL-2.0 23ad7092fSWeilin Wangimport re 33ad7092fSWeilin Wangimport csv 43ad7092fSWeilin Wangimport json 53ad7092fSWeilin Wangimport argparse 63ad7092fSWeilin Wangfrom pathlib import Path 73ad7092fSWeilin Wangimport subprocess 83ad7092fSWeilin Wang 93ad7092fSWeilin Wangclass Validator: 103ad7092fSWeilin Wang def __init__(self, rulefname, reportfname='', t=5, debug=False, datafname='', fullrulefname='', workload='true', metrics=''): 113ad7092fSWeilin Wang self.rulefname = rulefname 123ad7092fSWeilin Wang self.reportfname = reportfname 133ad7092fSWeilin Wang self.rules = None 14*1203a63dSWeilin Wang self.collectlist:str = metrics 15*1203a63dSWeilin Wang self.metrics = self.__set_metrics(metrics) 16*1203a63dSWeilin Wang self.skiplist = set() 173ad7092fSWeilin Wang self.tolerance = t 183ad7092fSWeilin Wang 193ad7092fSWeilin Wang self.workloads = [x for x in workload.split(",") if x] 203ad7092fSWeilin Wang self.wlidx = 0 # idx of current workloads 213ad7092fSWeilin Wang self.allresults = dict() # metric results of all workload 223ad7092fSWeilin Wang self.allignoremetrics = dict() # metrics with no results or negative results 233ad7092fSWeilin Wang self.allfailtests = dict() 243ad7092fSWeilin Wang self.alltotalcnt = dict() 253ad7092fSWeilin Wang self.allpassedcnt = dict() 263ad7092fSWeilin Wang self.allerrlist = dict() 273ad7092fSWeilin Wang 283ad7092fSWeilin Wang self.results = dict() # metric results of current workload 293ad7092fSWeilin Wang # vars for test pass/failure statistics 303ad7092fSWeilin Wang self.ignoremetrics= set() # metrics with no results or negative results, neg result counts as a failed test 313ad7092fSWeilin Wang self.failtests = dict() 323ad7092fSWeilin Wang self.totalcnt = 0 333ad7092fSWeilin Wang self.passedcnt = 0 343ad7092fSWeilin Wang # vars for errors 353ad7092fSWeilin Wang self.errlist = list() 363ad7092fSWeilin Wang 373ad7092fSWeilin Wang # vars for Rule Generator 383ad7092fSWeilin Wang self.pctgmetrics = set() # Percentage rule 393ad7092fSWeilin Wang 403ad7092fSWeilin Wang # vars for debug 413ad7092fSWeilin Wang self.datafname = datafname 423ad7092fSWeilin Wang self.debug = debug 433ad7092fSWeilin Wang self.fullrulefname = fullrulefname 443ad7092fSWeilin Wang 45*1203a63dSWeilin Wang def __set_metrics(self, metrics=''): 46*1203a63dSWeilin Wang if metrics != '': 47*1203a63dSWeilin Wang return set(metrics.split(",")) 48*1203a63dSWeilin Wang else: 49*1203a63dSWeilin Wang return set() 50*1203a63dSWeilin Wang 513ad7092fSWeilin Wang def read_json(self, filename: str) -> dict: 523ad7092fSWeilin Wang try: 533ad7092fSWeilin Wang with open(Path(filename).resolve(), "r") as f: 543ad7092fSWeilin Wang data = json.loads(f.read()) 553ad7092fSWeilin Wang except OSError as e: 563ad7092fSWeilin Wang print(f"Error when reading file {e}") 573ad7092fSWeilin Wang sys.exit() 583ad7092fSWeilin Wang 593ad7092fSWeilin Wang return data 603ad7092fSWeilin Wang 613ad7092fSWeilin Wang def json_dump(self, data, output_file): 623ad7092fSWeilin Wang parent = Path(output_file).parent 633ad7092fSWeilin Wang if not parent.exists(): 643ad7092fSWeilin Wang parent.mkdir(parents=True) 653ad7092fSWeilin Wang 663ad7092fSWeilin Wang with open(output_file, "w+") as output_file: 673ad7092fSWeilin Wang json.dump(data, 683ad7092fSWeilin Wang output_file, 693ad7092fSWeilin Wang ensure_ascii=True, 703ad7092fSWeilin Wang indent=4) 713ad7092fSWeilin Wang 723ad7092fSWeilin Wang def get_results(self, idx:int = 0): 733ad7092fSWeilin Wang return self.results[idx] 743ad7092fSWeilin Wang 753ad7092fSWeilin Wang def get_bounds(self, lb, ub, error, alias={}, ridx:int = 0) -> list: 763ad7092fSWeilin Wang """ 773ad7092fSWeilin Wang Get bounds and tolerance from lb, ub, and error. 783ad7092fSWeilin Wang If missing lb, use 0.0; missing ub, use float('inf); missing error, use self.tolerance. 793ad7092fSWeilin Wang 803ad7092fSWeilin Wang @param lb: str/float, lower bound 813ad7092fSWeilin Wang @param ub: str/float, upper bound 823ad7092fSWeilin Wang @param error: float/str, error tolerance 833ad7092fSWeilin Wang @returns: lower bound, return inf if the lower bound is a metric value and is not collected 843ad7092fSWeilin Wang upper bound, return -1 if the upper bound is a metric value and is not collected 853ad7092fSWeilin Wang tolerance, denormalized base on upper bound value 863ad7092fSWeilin Wang """ 873ad7092fSWeilin Wang # init ubv and lbv to invalid values 883ad7092fSWeilin Wang def get_bound_value (bound, initval, ridx): 893ad7092fSWeilin Wang val = initval 903ad7092fSWeilin Wang if isinstance(bound, int) or isinstance(bound, float): 913ad7092fSWeilin Wang val = bound 923ad7092fSWeilin Wang elif isinstance(bound, str): 933ad7092fSWeilin Wang if bound == '': 943ad7092fSWeilin Wang val = float("inf") 953ad7092fSWeilin Wang elif bound in alias: 963ad7092fSWeilin Wang vall = self.get_value(alias[ub], ridx) 973ad7092fSWeilin Wang if vall: 983ad7092fSWeilin Wang val = vall[0] 993ad7092fSWeilin Wang elif bound.replace('.', '1').isdigit(): 1003ad7092fSWeilin Wang val = float(bound) 1013ad7092fSWeilin Wang else: 1023ad7092fSWeilin Wang print("Wrong bound: {0}".format(bound)) 1033ad7092fSWeilin Wang else: 1043ad7092fSWeilin Wang print("Wrong bound: {0}".format(bound)) 1053ad7092fSWeilin Wang return val 1063ad7092fSWeilin Wang 1073ad7092fSWeilin Wang ubv = get_bound_value(ub, -1, ridx) 1083ad7092fSWeilin Wang lbv = get_bound_value(lb, float('inf'), ridx) 1093ad7092fSWeilin Wang t = get_bound_value(error, self.tolerance, ridx) 1103ad7092fSWeilin Wang 1113ad7092fSWeilin Wang # denormalize error threshold 1123ad7092fSWeilin Wang denormerr = t * ubv / 100 if ubv != 100 and ubv > 0 else t 1133ad7092fSWeilin Wang 1143ad7092fSWeilin Wang return lbv, ubv, denormerr 1153ad7092fSWeilin Wang 1163ad7092fSWeilin Wang def get_value(self, name:str, ridx:int = 0) -> list: 1173ad7092fSWeilin Wang """ 1183ad7092fSWeilin Wang Get value of the metric from self.results. 1193ad7092fSWeilin Wang If result of this metric is not provided, the metric name will be added into self.ignoremetics and self.errlist. 1203ad7092fSWeilin Wang All future test(s) on this metric will fail. 1213ad7092fSWeilin Wang 1223ad7092fSWeilin Wang @param name: name of the metric 123*1203a63dSWeilin Wang @returns: list with value found in self.results; list is empty when value is not found. 1243ad7092fSWeilin Wang """ 1253ad7092fSWeilin Wang results = [] 1263ad7092fSWeilin Wang data = self.results[ridx] if ridx in self.results else self.results[0] 1273ad7092fSWeilin Wang if name not in self.ignoremetrics: 1283ad7092fSWeilin Wang if name in data: 1293ad7092fSWeilin Wang results.append(data[name]) 1303ad7092fSWeilin Wang elif name.replace('.', '1').isdigit(): 1313ad7092fSWeilin Wang results.append(float(name)) 1323ad7092fSWeilin Wang else: 1333ad7092fSWeilin Wang self.ignoremetrics.add(name) 1343ad7092fSWeilin Wang return results 1353ad7092fSWeilin Wang 1363ad7092fSWeilin Wang def check_bound(self, val, lb, ub, err): 1373ad7092fSWeilin Wang return True if val <= ub + err and val >= lb - err else False 1383ad7092fSWeilin Wang 1393ad7092fSWeilin Wang # Positive Value Sanity check 1403ad7092fSWeilin Wang def pos_val_test(self): 1413ad7092fSWeilin Wang """ 1423ad7092fSWeilin Wang Check if metrics value are non-negative. 1433ad7092fSWeilin Wang One metric is counted as one test. 1443ad7092fSWeilin Wang Failure: when metric value is negative or not provided. 1453ad7092fSWeilin Wang Metrics with negative value will be added into the self.failtests['PositiveValueTest'] and self.ignoremetrics. 1463ad7092fSWeilin Wang """ 147*1203a63dSWeilin Wang negmetric = dict() 1483ad7092fSWeilin Wang pcnt = 0 1493ad7092fSWeilin Wang tcnt = 0 150*1203a63dSWeilin Wang rerun = list() 1513ad7092fSWeilin Wang for name, val in self.get_results().items(): 152*1203a63dSWeilin Wang if val < 0: 153*1203a63dSWeilin Wang negmetric[name] = val 154*1203a63dSWeilin Wang rerun.append(name) 1553ad7092fSWeilin Wang else: 1563ad7092fSWeilin Wang pcnt += 1 1573ad7092fSWeilin Wang tcnt += 1 158*1203a63dSWeilin Wang if len(rerun) > 0 and len(rerun) < 20: 159*1203a63dSWeilin Wang second_results = dict() 160*1203a63dSWeilin Wang self.second_test(rerun, second_results) 161*1203a63dSWeilin Wang for name, val in second_results.items(): 162*1203a63dSWeilin Wang if name not in negmetric: continue 163*1203a63dSWeilin Wang if val >= 0: 164*1203a63dSWeilin Wang del negmetric[name] 165*1203a63dSWeilin Wang pcnt += 1 1663ad7092fSWeilin Wang 1673ad7092fSWeilin Wang self.failtests['PositiveValueTest']['Total Tests'] = tcnt 1683ad7092fSWeilin Wang self.failtests['PositiveValueTest']['Passed Tests'] = pcnt 169*1203a63dSWeilin Wang if len(negmetric.keys()): 170*1203a63dSWeilin Wang self.ignoremetrics.update(negmetric.keys()) 171*1203a63dSWeilin Wang negmessage = ["{0}(={1:.4f})".format(name, val) for name, val in negmetric.items()] 172*1203a63dSWeilin Wang self.failtests['PositiveValueTest']['Failed Tests'].append({'NegativeValue': negmessage}) 1733ad7092fSWeilin Wang 1743ad7092fSWeilin Wang return 1753ad7092fSWeilin Wang 1763ad7092fSWeilin Wang def evaluate_formula(self, formula:str, alias:dict, ridx:int = 0): 1773ad7092fSWeilin Wang """ 1783ad7092fSWeilin Wang Evaluate the value of formula. 1793ad7092fSWeilin Wang 1803ad7092fSWeilin Wang @param formula: the formula to be evaluated 1813ad7092fSWeilin Wang @param alias: the dict has alias to metric name mapping 1823ad7092fSWeilin Wang @returns: value of the formula is success; -1 if the one or more metric value not provided 1833ad7092fSWeilin Wang """ 1843ad7092fSWeilin Wang stack = [] 1853ad7092fSWeilin Wang b = 0 1863ad7092fSWeilin Wang errs = [] 1873ad7092fSWeilin Wang sign = "+" 1883ad7092fSWeilin Wang f = str() 1893ad7092fSWeilin Wang 1903ad7092fSWeilin Wang #TODO: support parenthesis? 1913ad7092fSWeilin Wang for i in range(len(formula)): 1923ad7092fSWeilin Wang if i+1 == len(formula) or formula[i] in ('+', '-', '*', '/'): 1933ad7092fSWeilin Wang s = alias[formula[b:i]] if i+1 < len(formula) else alias[formula[b:]] 1943ad7092fSWeilin Wang v = self.get_value(s, ridx) 1953ad7092fSWeilin Wang if not v: 1963ad7092fSWeilin Wang errs.append(s) 1973ad7092fSWeilin Wang else: 1983ad7092fSWeilin Wang f = f + "{0}(={1:.4f})".format(s, v[0]) 1993ad7092fSWeilin Wang if sign == "*": 2003ad7092fSWeilin Wang stack[-1] = stack[-1] * v 2013ad7092fSWeilin Wang elif sign == "/": 2023ad7092fSWeilin Wang stack[-1] = stack[-1] / v 2033ad7092fSWeilin Wang elif sign == '-': 2043ad7092fSWeilin Wang stack.append(-v[0]) 2053ad7092fSWeilin Wang else: 2063ad7092fSWeilin Wang stack.append(v[0]) 2073ad7092fSWeilin Wang if i + 1 < len(formula): 2083ad7092fSWeilin Wang sign = formula[i] 2093ad7092fSWeilin Wang f += sign 2103ad7092fSWeilin Wang b = i + 1 2113ad7092fSWeilin Wang 2123ad7092fSWeilin Wang if len(errs) > 0: 2133ad7092fSWeilin Wang return -1, "Metric value missing: "+','.join(errs) 2143ad7092fSWeilin Wang 2153ad7092fSWeilin Wang val = sum(stack) 2163ad7092fSWeilin Wang return val, f 2173ad7092fSWeilin Wang 2183ad7092fSWeilin Wang # Relationships Tests 2193ad7092fSWeilin Wang def relationship_test(self, rule: dict): 2203ad7092fSWeilin Wang """ 2213ad7092fSWeilin Wang Validate if the metrics follow the required relationship in the rule. 2223ad7092fSWeilin Wang eg. lower_bound <= eval(formula)<= upper_bound 2233ad7092fSWeilin Wang One rule is counted as ont test. 2243ad7092fSWeilin Wang Failure: when one or more metric result(s) not provided, or when formula evaluated outside of upper/lower bounds. 2253ad7092fSWeilin Wang 2263ad7092fSWeilin Wang @param rule: dict with metric name(+alias), formula, and required upper and lower bounds. 2273ad7092fSWeilin Wang """ 2283ad7092fSWeilin Wang alias = dict() 2293ad7092fSWeilin Wang for m in rule['Metrics']: 2303ad7092fSWeilin Wang alias[m['Alias']] = m['Name'] 2313ad7092fSWeilin Wang lbv, ubv, t = self.get_bounds(rule['RangeLower'], rule['RangeUpper'], rule['ErrorThreshold'], alias, ridx=rule['RuleIndex']) 2323ad7092fSWeilin Wang val, f = self.evaluate_formula(rule['Formula'], alias, ridx=rule['RuleIndex']) 2333ad7092fSWeilin Wang if val == -1: 2343ad7092fSWeilin Wang self.failtests['RelationshipTest']['Failed Tests'].append({'RuleIndex': rule['RuleIndex'], 'Description':f}) 2353ad7092fSWeilin Wang elif not self.check_bound(val, lbv, ubv, t): 2363ad7092fSWeilin Wang lb = rule['RangeLower'] 2373ad7092fSWeilin Wang ub = rule['RangeUpper'] 2383ad7092fSWeilin Wang if isinstance(lb, str): 2393ad7092fSWeilin Wang if lb in alias: 2403ad7092fSWeilin Wang lb = alias[lb] 2413ad7092fSWeilin Wang if isinstance(ub, str): 2423ad7092fSWeilin Wang if ub in alias: 2433ad7092fSWeilin Wang ub = alias[ub] 2443ad7092fSWeilin Wang self.failtests['RelationshipTest']['Failed Tests'].append({'RuleIndex': rule['RuleIndex'], 'Formula':f, 2453ad7092fSWeilin Wang 'RangeLower': lb, 'LowerBoundValue': self.get_value(lb), 2463ad7092fSWeilin Wang 'RangeUpper': ub, 'UpperBoundValue':self.get_value(ub), 2473ad7092fSWeilin Wang 'ErrorThreshold': t, 'CollectedValue': val}) 2483ad7092fSWeilin Wang else: 2493ad7092fSWeilin Wang self.passedcnt += 1 2503ad7092fSWeilin Wang self.failtests['RelationshipTest']['Passed Tests'] += 1 2513ad7092fSWeilin Wang self.totalcnt += 1 2523ad7092fSWeilin Wang self.failtests['RelationshipTest']['Total Tests'] += 1 2533ad7092fSWeilin Wang 2543ad7092fSWeilin Wang return 2553ad7092fSWeilin Wang 2563ad7092fSWeilin Wang 2573ad7092fSWeilin Wang # Single Metric Test 2583ad7092fSWeilin Wang def single_test(self, rule:dict): 2593ad7092fSWeilin Wang """ 2603ad7092fSWeilin Wang Validate if the metrics are in the required value range. 2613ad7092fSWeilin Wang eg. lower_bound <= metrics_value <= upper_bound 2623ad7092fSWeilin Wang One metric is counted as one test in this type of test. 2633ad7092fSWeilin Wang One rule may include one or more metrics. 2643ad7092fSWeilin Wang Failure: when the metric value not provided or the value is outside the bounds. 2653ad7092fSWeilin Wang This test updates self.total_cnt and records failed tests in self.failtest['SingleMetricTest']. 2663ad7092fSWeilin Wang 2673ad7092fSWeilin Wang @param rule: dict with metrics to validate and the value range requirement 2683ad7092fSWeilin Wang """ 2693ad7092fSWeilin Wang lbv, ubv, t = self.get_bounds(rule['RangeLower'], rule['RangeUpper'], rule['ErrorThreshold']) 2703ad7092fSWeilin Wang metrics = rule['Metrics'] 2713ad7092fSWeilin Wang passcnt = 0 2723ad7092fSWeilin Wang totalcnt = 0 273*1203a63dSWeilin Wang faillist = list() 274*1203a63dSWeilin Wang failures = dict() 275*1203a63dSWeilin Wang rerun = list() 2763ad7092fSWeilin Wang for m in metrics: 2773ad7092fSWeilin Wang totalcnt += 1 2783ad7092fSWeilin Wang result = self.get_value(m['Name']) 279*1203a63dSWeilin Wang if len(result) > 0 and self.check_bound(result[0], lbv, ubv, t) or m['Name'] in self.skiplist: 2803ad7092fSWeilin Wang passcnt += 1 2813ad7092fSWeilin Wang else: 282*1203a63dSWeilin Wang failures[m['Name']] = result 283*1203a63dSWeilin Wang rerun.append(m['Name']) 284*1203a63dSWeilin Wang 285*1203a63dSWeilin Wang if len(rerun) > 0 and len(rerun) < 20: 286*1203a63dSWeilin Wang second_results = dict() 287*1203a63dSWeilin Wang self.second_test(rerun, second_results) 288*1203a63dSWeilin Wang for name, val in second_results.items(): 289*1203a63dSWeilin Wang if name not in failures: continue 290*1203a63dSWeilin Wang if self.check_bound(val, lbv, ubv, t): 291*1203a63dSWeilin Wang passcnt += 1 292*1203a63dSWeilin Wang del failures[name] 293*1203a63dSWeilin Wang else: 294*1203a63dSWeilin Wang failures[name] = val 295*1203a63dSWeilin Wang self.results[0][name] = val 2963ad7092fSWeilin Wang 2973ad7092fSWeilin Wang self.totalcnt += totalcnt 2983ad7092fSWeilin Wang self.passedcnt += passcnt 2993ad7092fSWeilin Wang self.failtests['SingleMetricTest']['Total Tests'] += totalcnt 3003ad7092fSWeilin Wang self.failtests['SingleMetricTest']['Passed Tests'] += passcnt 301*1203a63dSWeilin Wang if len(failures.keys()) != 0: 302*1203a63dSWeilin Wang faillist = [{'MetricName':name, 'CollectedValue':val} for name, val in failures.items()] 3033ad7092fSWeilin Wang self.failtests['SingleMetricTest']['Failed Tests'].append({'RuleIndex':rule['RuleIndex'], 3043ad7092fSWeilin Wang 'RangeLower': rule['RangeLower'], 3053ad7092fSWeilin Wang 'RangeUpper': rule['RangeUpper'], 3063ad7092fSWeilin Wang 'ErrorThreshold':rule['ErrorThreshold'], 3073ad7092fSWeilin Wang 'Failure':faillist}) 3083ad7092fSWeilin Wang 3093ad7092fSWeilin Wang return 3103ad7092fSWeilin Wang 3113ad7092fSWeilin Wang def create_report(self): 3123ad7092fSWeilin Wang """ 3133ad7092fSWeilin Wang Create final report and write into a JSON file. 3143ad7092fSWeilin Wang """ 3153ad7092fSWeilin Wang alldata = list() 3163ad7092fSWeilin Wang for i in range(0, len(self.workloads)): 3173ad7092fSWeilin Wang reportstas = {"Total Rule Count": self.alltotalcnt[i], "Passed Rule Count": self.allpassedcnt[i]} 3183ad7092fSWeilin Wang data = {"Metric Validation Statistics": reportstas, "Tests in Category": self.allfailtests[i], 3193ad7092fSWeilin Wang "Errors":self.allerrlist[i]} 3203ad7092fSWeilin Wang alldata.append({"Workload": self.workloads[i], "Report": data}) 3213ad7092fSWeilin Wang 3223ad7092fSWeilin Wang json_str = json.dumps(alldata, indent=4) 3233ad7092fSWeilin Wang print("Test validation finished. Final report: ") 3243ad7092fSWeilin Wang print(json_str) 3253ad7092fSWeilin Wang 3263ad7092fSWeilin Wang if self.debug: 3273ad7092fSWeilin Wang allres = [{"Workload": self.workloads[i], "Results": self.allresults[i]} for i in range(0, len(self.workloads))] 3283ad7092fSWeilin Wang self.json_dump(allres, self.datafname) 3293ad7092fSWeilin Wang 3303ad7092fSWeilin Wang def check_rule(self, testtype, metric_list): 3313ad7092fSWeilin Wang """ 3323ad7092fSWeilin Wang Check if the rule uses metric(s) that not exist in current platform. 3333ad7092fSWeilin Wang 3343ad7092fSWeilin Wang @param metric_list: list of metrics from the rule. 3353ad7092fSWeilin Wang @return: False when find one metric out in Metric file. (This rule should not skipped.) 3363ad7092fSWeilin Wang True when all metrics used in the rule are found in Metric file. 3373ad7092fSWeilin Wang """ 3383ad7092fSWeilin Wang if testtype == "RelationshipTest": 3393ad7092fSWeilin Wang for m in metric_list: 3403ad7092fSWeilin Wang if m['Name'] not in self.metrics: 3413ad7092fSWeilin Wang return False 3423ad7092fSWeilin Wang return True 3433ad7092fSWeilin Wang 3443ad7092fSWeilin Wang # Start of Collector and Converter 345*1203a63dSWeilin Wang def convert(self, data: list, metricvalues:dict): 3463ad7092fSWeilin Wang """ 3473ad7092fSWeilin Wang Convert collected metric data from the -j output to dict of {metric_name:value}. 3483ad7092fSWeilin Wang """ 3493ad7092fSWeilin Wang for json_string in data: 3503ad7092fSWeilin Wang try: 3513ad7092fSWeilin Wang result =json.loads(json_string) 3523ad7092fSWeilin Wang if "metric-unit" in result and result["metric-unit"] != "(null)" and result["metric-unit"] != "": 3533ad7092fSWeilin Wang name = result["metric-unit"].split(" ")[1] if len(result["metric-unit"].split(" ")) > 1 \ 3543ad7092fSWeilin Wang else result["metric-unit"] 355*1203a63dSWeilin Wang metricvalues[name.lower()] = float(result["metric-value"]) 3563ad7092fSWeilin Wang except ValueError as error: 3573ad7092fSWeilin Wang continue 3583ad7092fSWeilin Wang return 3593ad7092fSWeilin Wang 360*1203a63dSWeilin Wang def _run_perf(self, metric, workload: str): 361*1203a63dSWeilin Wang tool = 'perf' 362*1203a63dSWeilin Wang command = [tool, 'stat', '-j', '-M', f"{metric}", "-a"] 363*1203a63dSWeilin Wang wl = workload.split() 364*1203a63dSWeilin Wang command.extend(wl) 365*1203a63dSWeilin Wang print(" ".join(command)) 366*1203a63dSWeilin Wang cmd = subprocess.run(command, stderr=subprocess.PIPE, encoding='utf-8') 367*1203a63dSWeilin Wang data = [x+'}' for x in cmd.stderr.split('}\n') if x] 368*1203a63dSWeilin Wang return data 369*1203a63dSWeilin Wang 370*1203a63dSWeilin Wang 371*1203a63dSWeilin Wang def collect_perf(self, workload: str): 3723ad7092fSWeilin Wang """ 3733ad7092fSWeilin Wang Collect metric data with "perf stat -M" on given workload with -a and -j. 3743ad7092fSWeilin Wang """ 3753ad7092fSWeilin Wang self.results = dict() 3763ad7092fSWeilin Wang print(f"Starting perf collection") 377*1203a63dSWeilin Wang print(f"Long workload: {workload}") 3783ad7092fSWeilin Wang collectlist = dict() 3793ad7092fSWeilin Wang if self.collectlist != "": 3803ad7092fSWeilin Wang collectlist[0] = {x for x in self.collectlist.split(",")} 3813ad7092fSWeilin Wang else: 3823ad7092fSWeilin Wang collectlist[0] = set(list(self.metrics)) 3833ad7092fSWeilin Wang # Create metric set for relationship rules 3843ad7092fSWeilin Wang for rule in self.rules: 3853ad7092fSWeilin Wang if rule["TestType"] == "RelationshipTest": 3863ad7092fSWeilin Wang metrics = [m["Name"] for m in rule["Metrics"]] 3873ad7092fSWeilin Wang if not any(m not in collectlist[0] for m in metrics): 388a0f1cc18SWeilin Wang collectlist[rule["RuleIndex"]] = [",".join(list(set(metrics)))] 3893ad7092fSWeilin Wang 3903ad7092fSWeilin Wang for idx, metrics in collectlist.items(): 391*1203a63dSWeilin Wang if idx == 0: wl = "true" 392*1203a63dSWeilin Wang else: wl = workload 3933ad7092fSWeilin Wang for metric in metrics: 394*1203a63dSWeilin Wang data = self._run_perf(metric, wl) 395*1203a63dSWeilin Wang if idx not in self.results: self.results[idx] = dict() 396*1203a63dSWeilin Wang self.convert(data, self.results[idx]) 397*1203a63dSWeilin Wang return 398*1203a63dSWeilin Wang 399*1203a63dSWeilin Wang def second_test(self, collectlist, second_results): 400*1203a63dSWeilin Wang workload = self.workloads[self.wlidx] 401*1203a63dSWeilin Wang for metric in collectlist: 402*1203a63dSWeilin Wang data = self._run_perf(metric, workload) 403*1203a63dSWeilin Wang self.convert(data, second_results) 404*1203a63dSWeilin Wang 4053ad7092fSWeilin Wang # End of Collector and Converter 4063ad7092fSWeilin Wang 4073ad7092fSWeilin Wang # Start of Rule Generator 4083ad7092fSWeilin Wang def parse_perf_metrics(self): 4093ad7092fSWeilin Wang """ 4103ad7092fSWeilin Wang Read and parse perf metric file: 4113ad7092fSWeilin Wang 1) find metrics with '1%' or '100%' as ScaleUnit for Percent check 4123ad7092fSWeilin Wang 2) create metric name list 4133ad7092fSWeilin Wang """ 4143ad7092fSWeilin Wang command = ['perf', 'list', '-j', '--details', 'metrics'] 4153ad7092fSWeilin Wang cmd = subprocess.run(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, encoding='utf-8') 4163ad7092fSWeilin Wang try: 4173ad7092fSWeilin Wang data = json.loads(cmd.stdout) 4183ad7092fSWeilin Wang for m in data: 4193ad7092fSWeilin Wang if 'MetricName' not in m: 4203ad7092fSWeilin Wang print("Warning: no metric name") 4213ad7092fSWeilin Wang continue 422*1203a63dSWeilin Wang name = m['MetricName'].lower() 4233ad7092fSWeilin Wang self.metrics.add(name) 4243ad7092fSWeilin Wang if 'ScaleUnit' in m and (m['ScaleUnit'] == '1%' or m['ScaleUnit'] == '100%'): 4253ad7092fSWeilin Wang self.pctgmetrics.add(name.lower()) 4263ad7092fSWeilin Wang except ValueError as error: 4273ad7092fSWeilin Wang print(f"Error when parsing metric data") 4283ad7092fSWeilin Wang sys.exit() 4293ad7092fSWeilin Wang 4303ad7092fSWeilin Wang return 4313ad7092fSWeilin Wang 432*1203a63dSWeilin Wang def remove_unsupported_rules(self, rules): 433a0f1cc18SWeilin Wang new_rules = [] 434a0f1cc18SWeilin Wang for rule in rules: 435a0f1cc18SWeilin Wang add_rule = True 436a0f1cc18SWeilin Wang for m in rule["Metrics"]: 437*1203a63dSWeilin Wang if m["Name"] in self.skiplist or m["Name"] not in self.metrics: 438a0f1cc18SWeilin Wang add_rule = False 439a0f1cc18SWeilin Wang break 440a0f1cc18SWeilin Wang if add_rule: 441a0f1cc18SWeilin Wang new_rules.append(rule) 442a0f1cc18SWeilin Wang return new_rules 443a0f1cc18SWeilin Wang 4443ad7092fSWeilin Wang def create_rules(self): 4453ad7092fSWeilin Wang """ 4463ad7092fSWeilin Wang Create full rules which includes: 4473ad7092fSWeilin Wang 1) All the rules from the "relationshi_rules" file 4483ad7092fSWeilin Wang 2) SingleMetric rule for all the 'percent' metrics 4493ad7092fSWeilin Wang 4503ad7092fSWeilin Wang Reindex all the rules to avoid repeated RuleIndex 4513ad7092fSWeilin Wang """ 452a0f1cc18SWeilin Wang data = self.read_json(self.rulefname) 453a0f1cc18SWeilin Wang rules = data['RelationshipRules'] 454*1203a63dSWeilin Wang self.skiplist = set([name.lower() for name in data['SkipList']]) 455*1203a63dSWeilin Wang self.rules = self.remove_unsupported_rules(rules) 4563ad7092fSWeilin Wang pctgrule = {'RuleIndex':0, 4573ad7092fSWeilin Wang 'TestType':'SingleMetricTest', 4583ad7092fSWeilin Wang 'RangeLower':'0', 4593ad7092fSWeilin Wang 'RangeUpper': '100', 4603ad7092fSWeilin Wang 'ErrorThreshold': self.tolerance, 4613ad7092fSWeilin Wang 'Description':'Metrics in percent unit have value with in [0, 100]', 462*1203a63dSWeilin Wang 'Metrics': [{'Name': m.lower()} for m in self.pctgmetrics]} 4633ad7092fSWeilin Wang self.rules.append(pctgrule) 4643ad7092fSWeilin Wang 4653ad7092fSWeilin Wang # Re-index all rules to avoid repeated RuleIndex 4663ad7092fSWeilin Wang idx = 1 4673ad7092fSWeilin Wang for r in self.rules: 4683ad7092fSWeilin Wang r['RuleIndex'] = idx 4693ad7092fSWeilin Wang idx += 1 4703ad7092fSWeilin Wang 4713ad7092fSWeilin Wang if self.debug: 4723ad7092fSWeilin Wang #TODO: need to test and generate file name correctly 4733ad7092fSWeilin Wang data = {'RelationshipRules':self.rules, 'SupportedMetrics': [{"MetricName": name} for name in self.metrics]} 4743ad7092fSWeilin Wang self.json_dump(data, self.fullrulefname) 4753ad7092fSWeilin Wang 4763ad7092fSWeilin Wang return 4773ad7092fSWeilin Wang # End of Rule Generator 4783ad7092fSWeilin Wang 4793ad7092fSWeilin Wang def _storewldata(self, key): 4803ad7092fSWeilin Wang ''' 4813ad7092fSWeilin Wang Store all the data of one workload into the corresponding data structure for all workloads. 4823ad7092fSWeilin Wang @param key: key to the dictionaries (index of self.workloads). 4833ad7092fSWeilin Wang ''' 4843ad7092fSWeilin Wang self.allresults[key] = self.results 4853ad7092fSWeilin Wang self.allignoremetrics[key] = self.ignoremetrics 4863ad7092fSWeilin Wang self.allfailtests[key] = self.failtests 4873ad7092fSWeilin Wang self.alltotalcnt[key] = self.totalcnt 4883ad7092fSWeilin Wang self.allpassedcnt[key] = self.passedcnt 4893ad7092fSWeilin Wang self.allerrlist[key] = self.errlist 4903ad7092fSWeilin Wang 4913ad7092fSWeilin Wang #Initialize data structures before data validation of each workload 4923ad7092fSWeilin Wang def _init_data(self): 4933ad7092fSWeilin Wang 4943ad7092fSWeilin Wang testtypes = ['PositiveValueTest', 'RelationshipTest', 'SingleMetricTest'] 4953ad7092fSWeilin Wang self.results = dict() 4963ad7092fSWeilin Wang self.ignoremetrics= set() 4973ad7092fSWeilin Wang self.errlist = list() 4983ad7092fSWeilin Wang self.failtests = {k:{'Total Tests':0, 'Passed Tests':0, 'Failed Tests':[]} for k in testtypes} 4993ad7092fSWeilin Wang self.totalcnt = 0 5003ad7092fSWeilin Wang self.passedcnt = 0 5013ad7092fSWeilin Wang 5023ad7092fSWeilin Wang def test(self): 5033ad7092fSWeilin Wang ''' 5043ad7092fSWeilin Wang The real entry point of the test framework. 5053ad7092fSWeilin Wang This function loads the validation rule JSON file and Standard Metric file to create rules for 5063ad7092fSWeilin Wang testing and namemap dictionaries. 5073ad7092fSWeilin Wang It also reads in result JSON file for testing. 5083ad7092fSWeilin Wang 5093ad7092fSWeilin Wang In the test process, it passes through each rule and launch correct test function bases on the 5103ad7092fSWeilin Wang 'TestType' field of the rule. 5113ad7092fSWeilin Wang 5123ad7092fSWeilin Wang The final report is written into a JSON file. 5133ad7092fSWeilin Wang ''' 514a0f1cc18SWeilin Wang if not self.collectlist: 5153ad7092fSWeilin Wang self.parse_perf_metrics() 5163ad7092fSWeilin Wang self.create_rules() 5173ad7092fSWeilin Wang for i in range(0, len(self.workloads)): 518*1203a63dSWeilin Wang self.wlidx = i 5193ad7092fSWeilin Wang self._init_data() 520*1203a63dSWeilin Wang self.collect_perf(self.workloads[i]) 5213ad7092fSWeilin Wang # Run positive value test 5223ad7092fSWeilin Wang self.pos_val_test() 5233ad7092fSWeilin Wang for r in self.rules: 5243ad7092fSWeilin Wang # skip rules that uses metrics not exist in this platform 5253ad7092fSWeilin Wang testtype = r['TestType'] 5263ad7092fSWeilin Wang if not self.check_rule(testtype, r['Metrics']): 5273ad7092fSWeilin Wang continue 5283ad7092fSWeilin Wang if testtype == 'RelationshipTest': 5293ad7092fSWeilin Wang self.relationship_test(r) 5303ad7092fSWeilin Wang elif testtype == 'SingleMetricTest': 5313ad7092fSWeilin Wang self.single_test(r) 5323ad7092fSWeilin Wang else: 5333ad7092fSWeilin Wang print("Unsupported Test Type: ", testtype) 5343ad7092fSWeilin Wang self.errlist.append("Unsupported Test Type from rule: " + r['RuleIndex']) 5353ad7092fSWeilin Wang self._storewldata(i) 5363ad7092fSWeilin Wang print("Workload: ", self.workloads[i]) 5373ad7092fSWeilin Wang print("Total metrics collected: ", self.failtests['PositiveValueTest']['Total Tests']) 5383ad7092fSWeilin Wang print("Non-negative metric count: ", self.failtests['PositiveValueTest']['Passed Tests']) 5393ad7092fSWeilin Wang print("Total Test Count: ", self.totalcnt) 5403ad7092fSWeilin Wang print("Passed Test Count: ", self.passedcnt) 5413ad7092fSWeilin Wang 5423ad7092fSWeilin Wang self.create_report() 5433ad7092fSWeilin Wang return sum(self.alltotalcnt.values()) != sum(self.allpassedcnt.values()) 5443ad7092fSWeilin Wang# End of Class Validator 5453ad7092fSWeilin Wang 5463ad7092fSWeilin Wang 5473ad7092fSWeilin Wangdef main() -> None: 5483ad7092fSWeilin Wang parser = argparse.ArgumentParser(description="Launch metric value validation") 5493ad7092fSWeilin Wang 5503ad7092fSWeilin Wang parser.add_argument("-rule", help="Base validation rule file", required=True) 5513ad7092fSWeilin Wang parser.add_argument("-output_dir", help="Path for validator output file, report file", required=True) 5523ad7092fSWeilin Wang parser.add_argument("-debug", help="Debug run, save intermediate data to files", action="store_true", default=False) 5533ad7092fSWeilin Wang parser.add_argument("-wl", help="Workload to run while data collection", default="true") 5543ad7092fSWeilin Wang parser.add_argument("-m", help="Metric list to validate", default="") 5553ad7092fSWeilin Wang args = parser.parse_args() 5563ad7092fSWeilin Wang outpath = Path(args.output_dir) 5573ad7092fSWeilin Wang reportf = Path.joinpath(outpath, 'perf_report.json') 5583ad7092fSWeilin Wang fullrule = Path.joinpath(outpath, 'full_rule.json') 5593ad7092fSWeilin Wang datafile = Path.joinpath(outpath, 'perf_data.json') 5603ad7092fSWeilin Wang 5613ad7092fSWeilin Wang validator = Validator(args.rule, reportf, debug=args.debug, 5623ad7092fSWeilin Wang datafname=datafile, fullrulefname=fullrule, workload=args.wl, 5633ad7092fSWeilin Wang metrics=args.m) 5643ad7092fSWeilin Wang ret = validator.test() 5653ad7092fSWeilin Wang 5663ad7092fSWeilin Wang return ret 5673ad7092fSWeilin Wang 5683ad7092fSWeilin Wang 5693ad7092fSWeilin Wangif __name__ == "__main__": 5703ad7092fSWeilin Wang import sys 5713ad7092fSWeilin Wang sys.exit(main()) 5723ad7092fSWeilin Wang 5733ad7092fSWeilin Wang 5743ad7092fSWeilin Wang 575