
com.tngtech.archunit.library.freeze.FreezingArchRule Maven / Gradle / Ivy
/*
* Copyright 2014-2022 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.library.freeze;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Properties;
import java.util.Set;
import java.util.function.Predicate;
import com.tngtech.archunit.ArchConfiguration;
import com.tngtech.archunit.PublicAPI;
import com.tngtech.archunit.core.domain.JavaClasses;
import com.tngtech.archunit.lang.ArchRule;
import com.tngtech.archunit.lang.EvaluationResult;
import com.tngtech.archunit.lang.Priority;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.tngtech.archunit.PublicAPI.Usage.ACCESS;
import static com.tngtech.archunit.library.freeze.ViolationStoreFactory.FREEZE_STORE_PROPERTY_NAME;
import static java.util.stream.Collectors.toList;
/**
* A decorator around an existing {@link ArchRule} that "freezes" the state of all violations on the first call instead of failing the test.
* This means in particular that the first run of a {@link FreezingArchRule} will always pass.
* Consecutive calls will only fail if "unknown" violations are introduced (read below for further explanations when a violation is "unknown").
* Once resolved, initially "known" violations will fail again if they were re-introduced.
*
* You might consider using this class when introducing a new {@link ArchRule} to an existing project that causes too many violations to solve
* at the current time. A typical example is a huge legacy project where a new rule might cause thousands of violations. Even if it is impossible
* to fix all those violations at the moment, it is typically a good idea to a) make sure no further violations are introduced and
* b) incrementally fix those violations over time one by one.
*
* {@link FreezingArchRule} uses two concepts to support this use case:
*
* -
* a {@link ViolationStore} to store the result of the current evaluation and retrieve the result of the previous evaluation of this rule.
* The default {@link ViolationStore} stores violations in plain text files
* within the path specified by {@code freeze.store.default.path} of
* {@value com.tngtech.archunit.ArchConfiguration#ARCHUNIT_PROPERTIES_RESOURCE_NAME} (default: {@code archunit_store})
* A custom implementation can be provided in two ways.
* Either programmatically via {@link #persistIn(ViolationStore)}, or by specifying the fully qualified class name within
* {@value com.tngtech.archunit.ArchConfiguration#ARCHUNIT_PROPERTIES_RESOURCE_NAME}, e.g.
* freeze.store=com.fully.qualified.MyViolationStore
*
* -
* a {@link ViolationLineMatcher} to decide which violations are "known", i.e. have already been present in the previous evaluation.
* The default {@link ViolationLineMatcher} compares violations ignoring the line number of their source code location
* and auto-generated numbers of anonymous classes or lambda expressions.
* A custom implementation can be configured in two ways.
* Again either programmatically via {@link #associateViolationLinesVia(ViolationLineMatcher)}, or within
* {@value com.tngtech.archunit.ArchConfiguration#ARCHUNIT_PROPERTIES_RESOURCE_NAME}, e.g.
* freeze.lineMatcher=com.fully.qualified.MyViolationLineMatcher
*
*
*/
@PublicAPI(usage = ACCESS)
public final class FreezingArchRule implements ArchRule {
private static final Logger log = LoggerFactory.getLogger(FreezingArchRule.class);
private static final String FREEZE_REFREEZE_PROPERTY_NAME = "freeze.refreeze";
private final ArchRule delegate;
private final ViolationStoreLineBreakAdapter store;
private final ViolationLineMatcher matcher;
private FreezingArchRule(ArchRule delegate, ViolationStore store, ViolationLineMatcher matcher) {
this(delegate, new ViolationStoreLineBreakAdapter(store), matcher);
}
private FreezingArchRule(ArchRule delegate, ViolationStoreLineBreakAdapter store, ViolationLineMatcher matcher) {
this.delegate = checkNotNull(delegate);
this.store = store;
this.matcher = checkNotNull(matcher);
}
@Override
@PublicAPI(usage = ACCESS)
public void check(JavaClasses classes) {
Assertions.check(this, classes);
}
@Override
@PublicAPI(usage = ACCESS)
public FreezingArchRule because(String reason) {
return new FreezingArchRule(delegate.because(reason), store, matcher);
}
@Override
@PublicAPI(usage = ACCESS)
public ArchRule allowEmptyShould(boolean allowEmptyShould) {
return new FreezingArchRule(delegate.allowEmptyShould(allowEmptyShould), store, matcher);
}
@Override
@PublicAPI(usage = ACCESS)
public FreezingArchRule as(String newDescription) {
return new FreezingArchRule(delegate.as(newDescription), store, matcher);
}
@Override
@PublicAPI(usage = ACCESS)
public EvaluationResult evaluate(JavaClasses classes) {
store.initialize(ArchConfiguration.get().getSubProperties(FREEZE_STORE_PROPERTY_NAME));
EvaluationResultLineBreakAdapter result = new EvaluationResultLineBreakAdapter(delegate.evaluate(classes));
if (!store.contains(delegate) || refreezeViolations()) {
return storeViolationsAndReturnSuccess(result);
} else {
return removeObsoleteViolationsFromStoreAndReturnNewViolations(result);
}
}
private boolean refreezeViolations() {
String configuredRefreeze = ArchConfiguration.get().getPropertyOrDefault(FREEZE_REFREEZE_PROPERTY_NAME, Boolean.FALSE.toString());
return Boolean.parseBoolean(configuredRefreeze);
}
private EvaluationResult storeViolationsAndReturnSuccess(EvaluationResultLineBreakAdapter result) {
log.debug("No results present for rule '{}'. Freezing rule result...", delegate.getDescription());
store.save(delegate, result.getViolations());
return new EvaluationResult(delegate, result.getPriority());
}
private EvaluationResult removeObsoleteViolationsFromStoreAndReturnNewViolations(EvaluationResultLineBreakAdapter result) {
log.debug("Found frozen result for rule '{}'", delegate.getDescription());
final List knownViolations = store.getViolations(delegate);
CategorizedViolations categorizedViolations = new CategorizedViolations(matcher, result, knownViolations);
removeObsoleteViolationsFromStore(categorizedViolations);
return filterOutKnownViolations(result, categorizedViolations.getKnownActualViolations());
}
private void removeObsoleteViolationsFromStore(CategorizedViolations categorizedViolations) {
List solvedViolations = categorizedViolations.getStoredSolvedViolations();
log.debug("Removing {} obsolete violations from store: {}", solvedViolations.size(), solvedViolations);
if (!solvedViolations.isEmpty()) {
store.save(delegate, categorizedViolations.getStoredUnsolvedViolations());
}
}
private EvaluationResult filterOutKnownViolations(EvaluationResultLineBreakAdapter result, final Set knownActualViolations) {
log.debug("Filtering out known violations: {}", knownActualViolations);
return result.filterDescriptionsMatching(violation -> !knownActualViolations.contains(violation));
}
@Override
@PublicAPI(usage = ACCESS)
public String getDescription() {
return delegate.getDescription();
}
/**
* Allows to reconfigure the {@link ViolationStore} to use. The {@link ViolationStore} will be used to store the initial state of a
* {@link FreezingArchRule} and update this state on further evaluation of this rule.
*
* @param store The {@link ViolationStore} to use
* @return An adjusted {@link FreezingArchRule} which will store violations in the passed {@link ViolationStore}
* @see FreezingArchRule
*/
@PublicAPI(usage = ACCESS)
public FreezingArchRule persistIn(ViolationStore store) {
return new FreezingArchRule(delegate, store, matcher);
}
/**
* Allows to reconfigure how this {@link FreezingArchRule} will decide if an occurring violation is known or not.
*
* @param matcher A {@link ViolationLineMatcher} that decides which lines of a violation description are known and which are unknown and should
* cause a failure of this rule
* @return An adjusted {@link FreezingArchRule} which will compare occurring violations to stored ones with the given {@link ViolationLineMatcher}
* @see FreezingArchRule
*/
@PublicAPI(usage = ACCESS)
public FreezingArchRule associateViolationLinesVia(ViolationLineMatcher matcher) {
return new FreezingArchRule(delegate, store, matcher);
}
@Override
public String toString() {
return getClass().getSimpleName() + "{" + delegate + "}";
}
/**
* @param rule An {@link ArchRule} that should be "frozen" on the first call, i.e. all occurring violations will be stored for comparison
* on consecutive calls.
* @return A {@link FreezingArchRule} wrapping the original rule
* @see FreezingArchRule
*/
@PublicAPI(usage = ACCESS)
public static FreezingArchRule freeze(ArchRule rule) {
return new FreezingArchRule(rule, ViolationStoreFactory.create(), ViolationLineMatcherFactory.create());
}
static String ensureUnixLineBreaks(String string) {
return string.replaceAll("\r\n", "\n");
}
private static List ensureUnixLineBreaks(List strings) {
return strings.stream().map(FreezingArchRule::ensureUnixLineBreaks).collect(toList());
}
private static class CategorizedViolations {
private final Set knownActualViolations = new HashSet<>();
private final List storedSolvedViolations;
private final List storedUnsolvedViolations = new ArrayList<>();
CategorizedViolations(ViolationLineMatcher matcher, EvaluationResultLineBreakAdapter actualResult, List storedViolations) {
List storedViolationsLeft = new ArrayList<>(storedViolations);
for (String actualViolation : actualResult.getViolations()) {
for (Iterator iterator = storedViolationsLeft.iterator(); iterator.hasNext(); ) {
String storedViolation = iterator.next();
if (matcher.matches(actualViolation, storedViolation)) {
iterator.remove();
knownActualViolations.add(actualViolation);
storedUnsolvedViolations.add(storedViolation);
break;
}
}
}
storedSolvedViolations = new ArrayList<>(storedViolations);
storedSolvedViolations.removeAll(storedUnsolvedViolations);
}
Set getKnownActualViolations() {
return knownActualViolations;
}
List getStoredSolvedViolations() {
return storedSolvedViolations;
}
List getStoredUnsolvedViolations() {
return storedUnsolvedViolations;
}
}
private static class ViolationStoreLineBreakAdapter {
private final ViolationStore store;
ViolationStoreLineBreakAdapter(ViolationStore store) {
this.store = checkNotNull(store);
}
void initialize(Properties properties) {
store.initialize(properties);
}
boolean contains(ArchRule rule) {
return store.contains(rule);
}
List getViolations(ArchRule rule) {
return ensureUnixLineBreaks(store.getViolations(rule));
}
void save(ArchRule rule, List violations) {
store.save(rule, ensureUnixLineBreaks(violations));
}
}
private static class EvaluationResultLineBreakAdapter {
private final EvaluationResult result;
private EvaluationResultLineBreakAdapter(EvaluationResult result) {
this.result = checkNotNull(result);
}
List getViolations() {
return ensureUnixLineBreaks(result.getFailureReport().getDetails());
}
Priority getPriority() {
return result.getPriority();
}
EvaluationResult filterDescriptionsMatching(final Predicate predicate) {
return result.filterDescriptionsMatching(input -> predicate.test(ensureUnixLineBreaks(input)));
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy