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