Skip to content

Commit 7c1a4df

Browse files
authored
Verify that the rule input is not empty #774
The input for a rule should normally not be empty, since then the test will always be green because it doesn't test anything. With this change, it is now possible to verify that the input of a rule is not empty. If the input is empty, an `AssertionError` is thrown. The verification if the input is empty can also be controlled via the configuration. By setting the value `archRule.verifyInputNotEmpty` to `true` or `false`, we can turn on or off the verification. The default value is `true` which means this is a breaking change. However, with the configuration, the old behavior can easily be restored. Resolve #178
2 parents 502ecd5 + fb2ba27 commit 7c1a4df

File tree

6 files changed

+123
-8
lines changed

6 files changed

+123
-8
lines changed

.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,4 @@ _site
1313
.jekyll-cache
1414
.asciidoctor
1515
verification-results
16+
.java-version

archunit/src/main/java/com/tngtech/archunit/lang/ArchRule.java

+22
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323

2424
import com.google.common.base.Predicate;
2525
import com.google.common.collect.ImmutableSet;
26+
import com.tngtech.archunit.ArchConfiguration;
2627
import com.tngtech.archunit.Internal;
2728
import com.tngtech.archunit.PublicAPI;
2829
import com.tngtech.archunit.base.Optional;
@@ -36,9 +37,11 @@
3637
import com.tngtech.archunit.lang.syntax.elements.ClassesThat;
3738
import com.tngtech.archunit.lang.syntax.elements.GivenClasses;
3839

40+
import static com.google.common.collect.Iterables.isEmpty;
3941
import static com.google.common.io.Resources.readLines;
4042
import static com.tngtech.archunit.PublicAPI.Usage.ACCESS;
4143
import static com.tngtech.archunit.base.ClassLoaders.getCurrentClassLoader;
44+
import static java.lang.Boolean.TRUE;
4245
import static java.nio.charset.StandardCharsets.UTF_8;
4346

4447
/**
@@ -175,6 +178,8 @@ static String createBecauseDescription(ArchRule rule, String reason) {
175178
}
176179

177180
private static class SimpleArchRule<T> implements ArchRule {
181+
private static final String FAIL_ON_EMPTY_SHOULD_PROPERTY_NAME = "archRule.failOnEmptyShould";
182+
178183
private final Priority priority;
179184
private final ClassesTransformer<T> classesTransformer;
180185
private final ArchCondition<T> condition;
@@ -206,6 +211,8 @@ public ArchRule because(String reason) {
206211
@Override
207212
public EvaluationResult evaluate(JavaClasses classes) {
208213
Iterable<T> allObjects = classesTransformer.transform(classes);
214+
verifyNoEmptyShouldIfEnabled(allObjects);
215+
209216
condition.init(allObjects);
210217
ConditionEvents events = new ConditionEvents();
211218
for (T object : allObjects) {
@@ -215,6 +222,21 @@ public EvaluationResult evaluate(JavaClasses classes) {
215222
return new EvaluationResult(this, events, priority);
216223
}
217224

225+
private void verifyNoEmptyShouldIfEnabled(Iterable<T> allObjects) {
226+
if (isEmpty(allObjects) && isFailOnEmptyShouldEnabled()) {
227+
throw new AssertionError("Rule failed to check any classes. " +
228+
"This means either that no classes have been passed to the rule at all, " +
229+
"or that no classes passed to the rule matched the `that()` clause. " +
230+
"To allow rules being evaluated without checking any classes you can set the ArchUnit property " +
231+
FAIL_ON_EMPTY_SHOULD_PROPERTY_NAME + " = " + false);
232+
}
233+
}
234+
235+
private boolean isFailOnEmptyShouldEnabled() {
236+
return ArchConfiguration.get().getPropertyOrDefault(FAIL_ON_EMPTY_SHOULD_PROPERTY_NAME, TRUE.toString())
237+
.equals(TRUE.toString());
238+
}
239+
218240
@Override
219241
public String getDescription() {
220242
return overriddenDescription.isPresent() ?

archunit/src/test/java/com/tngtech/archunit/lang/ArchRuleTest.java

+54-8
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,23 @@
11
package com.tngtech.archunit.lang;
22

3+
import java.io.File;
4+
import java.io.IOException;
5+
import java.util.Iterator;
6+
import java.util.List;
7+
import java.util.SortedSet;
8+
import java.util.TreeSet;
9+
310
import com.google.common.base.Joiner;
411
import com.google.common.base.Splitter;
512
import com.google.common.io.Files;
13+
import com.tngtech.archunit.ArchConfiguration;
614
import com.tngtech.archunit.core.domain.JavaClass;
715
import com.tngtech.archunit.core.domain.JavaClasses;
816
import com.tngtech.archunit.core.domain.JavaClassesTest;
17+
import com.tngtech.archunit.core.importer.testexamples.SomeClass;
918
import com.tngtech.archunit.lang.ArchConditionTest.ConditionWithInitAndFinish;
1019
import com.tngtech.archunit.lang.syntax.ArchRuleDefinition;
20+
import com.tngtech.archunit.testutil.ArchConfigurationRule;
1121
import org.hamcrest.Description;
1222
import org.hamcrest.TypeSafeMatcher;
1323
import org.junit.After;
@@ -16,13 +26,6 @@
1626
import org.junit.Test;
1727
import org.junit.rules.ExpectedException;
1828

19-
import java.io.File;
20-
import java.io.IOException;
21-
import java.util.Iterator;
22-
import java.util.List;
23-
import java.util.SortedSet;
24-
import java.util.TreeSet;
25-
2629
import static com.google.common.collect.Lists.newArrayList;
2730
import static com.tngtech.archunit.core.domain.TestUtils.importClasses;
2831
import static com.tngtech.archunit.core.domain.TestUtils.importClassesWithContext;
@@ -31,13 +34,19 @@
3134
import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.all;
3235
import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.classes;
3336
import static com.tngtech.archunit.testutil.TestUtils.toUri;
37+
import static java.lang.Boolean.FALSE;
3438
import static java.nio.charset.StandardCharsets.UTF_8;
3539
import static org.assertj.core.api.Assertions.assertThat;
3640

3741
public class ArchRuleTest {
42+
private static final String FAIL_ON_EMPTY_SHOULD_PROPERTY_NAME = "archRule.failOnEmptyShould";
43+
3844
@Rule
3945
public final ExpectedException thrown = ExpectedException.none();
4046

47+
@Rule
48+
public final ArchConfigurationRule archConfigurationRule = new ArchConfigurationRule();
49+
4150
@Before
4251
public void setUp() {
4352
ignoreFile().delete();
@@ -159,6 +168,35 @@ public void finish(ConditionEvents events) {
159168
assertThat(condition.eventsFromFinish.getViolating()).hasSize(1);
160169
}
161170

171+
@Test
172+
public void evaluation_fails_because_of_empty_set_of_classes_with_default_fail_on_empty_should() {
173+
archConfigurationRule.removeProperty(FAIL_ON_EMPTY_SHOULD_PROPERTY_NAME);
174+
175+
thrown.expect(AssertionError.class);
176+
thrown.expectMessage("Rule failed to check any classes");
177+
thrown.expectMessage(FAIL_ON_EMPTY_SHOULD_PROPERTY_NAME);
178+
179+
classes().should(ALWAYS_BE_VALID).evaluate(importClasses());
180+
}
181+
182+
@Test
183+
public void evaluation_fails_because_of_empty_set_of_classes_after_filter_with_default_fail_on_empty_should() {
184+
archConfigurationRule.removeProperty(FAIL_ON_EMPTY_SHOULD_PROPERTY_NAME);
185+
186+
thrown.expect(AssertionError.class);
187+
thrown.expectMessage("Rule failed to check any classes");
188+
thrown.expectMessage(FAIL_ON_EMPTY_SHOULD_PROPERTY_NAME);
189+
190+
classes().that().doNotHaveSimpleName(SomeClass.class.getSimpleName()).should(ALWAYS_BE_VALID).evaluate(importClasses(SomeClass.class));
191+
}
192+
193+
@Test
194+
public void evaluation_passes_on_empty_set_of_classes_with_deactivated_fail_on_empty_should() {
195+
ArchConfiguration.get().setProperty(FAIL_ON_EMPTY_SHOULD_PROPERTY_NAME, FALSE.toString());
196+
197+
classes().should(ALWAYS_BE_VALID).evaluate(importClasses());
198+
}
199+
162200
private ClassesTransformer<String> strings() {
163201
return new AbstractClassesTransformer<String>("strings") {
164202
@Override
@@ -250,10 +288,18 @@ public void check(JavaClass item, ConditionEvents events) {
250288
}
251289
};
252290

291+
private static final ArchCondition<JavaClass> ALWAYS_BE_VALID =
292+
new ArchCondition<JavaClass>("always be valid") {
293+
@Override
294+
public void check(JavaClass item, ConditionEvents events) {
295+
}
296+
};
297+
298+
@SuppressWarnings({"unused", "ResultOfMethodCallIgnored"})
253299
private static class ClassAccessingStringTwoTimes {
254300
void execute() {
255301
"foo".length();
256302
"bar".replaceAll("a", "b");
257303
}
258304
}
259-
}
305+
}

archunit/src/test/java/com/tngtech/archunit/testutil/ArchConfigurationRule.java

+19
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,15 @@
11
package com.tngtech.archunit.testutil;
22

3+
import java.lang.reflect.AccessibleObject;
4+
import java.lang.reflect.InvocationTargetException;
35
import java.util.concurrent.Callable;
46

57
import com.tngtech.archunit.ArchConfiguration;
68
import org.junit.rules.ExternalResource;
79

10+
import static com.tngtech.archunit.testutil.ReflectionTestUtils.field;
11+
import static com.tngtech.archunit.testutil.ReflectionTestUtils.method;
12+
813
public class ArchConfigurationRule extends ExternalResource {
914
private boolean resolveMissingDependenciesFromClassPath = ArchConfiguration.get().resolveMissingDependenciesFromClassPath();
1015

@@ -24,6 +29,20 @@ public ArchConfigurationRule resolveAdditionalDependenciesFromClassPath(boolean
2429
return this;
2530
}
2631

32+
public void removeProperty(String propertyName) {
33+
try {
34+
Object properties = accessible(field(ArchConfiguration.class, "properties")).get(ArchConfiguration.get());
35+
accessible(method(properties.getClass(), "remove", String.class)).invoke(properties, propertyName);
36+
} catch (IllegalAccessException | InvocationTargetException e) {
37+
throw new RuntimeException(e);
38+
}
39+
}
40+
41+
private <T extends AccessibleObject> T accessible(T member) {
42+
member.setAccessible(true);
43+
return member;
44+
}
45+
2746
@Override
2847
protected void before() {
2948
ArchConfiguration.get().reset();
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
enableMd5InClassSources=true
2+
archRule.failOnEmptyShould=false

docs/userguide/010_Advanced_Configuration.adoc

+26
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,32 @@ If this feature is enabled, the MD5 sum can be queried as
9898
javaClass.getSource().get().getMd5sum()
9999
----
100100

101+
=== Fail Rules on Empty Should
102+
103+
By default, ArchUnit will forbid the should-part of rules to be evaluated against an empty set of classes.
104+
The reason is that this can lead to rules that by accident do not check any classes at all.
105+
Take for example
106+
107+
[source,java,options="nowrap"]
108+
----
109+
classes().that().resideInAPackage("com.myapp.old").should()...
110+
----
111+
112+
Now consider somebody renames the package `old` to `newer`.
113+
The rule will now always evaluate successfully without any reported error.
114+
However, it actually does not check any classes at all anymore.
115+
This is likely not what most users want.
116+
Thus, by default ArchUnit will fail checking the rule in this case.
117+
If you want to allow evaluating such rules,
118+
i.e. where the actual input to the should-conditionis empty,
119+
you can set the following property:
120+
121+
[source,options="nowrap"]
122+
.archunit.properties
123+
----
124+
archRule.failOnEmptyShould=false
125+
----
126+
101127
=== Custom Error Messages
102128

103129
You can configure a custom format to display the failures of a rule.

0 commit comments

Comments
 (0)