From 87cd3f2c3abc6aec1bfeb692ad255ebc8ea96cdc Mon Sep 17 00:00:00 2001 From: Alex ROBUCHON Date: Fri, 16 Feb 2018 13:57:36 +0000 Subject: [PATCH] Add support for expression groups The provider can be called with -DgroupsExpression=and(group1,group2) for which the grammar would support and(GROUP...), or(GROUP...) and not(GROUP) Signed-off-by: Alex ROBUCHON --- .../maven/surefire/CuppaSurefireProvider.java | 59 ++-- .../main/java/org/forgerock/cuppa/Runner.java | 5 +- .../filters/expression/AndCondition.java | 48 +++ .../filters/expression/Condition.java | 35 ++ .../filters/expression/ConditionFactory.java | 58 ++++ .../filters/expression/ConditionWrapper.java | 16 + .../filters/expression/ContainsCondition.java | 41 +++ .../filters/expression/ExpressionParser.java | 95 ++++++ .../ExpressionTagTestBlockFilter.java | 85 +++++ .../filters/expression/NotCondition.java | 37 +++ .../filters/expression/OrCondition.java | 48 +++ .../filters/expression/package-info.java | 20 ++ .../cuppa/internal/package-info.java | 2 +- .../java/org/forgerock/cuppa/model/Tags.java | 29 +- .../org/forgerock/cuppa/package-info.java | 2 +- .../java/org/forgerock/cuppa/TaggedTests.java | 17 +- .../ExpressionTagTestBlockFilterTest.java | 305 ++++++++++++++++++ docs/_docs/tagging-tests.md | 18 +- 18 files changed, 881 insertions(+), 39 deletions(-) create mode 100644 cuppa/src/main/java/org/forgerock/cuppa/internal/filters/expression/AndCondition.java create mode 100644 cuppa/src/main/java/org/forgerock/cuppa/internal/filters/expression/Condition.java create mode 100644 cuppa/src/main/java/org/forgerock/cuppa/internal/filters/expression/ConditionFactory.java create mode 100644 cuppa/src/main/java/org/forgerock/cuppa/internal/filters/expression/ConditionWrapper.java create mode 100644 cuppa/src/main/java/org/forgerock/cuppa/internal/filters/expression/ContainsCondition.java create mode 100644 cuppa/src/main/java/org/forgerock/cuppa/internal/filters/expression/ExpressionParser.java create mode 100644 cuppa/src/main/java/org/forgerock/cuppa/internal/filters/expression/ExpressionTagTestBlockFilter.java create mode 100644 cuppa/src/main/java/org/forgerock/cuppa/internal/filters/expression/NotCondition.java create mode 100644 cuppa/src/main/java/org/forgerock/cuppa/internal/filters/expression/OrCondition.java create mode 100644 cuppa/src/main/java/org/forgerock/cuppa/internal/filters/expression/package-info.java create mode 100644 cuppa/src/test/java/org/forgerock/cuppa/internal/filters/expression/ExpressionTagTestBlockFilterTest.java diff --git a/cuppa-surefire/src/main/java/org/forgerock/cuppa/maven/surefire/CuppaSurefireProvider.java b/cuppa-surefire/src/main/java/org/forgerock/cuppa/maven/surefire/CuppaSurefireProvider.java index eae88d1..76933ad 100644 --- a/cuppa-surefire/src/main/java/org/forgerock/cuppa/maven/surefire/CuppaSurefireProvider.java +++ b/cuppa-surefire/src/main/java/org/forgerock/cuppa/maven/surefire/CuppaSurefireProvider.java @@ -16,8 +16,9 @@ package org.forgerock.cuppa.maven.surefire; +import static java.util.Collections.emptySet; + import java.util.Arrays; -import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Set; @@ -52,43 +53,55 @@ public final class CuppaSurefireProvider extends AbstractProvider { public CuppaSurefireProvider(ProviderParameters parameters) { this.providerParameters = parameters; Map properties = parameters.getProviderProperties(); - tags = new Tags(getTags(properties), getExcludedTags(properties)); + tags = new Tags(getIncludedTags(properties), getExcludedTags(properties), + getExpressionTags(properties)); + + if (!tags.expressionTags.isEmpty() && (!tags.tags.isEmpty() || !tags.excludedTags.isEmpty())) { + throw new RuntimeException("Use of groupsExpression/tagsExpression cannot be used with " + + "excludedGroups/excludedTags or groups/tags"); + } + } + + private String getExpressionTags(Map properties) { + return getTagsFromPropertiesOrSystem("groupsExpression", "tagsExpression", properties); + } + + private Set getIncludedTags(Map properties) { + return split(getTagsFromPropertiesOrSystem("groups", "tags", properties)); } - private Set getTags(Map properties) { - String groups = properties.get("groups"); + private Set getExcludedTags(Map properties) { + return split(getTagsFromPropertiesOrSystem("excludedGroups", "excludedTags", properties)); + } + + private String getTagsFromPropertiesOrSystem(String groupName, String tagName, + Map properties) { + String groups = properties.get(groupName); if (groups == null) { - groups = System.getProperty("groups"); + groups = System.getProperty(groupName); } - String tags = properties.get("tags"); - String overrideTags = System.getProperty("tags"); + String tags = properties.get(tagName); + String overrideTags = System.getProperty(tagName); return getTags(groups, overrideTags == null ? tags : overrideTags); } - private Set getTags(String groups, String tags) { + private String getTags(String groups, String tags) { if (groups != null && tags != null) { - throw new RuntimeException("Use of 'groups/excludedGroups' and 'tags/excludedTags' " - + "are mutually exclusive."); + throw new RuntimeException("Use of 'groups/excludedGroups/groupsExpression' and" + + " 'tags/excludedTags/tagsExpression are mutually exclusive."); } else if (groups != null) { - return split(groups); + return groups; } else if (tags != null) { - return split(tags); + return tags; } else { - return Collections.emptySet(); + return ""; } } - private Set getExcludedTags(Map properties) { - String excludedGroups = properties.get("excludedGroups"); - if (excludedGroups == null) { - excludedGroups = System.getProperty("excludedGroups"); - } - String excludedTags = properties.get("excludedTags"); - String overrideExcludedTags = System.getProperty("excludedTags"); - return getTags(excludedGroups, overrideExcludedTags == null ? excludedTags : overrideExcludedTags); - } - private Set split(String s) { + if (s.isEmpty()) { + return emptySet(); + } return Arrays.stream(s.split(",")).map(String::trim).collect(Collectors.toSet()); } diff --git a/cuppa/src/main/java/org/forgerock/cuppa/Runner.java b/cuppa/src/main/java/org/forgerock/cuppa/Runner.java index 231b0e1..5d32f37 100644 --- a/cuppa/src/main/java/org/forgerock/cuppa/Runner.java +++ b/cuppa/src/main/java/org/forgerock/cuppa/Runner.java @@ -33,6 +33,7 @@ import org.forgerock.cuppa.internal.filters.EmptyTestBlockFilter; import org.forgerock.cuppa.internal.filters.OnlyTestBlockFilter; import org.forgerock.cuppa.internal.filters.TagTestBlockFilter; +import org.forgerock.cuppa.internal.filters.expression.ExpressionTagTestBlockFilter; import org.forgerock.cuppa.model.Tags; import org.forgerock.cuppa.model.TestBlock; import org.forgerock.cuppa.model.TestBlockBuilder; @@ -77,8 +78,8 @@ public Runner(Tags runTags) { * @param configuration Cuppa configuration to control the behaviour of the runner. */ public Runner(Tags runTags, Configuration configuration) { - coreTestTransforms = Arrays.asList(new OnlyTestBlockFilter(), new TagTestBlockFilter(runTags), - new EmptyTestBlockFilter()); + coreTestTransforms = Arrays.asList(new OnlyTestBlockFilter(), new ExpressionTagTestBlockFilter(runTags), + new TagTestBlockFilter(runTags), new EmptyTestBlockFilter()); this.configuration = configuration; } diff --git a/cuppa/src/main/java/org/forgerock/cuppa/internal/filters/expression/AndCondition.java b/cuppa/src/main/java/org/forgerock/cuppa/internal/filters/expression/AndCondition.java new file mode 100644 index 0000000..396328c --- /dev/null +++ b/cuppa/src/main/java/org/forgerock/cuppa/internal/filters/expression/AndCondition.java @@ -0,0 +1,48 @@ +/* + * Copyright 2018 ForgeRock AS. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.forgerock.cuppa.internal.filters.expression; + +import java.util.Collection; +import java.util.Collections; + +/** + * A condition that composes other conditions with a logical AND. + */ +class AndCondition extends ConditionWrapper { + + public static final AndCondition EMPTY = new AndCondition(Collections.emptyList()); + private final Collection conditions; + + /** + * Constructor. + * + * @param conditions a list of condition to compose. + */ + AndCondition(Collection conditions) { + this.conditions = Collections.unmodifiableCollection(conditions); + } + + @Override + public boolean shouldRun(Collection tags) { + return conditions.stream().allMatch(c -> c.shouldRun(tags)); + } + + @Override + public ConditionWrapper setConditions(Collection conditions) { + return new AndCondition(conditions); + } +} diff --git a/cuppa/src/main/java/org/forgerock/cuppa/internal/filters/expression/Condition.java b/cuppa/src/main/java/org/forgerock/cuppa/internal/filters/expression/Condition.java new file mode 100644 index 0000000..86224d8 --- /dev/null +++ b/cuppa/src/main/java/org/forgerock/cuppa/internal/filters/expression/Condition.java @@ -0,0 +1,35 @@ +/* + * Copyright 2018 ForgeRock AS. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.forgerock.cuppa.internal.filters.expression; + +import java.util.Collection; + +/** + * A condition used by {@link ExpressionTagTestBlockFilter}. + */ +@FunctionalInterface +public interface Condition { + + /** + * Check if the list of tags is compliant with the condition. + * + * @param tags The collection of tags. + * @return true if the condition complies with the tags supplied, false otherwise. + */ + boolean shouldRun(Collection tags); + +} diff --git a/cuppa/src/main/java/org/forgerock/cuppa/internal/filters/expression/ConditionFactory.java b/cuppa/src/main/java/org/forgerock/cuppa/internal/filters/expression/ConditionFactory.java new file mode 100644 index 0000000..c0ef96b --- /dev/null +++ b/cuppa/src/main/java/org/forgerock/cuppa/internal/filters/expression/ConditionFactory.java @@ -0,0 +1,58 @@ +package org.forgerock.cuppa.internal.filters.expression; + +import java.util.Arrays; +import java.util.List; + +/** + * A factory to creates {@link ConditionWrapper}. + */ +final class ConditionFactory { + + /** + * link the operator to an instance of {@link Condition}. + */ + enum ConditionEnum { + AND("and", AndCondition.EMPTY), + OR("or", OrCondition.EMPTY), + NOT("not", NotCondition.EMPTY); + + private String operator; + private ConditionWrapper condition; + + ConditionEnum(String operator, ConditionWrapper condition) { + this.operator = operator; + this.condition = condition; + } + + /** + * Get a ConditionEnum fron an string operator. + * @param operator The operator we want to get the associated enum + * @return The ConditionEnum + */ + static ConditionEnum getFromOperator(String operator) { + return Arrays.stream(ConditionEnum.values()) + .filter(c -> c.operator.equals(operator)) + .findFirst() + .orElseThrow(() -> new IllegalArgumentException(operator + " is not supported. The list of " + + "supported operator is and, or, not")); + } + + Condition getCondition(List tags) { + return condition.setConditions(tags); + } + } + + private ConditionFactory() { + } + + /** + * Create a {@link Condition}. + * @param operator The operator we want to create a condition for. + * @param tags The list of conditions that {@link ConditionWrapper} will include. + * @return A condition + */ + static Condition get(String operator, List tags) { + return ConditionEnum.getFromOperator(operator.trim().toLowerCase()) + .getCondition(tags); + } +} diff --git a/cuppa/src/main/java/org/forgerock/cuppa/internal/filters/expression/ConditionWrapper.java b/cuppa/src/main/java/org/forgerock/cuppa/internal/filters/expression/ConditionWrapper.java new file mode 100644 index 0000000..eb044d4 --- /dev/null +++ b/cuppa/src/main/java/org/forgerock/cuppa/internal/filters/expression/ConditionWrapper.java @@ -0,0 +1,16 @@ +package org.forgerock.cuppa.internal.filters.expression; + +import java.util.Collection; + +/** + * A condition wrapper for Condition that needs to wrap other conditions. + */ +abstract class ConditionWrapper implements Condition { + + /** + * Create a new Condition with the given conditions. + * @param conditions the collection of conditions + * @return a new condition + */ + abstract ConditionWrapper setConditions(Collection conditions); +} diff --git a/cuppa/src/main/java/org/forgerock/cuppa/internal/filters/expression/ContainsCondition.java b/cuppa/src/main/java/org/forgerock/cuppa/internal/filters/expression/ContainsCondition.java new file mode 100644 index 0000000..7efb307 --- /dev/null +++ b/cuppa/src/main/java/org/forgerock/cuppa/internal/filters/expression/ContainsCondition.java @@ -0,0 +1,41 @@ +/* + * Copyright 2018 ForgeRock AS. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.forgerock.cuppa.internal.filters.expression; + +import java.util.Collection; + +/** + * A condition that checks if a tag is contains in a collection of tags. + */ +class ContainsCondition implements Condition { + + private String tag; + + /** + * Constructor. + * + * @param tag A group/tag we want to search for. + */ + ContainsCondition(String tag) { + this.tag = tag; + } + + @Override + public boolean shouldRun(Collection tags) { + return tags.contains(tag); + } +} diff --git a/cuppa/src/main/java/org/forgerock/cuppa/internal/filters/expression/ExpressionParser.java b/cuppa/src/main/java/org/forgerock/cuppa/internal/filters/expression/ExpressionParser.java new file mode 100644 index 0000000..1c8bc6d --- /dev/null +++ b/cuppa/src/main/java/org/forgerock/cuppa/internal/filters/expression/ExpressionParser.java @@ -0,0 +1,95 @@ +package org.forgerock.cuppa.internal.filters.expression; + +import java.util.ArrayList; +import java.util.List; +import java.util.Stack; + +/** + * This class is responsible to parse an expression tag to a {@link Condition} . + */ +final class ExpressionParser { + + private ExpressionParser() { + } + + /** + * Parse the expressionTags to a Condition. + * @param expressionTags the expression to parse + * @return The condition + */ + static Condition parse(String expressionTags) { + if (expressionTags.isEmpty()) { + return (t) -> true; + } + + Stack operators = new Stack<>(); + Stack numberOfGroupsPerOperator = new Stack<>(); + Stack groups = new Stack<>(); + + String currentWord = ""; + char[] chars = expressionTags.toCharArray(); + for (int i = 0; i < chars.length; i++) { + char c = chars[i]; + switch (c) { + case '(': + pushOperator(expressionTags, operators, numberOfGroupsPerOperator, currentWord, i); + currentWord = ""; + break; + case ',': + case ';': + addEqualCondition(groups, currentWord, numberOfGroupsPerOperator); + currentWord = ""; + break; + case ')': + pushComplexCondition(operators, numberOfGroupsPerOperator, groups, currentWord); + currentWord = ""; + break; + default: + currentWord += c; + break; + } + } + + if (groups.size() != 1 || numberOfGroupsPerOperator.size() != 0 || operators.size() != 0) { + throw new IllegalArgumentException("malformed expression " + expressionTags); + } + + return groups.pop(); + } + + private static void pushComplexCondition(Stack operators, Stack numberOfGroupsPerOperator, + Stack groups, String currentWord) { + addEqualCondition(groups, currentWord, numberOfGroupsPerOperator); + + String operator = operators.pop(); + int number = numberOfGroupsPerOperator.pop(); + List tags = new ArrayList<>(number); + + for (; number > 0; number--) { + tags.add(groups.pop()); + } + + groups.push(ConditionFactory.get(operator, tags)); + if (!numberOfGroupsPerOperator.isEmpty()) { + numberOfGroupsPerOperator.push(numberOfGroupsPerOperator.pop() + 1); + } + } + + private static void pushOperator(String expressionTags, Stack operators, + Stack numberOfGroupsPerOperator, String currentWord, int i) { + if ("".equals(currentWord)) { + throw new IllegalArgumentException("malformed expression ( " + expressionTags + " ). " + + "An operator was expected column " + i); + } + operators.push(currentWord); + numberOfGroupsPerOperator.push(0); + } + + private static void addEqualCondition(Stack groups, String currentWord, Stack numbers) { + String trimmedWord = currentWord.trim(); + if (!"".equals(trimmedWord)) { + groups.push(new ContainsCondition(trimmedWord)); + numbers.push(numbers.pop() + 1); + } + } +} diff --git a/cuppa/src/main/java/org/forgerock/cuppa/internal/filters/expression/ExpressionTagTestBlockFilter.java b/cuppa/src/main/java/org/forgerock/cuppa/internal/filters/expression/ExpressionTagTestBlockFilter.java new file mode 100644 index 0000000..3a08095 --- /dev/null +++ b/cuppa/src/main/java/org/forgerock/cuppa/internal/filters/expression/ExpressionTagTestBlockFilter.java @@ -0,0 +1,85 @@ +/* + * Copyright 2018 ForgeRock AS. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.forgerock.cuppa.internal.filters.expression; + +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.function.Function; +import java.util.stream.Collectors; + +import org.forgerock.cuppa.model.Options; +import org.forgerock.cuppa.model.Tags; +import org.forgerock.cuppa.model.TagsOption; +import org.forgerock.cuppa.model.Test; +import org.forgerock.cuppa.model.TestBlock; + + +/** + * Filter the tests according to the expression tag. + * It uses a set of conditions to decide if it should run a test. + * + * @see Condition + */ +public final class ExpressionTagTestBlockFilter implements Function { + private final Tags runTags; + private final Condition condition; + + /** + * Creates a new filter. + * + * @param runTags runTags + */ + public ExpressionTagTestBlockFilter(Tags runTags) { + this.runTags = runTags; + this.condition = ExpressionParser.parse(runTags.expressionTags); + } + + @Override + public TestBlock apply(TestBlock testBlock) { + if (runTags.expressionTags.isEmpty()) { + return testBlock; + } + + return filterTests(testBlock, Collections.emptySet()); + } + + private TestBlock filterTests(TestBlock testBlock, Set parentBlockTags) { + Set blockTags = union(getTags(testBlock.options), parentBlockTags); + List testBlocks = testBlock.testBlocks.stream() + .map(b -> filterTests(b, blockTags)) + .collect(Collectors.toList()); + List tests = testBlock.tests.stream() + .filter(t -> condition.shouldRun(union(getTags(t.options), blockTags))) + .collect(Collectors.toList()); + return testBlock.toBuilder() + .setTestBlocks(testBlocks) + .setTests(tests) + .build(); + } + + private Set getTags(Options options) { + return options.get(TagsOption.class).orElse(Collections.emptySet()); + } + + private Set union(Set a, Set b) { + Set union = new HashSet<>(a); + union.addAll(b); + return union; + } +} diff --git a/cuppa/src/main/java/org/forgerock/cuppa/internal/filters/expression/NotCondition.java b/cuppa/src/main/java/org/forgerock/cuppa/internal/filters/expression/NotCondition.java new file mode 100644 index 0000000..bcd93f0 --- /dev/null +++ b/cuppa/src/main/java/org/forgerock/cuppa/internal/filters/expression/NotCondition.java @@ -0,0 +1,37 @@ +package org.forgerock.cuppa.internal.filters.expression; + +import java.util.Collection; +import java.util.Collections; +import java.util.Optional; + +/** + * A wrapper condition that inverse the wrapped condition. + */ +class NotCondition extends ConditionWrapper { + + public static final NotCondition EMPTY = new NotCondition(Collections.emptyList()); + private final Optional condition; + + /** + * Constructor. + * + * @param conditions a singletonList of condition. + */ + NotCondition(Collection conditions) { + if (conditions.size() > 1) { + throw new IllegalArgumentException(NotCondition.class + " cannot have more than one tag"); + } + this.condition = conditions.stream().findFirst(); + } + + @Override + public boolean shouldRun(Collection tags) { + return !condition.orElse(c -> true).shouldRun(tags); + } + + @Override + public ConditionWrapper setConditions(Collection conditions) { + return new NotCondition(conditions); + } +} + diff --git a/cuppa/src/main/java/org/forgerock/cuppa/internal/filters/expression/OrCondition.java b/cuppa/src/main/java/org/forgerock/cuppa/internal/filters/expression/OrCondition.java new file mode 100644 index 0000000..8e3e817 --- /dev/null +++ b/cuppa/src/main/java/org/forgerock/cuppa/internal/filters/expression/OrCondition.java @@ -0,0 +1,48 @@ +/* + * Copyright 2018 ForgeRock AS. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.forgerock.cuppa.internal.filters.expression; + +import java.util.Collection; +import java.util.Collections; + +/** + * A condition that composes other conditions with a logical OR. + */ +class OrCondition extends ConditionWrapper { + + public static final OrCondition EMPTY = new OrCondition(Collections.emptyList()); + private final Collection conditions; + + /** + * Constructor. + * + * @param conditions a list of condition to compose. + */ + OrCondition(Collection conditions) { + this.conditions = Collections.unmodifiableCollection(conditions); + } + + @Override + public boolean shouldRun(Collection tags) { + return conditions.stream().anyMatch(c -> c.shouldRun(tags)); + } + + @Override + public ConditionWrapper setConditions(Collection conditions) { + return new OrCondition(conditions); + } +} diff --git a/cuppa/src/main/java/org/forgerock/cuppa/internal/filters/expression/package-info.java b/cuppa/src/main/java/org/forgerock/cuppa/internal/filters/expression/package-info.java new file mode 100644 index 0000000..357a68b --- /dev/null +++ b/cuppa/src/main/java/org/forgerock/cuppa/internal/filters/expression/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright 2018 ForgeRock AS. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Classes related to the expression group filter. + */ +package org.forgerock.cuppa.internal.filters.expression; diff --git a/cuppa/src/main/java/org/forgerock/cuppa/internal/package-info.java b/cuppa/src/main/java/org/forgerock/cuppa/internal/package-info.java index 52945c2..b1b796a 100644 --- a/cuppa/src/main/java/org/forgerock/cuppa/internal/package-info.java +++ b/cuppa/src/main/java/org/forgerock/cuppa/internal/package-info.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2016 ForgeRock AS. + * Copyright 2015-2018 ForgeRock AS. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/cuppa/src/main/java/org/forgerock/cuppa/model/Tags.java b/cuppa/src/main/java/org/forgerock/cuppa/model/Tags.java index f194fd1..c16a74f 100644 --- a/cuppa/src/main/java/org/forgerock/cuppa/model/Tags.java +++ b/cuppa/src/main/java/org/forgerock/cuppa/model/Tags.java @@ -16,7 +16,8 @@ package org.forgerock.cuppa.model; -import java.util.Collections; +import static java.util.Collections.emptySet; + import java.util.Set; /** @@ -27,7 +28,7 @@ public class Tags { /** * No tags specified, meaning no tests should be filtered from the test run. */ - public static final Tags EMPTY_TAGS = new Tags(Collections.emptySet(), Collections.emptySet()); + public static final Tags EMPTY_TAGS = new Tags(emptySet(), emptySet(), ""); /** * The set of tags which tests must be tagged with to be included in the test run. @@ -39,15 +40,23 @@ public class Tags { */ public final Set excludedTags; + /** + * An expression of tags using condition to create complex tag filtering. + */ + public final String expressionTags; + /** * Constructs a {@code Tags} instance with the specified tags and anti-tags (excluded tags). * * @param tags The set of tags which tests must be tagged with to be included in the test run. * @param excludedTags The set of excluded tags which tests must not be tagged with to be included * in the test run. + * @param expressionTags An expression using condition to create complex tag filtering + * {@link org.forgerock.cuppa.internal.filters.expression.Condition} */ - public Tags(Set tags, Set excludedTags) { + public Tags(Set tags, Set excludedTags, String expressionTags) { this.tags = tags; + this.expressionTags = expressionTags; this.excludedTags = excludedTags; } @@ -58,7 +67,7 @@ public Tags(Set tags, Set excludedTags) { * @return The {@code Tags} instance. */ public static Tags tags(Set tags) { - return new Tags(tags, Collections.emptySet()); + return new Tags(tags, emptySet(), ""); } /** @@ -69,6 +78,16 @@ public static Tags tags(Set tags) { * @return The {@code Tags} instance. */ public static Tags excludedTags(Set excludedTags) { - return new Tags(Collections.emptySet(), excludedTags); + return new Tags(emptySet(), excludedTags, ""); + } + + /** + * Constructs a {@code Tags} instance with the specified expression tag. + * + * @param expressionTag The expression tag + * @return The {@code Tags} instance. + */ + public static Tags expressionTags(String expressionTag) { + return new Tags(emptySet(), emptySet(), expressionTag); } } diff --git a/cuppa/src/main/java/org/forgerock/cuppa/package-info.java b/cuppa/src/main/java/org/forgerock/cuppa/package-info.java index 38c5320..00a9d39 100644 --- a/cuppa/src/main/java/org/forgerock/cuppa/package-info.java +++ b/cuppa/src/main/java/org/forgerock/cuppa/package-info.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2016 ForgeRock AS. + * Copyright 2015-2018 ForgeRock AS. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/cuppa/src/test/java/org/forgerock/cuppa/TaggedTests.java b/cuppa/src/test/java/org/forgerock/cuppa/TaggedTests.java index 9035bb1..bae5442 100644 --- a/cuppa/src/test/java/org/forgerock/cuppa/TaggedTests.java +++ b/cuppa/src/test/java/org/forgerock/cuppa/TaggedTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2016 ForgeRock AS. + * Copyright 2018 ForgeRock AS. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,11 +16,18 @@ package org.forgerock.cuppa; -import static org.forgerock.cuppa.Cuppa.*; +import static org.forgerock.cuppa.Cuppa.describe; +import static org.forgerock.cuppa.Cuppa.it; +import static org.forgerock.cuppa.Cuppa.tags; +import static org.forgerock.cuppa.Cuppa.with; import static org.forgerock.cuppa.TestCuppaSupport.defineTests; import static org.forgerock.cuppa.TestCuppaSupport.runTests; import static org.mockito.Matchers.any; -import static org.mockito.Mockito.*; +import static org.mockito.Mockito.anyListOf; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; import java.util.Arrays; import java.util.Collections; @@ -374,7 +381,7 @@ public void shouldNotRunTestsWhichMatchBothRunTagsAndExcludedTags() throws Excep }); //When - runTests(rootBlock, reporter, new Tags(tags, excludedTags)); + runTests(rootBlock, reporter, new Tags(tags, excludedTags, "")); //Then verify(testFunction, never()).apply(); @@ -401,7 +408,7 @@ public void shouldRunTestsWhichMatchRunTagAndNotAntiTag() throws Exception { }); //When - runTests(rootBlock, reporter, new Tags(tags, excludedTags)); + runTests(rootBlock, reporter, new Tags(tags, excludedTags, "")); //Then verify(testFunctionNotRun, never()).apply(); diff --git a/cuppa/src/test/java/org/forgerock/cuppa/internal/filters/expression/ExpressionTagTestBlockFilterTest.java b/cuppa/src/test/java/org/forgerock/cuppa/internal/filters/expression/ExpressionTagTestBlockFilterTest.java new file mode 100644 index 0000000..d029adb --- /dev/null +++ b/cuppa/src/test/java/org/forgerock/cuppa/internal/filters/expression/ExpressionTagTestBlockFilterTest.java @@ -0,0 +1,305 @@ +/* + * Copyright 2018 ForgeRock AS. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +package org.forgerock.cuppa.internal.filters.expression; + +import static java.util.Collections.emptySet; +import static org.forgerock.cuppa.model.TestBlockType.ROOT; +import static org.forgerock.cuppa.model.TestBlockType.WHEN; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; + +import org.forgerock.cuppa.model.Options; +import org.forgerock.cuppa.model.Tags; +import org.forgerock.cuppa.model.TagsOption; +import org.forgerock.cuppa.model.Test; +import org.forgerock.cuppa.model.TestBlock; +import org.forgerock.cuppa.model.TestBlockBuilder; +import org.forgerock.cuppa.model.TestBuilder; +import org.testng.Assert; +import org.testng.annotations.BeforeTest; + + +/** + * Tests the {@link ExpressionTagTestBlockFilter} class. + */ +public class ExpressionTagTestBlockFilterTest { + + private TestBlock root; + + @BeforeTest + public void before() { + TestBlock child = new TestBlockBuilder() + .setType(WHEN) + .setTestClass(ExpressionTagTestBlockFilterTest.class) + .setTests(getTests("child-tag ")) + .setDescription("child description") + .setOptions(createOption("child-tag")) + .build(); + root = new TestBlockBuilder() + .setType(ROOT) + .setTestClass(ExpressionTagTestBlockFilterTest.class) + .setTests(getTests("")) + .setDescription("root description") + .setTestBlocks(Collections.singletonList(child)).build(); + } + + private static List getTests(String tags) { + List tests = new ArrayList<>(); + tests.add(buildNormalTest(tags + "other", createOption("other"))); + tests.add(buildNormalTest(tags + "ui", createOption("ui"))); + tests.add(buildNormalTest(tags + "ui other", createOption("ui", "other"))); + tests.add(buildNormalTest(tags + "smoke", createOption("smoke"))); + tests.add(buildNormalTest(tags + "smoke other", createOption("smoke", "other"))); + return tests; + } + + private static Test buildNormalTest(String description, Options options) { + return new TestBuilder() + .setDescription(description) + .setFunction(Optional.empty()) + .setTestClass(ExpressionTagTestBlockFilterTest.class) + .setOptions(options).build(); + } + + @org.testng.annotations.Test + public void noTagsReturnAll() throws Exception { + ExpressionTagTestBlockFilter filter = new ExpressionTagTestBlockFilter(new Tags(emptySet(), emptySet(), "")); + TestBlock testBlocks = filter.apply(root); + + List testsDescription = getTestsDescription(testBlocks); + Assert.assertEquals(testsDescription.size(), 10); + } + + @org.testng.annotations.Test + public void emptyIntersectionOf2GroupsIsEmpty() throws Exception { + ExpressionTagTestBlockFilter filter = new ExpressionTagTestBlockFilter(new Tags(emptySet(), emptySet(), + "and(ui,smoke)")); + TestBlock testBlocks = filter.apply(root); + + List testsDescription = getTestsDescription(testBlocks); + Assert.assertTrue(testsDescription.isEmpty()); + } + + + @org.testng.annotations.Test + public void intersectionOf2GroupsSuccess() throws Exception { + ExpressionTagTestBlockFilter filter = new ExpressionTagTestBlockFilter(new Tags(emptySet(), emptySet(), + "and(child-tag,smoke)")); + TestBlock testBlocks = filter.apply(root); + + List testsDescription = getTestsDescription(testBlocks); + Assert.assertEquals(testsDescription.size(), 2); + Assert.assertTrue(testsDescription.contains("child-tag smoke")); + Assert.assertTrue(testsDescription.contains("child-tag smoke other")); + } + + @org.testng.annotations.Test + public void intersectionOf2GroupsWithNOTSuccess() throws Exception { + ExpressionTagTestBlockFilter filter = new ExpressionTagTestBlockFilter(new Tags(emptySet(), emptySet(), + "not(and(child-tag,smoke))")); + TestBlock testBlocks = filter.apply(root); + + List testsDescription = getTestsDescription(testBlocks); + Assert.assertEquals(testsDescription.size(), 8); + Assert.assertTrue(testsDescription.contains("child-tag ui")); + Assert.assertTrue(testsDescription.contains("ui")); + Assert.assertTrue(testsDescription.contains("child-tag ui other")); + Assert.assertTrue(testsDescription.contains("ui other")); + Assert.assertTrue(testsDescription.contains("smoke other")); + Assert.assertTrue(testsDescription.contains("other")); + Assert.assertTrue(testsDescription.contains("smoke")); + Assert.assertTrue(testsDescription.contains("child-tag other")); + } + + @org.testng.annotations.Test + public void notAGroupSuccess() throws Exception { + ExpressionTagTestBlockFilter filter = new ExpressionTagTestBlockFilter(new Tags(emptySet(), emptySet(), + "not(smoke)")); + TestBlock testBlocks = filter.apply(root); + + List testsDescription = getTestsDescription(testBlocks); + Assert.assertEquals(testsDescription.size(), 6); + Assert.assertTrue(testsDescription.contains("child-tag ui")); + Assert.assertTrue(testsDescription.contains("ui")); + Assert.assertTrue(testsDescription.contains("child-tag ui other")); + Assert.assertTrue(testsDescription.contains("ui other")); + Assert.assertTrue(testsDescription.contains("other")); + Assert.assertTrue(testsDescription.contains("child-tag other")); + } + + @org.testng.annotations.Test + public void intersectionOf2GroupsWithOperatorUppercaseSuccess() throws Exception { + ExpressionTagTestBlockFilter filter = new ExpressionTagTestBlockFilter(new Tags(emptySet(), emptySet(), + "AND(child-tag,smoke)")); + TestBlock testBlocks = filter.apply(root); + + List testsDescription = getTestsDescription(testBlocks); + Assert.assertEquals(testsDescription.size(), 2); + Assert.assertTrue(testsDescription.contains("child-tag smoke")); + Assert.assertTrue(testsDescription.contains("child-tag smoke other")); + } + + @org.testng.annotations.Test + public void intersectionOf3GroupsSuccess() throws Exception { + ExpressionTagTestBlockFilter filter = new ExpressionTagTestBlockFilter(new Tags(emptySet(), emptySet(), + "and(child-tag,smoke,other)")); + TestBlock testBlocks = filter.apply(root); + + List testsDescription = getTestsDescription(testBlocks); + Assert.assertEquals(testsDescription.size(), 1); + Assert.assertTrue(testsDescription.contains("child-tag smoke other")); + } + + @org.testng.annotations.Test + public void intersectionOf2GroupsAndIncludeOf1Success() throws Exception { + ExpressionTagTestBlockFilter filter = new ExpressionTagTestBlockFilter(new Tags(emptySet(), emptySet(), + "or(ui,and(child-tag,smoke))")); + TestBlock testBlocks = filter.apply(root); + + List testsDescription = getTestsDescription(testBlocks); + Assert.assertEquals(testsDescription.size(), 6); + Assert.assertTrue(testsDescription.contains("child-tag smoke")); + Assert.assertTrue(testsDescription.contains("child-tag smoke other")); + Assert.assertTrue(testsDescription.contains("child-tag ui")); + Assert.assertTrue(testsDescription.contains("ui")); + Assert.assertTrue(testsDescription.contains("child-tag ui other")); + Assert.assertTrue(testsDescription.contains("ui other")); + } + + @org.testng.annotations.Test + public void intersectionOf3GroupsAndIncludeOf1Success() throws Exception { + ExpressionTagTestBlockFilter filter = new ExpressionTagTestBlockFilter(new Tags(emptySet(), emptySet(), + "or(ui,and(child-tag,smoke,other))")); + TestBlock testBlocks = filter.apply(root); + + List testsDescription = getTestsDescription(testBlocks); + Assert.assertEquals(testsDescription.size(), 5); + Assert.assertTrue(testsDescription.contains("child-tag smoke other")); + Assert.assertTrue(testsDescription.contains("ui")); + Assert.assertTrue(testsDescription.contains("child-tag ui")); + Assert.assertTrue(testsDescription.contains("ui other")); + Assert.assertTrue(testsDescription.contains("child-tag ui other")); + } + + @org.testng.annotations.Test + public void intersectionOf2GroupsAnd2IncludeOf1Success() throws Exception { + ExpressionTagTestBlockFilter filter = new ExpressionTagTestBlockFilter(new Tags(emptySet(), emptySet(), + "or(ui,and(child-tag,smoke),other)")); + TestBlock testBlocks = filter.apply(root); + + List testsDescription = getTestsDescription(testBlocks); + Assert.assertEquals(testsDescription.size(), 9); + Assert.assertTrue(testsDescription.contains("child-tag smoke")); + Assert.assertTrue(testsDescription.contains("child-tag smoke other")); + Assert.assertTrue(testsDescription.contains("child-tag ui")); + Assert.assertTrue(testsDescription.contains("ui")); + Assert.assertTrue(testsDescription.contains("child-tag ui other")); + Assert.assertTrue(testsDescription.contains("ui other")); + Assert.assertTrue(testsDescription.contains("smoke other")); + Assert.assertTrue(testsDescription.contains("other")); + Assert.assertTrue(testsDescription.contains("child-tag other")); + } + + @org.testng.annotations.Test + public void intersectionOf2GroupsAnd2IncludeOf1WithSpacesSuccess() throws Exception { + ExpressionTagTestBlockFilter filter = new ExpressionTagTestBlockFilter(new Tags(emptySet(), emptySet(), + "or(ui , and ( child-tag, smoke),other )")); + TestBlock testBlocks = filter.apply(root); + + List testsDescription = getTestsDescription(testBlocks); + Assert.assertEquals(testsDescription.size(), 9); + Assert.assertTrue(testsDescription.contains("child-tag smoke")); + Assert.assertTrue(testsDescription.contains("child-tag smoke other")); + Assert.assertTrue(testsDescription.contains("child-tag ui")); + Assert.assertTrue(testsDescription.contains("ui")); + Assert.assertTrue(testsDescription.contains("child-tag ui other")); + Assert.assertTrue(testsDescription.contains("ui other")); + Assert.assertTrue(testsDescription.contains("smoke other")); + Assert.assertTrue(testsDescription.contains("other")); + Assert.assertTrue(testsDescription.contains("child-tag other")); + } + + @org.testng.annotations.Test(expectedExceptions = IllegalArgumentException.class) + public void missingRightParenthesisThrowException() throws Exception { + ExpressionTagTestBlockFilter filter = new ExpressionTagTestBlockFilter(new Tags(emptySet(), emptySet(), + "or(ui,and(child-tag,smoke),other")); + filter.apply(root); + } + + @org.testng.annotations.Test(expectedExceptions = IllegalArgumentException.class) + public void missingOperatorThrowException() throws Exception { + ExpressionTagTestBlockFilter filter = new ExpressionTagTestBlockFilter(new Tags(emptySet(), emptySet(), + "or(ui,(child-tag,smoke),other)")); + filter.apply(root); + } + + @org.testng.annotations.Test(expectedExceptions = IllegalArgumentException.class) + public void unknownOperatorThrowException() throws Exception { + ExpressionTagTestBlockFilter filter = new ExpressionTagTestBlockFilter(new Tags(emptySet(), emptySet(), + "unknown(ui,(child-tag,smoke),other)")); + filter.apply(root); + } + + @org.testng.annotations.Test(expectedExceptions = IllegalArgumentException.class) + public void notSingleRootThowAnException() throws Exception { + ExpressionTagTestBlockFilter filter = new ExpressionTagTestBlockFilter(new Tags(emptySet(), emptySet(), + "or(ui),and(other)")); + filter.apply(root); + } + + @org.testng.annotations.Test + public void emptyGroupisValid() throws Exception { + ExpressionTagTestBlockFilter filter = new ExpressionTagTestBlockFilter(new Tags(emptySet(), emptySet(), + "and()")); + TestBlock testBlocks = filter.apply(root); + + List testsDescription = getTestsDescription(testBlocks); + Assert.assertEquals(testsDescription.size(), 10); + } + + private List getTestsDescription(TestBlock testBlock) { + Set subTestsDescription = testBlock.testBlocks.stream() + .flatMap(tb -> getTestsDescription(tb).stream()) + .collect(Collectors.toSet()); + + Set description = testBlock.tests.stream().map(t -> t.description).collect(Collectors.toSet()); + + return union(subTestsDescription, description); + } + + private static Options createOption(String... tags) { + return Options.EMPTY.set(new TagsOption(new HashSet<>(Arrays.asList(tags)))); + } + + private List union(Collection list1, Collection list2) { + Set set = new HashSet<>(); + + set.addAll(list1); + set.addAll(list2); + + return new ArrayList<>(set); + } +} diff --git a/docs/_docs/tagging-tests.md b/docs/_docs/tagging-tests.md index cc67f1b..4799802 100644 --- a/docs/_docs/tagging-tests.md +++ b/docs/_docs/tagging-tests.md @@ -35,14 +35,28 @@ For example, to run all tests except tests tagged with `slow`: mvn -DexcludedTags=slow test ``` +If you want more flexibility you can use an expression. +For example, to run all the tests with `fast` tag or with `smoke` and `ui` tags excluding all `slow` tags : + +```bash +mvn -DgroupsExpression="and(or(fast,and(smoke,ui)),not(slow))" +``` + + + ## Tagging a Block of Tests