| # Copyright (c) 2012 The Chromium OS Authors. All rights reserved. |
| # Use of this source code is governed by a BSD-style license that can be |
| # found in the LICENSE file. |
| # |
| # This module contains the FuzzyCheck class that uses the validators |
| # defined in the validators modules. The validators make |
| # use of the FuzzyComparator class that allows validations to be made in |
| # a fuzzy way that leads to a score instead of a true/false decision. |
| import math |
| import re |
| |
| from table import Table |
| |
| class FuzzyCheck(object): |
| """ |
| This class can validate a list of gestures to a previously defined set of |
| validators. This is used to check if the generated gestures actually |
| represent the desired behavior. |
| To allow more fine-grained results the output is a score instead of a |
| match/no-match decision. |
| There are two types of validators: expected or unexpected. |
| Expected validators describe the expected behavior in the order it should |
| happen. Whenever one of these validators cannot handle a gesture it is |
| treated as unexpected and checked by the unexpected validators. A common |
| case is expecting a ButtonClick, but allowing a certain amount of |
| unexpected Motion to happen. |
| """ |
| def __init__(self): |
| self.expected = [] |
| self.unexpected = [] |
| |
| def Check(self, event_list): |
| """ |
| Validate gestures against the validators of this object. The expected |
| and unexpected variables have to be set up before calling this method. |
| This method will return a score between 0..1. It might also return False |
| which is different from 0. False describes a complete test-failure, i.e. |
| something has happened that should not have happened. |
| When it comes to usability impact, it is better when nothing happens, as if |
| something completely wrong happens. This is why 'nothing' happened is rated |
| slightly better (score=0) as a non-matching behavior (score=False). |
| """ |
| |
| # special case for empty event list |
| if len(event_list) == 0: |
| if len(self.expected) == 0: |
| return 1, "success, no events expected" |
| else: |
| return 0, "failure, expected events but nothing happened" |
| |
| # select first validator |
| current_validator = None |
| validators = list(reversed(self.expected)) |
| if len(self.expected) > 0: |
| current_validator = validators.pop() |
| |
| table = Table(" ") |
| table.title = "Validation Log" |
| table.header("start - end", "event", "validator (* unexpected)") |
| |
| def AddRow(event, validator, unexpected=False): |
| if unexpected: |
| validator = "*" + str(validator) |
| table.row("%f - %f" % (event.start, event.end), event, validator) |
| |
| def CheckUnexpectedEvent(event): |
| for validator in self.unexpected: |
| if validator.Accept(event): |
| validator.Validate(event) |
| AddRow(event, validator, True) |
| return True |
| # no validator found. Fail. |
| AddRow(event, "no matching validator") |
| return False |
| |
| failed = False |
| for event in event_list: |
| if failed: |
| AddRow(event, "-") |
| continue |
| |
| if current_validator is None: |
| # No validator left. All events unexpected. |
| if not CheckUnexpectedEvent(event): |
| failed = True |
| else: |
| if not current_validator.Accept(event): |
| if (current_validator.score > 0 and len(validators) > 0 |
| and validators[-1].Accept(event)): |
| # transition to next validator |
| current_validator = validators.pop() |
| current_validator.Validate(event) |
| AddRow(event, current_validator) |
| else: |
| # unexpected event |
| if not CheckUnexpectedEvent(event): |
| failed = True |
| else: |
| # accepted by current validator |
| current_validator.Validate(event) |
| AddRow(event, current_validator) |
| |
| score_table = self._BuildScoreTable(False) |
| report = str(table) + "\n" + str(score_table) |
| |
| # calculate score |
| if failed: |
| score = False |
| else: |
| score = 1 |
| |
| # expected validators score |
| for validator in self.expected: |
| if validator.score is False: |
| return False, report |
| else: |
| score = score * validator.score |
| |
| # unexpected validators score |
| for validator in self.unexpected: |
| if validator.score is False: |
| return False, report |
| else: |
| score = score * validator.score |
| |
| score_table = self._BuildScoreTable(score) |
| report = str(table) + "\n" + str(score_table) |
| |
| return score, report |
| |
| def _BuildScoreTable(self, overall): |
| table = Table(" ") |
| table.title = "Score Breakdown" |
| |
| table.header("expected validator", "score") |
| for validator in self.expected: |
| table.row(validator, validator.score) |
| |
| |
| table.header("unexpected validator", "score") |
| for validator in self.unexpected: |
| table.row(validator, validator.score) |
| |
| table.footer = "overall score: " + str(overall) |
| return table |
| |
| |
| ################################################################################ |
| # Fuzzy Comparator |
| ################################################################################ |
| |
| class FuzzyComparator(object): |
| """ |
| This class is able to calculate fuzzy comparisons to return a scoring |
| value. Fuzzy comparisons are inequality/equality operators with |
| an addition range of validity. The result of the comparison is not |
| a true/false decision but a score based on the range of validity. |
| The comparison string format is: |
| "[op] value [ ~ range ]" |
| With the following options for op: ==, >, >=, <, <= |
| Value being the integer value we Compare to. |
| The range of validity is: [value-range .. value+range] |
| If op is omitted we assume op to be == and if range is omitted |
| it is assumed to be 0. |
| See FuzzyComparator.Compare for information about the return value of |
| comparisons. |
| Example: |
| fuzzy = FuzzyComparator("== 100 ~10") |
| fuzzy.Compare(100) -> 1 |
| fuzzy.Compare(105) -> 0.5 |
| fuzzy.Compare(90) -> 0 |
| fuzzy.Compare(89) -> False |
| """ |
| def __init__(self, input): |
| if isinstance(input, str): |
| m = re.match("([><=]=?)?[ ]*(\\-?[0-9]+\\.?[0-9]*)[ ]*" + |
| "(~[ ]*[0-9]+\\.?[0-9]*)?", input) |
| |
| if not m: |
| raise ValueError("Malformed comparator: '" + input + "'") |
| |
| operator = m.group(1) |
| value = m.group(2) |
| fuzzyness = m.group(3) |
| |
| if value is None: |
| raise ValueError("Malformed comparator: '" + input + "'") |
| |
| self._target = float(value) |
| |
| if operator is not None: |
| self._operator_str = m.group(1) |
| else: |
| self._operator_str = "==" |
| |
| self._operator = FuzzyComparator._operator_mapping[self._operator_str] |
| if fuzzyness is not None: |
| self._fuzzyness = float(fuzzyness[1:]) |
| else: |
| self._fuzzyness = 0 |
| else: |
| self._target = float(input) |
| self._fuzzyness = 0 |
| self._operator = FuzzyComparator._CompareEq |
| self._operator_str = "==" |
| |
| def __str__(self): |
| result = self._operator_str + "{0:.4g}".format(self._target) |
| if self._fuzzyness > 0: |
| result = result + "~{0:.4g}".format(self._fuzzyness) |
| return result |
| |
| def Compare(self, value): |
| """ |
| compares a number with the comparison string provided when constructing |
| this object. The return value is: |
| False: if the value is out of the valid range |
| 1: if the value is matched by the unequality/equality operation |
| 0..1: if the value is somewhere in the range of validity. |
| """ |
| score = self._operator(self, value) |
| if score < 0: |
| return False |
| elif score > 1: |
| return 1 |
| else: |
| return score |
| |
| def _CompareEq(self, value): |
| if self._fuzzyness > 0: |
| return (self._fuzzyness - math.fabs(self._target - value)) / \ |
| self._fuzzyness |
| else: |
| return value == self._target |
| |
| def _CompareGte(self, value): |
| if self._fuzzyness > 0: |
| return self._CompareGt(value) |
| else: |
| return value >= self._target |
| |
| def _CompareGt(self, value): |
| if self._fuzzyness > 0: |
| return float(value - self._target + self._fuzzyness) / self._fuzzyness |
| else: |
| return value > self._target |
| |
| def _CompareLte(self, value): |
| if self._fuzzyness > 0: |
| return self._CompareLt(value) |
| else: |
| return value <= self._target |
| |
| def _CompareLt(self, value): |
| if self._fuzzyness > 0: |
| return float(self._target - value + self._fuzzyness) / self._fuzzyness |
| else: |
| return value < self._target |
| |
| _operator_mapping = { |
| "==": _CompareEq, |
| "=" : _CompareEq, |
| ">=": _CompareGte, |
| ">" : _CompareGt, |
| "<=": _CompareLte, |
| "<" : _CompareLt, |
| } |