Skip to content

Commit

Permalink
Merge pull request TNG#252 from TNG/enhance-freezing-archrule
Browse files Browse the repository at this point in the history
Enhance FreezingArchRule
  • Loading branch information
codecholeric authored Oct 24, 2019
2 parents 4a6811f + e946a3f commit 989918f
Show file tree
Hide file tree
Showing 11 changed files with 375 additions and 72 deletions.
80 changes: 67 additions & 13 deletions archunit/src/main/java/com/tngtech/archunit/ArchConfiguration.java
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;

import com.google.common.base.Joiner;
import com.google.common.base.Splitter;
Expand Down Expand Up @@ -53,11 +54,6 @@ public final class ArchConfiguration {

private static final Logger LOG = LoggerFactory.getLogger(ArchConfiguration.class);

private static final Map<String, String> PROPERTY_DEFAULTS = ImmutableMap.of(
RESOLVE_MISSING_DEPENDENCIES_FROM_CLASS_PATH, Boolean.TRUE.toString(),
ENABLE_MD5_IN_CLASS_SOURCES, Boolean.FALSE.toString()
);

private static final Supplier<ArchConfiguration> INSTANCE = Suppliers.memoize(new Supplier<ArchConfiguration>() {
@Override
public ArchConfiguration get() {
Expand All @@ -71,7 +67,7 @@ public static ArchConfiguration get() {
}

private final String propertiesResourceName;
private final Properties properties = new Properties();
private final PropertiesOverwritableBySystemProperties properties = new PropertiesOverwritableBySystemProperties();

private ArchConfiguration() {
this(ARCHUNIT_PROPERTIES_RESOURCE_NAME);
Expand Down Expand Up @@ -106,7 +102,7 @@ public void reset() {

@PublicAPI(usage = ACCESS)
public boolean resolveMissingDependenciesFromClassPath() {
return Boolean.valueOf(propertyOrDefault(properties, RESOLVE_MISSING_DEPENDENCIES_FROM_CLASS_PATH));
return Boolean.parseBoolean(properties.getProperty(RESOLVE_MISSING_DEPENDENCIES_FROM_CLASS_PATH));
}

@PublicAPI(usage = ACCESS)
Expand All @@ -116,7 +112,7 @@ public void setResolveMissingDependenciesFromClassPath(boolean newValue) {

@PublicAPI(usage = ACCESS)
public boolean md5InClassSourcesEnabled() {
return Boolean.valueOf(propertyOrDefault(properties, ENABLE_MD5_IN_CLASS_SOURCES));
return Boolean.parseBoolean(properties.getProperty(ENABLE_MD5_IN_CLASS_SOURCES));
}

@PublicAPI(usage = ACCESS)
Expand Down Expand Up @@ -156,7 +152,7 @@ public void setExtensionProperties(String extensionIdentifier, Properties proper
clearPropertiesWithPrefix(propertyPrefix);
for (String propertyName : properties.stringPropertyNames()) {
String fullPropertyName = propertyPrefix + "." + propertyName;
this.properties.put(fullPropertyName, properties.getProperty(propertyName));
this.properties.setProperty(fullPropertyName, properties.getProperty(propertyName));
}
}

Expand Down Expand Up @@ -197,6 +193,10 @@ public ExtensionProperties configureExtension(String extensionIdentifier) {
*/
@PublicAPI(usage = ACCESS)
public Properties getSubProperties(String propertyPrefix) {
return getSubProperties(propertyPrefix, properties.getMergedProperties());
}

private static Properties getSubProperties(String propertyPrefix, Properties properties) {
Properties result = new Properties();
for (String key : filterNamesWithPrefix(properties.stringPropertyNames(), propertyPrefix)) {
String extensionPropertyKey = removePrefix(key, propertyPrefix);
Expand All @@ -205,7 +205,7 @@ public Properties getSubProperties(String propertyPrefix) {
return result;
}

private Iterable<String> filterNamesWithPrefix(Iterable<String> propertyNames, String prefix) {
private static Iterable<String> filterNamesWithPrefix(Iterable<String> propertyNames, String prefix) {
List<String> result = new ArrayList<>();
String fullPrefix = prefix + ".";
for (String propertyName : propertyNames) {
Expand All @@ -216,7 +216,7 @@ private Iterable<String> filterNamesWithPrefix(Iterable<String> propertyNames, S
return result;
}

private String removePrefix(String string, String prefix) {
private static String removePrefix(String string, String prefix) {
return string.substring(prefix.length() + 1);
}

Expand Down Expand Up @@ -256,8 +256,62 @@ public void setProperty(String propertyName, String value) {
properties.setProperty(propertyName, value);
}

private String propertyOrDefault(Properties properties, String propertyName) {
return properties.getProperty(propertyName, PROPERTY_DEFAULTS.get(propertyName));
private static class PropertiesOverwritableBySystemProperties {
private static final Properties PROPERTY_DEFAULTS = createProperties(ImmutableMap.of(
RESOLVE_MISSING_DEPENDENCIES_FROM_CLASS_PATH, Boolean.TRUE.toString(),
ENABLE_MD5_IN_CLASS_SOURCES, Boolean.FALSE.toString()
));

private final Properties properties = createProperties(PROPERTY_DEFAULTS);

void clear() {
properties.clear();
properties.putAll(PROPERTY_DEFAULTS);
}

void load(InputStream inputStream) throws IOException {
properties.load(inputStream);
}

Set<String> stringPropertyNames() {
return getMergedProperties().stringPropertyNames();
}

boolean containsKey(String propertyName) {
return getMergedProperties().containsKey(propertyName);
}

String getProperty(String propertyName) {
return getMergedProperties().getProperty(propertyName);
}

String getProperty(String propertyName, String defaultValue) {
return getMergedProperties().getProperty(propertyName, defaultValue);
}

void setProperty(String propertyName, String value) {
properties.setProperty(propertyName, value);
}

void remove(String propertyName) {
properties.remove(propertyName);
}

Properties getMergedProperties() {
Properties result = createProperties(this.properties);
Properties overwritten = getSubProperties("archunit", System.getProperties());
if (!overwritten.isEmpty()) {
LOG.info("Merging properties: The following properties have been overwritten by system properties: {}", overwritten);
}
result.putAll(overwritten);
return result;
}

private static Properties createProperties(Map<?, ?> entries) {
Properties result = new Properties();
result.putAll(entries);
return result;
}
}

public final class ExtensionProperties {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@

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;
import static com.tngtech.archunit.library.freeze.ViolationStoreFactory.FREEZE_STORE_PROPERTY_NAME;

/**
* A decorator around an existing {@link ArchRule} that "freezes" the state of all violations on the first call instead of failing the test.
Expand Down Expand Up @@ -102,7 +102,7 @@ public FreezingArchRule as(String newDescription) {
@Override
@PublicAPI(usage = ACCESS)
public EvaluationResult evaluate(JavaClasses classes) {
store.initialize(ArchConfiguration.get().getSubProperties(FREEZE_STORE_PROPERTY));
store.initialize(ArchConfiguration.get().getSubProperties(FREEZE_STORE_PROPERTY_NAME));

EvaluationResult result = delegate.evaluate(classes);
if (!store.contains(delegate)) {
Expand All @@ -129,7 +129,9 @@ private EvaluationResult removeObsoleteViolationsFromStoreAndReturnNewViolations
private void removeObsoleteViolationsFromStore(CategorizedViolations categorizedViolations) {
List<String> solvedViolations = categorizedViolations.getStoredSolvedViolations();
log.debug("Removing {} obsolete violations from store: {}", solvedViolations.size(), solvedViolations);
store.save(delegate, categorizedViolations.getStoredUnsolvedViolations());
if (!solvedViolations.isEmpty()) {
store.save(delegate, categorizedViolations.getStoredUnsolvedViolations());
}
}

private EvaluationResult filterOutKnownViolations(EvaluationResult result, final Set<String> knownActualViolations) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@
package com.tngtech.archunit.library.freeze;

class StoreUpdateFailedException extends RuntimeException {
StoreUpdateFailedException(String message) {
super(message);
}

StoreUpdateFailedException(Throwable cause) {
super(cause);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,14 +39,16 @@
import static com.google.common.base.Preconditions.checkState;
import static com.google.common.io.Files.toByteArray;
import static com.tngtech.archunit.base.ReflectionUtils.newInstanceOf;
import static java.lang.Boolean.FALSE;
import static java.lang.Boolean.TRUE;
import static java.nio.charset.StandardCharsets.UTF_8;

class ViolationStoreFactory {
static final String FREEZE_STORE_PROPERTY = "freeze.store";
static final String FREEZE_STORE_PROPERTY_NAME = "freeze.store";

static ViolationStore create() {
return ArchConfiguration.get().containsProperty(FREEZE_STORE_PROPERTY)
? createInstance(ArchConfiguration.get().getProperty(FREEZE_STORE_PROPERTY))
return ArchConfiguration.get().containsProperty(FREEZE_STORE_PROPERTY_NAME)
? createInstance(ArchConfiguration.get().getProperty(FREEZE_STORE_PROPERTY_NAME))
: new TextFileBasedViolationStore();
}

Expand All @@ -56,7 +58,7 @@ private static ViolationStore createInstance(String violationStoreClassName) {
return (ViolationStore) newInstanceOf(Class.forName(violationStoreClassName));
} catch (Exception e) {
String message = String.format("Could not instantiate %s of configured type '%s=%s'",
ViolationStore.class.getSimpleName(), FREEZE_STORE_PROPERTY, violationStoreClassName);
ViolationStore.class.getSimpleName(), FREEZE_STORE_PROPERTY_NAME, violationStoreClassName);
throw new StoreInitializationFailedException(message, e);
}
}
Expand All @@ -66,20 +68,42 @@ static class TextFileBasedViolationStore implements ViolationStore {
private static final Logger log = LoggerFactory.getLogger(TextFileBasedViolationStore.class);

private static final Pattern UNESCAPED_LINE_BREAK_PATTERN = Pattern.compile("(?<!\\\\)\n");
private static final String STORE_PATH_PROPERTY_NAME = "default.path";
private static final String STORE_PATH_DEFAULT = "archunit_store";
private static final String STORED_RULES_FILE_NAME = "stored.rules";
private static final String ALLOW_STORE_CREATION_PROPERTY_NAME = "default.allowStoreCreation";
private static final String ALLOW_STORE_CREATION_DEFAULT = "false";
private static final String ALLOW_STORE_UPDATE_PROPERTY_NAME = "default.allowStoreUpdate";
private static final String ALLOW_STORE_UPDATE_DEFAULT = "true";

private boolean storeCreationAllowed;
private boolean storeUpdateAllowed;
private File storeFolder;
private FileSyncedProperties storedRules;

@Override
public void initialize(Properties properties) {
String path = properties.getProperty("default.path", "archunit_store");
storeCreationAllowed = Boolean.parseBoolean(properties.getProperty(ALLOW_STORE_CREATION_PROPERTY_NAME, ALLOW_STORE_CREATION_DEFAULT));
storeUpdateAllowed = Boolean.parseBoolean(properties.getProperty(ALLOW_STORE_UPDATE_PROPERTY_NAME, ALLOW_STORE_UPDATE_DEFAULT));
String path = properties.getProperty(STORE_PATH_PROPERTY_NAME, STORE_PATH_DEFAULT);
storeFolder = new File(path);
ensureExistence(storeFolder);
File storedRulesFile = new File(storeFolder, "stored.rules");
File storedRulesFile = getStoredRulesFile();
log.info("Initializing {} at {}", TextFileBasedViolationStore.class.getSimpleName(), storedRulesFile.getAbsolutePath());
storedRules = new FileSyncedProperties(storedRulesFile);
checkInitialization(storedRules.initializationSuccessful(), "Cannot create rule store at %s", storedRulesFile.getAbsolutePath());
}

private File getStoredRulesFile() {
File rulesFile = new File(storeFolder, STORED_RULES_FILE_NAME);
if (!rulesFile.exists() && !storeCreationAllowed) {
throw new StoreInitializationFailedException(String.format(
"Creating new violation store is disabled (enable by configuration %s.%s=true)",
FREEZE_STORE_PROPERTY_NAME, ALLOW_STORE_CREATION_PROPERTY_NAME));
}
return rulesFile;
}

private void ensureExistence(File folder) {
checkState(folder.exists() && folder.isDirectory() || folder.mkdirs(), "Cannot create folder %s", folder.getAbsolutePath());
}
Expand All @@ -98,6 +122,11 @@ public boolean contains(ArchRule rule) {
@Override
public void save(ArchRule rule, List<String> violations) {
log.debug("Storing evaluated rule '{}' with {} violations: {}", rule.getDescription(), violations.size(), violations);
if (!storeUpdateAllowed) {
throw new StoreUpdateFailedException(String.format(
"Updating frozen violations is disabled (enable by configuration %s.%s=true)",
FREEZE_STORE_PROPERTY_NAME, ALLOW_STORE_UPDATE_PROPERTY_NAME));
}
String ruleFileName = ensureRuleFileName(rule);
write(violations, new File(storeFolder, ruleFileName));
}
Expand Down
Loading

0 comments on commit 989918f

Please sign in to comment.