Skip to content

Commit

Permalink
Added tagging to suites.
Browse files Browse the repository at this point in the history
  • Loading branch information
Ashley Frieze committed Nov 26, 2016
1 parent 09910db commit f5d2af8
Show file tree
Hide file tree
Showing 5 changed files with 511 additions and 9 deletions.
59 changes: 50 additions & 9 deletions src/main/java/com/greghaskins/spectrum/Spectrum.java
Original file line number Diff line number Diff line change
Expand Up @@ -157,15 +157,55 @@ public static void ignore(final String reason) {
}

/**
* Declare a {@link com.greghaskins.spectrum.Block} to be run before each spec in the suite.
*
* <p>
* Use this to perform setup actions that are common across tests in the context. If multiple
* {@code beforeEach} blocks are declared, they will run in declaration order.
* </p>
*
* @param block {@link com.greghaskins.spectrum.Block} to run once before each spec
* Tag the next item with strings defining its category. This will be used
* in conjunction with the current configuration of selected tags to determine
* whether this item can be included in this run.
* Tagging works in the following order of precedence. A system property is loaded
* with the default tags for each of inclusion and exclusion. Then the test class's
* use of {@link SpectrumOptions} is read for any overrides. Finally any calls during
* test definition to {@link #includeTags(String...)} or {@link #excludeTags(String...)}
* override the whole thing.
* @param tags tags for the item - as many as required.
*/
public static void tag(final String... tags) {
if (getCurrentSuiteBeingDeclared().excludesAny(tags)
|| !getCurrentSuiteBeingDeclared().allowsAny(tags)) {
ignore();
}
}

/**
* From this point forwards, the tags mentioned are included for specs.
* This overrides any previous selection for include and applies to the
* current suite and its children. If the list is empty, or no tags for inclusion
* are ever provided, then the default is to include all.
* @param tags tags to select for inclusion.
*/
public static void includeTags(final String... tags) {
getCurrentSuiteBeingDeclared().includeTags(tags);
}

/**
* From this point forwards, the tags mentioned will cause specs to be ignored.
* This overrides any previous selection for exclusion and applies to the
* current suite and its children. If the list is empty, or no tags for exclusion
* are ever provided, then the default is that nothing is excluded.
* @param tags tags to select for exclusion.
*/
public static void excludeTags(final String... tags) {
getCurrentSuiteBeingDeclared().excludeTags(tags);
}

/**
* Declare a {@link com.greghaskins.spectrum.Block} to be run before each spec in the suite.
*
* <p>
* Use this to perform setup actions that are common across tests in the context. If multiple
* {@code beforeEach} blocks are declared, they will run in declaration order.
* </p>
*
* @param block {@link com.greghaskins.spectrum.Block} to run once before each spec
*/
public static void beforeEach(final com.greghaskins.spectrum.Block block) {
getCurrentSuiteBeingDeclared().beforeEach(block);
}
Expand Down Expand Up @@ -287,7 +327,8 @@ default T get() {
* @see org.junit.runner.Runner
*/
public Spectrum(final Class<?> testClass) {
this(Description.createSuiteDescription(testClass), new ConstructorBlock(testClass));
this(Description.createSuiteDescription(testClass),
new TaggingBlock(testClass, new ConstructorBlock(testClass)));
}

Spectrum(final Description description, final com.greghaskins.spectrum.Block definitionBlock) {
Expand Down
54 changes: 54 additions & 0 deletions src/main/java/com/greghaskins/spectrum/SpectrumOptions.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package com.greghaskins.spectrum;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
* Options that can be applied to a test class annotated with {@link org.junit.runner.RunWith}
* with the {@link Spectrum} runner. E.g.<br>
* <pre><code class="java">
* &#064;RunWith(Spectrum.class)
* &#064;SpectrumOptions(includeTags={"wip","dev"})
* public class MyTest { ... }
* </code></pre>
*/
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
public @interface SpectrumOptions {
String INCLUDE_TAGS_PROPERTY = "spectrum.include.tags";
String EXCLUDE_TAGS_PROPERTY = "spectrum.exclude.tags";
String TAGS_SEPARATOR = ",";

/**
* Allows tags to be selected for controlling the test at coding time.
* This means you can supply tags which relate to, say, Work In Progress
* specs temporarily while developing them. See {@link Spectrum#tag(String...)}
* @return the tags hard coded for inclusion.
*/
String[] includeTags() default {};

/**
* Allows tags to be selected for controlling the test at coding time.
* This means you can supply tags while developing. See {@link Spectrum#tag(String...)}
* @return the tags hard coded for exclusion.
*/
String[] excludeTags() default {};

/**
* Which system property can be used to retrieve tags passed in by
* property. These tags are to be comma separated in the property.
* See {@link Spectrum#tag(String...)}
* @return the system property to use if not default.
*/
String includeTagsSystemProperty() default INCLUDE_TAGS_PROPERTY;

/**
* Which system property can be used to retrieve tags passed in by
* property. These tags are to be comma separated in the property.
* See {@link Spectrum#tag(String...)}
* @return the system property to use if not default.
*/
String excludeTagsSystemProperty() default EXCLUDE_TAGS_PROPERTY;
}
47 changes: 47 additions & 0 deletions src/main/java/com/greghaskins/spectrum/Suite.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import org.junit.runner.notification.RunNotifier;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
Expand All @@ -27,6 +28,9 @@ final class Suite implements Parent, Child {
private boolean ignored;
private boolean ignoreNext;

private Set<String> includedTags = new HashSet<>();
private Set<String> excludedTags = new HashSet<>();

/**
* The strategy for running the children within the suite.
*/
Expand Down Expand Up @@ -64,6 +68,8 @@ Suite addSuite(final String name, final ChildRunner childRunner) {
suite.beforeAll.addBlock(this.beforeAll);
suite.beforeEach.addBlock(this.beforeEach);
suite.afterEach.addBlock(this.afterEach);
suite.includedTags.addAll(this.includedTags);
suite.excludedTags.addAll(this.excludedTags);
addChild(suite);

return suite;
Expand Down Expand Up @@ -158,6 +164,47 @@ public boolean isIgnored() {
return this.ignored;
}

/**
* Do the inclusion tags allow any of the given tags.
* @param tags to check
* @return true if the inclusion tags are empty (meaning all) or contain
* at least one of the tags provided
*/
boolean allowsAny(String[] tags) {
return includedTags.isEmpty()
|| Arrays.stream(tags).filter(includedTags::contains).findFirst().isPresent();
}

/**
* Does the suite specifically exclude any of these tags.
* @param tags to check
* @return true if the tags provided have anything in common with the excluded tags.
*/
boolean excludesAny(String[] tags) {
return Arrays.stream(tags).filter(excludedTags::contains).findFirst().isPresent();
}

/**
* Apply this list as the inclusion list in place of what's there.
* @param tags to apply
*/
void includeTags(String[] tags) {
replaceSet(includedTags, tags);
}

/**
* Apply this list as the exclusion list in place of what's there.
* @param tags to apply
*/
void excludeTags(String[] tags) {
replaceSet(excludedTags, tags);
}

private static void replaceSet(Set<String> set, String[] newContent) {
set.clear();
Arrays.stream(newContent).forEach(set::add);
}

@Override
public void run(final RunNotifier notifier) {
if (testCount() == 0) {
Expand Down
88 changes: 88 additions & 0 deletions src/main/java/com/greghaskins/spectrum/TaggingBlock.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
package com.greghaskins.spectrum;

import static com.greghaskins.spectrum.Spectrum.excludeTags;
import static com.greghaskins.spectrum.Spectrum.includeTags;
import static com.greghaskins.spectrum.SpectrumOptions.EXCLUDE_TAGS_PROPERTY;
import static com.greghaskins.spectrum.SpectrumOptions.INCLUDE_TAGS_PROPERTY;
import static com.greghaskins.spectrum.SpectrumOptions.TAGS_SEPARATOR;

import org.junit.runners.model.TestClass;

import java.util.Arrays;
import java.util.Optional;

/**
* Decorator that applies tagging statements before calling decoratee.
* Reads the tggging from the System properties and then the options.
*/
final class TaggingBlock implements Block {
private final SpectrumOptions options;
private final Block decoratee;

TaggingBlock(final Class<?> klazz, final Block decoratee) {
this.options = new TestClass(klazz).getAnnotation(SpectrumOptions.class);
this.decoratee = decoratee;
}

@Override
public void run() throws Throwable {
final String[] systemIncludes = readSystemPropertyIncludes();
final String[] systemExcludes = readSystemPropertyExcludes();

final String[] annotationIncludes = readAnnotationIncludes();
final String[] annotationExcludes = readAnnotationExcludes();

// the annotation can provide nothing and so we drop back to
// the system properties - this is done separately
// for includes and excludes
includeTags(firstNonBlank(annotationIncludes, systemIncludes));
excludeTags(firstNonBlank(annotationExcludes, systemExcludes));

// pass control to the decoratee
decoratee.run();
}

private String[] firstNonBlank(final String[]... arrays) {
return Arrays.stream(arrays)
.filter(array -> array != null)
.filter(array -> array.length > 0)
.findFirst()
.orElse(new String[] {});
}

private String[] fromSystemProperty(final String property) {
return Optional.ofNullable(System.getProperty(property))
.map(string -> string.split(TAGS_SEPARATOR))
.filter(TaggingBlock::notArrayWithEmptyValue)
.orElse(null);
}

private static boolean notArrayWithEmptyValue(final String[] array) {
return !(array.length == 1 && array[0].isEmpty());
}

private String[] readSystemPropertyIncludes() {
return fromSystemProperty(Optional.ofNullable(options)
.map(SpectrumOptions::includeTagsSystemProperty)
.orElse(INCLUDE_TAGS_PROPERTY));
}

private String[] readSystemPropertyExcludes() {
return fromSystemProperty(Optional.ofNullable(options)
.map(SpectrumOptions::excludeTagsSystemProperty)
.orElse(EXCLUDE_TAGS_PROPERTY));
}

private String[] readAnnotationIncludes() {
return Optional.ofNullable(options)
.map(SpectrumOptions::includeTags)
.orElse(null);
}

private String[] readAnnotationExcludes() {
return Optional.ofNullable(options)
.map(SpectrumOptions::excludeTags)
.orElse(null);
}

}
Loading

0 comments on commit f5d2af8

Please sign in to comment.