Skip to content

Commit

Permalink
@AnalyzeClasses allows to specify a CacheMode now, where CacheMode.PE…
Browse files Browse the repository at this point in the history
…R_CLASS completely deactivates the cache by locations.

Resolves: #45
Signed-off-by: Peter Gafert <peter.gafert@tngtech.com>
  • Loading branch information
codecholeric committed May 16, 2018
1 parent 9267c4a commit 6b4a409
Show file tree
Hide file tree
Showing 6 changed files with 124 additions and 10 deletions.
Expand Up @@ -62,4 +62,11 @@
* @return The types of {@link ImportOption} to use for the import
*/
Class<? extends ImportOption>[] importOptions() default {};

/**
* Controls when the {@link ArchUnitRunner} clears the {@link ClassCache}.
*
* @return The {@link CacheMode} to use for this test class.
*/
CacheMode cacheMode() default CacheMode.FOREVER;
}
Expand Up @@ -51,10 +51,8 @@
* public static final ArchRule some_rule = //...
* }
* </code></pre>
* Important information about the caching behavior: The cache uses soft references, meaning that a small heap
* may dramatically reduce performance, if multiple classes running with {@link ArchUnitRunner} are executed.
* The cache will hold imported classes as long as there is sufficient memory, and reuse them, if the same
* locations (i.e. URLs) are imported.
*
* The runner will cache classes between test runs, for details please refer to {@link ClassCache}.
*/
@PublicAPI(usage = ACCESS)
public class ArchUnitRunner extends ParentRunner<ArchTestExecution> {
Expand Down
@@ -0,0 +1,51 @@
/*
* Copyright 2018 TNG Technology Consulting GmbH
*
* 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 com.tngtech.archunit.junit;

import java.lang.ref.SoftReference;

import com.tngtech.archunit.PublicAPI;
import com.tngtech.archunit.core.domain.JavaClasses;

import static com.tngtech.archunit.PublicAPI.Usage.ACCESS;

/**
* Determines how the JUnit test support caches classes.<br/>
* The test support can cache imported classes according to their location between several runs
* of different test classes, i.e. if <code>ATest</code> analyses <code>file:///some/path</code> and
* <code>BTest</code> analyses the same classes, the classes imported for <code>ATest</code>
* will be reused for <code>BTest</code>. If this is not desired, the {@link CacheMode}.{@link #PER_CLASS}
* can be used to completely deactivate caching between different test classes.
*
* @see ClassCache
*/
public enum CacheMode {
/**
* Signals that the {@link ClassCache} should cache test classes by location, i.e. between runs of different test classes.
*/
@PublicAPI(usage = ACCESS)
PER_CLASS,

/**
* Signals that the {@link ClassCache} should cache {@link JavaClasses} by location
* (i.e. the combination of URLs used to import these classes).
* The cache uses {@link SoftReference SoftReferences}, i.e. the heap will be
* freed, once it is needed, but this might cause a noticeable delay, once the garbage collector starts
* removing all those references at the last possible moment.
*/
@PublicAPI(usage = ACCESS)
FOREVER
}
Expand Up @@ -35,13 +35,29 @@
import com.tngtech.archunit.core.importer.Location;
import com.tngtech.archunit.core.importer.Locations;

import static com.tngtech.archunit.junit.CacheMode.FOREVER;

/**
* The {@link ClassCache} takes care of caching {@link JavaClasses} between test runs. On the one hand,
* it caches {@link JavaClasses} between different {@link ArchTest @ArchTest} evaluations,
* on the other hand, it caches {@link JavaClasses} between different test classes,
* i.e. if two test classes <code>ATest</code> and <code>BTest</code>
* import the same locations (e.g. packages, URLs, etc.), the imported {@link JavaClasses} from <code>ATest</code> will be
* reused for <code>BTest</code>. This behavior can be controlled by the supplied {@link CacheMode}.
* <br/><br/>
* Important information regarding performance: The cache uses soft references, meaning that a small heap
* may dramatically reduce performance, if multiple test classes are executed.
* The cache will hold imported classes as long as there is sufficient memory, and reuse them, if the same
* locations (i.e. URLs) are imported.
*/
class ClassCache {
@VisibleForTesting
final Map<Class<?>, JavaClasses> cachedByTest = new ConcurrentHashMap<>();
private final LoadingCache<LocationsKey, LazyJavaClasses> cachedByLocations =
@VisibleForTesting
final LoadingCache<LocationsKey, LazyJavaClasses> cachedByLocations =
CacheBuilder.newBuilder().softValues().build(new CacheLoader<LocationsKey, LazyJavaClasses>() {
@Override
public LazyJavaClasses load(LocationsKey key) throws Exception {
public LazyJavaClasses load(LocationsKey key) {
return new LazyJavaClasses(key.locations, key.importOptionTypes);
}
});
Expand All @@ -56,11 +72,19 @@ JavaClasses getClassesToAnalyzeFor(Class<?> testClass) {
}

LocationsKey locations = locationsToImport(testClass);
JavaClasses classes = cachedByLocations.getUnchecked(locations).get();

JavaClasses classes = getCacheMode(testClass) == FOREVER
? cachedByLocations.getUnchecked(locations).get()
: new LazyJavaClasses(locations.locations, locations.importOptionTypes).get();

cachedByTest.put(testClass, classes);
return classes;
}

private CacheMode getCacheMode(Class<?> testClass) {
return testClass.getAnnotation(AnalyzeClasses.class).cacheMode();
}

private LocationsKey locationsToImport(Class<?> testClass) {
AnalyzeClasses analyzeClasses = testClass.getAnnotation(AnalyzeClasses.class);
Set<Location> declaredLocations = ImmutableSet.<Location>builder()
Expand Down
Expand Up @@ -24,6 +24,7 @@
import org.mockito.junit.MockitoJUnit;
import org.mockito.junit.MockitoRule;

import static com.tngtech.archunit.junit.CacheMode.PER_CLASS;
import static com.tngtech.archunit.testutil.Assertions.assertThatClasses;
import static com.tngtech.java.junit.dataprovider.DataProviders.$;
import static com.tngtech.java.junit.dataprovider.DataProviders.$$;
Expand Down Expand Up @@ -61,7 +62,6 @@ public void loads_classes() {
}

@Test
@SuppressWarnings("unchecked")
public void reuses_loaded_classes_by_test() {
cache.getClassesToAnalyzeFor(TestClass.class);
cache.getClassesToAnalyzeFor(TestClass.class);
Expand All @@ -70,14 +70,22 @@ public void reuses_loaded_classes_by_test() {
}

@Test
@SuppressWarnings("unchecked")
public void reuses_loaded_classes_by_urls() {
public void reuses_loaded_classes_by_locations_if_cacheMode_is_FOREVER() {
cache.getClassesToAnalyzeFor(TestClass.class);
cache.getClassesToAnalyzeFor(EquivalentTestClass.class);

verifyNumberOfImports(1);
}

@Test
public void doesnt_reuse_loaded_classes_by_locations_if_cacheMode_is_PER_CLASS() {
cache.getClassesToAnalyzeFor(TestClassWithCacheModePerClass.class);
assertThat(cache.cachedByLocations.asMap()).as("Classes cached by location").isEmpty();

cache.getClassesToAnalyzeFor(EquivalentTestClass.class);
verifyNumberOfImports(2);
}

@Test
public void rejects_missing_analyze_annotation() {
thrown.expect(IllegalArgumentException.class);
Expand Down Expand Up @@ -194,6 +202,10 @@ public static class TestClass {
public static class EquivalentTestClass {
}

@AnalyzeClasses(packages = "com.tngtech.archunit.junit", cacheMode = PER_CLASS)
public static class TestClassWithCacheModePerClass {
}

@AnalyzeClasses(packagesOf = Rule.class)
public static class TestClassWithFilterJustByPackageOfClass {
}
Expand Down
22 changes: 22 additions & 0 deletions docs/userguide/009_JUnit_Support.adoc
Expand Up @@ -67,6 +67,7 @@ so they can be reused be several rules declared within the same class. On the ot
will cache the classes by location, so a second test, that wants to import classes from the same
URLs will reuse the classes previously imported as well. Note that this second caching uses
soft references, so the classes will be dropped from memory, if the heap runs low.
For further information see <<Controlling the Cache>>.

==== Controlling the Import

Expand Down Expand Up @@ -114,6 +115,27 @@ production code, and only consider code that is directly supplied and does not c
As explained in <<The Core API>>, you can write your own custom implementation of `ImportOption`
and then supply the type to `@AnalyzeClasses`.

==== Controlling the Cache

By default the `ArchUnitRunner` will cache all classes by location. This means that between different
test class runs imported Java classes will be reused, if the exact combination of locations has already
been imported.

If the heap runs low, and thus the garbage collector has to do a big sweep in one run,
this can cause a noticeable delay. On the other hand, if it is known, that no other test class will
reuse the imported Java classes, it would make sense to deactivate this cache.

This can be achieved by configuring `CacheMode.PER_CLASS`, e.g.

[source,java,options="nowrap"]
----
@AnalyzeClasses(packages = "com.myapp.special", cacheMode = CacheMode.PER_CLASS)
----

The Java classes imported during this test run will not be cached by location and just be reused within
the same test class. After all tests of this class have been run,
the imported Java classes will simply be dropped.

==== Ignoring Tests

The runner will ignore tests annotated with `@ArchIgnore`, for example:
Expand Down

0 comments on commit 6b4a409

Please sign in to comment.