From 6b4a409927eb9419d4d203b7fb44a74203bf1195 Mon Sep 17 00:00:00 2001 From: Peter Gafert Date: Wed, 16 May 2018 19:47:48 +0200 Subject: [PATCH] @AnalyzeClasses allows to specify a CacheMode now, where CacheMode.PER_CLASS completely deactivates the cache by locations. Resolves: #45 Signed-off-by: Peter Gafert --- .../archunit/junit/AnalyzeClasses.java | 7 +++ .../archunit/junit/ArchUnitRunner.java | 6 +-- .../com/tngtech/archunit/junit/CacheMode.java | 51 +++++++++++++++++++ .../tngtech/archunit/junit/ClassCache.java | 30 +++++++++-- .../archunit/junit/ClassCacheTest.java | 18 +++++-- docs/userguide/009_JUnit_Support.adoc | 22 ++++++++ 6 files changed, 124 insertions(+), 10 deletions(-) create mode 100644 archunit-junit/src/main/java/com/tngtech/archunit/junit/CacheMode.java diff --git a/archunit-junit/src/main/java/com/tngtech/archunit/junit/AnalyzeClasses.java b/archunit-junit/src/main/java/com/tngtech/archunit/junit/AnalyzeClasses.java index 34ceaffa8d..b918924781 100644 --- a/archunit-junit/src/main/java/com/tngtech/archunit/junit/AnalyzeClasses.java +++ b/archunit-junit/src/main/java/com/tngtech/archunit/junit/AnalyzeClasses.java @@ -62,4 +62,11 @@ * @return The types of {@link ImportOption} to use for the import */ Class[] 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; } diff --git a/archunit-junit/src/main/java/com/tngtech/archunit/junit/ArchUnitRunner.java b/archunit-junit/src/main/java/com/tngtech/archunit/junit/ArchUnitRunner.java index 034d78e761..06d1d9157b 100644 --- a/archunit-junit/src/main/java/com/tngtech/archunit/junit/ArchUnitRunner.java +++ b/archunit-junit/src/main/java/com/tngtech/archunit/junit/ArchUnitRunner.java @@ -51,10 +51,8 @@ * public static final ArchRule some_rule = //... * } * - * 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 { diff --git a/archunit-junit/src/main/java/com/tngtech/archunit/junit/CacheMode.java b/archunit-junit/src/main/java/com/tngtech/archunit/junit/CacheMode.java new file mode 100644 index 0000000000..5226f30cf9 --- /dev/null +++ b/archunit-junit/src/main/java/com/tngtech/archunit/junit/CacheMode.java @@ -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.
+ * The test support can cache imported classes according to their location between several runs + * of different test classes, i.e. if ATest analyses file:///some/path and + * BTest analyses the same classes, the classes imported for ATest + * will be reused for BTest. 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 +} diff --git a/archunit-junit/src/main/java/com/tngtech/archunit/junit/ClassCache.java b/archunit-junit/src/main/java/com/tngtech/archunit/junit/ClassCache.java index 0db41e5c6e..998f65b7bb 100644 --- a/archunit-junit/src/main/java/com/tngtech/archunit/junit/ClassCache.java +++ b/archunit-junit/src/main/java/com/tngtech/archunit/junit/ClassCache.java @@ -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 ATest and BTest + * import the same locations (e.g. packages, URLs, etc.), the imported {@link JavaClasses} from ATest will be + * reused for BTest. This behavior can be controlled by the supplied {@link CacheMode}. + *

+ * 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, JavaClasses> cachedByTest = new ConcurrentHashMap<>(); - private final LoadingCache cachedByLocations = + @VisibleForTesting + final LoadingCache cachedByLocations = CacheBuilder.newBuilder().softValues().build(new CacheLoader() { @Override - public LazyJavaClasses load(LocationsKey key) throws Exception { + public LazyJavaClasses load(LocationsKey key) { return new LazyJavaClasses(key.locations, key.importOptionTypes); } }); @@ -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 declaredLocations = ImmutableSet.builder() diff --git a/archunit-junit/src/test/java/com/tngtech/archunit/junit/ClassCacheTest.java b/archunit-junit/src/test/java/com/tngtech/archunit/junit/ClassCacheTest.java index 9a64b75715..f3c07c02a1 100644 --- a/archunit-junit/src/test/java/com/tngtech/archunit/junit/ClassCacheTest.java +++ b/archunit-junit/src/test/java/com/tngtech/archunit/junit/ClassCacheTest.java @@ -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.$$; @@ -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); @@ -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); @@ -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 { } diff --git a/docs/userguide/009_JUnit_Support.adoc b/docs/userguide/009_JUnit_Support.adoc index eb0a9ac2c4..877afe95ae 100644 --- a/docs/userguide/009_JUnit_Support.adoc +++ b/docs/userguide/009_JUnit_Support.adoc @@ -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 Import @@ -114,6 +115,27 @@ production code, and only consider code that is directly supplied and does not c As explained in <>, 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: