From 13777cf8a899510449d9cdb27dfad3247a4c2b68 Mon Sep 17 00:00:00 2001 From: Peter Gafert Date: Wed, 10 Apr 2024 00:57:59 +0200 Subject: [PATCH 1/3] fix Jekyll Gemfile Right now the ArchUnit website can't be started locally anymore, because Jekyll throws an error ``` Conversion error: Jekyll::Converters::Scss encountered an error while converting 'assets/css/main.scss': Broken pipe ``` From what I found online Jekyll 4 has become stricter in some way. I don't exactly understand why, because the scss file still seems to conform to the docs, but since I also found that GitHub pages uses Jekyll 3.9.5 at this time anyway, I figured we may as well just downgrade to that version, which then also fixes our problem. I don't remember the original reason why we weren't using the github-pages Gem, but from what I see right now it seems to work exactly as specified in the docs. It also fixes Jekyll to the correct version. So, I reverted the Gemfile to use the github-pages Gem instead of the jekyll Gem. Signed-off-by: Peter Gafert --- docs/Gemfile | 7 +- docs/Gemfile.lock | 263 ++++++++++++++++++++++++++++++++++++---------- 2 files changed, 208 insertions(+), 62 deletions(-) diff --git a/docs/Gemfile b/docs/Gemfile index 341a79d73f..574d70a047 100644 --- a/docs/Gemfile +++ b/docs/Gemfile @@ -1,10 +1,9 @@ source "https://rubygems.org" -# commented the line below to get rid of github authentication warnings -# gem "github-pages", group: :jekyll_plugins +gem "github-pages", "~> 231", group: :jekyll_plugins -# added the following line, same reason as above -gem "jekyll", group: :jekyll_plugins +# meanwhile this dependency also seem to be necessary to run Jekyll with current Ruby +gem "webrick" group :jekyll_plugins do gem "jekyll-paginate" diff --git a/docs/Gemfile.lock b/docs/Gemfile.lock index 503b04d659..55628c13e8 100644 --- a/docs/Gemfile.lock +++ b/docs/Gemfile.lock @@ -1,71 +1,204 @@ GEM remote: https://rubygems.org/ specs: - activesupport (7.0.7.2) + activesupport (7.1.3.2) + base64 + bigdecimal concurrent-ruby (~> 1.0, >= 1.0.2) + connection_pool (>= 2.2.5) + drb i18n (>= 1.6, < 2) minitest (>= 5.1) + mutex_m tzinfo (~> 2.0) addressable (2.8.6) public_suffix (>= 2.0.2, < 6.0) + base64 (0.2.0) + bigdecimal (3.1.7) + coffee-script (2.4.1) + coffee-script-source + execjs + coffee-script-source (1.12.2) colorator (1.1.0) - concurrent-ruby (1.2.2) + commonmarker (0.23.10) + concurrent-ruby (1.2.3) + connection_pool (2.4.1) + dnsruby (1.72.0) + simpleidn (~> 0.2.1) + drb (2.2.1) em-websocket (0.5.3) eventmachine (>= 0.12.9) http_parser.rb (~> 0) + ethon (0.16.0) + ffi (>= 1.15.0) eventmachine (1.2.7) - faraday (1.5.1) - faraday-em_http (~> 1.0) - faraday-em_synchrony (~> 1.0) - faraday-excon (~> 1.1) - faraday-httpclient (~> 1.0.1) - faraday-net_http (~> 1.0) - faraday-net_http_persistent (~> 1.1) - faraday-patron (~> 1.0) - multipart-post (>= 1.2, < 3) - ruby2_keywords (>= 0.0.4) - faraday-em_http (1.0.0) - faraday-em_synchrony (1.0.0) - faraday-excon (1.1.0) - faraday-httpclient (1.0.1) - faraday-net_http (1.0.1) - faraday-net_http_persistent (1.2.0) - faraday-patron (1.0.0) + execjs (2.9.1) + faraday (2.9.0) + faraday-net_http (>= 2.0, < 3.2) + faraday-net_http (3.1.0) + net-http ffi (1.16.3) forwardable-extended (2.6.0) - gemoji (4.0.0) - google-protobuf (3.25.1) + gemoji (4.1.0) + github-pages (231) + github-pages-health-check (= 1.18.2) + jekyll (= 3.9.5) + jekyll-avatar (= 0.8.0) + jekyll-coffeescript (= 1.2.2) + jekyll-commonmark-ghpages (= 0.4.0) + jekyll-default-layout (= 0.1.5) + jekyll-feed (= 0.17.0) + jekyll-gist (= 1.5.0) + jekyll-github-metadata (= 2.16.1) + jekyll-include-cache (= 0.2.1) + jekyll-mentions (= 1.6.0) + jekyll-optional-front-matter (= 0.3.2) + jekyll-paginate (= 1.1.0) + jekyll-readme-index (= 0.3.0) + jekyll-redirect-from (= 0.16.0) + jekyll-relative-links (= 0.6.1) + jekyll-remote-theme (= 0.4.3) + jekyll-sass-converter (= 1.5.2) + jekyll-seo-tag (= 2.8.0) + jekyll-sitemap (= 1.4.0) + jekyll-swiss (= 1.0.0) + jekyll-theme-architect (= 0.2.0) + jekyll-theme-cayman (= 0.2.0) + jekyll-theme-dinky (= 0.2.0) + jekyll-theme-hacker (= 0.2.0) + jekyll-theme-leap-day (= 0.2.0) + jekyll-theme-merlot (= 0.2.0) + jekyll-theme-midnight (= 0.2.0) + jekyll-theme-minimal (= 0.2.0) + jekyll-theme-modernist (= 0.2.0) + jekyll-theme-primer (= 0.6.0) + jekyll-theme-slate (= 0.2.0) + jekyll-theme-tactile (= 0.2.0) + jekyll-theme-time-machine (= 0.2.0) + jekyll-titles-from-headings (= 0.5.3) + jemoji (= 0.13.0) + kramdown (= 2.4.0) + kramdown-parser-gfm (= 1.1.0) + liquid (= 4.0.4) + mercenary (~> 0.3) + minima (= 2.5.1) + nokogiri (>= 1.13.6, < 2.0) + rouge (= 3.30.0) + terminal-table (~> 1.4) + github-pages-health-check (1.18.2) + addressable (~> 2.3) + dnsruby (~> 1.60) + octokit (>= 4, < 8) + public_suffix (>= 3.0, < 6.0) + typhoeus (~> 1.3) html-pipeline (2.14.3) activesupport (>= 2) nokogiri (>= 1.4) http_parser.rb (0.8.0) - i18n (1.14.1) + i18n (1.14.4) concurrent-ruby (~> 1.0) - jekyll (4.3.3) + jekyll (3.9.5) addressable (~> 2.4) colorator (~> 1.0) em-websocket (~> 0.5) - i18n (~> 1.0) - jekyll-sass-converter (>= 2.0, < 4.0) + i18n (>= 0.7, < 2) + jekyll-sass-converter (~> 1.0) jekyll-watch (~> 2.0) - kramdown (~> 2.3, >= 2.3.1) - kramdown-parser-gfm (~> 1.0) + kramdown (>= 1.17, < 3) liquid (~> 4.0) - mercenary (>= 0.3.6, < 0.5) + mercenary (~> 0.3.3) pathutil (~> 0.9) - rouge (>= 3.0, < 5.0) + rouge (>= 1.7, < 4) safe_yaml (~> 1.0) - terminal-table (>= 1.8, < 4.0) - webrick (~> 1.7) + jekyll-avatar (0.8.0) + jekyll (>= 3.0, < 5.0) + jekyll-coffeescript (1.2.2) + coffee-script (~> 2.2) + coffee-script-source (~> 1.12) + jekyll-commonmark (1.4.0) + commonmarker (~> 0.22) + jekyll-commonmark-ghpages (0.4.0) + commonmarker (~> 0.23.7) + jekyll (~> 3.9.0) + jekyll-commonmark (~> 1.4.0) + rouge (>= 2.0, < 5.0) + jekyll-default-layout (0.1.5) + jekyll (>= 3.0, < 5.0) jekyll-feed (0.17.0) jekyll (>= 3.7, < 5.0) jekyll-gist (1.5.0) octokit (~> 4.2) + jekyll-github-metadata (2.16.1) + jekyll (>= 3.4, < 5.0) + octokit (>= 4, < 7, != 4.4.0) + jekyll-include-cache (0.2.1) + jekyll (>= 3.7, < 5.0) + jekyll-mentions (1.6.0) + html-pipeline (~> 2.3) + jekyll (>= 3.7, < 5.0) + jekyll-optional-front-matter (0.3.2) + jekyll (>= 3.0, < 5.0) jekyll-paginate (1.1.0) - jekyll-sass-converter (3.0.0) - sass-embedded (~> 1.54) + jekyll-readme-index (0.3.0) + jekyll (>= 3.0, < 5.0) + jekyll-redirect-from (0.16.0) + jekyll (>= 3.3, < 5.0) + jekyll-relative-links (0.6.1) + jekyll (>= 3.3, < 5.0) + jekyll-remote-theme (0.4.3) + addressable (~> 2.0) + jekyll (>= 3.5, < 5.0) + jekyll-sass-converter (>= 1.0, <= 3.0.0, != 2.0.0) + rubyzip (>= 1.3.0, < 3.0) + jekyll-sass-converter (1.5.2) + sass (~> 3.4) + jekyll-seo-tag (2.8.0) + jekyll (>= 3.8, < 5.0) jekyll-sitemap (1.4.0) jekyll (>= 3.7, < 5.0) + jekyll-swiss (1.0.0) + jekyll-theme-architect (0.2.0) + jekyll (> 3.5, < 5.0) + jekyll-seo-tag (~> 2.0) + jekyll-theme-cayman (0.2.0) + jekyll (> 3.5, < 5.0) + jekyll-seo-tag (~> 2.0) + jekyll-theme-dinky (0.2.0) + jekyll (> 3.5, < 5.0) + jekyll-seo-tag (~> 2.0) + jekyll-theme-hacker (0.2.0) + jekyll (> 3.5, < 5.0) + jekyll-seo-tag (~> 2.0) + jekyll-theme-leap-day (0.2.0) + jekyll (> 3.5, < 5.0) + jekyll-seo-tag (~> 2.0) + jekyll-theme-merlot (0.2.0) + jekyll (> 3.5, < 5.0) + jekyll-seo-tag (~> 2.0) + jekyll-theme-midnight (0.2.0) + jekyll (> 3.5, < 5.0) + jekyll-seo-tag (~> 2.0) + jekyll-theme-minimal (0.2.0) + jekyll (> 3.5, < 5.0) + jekyll-seo-tag (~> 2.0) + jekyll-theme-modernist (0.2.0) + jekyll (> 3.5, < 5.0) + jekyll-seo-tag (~> 2.0) + jekyll-theme-primer (0.6.0) + jekyll (> 3.5, < 5.0) + jekyll-github-metadata (~> 2.9) + jekyll-seo-tag (~> 2.0) + jekyll-theme-slate (0.2.0) + jekyll (> 3.5, < 5.0) + jekyll-seo-tag (~> 2.0) + jekyll-theme-tactile (0.2.0) + jekyll (> 3.5, < 5.0) + jekyll-seo-tag (~> 2.0) + jekyll-theme-time-machine (0.2.0) + jekyll (> 3.5, < 5.0) + jekyll-seo-tag (~> 2.0) + jekyll-titles-from-headings (0.5.3) + jekyll (>= 3.3, < 5.0) jekyll-watch (2.2.1) listen (~> 3.0) jemoji (0.13.0) @@ -77,54 +210,68 @@ GEM kramdown-parser-gfm (1.1.0) kramdown (~> 2.0) liquid (4.0.4) - listen (3.8.0) + listen (3.9.0) rb-fsevent (~> 0.10, >= 0.10.3) rb-inotify (~> 0.9, >= 0.9.10) - mercenary (0.4.0) - mini_portile2 (2.8.1) - minitest (5.19.0) - multipart-post (2.1.1) - nokogiri (1.14.3) - mini_portile2 (~> 2.8.0) + mercenary (0.3.6) + minima (2.5.1) + jekyll (>= 3.5, < 5.0) + jekyll-feed (~> 0.9) + jekyll-seo-tag (~> 2.1) + minitest (5.22.3) + mutex_m (0.2.0) + net-http (0.4.1) + uri + nokogiri (1.16.3-x86_64-linux) racc (~> 1.4) - octokit (4.21.0) - faraday (>= 0.9) - sawyer (~> 0.8.0, >= 0.5.3) + octokit (4.25.1) + faraday (>= 1, < 3) + sawyer (~> 0.9) pathutil (0.16.2) forwardable-extended (~> 2.6) - public_suffix (5.0.4) - racc (1.6.2) - rake (13.1.0) + public_suffix (5.0.5) + racc (1.7.3) rb-fsevent (0.11.2) rb-inotify (0.10.1) ffi (~> 1.0) rexml (3.2.6) - rouge (4.2.0) - ruby2_keywords (0.0.5) + rouge (3.30.0) + rubyzip (2.3.2) safe_yaml (1.0.5) - sass-embedded (1.69.5) - google-protobuf (~> 3.23) - rake (>= 13.0.0) - sawyer (0.8.2) + sass (3.7.4) + sass-listen (~> 4.0.0) + sass-listen (4.0.0) + rb-fsevent (~> 0.9, >= 0.9.4) + rb-inotify (~> 0.9, >= 0.9.7) + sawyer (0.9.2) addressable (>= 2.3.5) - faraday (> 0.8, < 2.0) - terminal-table (3.0.2) - unicode-display_width (>= 1.1.1, < 3) + faraday (>= 0.17.3, < 3) + simpleidn (0.2.1) + unf (~> 0.1.4) + terminal-table (1.8.0) + unicode-display_width (~> 1.1, >= 1.1.1) + typhoeus (1.4.1) + ethon (>= 0.9.0) tzinfo (2.0.6) concurrent-ruby (~> 1.0) - unicode-display_width (2.5.0) + unf (0.1.4) + unf_ext + unf_ext (0.0.9.1) + unicode-display_width (1.8.0) + uri (0.13.0) webrick (1.8.1) PLATFORMS ruby DEPENDENCIES - jekyll + github-pages (~> 231) jekyll-feed jekyll-gist jekyll-paginate jekyll-sitemap jemoji + webrick BUNDLED WITH - 2.1.4 + 2.3.25 From 5740ed81c3d8f8e08116b214163aea65e959bfaa Mon Sep 17 00:00:00 2001 From: Peter Gafert Date: Wed, 27 Mar 2024 23:47:56 +0100 Subject: [PATCH 2/3] support filtering `@ArchTest` members by system property for JUnit 5 One inconvenience of the current state of the `ArchUnitTestEngine` is the fact that there is no simple way to execute a single rule. For methods annotated with `@ArchTest` this might be supported by the IDE, but for fields there is no convenient way except of commenting all other `@ArchTest` annotations. We now provide a simple way to run any member in isolation by specifying a system property / `ArchConfiguration` value. This change allows to pass a simple test name filter that for now only supports exact member matches. E.g. specifying `-Darchunit.junit.testFilter=my_custom_rule` would run the rule declared in a field or member with name `my_custom_rule`. Multiple members can be joined by `,` and are then all executed in one run. Signed-off-by: Peter Gafert --- ...rchUnitSystemPropertyTestFilterJUnit5.java | 61 +++++++++++++++++++ .../junit/internal/ArchUnitTestEngine.java | 4 ++ .../internal/ArchUnitTestEngineTest.java | 39 +++++++++++- .../testexamples/subtwo/SimpleRules.java | 3 +- .../testutil/SystemPropertiesExtension.java | 21 +++++++ docs/userguide/009_JUnit_Support.adoc | 19 ++++++ 6 files changed, 145 insertions(+), 2 deletions(-) create mode 100644 archunit-junit/junit5/engine/src/main/java/com/tngtech/archunit/junit/internal/ArchUnitSystemPropertyTestFilterJUnit5.java create mode 100644 archunit-junit/junit5/engine/src/test/java/com/tngtech/archunit/junit/internal/testutil/SystemPropertiesExtension.java diff --git a/archunit-junit/junit5/engine/src/main/java/com/tngtech/archunit/junit/internal/ArchUnitSystemPropertyTestFilterJUnit5.java b/archunit-junit/junit5/engine/src/main/java/com/tngtech/archunit/junit/internal/ArchUnitSystemPropertyTestFilterJUnit5.java new file mode 100644 index 0000000000..ae9c3f12c3 --- /dev/null +++ b/archunit-junit/junit5/engine/src/main/java/com/tngtech/archunit/junit/internal/ArchUnitSystemPropertyTestFilterJUnit5.java @@ -0,0 +1,61 @@ +/* + * Copyright 2014-2024 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.internal; + +import java.util.List; +import java.util.Set; +import java.util.function.Predicate; + +import com.google.common.base.Splitter; +import com.google.common.collect.ImmutableSet; +import com.tngtech.archunit.ArchConfiguration; +import org.junit.platform.engine.TestDescriptor; +import org.junit.platform.engine.UniqueId; + +import static com.tngtech.archunit.junit.internal.ArchUnitTestDescriptor.FIELD_SEGMENT_TYPE; +import static com.tngtech.archunit.junit.internal.ArchUnitTestDescriptor.METHOD_SEGMENT_TYPE; + +class ArchUnitSystemPropertyTestFilterJUnit5 { + private static final String JUNIT_TEST_FILTER_PROPERTY_NAME = "junit.testFilter"; + private static final Set MEMBER_SEGMENT_TYPES = ImmutableSet.of(FIELD_SEGMENT_TYPE, METHOD_SEGMENT_TYPE); + + void filter(TestDescriptor descriptor) { + ArchConfiguration configuration = ArchConfiguration.get(); + if (!configuration.containsProperty(JUNIT_TEST_FILTER_PROPERTY_NAME)) { + return; + } + + String testFilterProperty = configuration.getProperty(JUNIT_TEST_FILTER_PROPERTY_NAME); + List memberNames = Splitter.on(",").splitToList(testFilterProperty); + Predicate shouldRunPredicate = testDescriptor -> memberNameMatches(testDescriptor, memberNames); + removeNonMatching(descriptor, shouldRunPredicate); + } + + private void removeNonMatching(TestDescriptor descriptor, Predicate shouldRunPredicate) { + ImmutableSet.copyOf(descriptor.getChildren()) + .forEach(child -> removeNonMatching(child, shouldRunPredicate)); + + if (descriptor.getChildren().isEmpty() && !shouldRunPredicate.test(descriptor)) { + descriptor.removeFromHierarchy(); + } + } + + private static boolean memberNameMatches(TestDescriptor testDescriptor, List memberNames) { + UniqueId.Segment lastSegment = testDescriptor.getUniqueId().getLastSegment(); + return MEMBER_SEGMENT_TYPES.contains(lastSegment.getType()) + && memberNames.stream().anyMatch(it -> lastSegment.getValue().equals(it)); + } +} diff --git a/archunit-junit/junit5/engine/src/main/java/com/tngtech/archunit/junit/internal/ArchUnitTestEngine.java b/archunit-junit/junit5/engine/src/main/java/com/tngtech/archunit/junit/internal/ArchUnitTestEngine.java index 82ca1ce7b7..a1fb4758d1 100644 --- a/archunit-junit/junit5/engine/src/main/java/com/tngtech/archunit/junit/internal/ArchUnitTestEngine.java +++ b/archunit-junit/junit5/engine/src/main/java/com/tngtech/archunit/junit/internal/ArchUnitTestEngine.java @@ -71,6 +71,8 @@ public final class ArchUnitTestEngine extends HierarchicalTestEngine { static final String UNIQUE_ID = "archunit"; + private final ArchUnitSystemPropertyTestFilterJUnit5 systemPropertyTestFilter = new ArchUnitSystemPropertyTestFilterJUnit5(); + @SuppressWarnings("FieldMayBeFinal") private SharedCache cache = new SharedCache(); // NOTE: We want to change this in tests -> no static/final reference @@ -90,6 +92,8 @@ public TestDescriptor discover(EngineDiscoveryRequest discoveryRequest, UniqueId resolveRequestedFields(discoveryRequest, uniqueId, result); resolveRequestedUniqueIds(discoveryRequest, uniqueId, result); + systemPropertyTestFilter.filter(result); + return result; } diff --git a/archunit-junit/junit5/engine/src/test/java/com/tngtech/archunit/junit/internal/ArchUnitTestEngineTest.java b/archunit-junit/junit5/engine/src/test/java/com/tngtech/archunit/junit/internal/ArchUnitTestEngineTest.java index 5e57aec6ee..1fac69668c 100644 --- a/archunit-junit/junit5/engine/src/test/java/com/tngtech/archunit/junit/internal/ArchUnitTestEngineTest.java +++ b/archunit-junit/junit5/engine/src/test/java/com/tngtech/archunit/junit/internal/ArchUnitTestEngineTest.java @@ -54,6 +54,7 @@ import com.tngtech.archunit.junit.internal.testexamples.wrong.WrongRuleMethodNotStatic; import com.tngtech.archunit.junit.internal.testexamples.wrong.WrongRuleMethodWrongParameters; import com.tngtech.archunit.junit.internal.testutil.LogCaptor; +import com.tngtech.archunit.junit.internal.testutil.SystemPropertiesExtension; import com.tngtech.archunit.junit.internal.testutil.TestLogExtension; import com.tngtech.archunit.testutil.TestLogRecorder.RecordedLogEvent; import org.junit.jupiter.api.AfterEach; @@ -117,7 +118,7 @@ import static org.mockito.Mockito.verifyNoMoreInteractions; import static org.mockito.Mockito.when; -@ExtendWith(MockitoExtension.class) +@ExtendWith({MockitoExtension.class, SystemPropertiesExtension.class}) @MockitoSettings(strictness = Strictness.WARN) class ArchUnitTestEngineTest { @Mock @@ -760,6 +761,42 @@ void filtering_included_packages() { simpleRulesId(engineId)); } + @Test + void filtering_specific_rule_by_system_property() { + System.setProperty("archunit.junit.testFilter", SimpleRules.SIMPLE_RULE_FIELD_TWO_NAME); + + EngineDiscoveryTestRequest discoveryRequest = new EngineDiscoveryTestRequest() + .withPackage(SimpleRuleLibrary.class.getPackage().getName()) + .withPackageNameFilter(excludePackageNames(WrongRuleMethodNotStatic.class.getPackage().getName())); + + TestDescriptor rootDescriptor = testEngine.discover(discoveryRequest, engineId); + + Set leafIds = getAllLeafUniqueIds(rootDescriptor); + assertThat(leafIds).isNotEmpty(); + leafIds.forEach(leafId -> { + assertThat(leafId.getLastSegment().getType()).isEqualTo(FIELD_SEGMENT_TYPE); + assertThat(leafId.getLastSegment().getValue()).isEqualTo(SimpleRules.SIMPLE_RULE_FIELD_TWO_NAME); + }); + } + + @Test + void filtering_specific_rules_by_system_property() { + System.setProperty("archunit.junit.testFilter", "simple_rule_method_two,simple_rule,some_non_existing_rule"); + + EngineDiscoveryTestRequest discoveryRequest = new EngineDiscoveryTestRequest() + .withClass(SimpleRuleLibrary.class); + + TestDescriptor rootDescriptor = testEngine.discover(discoveryRequest, engineId); + + Set leafIds = getAllLeafUniqueIds(rootDescriptor); + assertThat(leafIds).containsOnly( + simpleRulesInLibraryId(engineId) + .append(METHOD_SEGMENT_TYPE, SimpleRules.SIMPLE_RULE_METHOD_TWO_NAME), + simpleRuleFieldTestId(engineId + .append(CLASS_SEGMENT_TYPE, SimpleRuleLibrary.class.getName()) + .append(FIELD_SEGMENT_TYPE, SimpleRuleLibrary.RULES_TWO_FIELD))); + } + @Test void all_without_filters() { EngineDiscoveryTestRequest discoveryRequest = new EngineDiscoveryTestRequest() diff --git a/archunit-junit/junit5/engine/src/test/java/com/tngtech/archunit/junit/internal/testexamples/subtwo/SimpleRules.java b/archunit-junit/junit5/engine/src/test/java/com/tngtech/archunit/junit/internal/testexamples/subtwo/SimpleRules.java index 582bf8f8a2..727f3d60a8 100644 --- a/archunit-junit/junit5/engine/src/test/java/com/tngtech/archunit/junit/internal/testexamples/subtwo/SimpleRules.java +++ b/archunit-junit/junit5/engine/src/test/java/com/tngtech/archunit/junit/internal/testexamples/subtwo/SimpleRules.java @@ -32,5 +32,6 @@ public static void simple_rule_method_two(JavaClasses classes) { public static final String SIMPLE_RULE_FIELD_TWO_NAME = "simple_rule_field_two"; public static final Set RULE_FIELD_NAMES = ImmutableSet.of(SIMPLE_RULE_FIELD_ONE_NAME, SIMPLE_RULE_FIELD_TWO_NAME); public static final String SIMPLE_RULE_METHOD_ONE_NAME = "simple_rule_method_one"; - public static final Set RULE_METHOD_NAMES = ImmutableSet.of(SIMPLE_RULE_METHOD_ONE_NAME, "simple_rule_method_two"); + public static final String SIMPLE_RULE_METHOD_TWO_NAME = "simple_rule_method_two"; + public static final Set RULE_METHOD_NAMES = ImmutableSet.of(SIMPLE_RULE_METHOD_ONE_NAME, SIMPLE_RULE_METHOD_TWO_NAME); } diff --git a/archunit-junit/junit5/engine/src/test/java/com/tngtech/archunit/junit/internal/testutil/SystemPropertiesExtension.java b/archunit-junit/junit5/engine/src/test/java/com/tngtech/archunit/junit/internal/testutil/SystemPropertiesExtension.java new file mode 100644 index 0000000000..5a865758a9 --- /dev/null +++ b/archunit-junit/junit5/engine/src/test/java/com/tngtech/archunit/junit/internal/testutil/SystemPropertiesExtension.java @@ -0,0 +1,21 @@ +package com.tngtech.archunit.junit.internal.testutil; + +import java.util.Properties; + +import org.junit.jupiter.api.extension.AfterEachCallback; +import org.junit.jupiter.api.extension.BeforeEachCallback; +import org.junit.jupiter.api.extension.ExtensionContext; + +public class SystemPropertiesExtension implements BeforeEachCallback, AfterEachCallback { + private Properties properties; + + @Override + public void beforeEach(ExtensionContext extensionContext) { + properties = (Properties) System.getProperties().clone(); + } + + @Override + public void afterEach(ExtensionContext extensionContext) { + System.setProperties(properties); + } +} diff --git a/docs/userguide/009_JUnit_Support.adoc b/docs/userguide/009_JUnit_Support.adoc index 9d7b47f8be..660b93ac37 100644 --- a/docs/userguide/009_JUnit_Support.adoc +++ b/docs/userguide/009_JUnit_Support.adoc @@ -198,6 +198,25 @@ The runner will include all `@ArchTest` annotated members within `ServiceRules` them against the classes declared within `@AnalyzeClasses` on `ArchitectureTest`. This also allows an easy reuse of a rule library in different projects or modules. +==== Executing Single Rules + +When using ArchUnit's JUnit 5 support it is possible to filter specific rules (e.g. `@ArchTest` fields) via `archunit.properties` (compare <>). + +[source,options="nowrap"] +.archunit.properties +---- +# Specify the field or method name here. Multiple names can be joined by ',' +junit.testFilter=my_custom_rule_field +---- + +As always with `archunit.properties`, this can also be passed dynamically using a system property, +E.g. passing + +[source,options="nowrap"] +---- +-Darchunit.junit.testFilter=my_custom_rule_field +---- + ==== Generating Display Names ArchUnit offers the possibility to generate more readable names in the test report by replacing underscores in the From 14d7f4d69e026a0e56375715614dfdeb89241304 Mon Sep 17 00:00:00 2001 From: Peter Gafert Date: Thu, 28 Mar 2024 13:49:03 +0100 Subject: [PATCH 3/3] support filtering `@ArchTest` members by system property for JUnit 4 One inconvenience of the current state of the `ArchUnitRunner` is the fact that there is no simple way to execute a single rule. For methods annotated with `@ArchTest` this might be supported by the IDE, but for fields there is no convenient way except of commenting all other `@ArchTest` annotations. We now provide a simple way to run any member in isolation by specifying a system property / `ArchConfiguration` value. This change allows to pass a simple test name filter that for now only supports exact member matches. E.g. specifying `-Darchunit.junit.testFilter=my_custom_rule` would run the rule declared in a field or member with name `my_custom_rule`. Multiple members can be joined by `,` and are then all executed in one run. Unfortunately, filtering in JUnit 4 is tied to only the `Description` of the current test. Since we don't want to couple text parsing of the description to filtering (e.g. if we change the description in the future), we have to introduce a hack to transmit the original member name to the `Filter`. Due to the fact that `Description` is effectively final and has no intended way for our use case, we (ab-)use the `annotations` of the test member to pass along the information in a custom annotation. This should have no further effect, since no infrastructure can react to unknown annotations, which may be present at any time for given tests. Signed-off-by: Peter Gafert --- .../junit/internal/ArchRuleExecution.java | 5 +- .../junit/internal/ArchTestExecution.java | 16 ++++- .../junit/internal/ArchTestMetaInfo.java | 72 +++++++++++++++++++ .../internal/ArchTestMethodExecution.java | 4 +- .../internal/ArchUnitRunnerInternal.java | 7 ++ ...rchUnitSystemPropertyTestFilterJunit4.java | 57 +++++++++++++++ .../ArchUnitRunnerRunsRuleSetsTest.java | 44 ++++++++++++ docs/userguide/009_JUnit_Support.adoc | 2 +- 8 files changed, 196 insertions(+), 11 deletions(-) create mode 100644 archunit-junit/junit4/src/main/java/com/tngtech/archunit/junit/internal/ArchTestMetaInfo.java create mode 100644 archunit-junit/junit4/src/main/java/com/tngtech/archunit/junit/internal/ArchUnitSystemPropertyTestFilterJunit4.java diff --git a/archunit-junit/junit4/src/main/java/com/tngtech/archunit/junit/internal/ArchRuleExecution.java b/archunit-junit/junit4/src/main/java/com/tngtech/archunit/junit/internal/ArchRuleExecution.java index f5386c26fa..61a6f223e8 100644 --- a/archunit-junit/junit4/src/main/java/com/tngtech/archunit/junit/internal/ArchRuleExecution.java +++ b/archunit-junit/junit4/src/main/java/com/tngtech/archunit/junit/internal/ArchRuleExecution.java @@ -22,8 +22,6 @@ import com.tngtech.archunit.lang.ArchRule; import org.junit.runner.Description; -import static com.tngtech.archunit.junit.internal.DisplayNameResolver.determineDisplayName; - class ArchRuleExecution extends ArchTestExecution { private final Field ruleField; @@ -50,8 +48,7 @@ Result evaluateOn(JavaClasses classes) { @Override Description describeSelf() { - String testName = formatWithPath(ruleField.getName()); - return Description.createTestDescription(getTestClass(), determineDisplayName(testName), ruleField.getAnnotations()); + return createDescription(ruleField); } @Override diff --git a/archunit-junit/junit4/src/main/java/com/tngtech/archunit/junit/internal/ArchTestExecution.java b/archunit-junit/junit4/src/main/java/com/tngtech/archunit/junit/internal/ArchTestExecution.java index 0f6155f87b..415479a8b1 100644 --- a/archunit-junit/junit4/src/main/java/com/tngtech/archunit/junit/internal/ArchTestExecution.java +++ b/archunit-junit/junit4/src/main/java/com/tngtech/archunit/junit/internal/ArchTestExecution.java @@ -15,7 +15,11 @@ */ package com.tngtech.archunit.junit.internal; +import java.lang.annotation.Annotation; +import java.lang.reflect.AnnotatedElement; import java.lang.reflect.Field; +import java.lang.reflect.Member; +import java.util.Arrays; import java.util.List; import java.util.stream.Stream; @@ -24,6 +28,7 @@ import org.junit.runner.notification.Failure; import org.junit.runner.notification.RunNotifier; +import static com.tngtech.archunit.junit.internal.DisplayNameResolver.determineDisplayName; import static com.tngtech.archunit.junit.internal.ReflectionUtils.getValueOrThrowException; import static java.util.stream.Collectors.joining; @@ -47,11 +52,16 @@ public String toString() { return describeSelf().toString(); } - Class getTestClass() { - return testClassPath.get(0); + Description createDescription(T member) { + Annotation[] annotations = Stream.concat( + Arrays.stream(member.getAnnotations()), + Stream.of(new ArchTestMetaInfo.Instance(member.getName())) + ).toArray(Annotation[]::new); + String testName = formatWithPath(member.getName()); + return Description.createTestDescription(testClassPath.get(0), determineDisplayName(testName), annotations); } - String formatWithPath(String testName) { + private String formatWithPath(String testName) { if (testClassPath.size() <= 1) { return testName; } diff --git a/archunit-junit/junit4/src/main/java/com/tngtech/archunit/junit/internal/ArchTestMetaInfo.java b/archunit-junit/junit4/src/main/java/com/tngtech/archunit/junit/internal/ArchTestMetaInfo.java new file mode 100644 index 0000000000..dc8016e387 --- /dev/null +++ b/archunit-junit/junit4/src/main/java/com/tngtech/archunit/junit/internal/ArchTestMetaInfo.java @@ -0,0 +1,72 @@ +/* + * Copyright 2014-2024 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.internal; + +import java.lang.annotation.Annotation; +import java.util.Objects; + +import org.junit.runner.Description; +import org.junit.runner.Runner; +import org.junit.runner.manipulation.Filter; + +/** + * Hack to transport meta-information from {@link ArchTestExecution} to {@link ArchUnitSystemPropertyTestFilterJunit4}. + * Unfortunately, the {@link Filter} interface doesn't allow access to the original child of the {@link Runner}, + * but only the {@link Description}, which is not suitable to obtain the original member name reliably. + */ +@interface ArchTestMetaInfo { + String memberName(); + + class Instance implements ArchTestMetaInfo, Annotation { + private final String memberName; + + Instance(String memberName) { + this.memberName = memberName; + } + + @Override + public String memberName() { + return memberName; + } + + @Override + public Class annotationType() { + return ArchTestMetaInfo.class; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + Instance instance = (Instance) o; + return Objects.equals(memberName, instance.memberName); + } + + @Override + public int hashCode() { + return Objects.hash(memberName); + } + + @Override + public String toString() { + return "@" + ArchTestMetaInfo.class.getSimpleName() + "(" + memberName + ")"; + } + } +} diff --git a/archunit-junit/junit4/src/main/java/com/tngtech/archunit/junit/internal/ArchTestMethodExecution.java b/archunit-junit/junit4/src/main/java/com/tngtech/archunit/junit/internal/ArchTestMethodExecution.java index 38c485708c..48f41d9689 100644 --- a/archunit-junit/junit4/src/main/java/com/tngtech/archunit/junit/internal/ArchTestMethodExecution.java +++ b/archunit-junit/junit4/src/main/java/com/tngtech/archunit/junit/internal/ArchTestMethodExecution.java @@ -23,7 +23,6 @@ import com.tngtech.archunit.junit.ArchTest; import org.junit.runner.Description; -import static com.tngtech.archunit.junit.internal.DisplayNameResolver.determineDisplayName; import static com.tngtech.archunit.junit.internal.ReflectionUtils.invokeMethod; class ArchTestMethodExecution extends ArchTestExecution { @@ -55,8 +54,7 @@ private void executeTestMethod(JavaClasses classes) { @Override Description describeSelf() { - String testName = formatWithPath(testMethod.getName()); - return Description.createTestDescription(getTestClass(), determineDisplayName(testName), testMethod.getAnnotations()); + return createDescription(testMethod); } @Override diff --git a/archunit-junit/junit4/src/main/java/com/tngtech/archunit/junit/internal/ArchUnitRunnerInternal.java b/archunit-junit/junit4/src/main/java/com/tngtech/archunit/junit/internal/ArchUnitRunnerInternal.java index 197241bdf8..2fd97c2419 100644 --- a/archunit-junit/junit4/src/main/java/com/tngtech/archunit/junit/internal/ArchUnitRunnerInternal.java +++ b/archunit-junit/junit4/src/main/java/com/tngtech/archunit/junit/internal/ArchUnitRunnerInternal.java @@ -32,6 +32,7 @@ import com.tngtech.archunit.junit.CacheMode; import com.tngtech.archunit.junit.LocationProvider; import org.junit.runner.Description; +import org.junit.runner.manipulation.NoTestsRemainException; import org.junit.runner.notification.RunNotifier; import org.junit.runners.ParentRunner; import org.junit.runners.model.FrameworkField; @@ -53,6 +54,12 @@ final class ArchUnitRunnerInternal extends ParentRunner imple ArchUnitRunnerInternal(Class testClass) throws InitializationError { super(testClass); checkAnnotation(testClass); + + try { + ArchUnitSystemPropertyTestFilterJunit4.filter(this); + } catch (NoTestsRemainException e) { + throw new InitializationError(e); + } } private static AnalyzeClasses checkAnnotation(Class testClass) { diff --git a/archunit-junit/junit4/src/main/java/com/tngtech/archunit/junit/internal/ArchUnitSystemPropertyTestFilterJunit4.java b/archunit-junit/junit4/src/main/java/com/tngtech/archunit/junit/internal/ArchUnitSystemPropertyTestFilterJunit4.java new file mode 100644 index 0000000000..4624e04614 --- /dev/null +++ b/archunit-junit/junit4/src/main/java/com/tngtech/archunit/junit/internal/ArchUnitSystemPropertyTestFilterJunit4.java @@ -0,0 +1,57 @@ +/* + * Copyright 2014-2024 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.internal; + +import java.util.List; + +import com.google.common.base.Splitter; +import com.tngtech.archunit.ArchConfiguration; +import org.junit.runner.Description; +import org.junit.runner.manipulation.Filter; +import org.junit.runner.manipulation.NoTestsRemainException; +import org.junit.runners.ParentRunner; + +import static java.util.Objects.requireNonNull; + +class ArchUnitSystemPropertyTestFilterJunit4 extends Filter { + private static final String JUNIT_TEST_FILTER_PROPERTY_NAME = "junit.testFilter"; + private final List memberNames; + + private ArchUnitSystemPropertyTestFilterJunit4(List memberNames) { + this.memberNames = memberNames; + } + + @Override + public boolean shouldRun(Description description) { + ArchTestMetaInfo metaInfo = requireNonNull(description.getAnnotation(ArchTestMetaInfo.class)); + return memberNames.contains(metaInfo.memberName()); + } + + @Override + public String describe() { + return JUNIT_TEST_FILTER_PROPERTY_NAME + " = " + memberNames; + } + + static void filter(ParentRunner runner) throws NoTestsRemainException { + ArchConfiguration configuration = ArchConfiguration.get(); + if (!configuration.containsProperty(JUNIT_TEST_FILTER_PROPERTY_NAME)) { + return; + } + + String testFilterProperty = configuration.getProperty(JUNIT_TEST_FILTER_PROPERTY_NAME); + runner.filter(new ArchUnitSystemPropertyTestFilterJunit4(Splitter.on(",").splitToList(testFilterProperty))); + } +} diff --git a/archunit-junit/junit4/src/test/java/com/tngtech/archunit/junit/internal/ArchUnitRunnerRunsRuleSetsTest.java b/archunit-junit/junit4/src/test/java/com/tngtech/archunit/junit/internal/ArchUnitRunnerRunsRuleSetsTest.java index 926ac7421f..4d18dce3d4 100644 --- a/archunit-junit/junit4/src/test/java/com/tngtech/archunit/junit/internal/ArchUnitRunnerRunsRuleSetsTest.java +++ b/archunit-junit/junit4/src/test/java/com/tngtech/archunit/junit/internal/ArchUnitRunnerRunsRuleSetsTest.java @@ -3,6 +3,7 @@ import java.lang.reflect.Method; import java.util.Collection; +import com.tngtech.archunit.ArchConfiguration; import com.tngtech.archunit.core.domain.JavaClass; import com.tngtech.archunit.core.domain.JavaClasses; import com.tngtech.archunit.junit.AnalyzeClasses; @@ -13,6 +14,7 @@ import com.tngtech.archunit.lang.ArchCondition; import com.tngtech.archunit.lang.ArchRule; import com.tngtech.archunit.lang.ConditionEvents; +import com.tngtech.archunit.testutil.ArchConfigurationRule; import org.assertj.core.api.iterable.Extractor; import org.junit.Before; import org.junit.Rule; @@ -49,12 +51,17 @@ import static com.tngtech.archunit.testutil.TestUtils.invoke; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.Mockito.atLeast; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; import static org.mockito.Mockito.when; public class ArchUnitRunnerRunsRuleSetsTest { @Rule public final MockitoRule mockitoRule = MockitoJUnit.rule(); + @Rule + public final ArchConfigurationRule archConfigurationRule = new ArchConfigurationRule(); @Mock private SharedCache cache; @@ -138,6 +145,25 @@ public void can_run_rule_method() { run(someMethodRuleName, runnerForRuleSet, verifyTestRan()); } + @Test + public void allows_to_run_single_rule_via_configuration() { + ArchConfiguration.get().setProperty("junit.testFilter", someFieldRuleName); + + newRunnerFor(ArchTestWithRuleSet.class).run(runNotifier); + + verifyOnlyTestsRan("Rules > " + someFieldRuleName); + } + + @Test + public void allows_to_run_selected_rules_via_configuration() { + ArchConfiguration.get().setProperty("junit.testFilter", + someFieldRuleName + "," + someOtherMethodRuleName + ",some_non_existing_rule"); + + newRunnerFor(ArchTestWithRuleLibrary.class).run(runNotifier); + + verifyOnlyTestsRan("ArchTestWithRuleSet > Rules > " + someFieldRuleName, someOtherMethodRuleName); + } + @Test public void ignores_field_rule_of_ignored_rule_set() { run(someFieldRuleName, runnerForIgnoredRuleSet, verifyTestIgnored()); @@ -246,6 +272,24 @@ private Runnable verifyTestRan() { }; } + private void verifyOnlyTestsRan(String... memberNames) { + // Ignore these invocations as they are irrelevant + verify(runNotifier, atLeast(0)).fireTestSuiteStarted(any()); + verify(runNotifier, atLeast(0)).fireTestFailure(any()); + verify(runNotifier, atLeast(0)).fireTestSuiteFinished(any()); + + for (String memberName : memberNames) { + verify(runNotifier).fireTestStarted(descriptionMemberName(memberName)); + verify(runNotifier).fireTestFinished(descriptionMemberName(memberName)); + } + + verifyNoMoreInteractions(runNotifier); + } + + private static Description descriptionMemberName(String memberName) { + return argThat(description -> description.getMethodName().equals(memberName)); + } + // extractingResultOf(..) only looks for public methods private Extractor resultOf(String methodName) { return input -> { diff --git a/docs/userguide/009_JUnit_Support.adoc b/docs/userguide/009_JUnit_Support.adoc index 660b93ac37..f19c96d2cb 100644 --- a/docs/userguide/009_JUnit_Support.adoc +++ b/docs/userguide/009_JUnit_Support.adoc @@ -200,7 +200,7 @@ This also allows an easy reuse of a rule library in different projects or module ==== Executing Single Rules -When using ArchUnit's JUnit 5 support it is possible to filter specific rules (e.g. `@ArchTest` fields) via `archunit.properties` (compare <>). +It is possible to filter specific rules (e.g. `@ArchTest` fields) via `archunit.properties` (compare <>). [source,options="nowrap"] .archunit.properties