From 058db25f6766d9ef7a194b9db226ddad2a358e0a Mon Sep 17 00:00:00 2001 From: Branden Archer Date: Tue, 22 Nov 2016 16:13:54 -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 include a scenario if an inclusion tag matches regardless if an exclusion tag also matches. scenario with an inclusionary. 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 there are inclusionary tags and none match, the scenario is rejected 2) If there are exclusionary tags and any match, the scenario is rejected 3) If there are ~ or ~- tags and none match, the scenario is rejected 4) If the above did not reject the scenario, it is accepted 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..06c2ee28b 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, " + "the scenario is excluded") + + 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 -~ "