/*
* Copyright 2017 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.util.ArrayList;
import java.util.EnumSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import com.google.common.base.Joiner;
import com.google.common.base.Splitter;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.tngtech.archunit.Internal;
import com.tngtech.archunit.core.domain.JavaFieldAccess.AccessType;
import org.junit.rules.TestRule;
import org.junit.runner.Description;
import org.junit.runners.model.Statement;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.collect.Iterables.getOnlyElement;
import static com.tngtech.archunit.core.domain.JavaConstructor.CONSTRUCTOR_NAME;
import static com.tngtech.archunit.junit.MessageAssertionChain.containsConsecutiveLines;
import static com.tngtech.archunit.junit.MessageAssertionChain.containsLine;
import static com.tngtech.archunit.junit.MessageAssertionChain.matchesLine;
import static java.lang.System.lineSeparator;
import static java.util.Collections.singleton;
import static java.util.regex.Pattern.quote;
@Internal
public class ExpectedViolation implements TestRule {
private final MessageAssertionChain assertionChain = new MessageAssertionChain();
private ExpectedViolation() {
}
@Override
public Statement apply(Statement base, Description description) {
return new ExpectedViolationStatement(base);
}
public static ExpectedViolation none() {
return new ExpectedViolation();
}
public ExpectedViolation ofRule(String ruleText) {
LinkedList<String> ruleLines = new LinkedList<>(Splitter.on(lineSeparator()).splitToList(ruleText));
checkArgument(!ruleLines.isEmpty(), "Rule text may not be empty");
if (ruleLines.size() == 1) {
addSingleLineRuleAssertion(getOnlyElement(ruleLines));
} else {
addMultiLineRuleAssertion(ruleLines);
}
return this;
}
private void addSingleLineRuleAssertion(String ruleText) {
assertionChain.add(matchesLine(String.format(
"Architecture Violation .* Rule '%s' was violated.*", quote(ruleText))));
}
private void addMultiLineRuleAssertion(LinkedList<String> ruleLines) {
assertionChain.add(matchesLine(String.format(
"Architecture Violation .* Rule '%s", quote(ruleLines.pollFirst()))));
assertionChain.add(matchesLine(String.format("%s' was violated.*", quote(ruleLines.pollLast()))));
assertionChain.add(containsConsecutiveLines(ruleLines));
}
public ExpectedViolation byAccess(ExpectedFieldAccess access) {
assertionChain.add(containsLine(access.expectedMessage()));
return this;
}
public ExpectedViolation byCall(ExpectedMethodCall call) {
assertionChain.add(containsLine(call.expectedMessage()));
return this;
}
public ExpectedViolation by(MessageAssertionChain.Link assertion) {
assertionChain.add(assertion);
return this;
}
public static PackageAssertionCreator javaPackageOf(Class<?> clazz) {
return new PackageAssertionCreator(clazz);
}
@Internal
public static class PackageAssertionCreator {
private final Class<?> clazz;
private PackageAssertionCreator(Class<?> clazz) {
this.clazz = clazz;
}
public MessageAssertionChain.Link notMatching(String packageIdentifier) {
return containsLine("Class %s doesn't reside in a package '%s'", clazz.getName(), packageIdentifier);
}
}
private class ExpectedViolationStatement extends Statement {
private final Statement base;
private ExpectedViolationStatement(Statement base) {
this.base = base;
}
@Override
public void evaluate() throws Throwable {
try {
base.evaluate();
throw new NoExpectedViolationException(assertionChain);
} catch (AssertionError assertionError) {
assertionChain.evaluate(assertionError);
}
}
}
private static class NoExpectedViolationException extends RuntimeException {
private NoExpectedViolationException(MessageAssertionChain assertionChain) {
super("Rule was not violated in the expected way: Expected " + assertionChain);
}
}
public static ExpectedAccessViolationCreationProcess from(Class<?> origin, String method, Class<?>... paramTypes) {
return new ExpectedAccessViolationCreationProcess(origin, method, paramTypes);
}
@Internal
public static class ExpectedAccessViolationCreationProcess {
private Origin origin;
private ExpectedAccessViolationCreationProcess(Class<?> clazz, String method, Class<?>[] paramTypes) {
origin = new Origin(clazz, method, paramTypes);
}
public ExpectedFieldAccessViolationBuilderStep1 accessing() {
return new ExpectedFieldAccessViolationBuilderStep1(origin, AccessType.GET, AccessType.SET);
}
public ExpectedFieldAccessViolationBuilderStep1 getting() {
return new ExpectedFieldAccessViolationBuilderStep1(origin, AccessType.GET);
}
public ExpectedFieldAccessViolationBuilderStep1 setting() {
return new ExpectedFieldAccessViolationBuilderStep1(origin, AccessType.SET);
}
public ExpectedMethodCallViolationBuilder toMethod(Class<?> target, String member, Class<?>... paramTypes) {
return new ExpectedMethodCallViolationBuilder(
origin, new MethodTarget(target, member, paramTypes));
}
public ExpectedMethodCallViolationBuilder toConstructor(Class<?> target, Class<?>... paramTypes) {
return new ExpectedMethodCallViolationBuilder(
origin, new ConstructorTarget(target, paramTypes));
}
}
private abstract static class ExpectedAccessViolationBuilder {
final Origin origin;
final Target target;
private ExpectedAccessViolationBuilder(Origin origin, Target target) {
this.origin = origin;
this.target = target;
}
}
@Internal
public static class ExpectedFieldAccessViolationBuilderStep1 {
private final Origin origin;
private final ImmutableSet<AccessType> accessType;
private ExpectedFieldAccessViolationBuilderStep1(Origin origin, AccessType... accessType) {
this.origin = origin;
this.accessType = ImmutableSet.copyOf(accessType);
}
public ExpectedFieldAccessViolationBuilderStep2 field(Class<?> target, String member) {
return new ExpectedFieldAccessViolationBuilderStep2(
origin, new FieldTarget(target, member, accessType));
}
}
@Internal
public static class ExpectedFieldAccessViolationBuilderStep2 extends ExpectedAccessViolationBuilder {
private ExpectedFieldAccessViolationBuilderStep2(Origin origin, FieldTarget target) {
super(origin, target);
}
public ExpectedFieldAccess inLine(int number) {
return new ExpectedFieldAccess(origin, target, number);
}
}
@Internal
public static class ExpectedMethodCallViolationBuilder extends ExpectedAccessViolationBuilder {
private ExpectedMethodCallViolationBuilder(Origin origin, MethodTarget target) {
super(origin, target);
}
public ExpectedMethodCall inLine(int number) {
return new ExpectedMethodCall(origin, target, number);
}
}
@Internal
public abstract static class ExpectedAccess {
private final Origin origin;
private final Target target;
private final int lineNumber;
private ExpectedAccess(Origin origin, Target target, int lineNumber) {
this.origin = origin;
this.target = target;
this.lineNumber = lineNumber;
}
String expectedMessage() {
return target.messageFor(this);
}
@Override
public String toString() {
return expectedMessage();
}
}
static class ExpectedFieldAccess extends ExpectedAccess {
private ExpectedFieldAccess(Origin origin, Target target, int lineNumber) {
super(origin, target, lineNumber);
}
}
static class ExpectedMethodCall extends ExpectedAccess {
private ExpectedMethodCall(Origin origin, Target target, int lineNumber) {
super(origin, target, lineNumber);
}
}
private abstract static class Member {
private final Class<?> clazz;
private final String memberName;
final List<String> params = new ArrayList<>();
private Member(Class<?> clazz, String memberName, Class<?>[] paramTypes) {
this.clazz = clazz;
this.memberName = memberName;
for (Class<?> paramType : paramTypes) {
params.add(paramType.getName());
}
}
String lineMessage(int number) {
return String.format("(%s.java:%d)", clazz.getSimpleName(), number);
}
@Override
public String toString() {
return String.format("%s.%s", clazz.getName(), memberName);
}
}
private static class Origin extends Member {
private Origin(Class<?> clazz, String methodName, Class<?>[] paramTypes) {
super(clazz, methodName, paramTypes);
}
@Override
public String toString() {
return String.format("%s(%s)", super.toString(), Joiner.on(", ").join(params));
}
}
private abstract static class Target extends Member {
private Target(Class<?> clazz, String memberName, Class<?>[] paramTypes) {
super(clazz, memberName, paramTypes);
}
String messageFor(ExpectedAccess access) {
return String.format(template(),
access.origin, access.target, access.origin.lineMessage(access.lineNumber));
}
abstract String template();
}
private static class FieldTarget extends Target {
private final Map<Set<AccessType>, String> accessDescription = ImmutableMap.of(
singleton(AccessType.GET), "gets",
singleton(AccessType.SET), "sets",
EnumSet.of(AccessType.GET, AccessType.SET), "accesses"
);
private final String accesses;
private FieldTarget(Class<?> clazz, String memberName, ImmutableSet<AccessType> accessTypes) {
super(clazz, memberName, new Class<?>[0]);
this.accesses = accessDescription.get(accessTypes);
}
@Override
String template() {
return "Method <%s> " + accesses + " field <%s> in %s";
}
}
private static class MethodTarget extends Target {
private MethodTarget(Class<?> clazz, String memberName, Class<?>[] paramTypes) {
super(clazz, memberName, paramTypes);
}
@Override
String template() {
return "Method <%s> calls method <%s> in %s";
}
@Override
public String toString() {
return String.format("%s(%s)", super.toString(), Joiner.on(", ").join(params));
}
}
private static class ConstructorTarget extends MethodTarget {
private ConstructorTarget(Class<?> clazz, Class<?>[] paramTypes) {
super(clazz, CONSTRUCTOR_NAME, paramTypes);
}
@Override
String template() {
return "Method <%s> calls constructor <%s> in %s";
}
}
}