diff --git a/llvm/utils/lit/lit/BooleanExpression.py b/llvm/utils/lit/lit/BooleanExpression.py --- a/llvm/utils/lit/lit/BooleanExpression.py +++ b/llvm/utils/lit/lit/BooleanExpression.py @@ -1,6 +1,6 @@ +import operator import re - class BooleanExpression: # A simple evaluator of boolean expressions. # @@ -10,12 +10,15 @@ # and_expr :: not_expr ('&&' not_expr)* # not_expr :: '!' not_expr # '(' or_expr ')' - # match_expr + # cmp_expr + # cmp_expr :: match_expr (cmp_op integer)? # match_expr :: braced_regex # identifier # braced_regex match_expr # identifier match_expr # identifier :: [-+=._a-zA-Z0-9]+ + # integer :: [-+0-9]+ + # cmp_op :: '<' | '>' | '<=' | '>=' | '==' | '!=' # braced_regex :: '{{' python_regex '}}' # Evaluates `string` as a boolean expression. @@ -24,21 +27,26 @@ # Variables in `variables` are true. # Regexes that match any variable in `variables` are true. # 'true' is true. + # Variables or Regexes in "cmp_expr" of form "var/regex cmpop int" + # are compared against versions + # Variables in versions are evaluated for true/false + # Regexes that match in versions must all evaluate true # All other identifiers are false. @staticmethod - def evaluate(string, variables): + def evaluate(string, variables, versions = {}): try: - parser = BooleanExpression(string, set(variables)) + parser = BooleanExpression(string, set(variables), versions) return parser.parseAll() except ValueError as e: raise ValueError(str(e) + ("\nin expression: %r" % string)) ##### - def __init__(self, string, variables): + def __init__(self, string, variables, versions): self.tokens = BooleanExpression.tokenize(string) self.variables = variables self.variables.add("true") + self.versions = versions self.value = None self.token = None @@ -47,7 +55,7 @@ # Tokenization pattern. Pattern = re.compile( - r"\A\s*([()]|&&|\|\||!|(?:[-+=._a-zA-Z0-9]+|\{\{.+?\}\})+)\s*(.*)\Z" + r"\A\s*([()]|&&|\|\||<=|>=|!=|==|!|<|>|(?:[-+=._a-zA-Z0-9]+|\{\{.+?\}\})+)\s*(.*)\Z" ) @staticmethod @@ -100,6 +108,47 @@ return False return True + @staticmethod + def isCmpExpression(token): + if ( + token == "<" + or token == ">" + or token == "<=" + or token == ">=" + or token == "!=" + or token == "==" + ): + return True + return False + + def parseCMP(self, regex): + cmpop = None + if self.accept("<"): + cmpop = operator.lt + elif self.accept(">"): + cmpop = operator.gt + elif self.accept(">="): + cmpop = operator.ge + elif self.accept("<="): + cmpop = operator.le + elif self.accept("!="): + cmpop = operator.ne + elif self.accept("=="): + cmpop = operator.eq + + if cmpop: + version_val = int(self.token) # Expecting int + matched_versions = list(filter(regex.match, self.versions.keys())) + if matched_versions: + # Make sure that ALL the version comparisons specified are true + self.value = all(cmpop(int(self.versions[var]), version_val) for var in matched_versions) + else: + self.value = False + self.token = next(self.tokens) + else: + raise ValueError( + "expected comparison operator: '<', '>', '>=', '<=', '!=', '=='") + def parseMATCH(self): regex = "" for part in filter(None, re.split(r"(\{\{.+?\}\})", self.token)): @@ -109,8 +158,11 @@ else: regex += re.escape(part) regex = re.compile(regex) - self.value = any(regex.fullmatch(var) for var in self.variables) self.token = next(self.tokens) + if BooleanExpression.isCmpExpression(self.token): + self.parseCMP(regex) + else: + self.value = any(regex.fullmatch(var) for var in self.variables) def parseNOT(self): if self.accept("!"): @@ -161,6 +213,35 @@ class TestBooleanExpression(unittest.TestCase): + def test_versions(self): + versions = { "version1" : "10", "version2" : "20" } + self.assertTrue(BooleanExpression.evaluate("version1 < 20", (), versions)) + self.assertTrue(BooleanExpression.evaluate("version2 < 21", (), versions)) + self.assertTrue(BooleanExpression.evaluate("{{version.*}} < 21", (), versions)) + + self.assertFalse(BooleanExpression.evaluate("version1 < 5", (), versions)) + self.assertFalse(BooleanExpression.evaluate("version2 < -15", (), versions)) + self.assertFalse(BooleanExpression.evaluate("{{version.*}} < 15", (), versions)) + + self.assertTrue(BooleanExpression.evaluate("version1 > 5", (), versions)) + self.assertTrue(BooleanExpression.evaluate("version2 > 15", (), versions)) + self.assertTrue(BooleanExpression.evaluate("{{version.*}} > 5", (), versions)) + + self.assertFalse(BooleanExpression.evaluate("version1 > 10", (), versions)) + self.assertFalse(BooleanExpression.evaluate("version2 > 30", (), versions)) + self.assertFalse(BooleanExpression.evaluate("{{version.*}} > 10", (), versions)) + + self.assertTrue(BooleanExpression.evaluate("version1 == 10 && version2 != 30", (), versions)) + self.assertTrue(BooleanExpression.evaluate("version1 <= 10 && version2 >= 20", (), versions)) + self.assertTrue(BooleanExpression.evaluate("version1 == 20 || version2 != 30", (), versions)) + + self.assertFalse(BooleanExpression.evaluate("version1 == 20 && version2 == 20", (), versions)) + self.assertTrue(BooleanExpression.evaluate("version1 == 20 || version2 == 20", (), versions)) + + self.assertFalse(BooleanExpression.evaluate("version3 < 21", (), versions)) + self.assertFalse(BooleanExpression.evaluate("version3 < 21 && version2 > 10", (), versions)) + self.assertTrue(BooleanExpression.evaluate("version3 < 21 || version2 > 10", (), versions)) + def test_variables(self): variables = {"its-true", "false-lol-true", "under_score", "e=quals", "d1g1ts"} self.assertTrue(BooleanExpression.evaluate("true", variables)) @@ -345,6 +426,10 @@ "{{}}", "couldn't parse text: '{{}}'\n" + "in expression: '{{}}'" ) + self.checkException( + "version1 < broken", + "invalid literal for int() with base 10: 'broken'", + ) if __name__ == "__main__": unittest.main() diff --git a/llvm/utils/lit/lit/Test.py b/llvm/utils/lit/lit/Test.py --- a/llvm/utils/lit/lit/Test.py +++ b/llvm/utils/lit/lit/Test.py @@ -370,12 +370,12 @@ return True - def getMissingRequiredFeaturesFromList(self, features): + def getMissingRequiredFeaturesFromList(self, features, versions): try: return [ item for item in self.requires - if not BooleanExpression.evaluate(item, features) + if not BooleanExpression.evaluate(item, features, versions) ] except ValueError as e: raise ValueError("Error in REQUIRES list:\n%s" % str(e)) @@ -389,7 +389,8 @@ """ features = self.config.available_features - return self.getMissingRequiredFeaturesFromList(features) + versions = self.config.available_versions + return self.getMissingRequiredFeaturesFromList(features, versions) def getUnsupportedFeatures(self): """ @@ -401,12 +402,13 @@ """ features = self.config.available_features + versions = self.config.available_versions try: return [ item for item in self.unsupported - if BooleanExpression.evaluate(item, features) + if BooleanExpression.evaluate(item, features, versions) ] except ValueError as e: raise ValueError("Error in UNSUPPORTED list:\n%s" % str(e)) diff --git a/llvm/utils/lit/lit/TestingConfig.py b/llvm/utils/lit/lit/TestingConfig.py --- a/llvm/utils/lit/lit/TestingConfig.py +++ b/llvm/utils/lit/lit/TestingConfig.py @@ -97,6 +97,8 @@ if litConfig.valgrindLeakCheck: available_features.append("vg_leak") + available_versions = {} + return TestingConfig( None, name="", @@ -111,6 +113,7 @@ available_features=available_features, pipefail=True, standalone_tests=False, + available_versions=available_versions, ) def load_from_path(self, path, litConfig): @@ -172,6 +175,7 @@ is_early=False, parallelism_group=None, standalone_tests=False, + available_versions={}, ): self.parent = parent self.name = str(name) @@ -184,6 +188,7 @@ self.test_source_root = test_source_root self.excludes = set(excludes) self.available_features = set(available_features) + self.available_versions = available_versions self.pipefail = pipefail self.standalone_tests = standalone_tests # This list is used by TestRunner.py to restrict running only tests that diff --git a/llvm/utils/lit/tests/Inputs/show-used-features/mixed.txt b/llvm/utils/lit/tests/Inputs/show-used-features/mixed.txt --- a/llvm/utils/lit/tests/Inputs/show-used-features/mixed.txt +++ b/llvm/utils/lit/tests/Inputs/show-used-features/mixed.txt @@ -1,4 +1,4 @@ -// REQUIRES: my-require-feature-2 || my-require-feature-3, my-{{[require]*}}-feature-4 +// REQUIRES: my-require-feature-2 || my-require-feature-3, my-{{[require]*}}-feature-4, my-require-version > 10 // UNSUPPORTED: my-unsupported-feature-2, my-unsupported-feature-3 && !my-{{[unsupported]*}}-feature-4 // XFAIL: my-xfail-feature-2, my-xfail-feature-3, my-{{[xfail]*}}-feature-4 diff --git a/llvm/utils/lit/tests/Inputs/shtest-format/lit.cfg b/llvm/utils/lit/tests/Inputs/shtest-format/lit.cfg --- a/llvm/utils/lit/tests/Inputs/shtest-format/lit.cfg +++ b/llvm/utils/lit/tests/Inputs/shtest-format/lit.cfg @@ -8,4 +8,5 @@ config.target_triple = "x86_64-unknown-unknown" config.available_features.add("target=%s" % config.target_triple) config.available_features.add("a-present-feature") +config.available_versions["a-version"] = "2" config.substitutions.append(("%{python}", '"%s"' % (sys.executable))) diff --git a/llvm/utils/lit/tests/Inputs/shtest-format/version-missing.txt b/llvm/utils/lit/tests/Inputs/shtest-format/version-missing.txt new file mode 100644 --- /dev/null +++ b/llvm/utils/lit/tests/Inputs/shtest-format/version-missing.txt @@ -0,0 +1,5 @@ +# REQUIRES with a unfulfilled version. Test should not run +REQUIRES: true +REQUIRES: a-version-missing > 2, true +REQUIRES: true +RUN: false diff --git a/llvm/utils/lit/tests/Inputs/shtest-format/version-present.txt b/llvm/utils/lit/tests/Inputs/shtest-format/version-present.txt new file mode 100644 --- /dev/null +++ b/llvm/utils/lit/tests/Inputs/shtest-format/version-present.txt @@ -0,0 +1,4 @@ +# REQUIRES with only true clauses and fulfilled version. Test should run. +REQUIRES: a-version >= 2, true, !not-true +REQUIRES: true +RUN: true diff --git a/llvm/utils/lit/tests/Inputs/shtest-format/version-unfulfilled.txt b/llvm/utils/lit/tests/Inputs/shtest-format/version-unfulfilled.txt new file mode 100644 --- /dev/null +++ b/llvm/utils/lit/tests/Inputs/shtest-format/version-unfulfilled.txt @@ -0,0 +1,5 @@ +# REQUIRES with a unfulfilled version. Test should not run +REQUIRES: true +REQUIRES: a-version > 2, true +REQUIRES: true +RUN: false diff --git a/llvm/utils/lit/tests/Inputs/shtest-format/version-unsupported-false.txt b/llvm/utils/lit/tests/Inputs/shtest-format/version-unsupported-false.txt new file mode 100644 --- /dev/null +++ b/llvm/utils/lit/tests/Inputs/shtest-format/version-unsupported-false.txt @@ -0,0 +1,3 @@ +# REQUIRES with only true clauses and fulfilled version. Test should run. +UNSUPPORTED: a-version < 2 +RUN: true diff --git a/llvm/utils/lit/tests/Inputs/shtest-format/version-unsupported-true.txt b/llvm/utils/lit/tests/Inputs/shtest-format/version-unsupported-true.txt new file mode 100644 --- /dev/null +++ b/llvm/utils/lit/tests/Inputs/shtest-format/version-unsupported-true.txt @@ -0,0 +1,3 @@ +# REQUIRES with only true clauses and fulfilled version. Test should not run. +UNSUPPORTED: a-version >= 2 +RUN: true diff --git a/llvm/utils/lit/tests/shtest-format.py b/llvm/utils/lit/tests/shtest-format.py --- a/llvm/utils/lit/tests/shtest-format.py +++ b/llvm/utils/lit/tests/shtest-format.py @@ -64,6 +64,11 @@ # CHECK: UNSUPPORTED: shtest-format :: unsupported-expr-true.txt # CHECK: UNRESOLVED: shtest-format :: unsupported-star.txt # CHECK: UNSUPPORTED: shtest-format :: unsupported_dir/some-test.txt +# CHECK: UNSUPPORTED: shtest-format :: version-missing.txt +# CHECK: PASS: shtest-format :: version-present.txt +# CHECK: UNSUPPORTED: shtest-format :: version-unfulfilled.txt +# CHECK: PASS: shtest-format :: version-unsupported-false.txt +# CHECK: UNSUPPORTED: shtest-format :: version-unsupported-true.txt # CHECK: PASS: shtest-format :: xfail-expr-false.txt # CHECK: XFAIL: shtest-format :: xfail-expr-true.txt # CHECK: XFAIL: shtest-format :: xfail-feature.txt @@ -86,8 +91,8 @@ # CHECK: shtest-format :: xpass.txt # CHECK: Testing Time: -# CHECK: Unsupported : 3 -# CHECK: Passed : 7 +# CHECK: Unsupported : 6 +# CHECK: Passed : 9 # CHECK: Expectedly Failed : 4 # CHECK: Unresolved : 3 # CHECK: Failed : 4 @@ -96,7 +101,7 @@ # XUNIT: # XUNIT-NEXT: -# XUNIT-NEXT: +# XUNIT-NEXT: # XUNIT: # XUNIT-NEXT: