From 569897930e3a3582b5ab8c1aaccf2f6a3b805f30 Mon Sep 17 00:00:00 2001 From: Branden Archer Date: Tue, 22 Nov 2016 12:44:00 -0500 Subject: [PATCH] Allow exclusionary tags to omit scenarios If a user passes in a tag for which no scenario should run, e.g. --tag=-A then the expectation is that no scenario with that tag would run. However, the previous behavior was to a scenario with an inclusionary tag regardless of the exclusionary tag. For example, if a scenario's tags were: ['A', 'B] and the user specified: --tag=-A --tag=B the scenario would run. This change changes the behavior of scenario filtering: 1) If an exclusionary tag matches the scenario is rejected. 2) If an inclusionary tag matches the scenario is accepted. 3) If a tag with ~ matches then it is accepted with some probability https://github.com/gabrielfalcao/lettuce/issues/498 --- lettuce/core.py | 45 +++++++++++++++++++---------- tests/unit/test_scenario_parsing.py | 12 ++++++++ 2 files changed, 42 insertions(+), 15 deletions(-) diff --git a/lettuce/core.py b/lettuce/core.py index c87e5636c..8264840aa 100644 --- a/lettuce/core.py +++ b/lettuce/core.py @@ -647,30 +647,39 @@ def matches_tags(self, tags): if tags is None: return True - has_exclusionary_tags = any([t.startswith('-') for t in tags]) + exclusionary_tags = [t[1:] for t in tags if t.startswith('-') and not t.startswith('-~')] + inclusionary_tags = [t for t in tags if not t.startswith('-') and not t.startswith('~')] - if not self.tags and not has_exclusionary_tags: - return False + if not isinstance(self.tags, list): + self.tags = [] - matched = [] + # If there are inclusionary tags, at least one must match + if len(inclusionary_tags) > 0: + matches = set(self.tags).intersection(inclusionary_tags) + if len(matches) == 0: + # This scenario did not match any of the inclusionary + # tags, and it must be excluded + return False + + # If there are exclusionary tags, if any match the scenario must + # be thrown out + if len(exclusionary_tags) > 0: + matches = set(self.tags).intersection(exclusionary_tags) + if len(matches) > 0: + # At least one exclusionary tag matches, omit the + # scenario + return False - if isinstance(self.tags, list): - for tag in self.tags: - if tag in tags: - return True - else: - self.tags = [] + matched = [] for tag in tags: exclude = tag.startswith('-') if exclude: tag = tag[1:] - fuzzable = tag.startswith('~') if fuzzable: tag = tag[1:] - result = tag in self.tags if fuzzable: fuzzed = [] for internal_tag in self.tags: @@ -679,14 +688,20 @@ def matches_tags(self, tags): fuzzed.append(ratio <= 80) else: fuzzed.append(ratio > 80) - result = any(fuzzed) + matched.append(result) elif exclude: result = tag not in self.tags + matched.append(result) - matched.append(result) + # Determine if any tags may optionally be included. + # If so, all must fail for the scenario to be omitted + if not all(matched): + return False - return all(matched) + # If the check make it here, there is no reason to + # disclude the test + return True @property def evaluated(self): diff --git a/tests/unit/test_scenario_parsing.py b/tests/unit/test_scenario_parsing.py index 8dec6d777..12c380e78 100644 --- a/tests/unit/test_scenario_parsing.py +++ b/tests/unit/test_scenario_parsing.py @@ -527,6 +527,18 @@ def test_scenario_matches_tags_excluding_when_scenario_has_no_tags(): assert scenario.matches_tags(['-nope', '-neither']) +def test_scenario_matches_tags_both_include_and_exclude_tags(): + ("When Scenario#matches_tags is called for a scenario " + "that has an inclusionary and exclusionary tag that matches, " + "and expects the scenario to not match") + + scenario = Scenario.from_string( + SCENARIO1, + original_string=SCENARIO1.strip(), + tags=['tag1', 'tag2']) + + assert not scenario.matches_tags(['tag1', '-tag2']) + def test_scenario_matches_tags_excluding_fuzzywuzzy(): ("When Scenario#matches_tags is called with a member starting with -~ "