Skip to content

Commit

Permalink
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
moditect#27 Adding cycle detection to architecture validation
Browse files Browse the repository at this point in the history
gunnarmorling committed Jan 27, 2019

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
1 parent 1202103 commit ff74950
Showing 17 changed files with 441 additions and 7 deletions.
Original file line number Diff line number Diff line change
@@ -48,6 +48,7 @@ public PackageReferenceHandler getPackageReferenceHandler(JavaFileManager jfm, D
configSupplier.get(),
options.getReportingPolicy(),
options.getUnconfiguredPackageReportingPolicy(),
options.getCycleReportingPolicy(),
options.createDotFile(),
log
);
Original file line number Diff line number Diff line change
@@ -36,6 +36,7 @@ public class DotSerializer implements ModelSerializer {
private final SortedSet<String> allPackages;
private final SortedMap<String, SortedSet<String>> allowedReads;
private final SortedMap<String, SortedSet<String>> disallowedReads;
private final SortedMap<String, SortedSet<String>> cycleReads;
private final SortedMap<String, SortedSet<String>> unknownReads;

public DotSerializer() {
@@ -46,6 +47,7 @@ public DotSerializer() {
allPackages = new TreeSet<>();
allowedReads = new TreeMap<>();
disallowedReads = new TreeMap<>();
cycleReads = new TreeMap<>();
unknownReads = new TreeMap<>();
}

@@ -59,6 +61,9 @@ public void addComponent(Component component) {
SortedSet<String> disallowed = new TreeSet<>();
disallowedReads.put(component.getName(), disallowed);

SortedSet<String> cycle = new TreeSet<>();
cycleReads.put(component.getName(), cycle);

SortedSet<String> unknown = new TreeSet<>();
unknownReads.put(component.getName(), unknown);

@@ -72,6 +77,9 @@ public void addComponent(Component component) {
else if (referencedPackage.getValue() == ReadKind.DISALLOWED) {
disallowed.add(referencedPackageName);
}
else if (referencedPackage.getValue() == ReadKind.CYCLE) {
cycle.add(referencedPackageName);
}
else {
unknown.add(referencedPackageName);
}
@@ -90,6 +98,7 @@ public String serialize() {

addSubGraph(sb, allowedReads, "Allowed", null);
addSubGraph(sb, disallowedReads, "Disallowed", "red");
addSubGraph(sb, cycleReads, "Cycle", "purple");
addSubGraph(sb, unknownReads, "Unknown", "yellow");

sb.append("}");
Original file line number Diff line number Diff line change
@@ -18,6 +18,7 @@
import java.io.IOException;
import java.io.Writer;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

@@ -26,6 +27,8 @@
import javax.tools.StandardLocation;

import org.moditect.deptective.internal.export.DotSerializer;
import org.moditect.deptective.internal.graph.Cycle;
import org.moditect.deptective.internal.graph.GraphUtils;
import org.moditect.deptective.internal.log.DeptectiveMessages;
import org.moditect.deptective.internal.log.Log;
import org.moditect.deptective.internal.model.Component;
@@ -50,23 +53,25 @@ public class PackageReferenceValidator implements PackageReferenceHandler {
private final PackageDependencies allowedPackageDependencies;
private final JavaFileManager jfm;
private final ReportingPolicy reportingPolicy;
private boolean createDotFile;
private final ReportingPolicy unconfiguredPackageReportingPolicy;
private final ReportingPolicy cycleReportingPolicy;
private final Map<String, Boolean> reportedUnconfiguredPackages;

private final PackageDependencies.Builder actualPackageDependencies;

private boolean createDotFile;
private String currentPackageName;
private Component currentComponent;

public PackageReferenceValidator(JavaFileManager jfm, PackageDependencies packageDependencies,
ReportingPolicy reportingPolicy, ReportingPolicy unconfiguredPackageReportingPolicy, boolean createDotFile,
ReportingPolicy reportingPolicy, ReportingPolicy unconfiguredPackageReportingPolicy,
ReportingPolicy cycleReportingPolicy, boolean createDotFile,
Log log) {
this.log = log;
this.allowedPackageDependencies = packageDependencies;
this.jfm = jfm;
this.reportingPolicy = reportingPolicy;
this.unconfiguredPackageReportingPolicy = unconfiguredPackageReportingPolicy;
this.cycleReportingPolicy = cycleReportingPolicy;
this.reportedUnconfiguredPackages = new HashMap<>();
this.actualPackageDependencies = PackageDependencies.builder();
this.createDotFile = createDotFile;
@@ -170,13 +175,36 @@ else if (currentComponent.allowedToRead(referencedComponent)) {
public void onCompletingCompilation() {
log.useSource(null);

List<Cycle<Component>> cycles = GraphUtils.detectCycles(allowedPackageDependencies.getComponents());

if (!cycles.isEmpty()) {
String cyclesAsString = "- " + cycles.stream()
.map(Cycle::toString)
.collect(Collectors.joining("," + System.lineSeparator() + "- "));

log.report(cycleReportingPolicy, DeptectiveMessages.CYCLE_IN_ARCHITECTURE, cyclesAsString);
}

if (!createDotFile) {
return;
}

if (!cycles.isEmpty()) {
for (Component.Builder component : actualPackageDependencies.getComponents()) {
for (Cycle<Component> cycle : cycles) {
if (contains(cycle, component.getName())) {
for (Component nodeInCycle : cycle.getNodes()) {
if (component.getReads().containsKey(nodeInCycle.getName())) {
component.addRead(nodeInCycle.getName(), ReadKind.CYCLE);
}
}
}
}
}
}

DotSerializer serializer = new DotSerializer();
PackageDependencies build = actualPackageDependencies.build();
build.serialize(serializer);
actualPackageDependencies.build().serialize(serializer);

try {
FileObject output = jfm.getFileForOutput(StandardLocation.CLASS_OUTPUT, "", "deptective.dot", null);
@@ -190,6 +218,16 @@ public void onCompletingCompilation() {
}
}

private boolean contains(Cycle<Component> cycle, String name) {
for (Component component : cycle.getNodes()) {
if (component.getName().equals(name)) {
return true;
}
}

return false;
}

private boolean isIgnoredDependency(String referencedPackageName) {
return "java.lang".equals(referencedPackageName) ||
allowedPackageDependencies.isWhitelisted(referencedPackageName) ||
Original file line number Diff line number Diff line change
@@ -29,6 +29,7 @@ public class DeptectiveMessages extends ListResourceBundle {
public static final String GENERATED_CONFIG = "deptective.generatedconfig";
public static final String GENERATED_DOT_REPRESENTATION = "deptective.dotrepresentation";
public static final String PACKAGE_CONTAINED_IN_MULTIPLE_COMPONENTS = "deptective.packageinmultiplecomponents";
public static final String CYCLE_IN_ARCHITECTURE = "deptective.cycleinarchitecture";

@Override
protected final Object[][] getContents() {
@@ -43,6 +44,12 @@ protected final Object[][] getContents() {
"Created DOT file representing the Deptective configuration at {0}" },
{ ERROR_PREFIX + PACKAGE_CONTAINED_IN_MULTIPLE_COMPONENTS,
"Multiple components match package {1}: {0}" },
{ ERROR_PREFIX + CYCLE_IN_ARCHITECTURE,
"Architecture model contains cycle(s) between these components: " + System.lineSeparator()
+ "{0}" },
{ WARNING_PREFIX + CYCLE_IN_ARCHITECTURE,
"Architecture model contains cycle(s) between these components: " + System.lineSeparator()
+ "{0}" },
};
}

Original file line number Diff line number Diff line change
@@ -22,6 +22,9 @@
import java.util.Map;
import java.util.Set;

import org.moditect.deptective.internal.graph.Dependency;
import org.moditect.deptective.internal.graph.Node;

/**
* Describes a component, a set of packages identified by one more naming patterns.
* <p>
@@ -30,7 +33,7 @@
*
* @author Gunnar Morling
*/
public class Component {
public class Component implements Node<Component> {

public static class Builder {

@@ -76,6 +79,10 @@ public Component build() {
public Map<String, ReadKind> getReads() {
return reads;
}

public String getName() {
return name;
}
}

private final String name;
@@ -122,4 +129,51 @@ public Map<String, ReadKind> getReads() {
public String toString() {
return name + " { contained=" + contained + ", reads=" + reads + "] }";
}

@Override
public String asShortString() {
return name;
}

@Override
public Dependency<Component> getOutgoingDependencyTo(Component node) {
return reads.entrySet()
.stream()
.filter(e -> e.getKey().equals(node.getName()))
.map(e -> new Dependency<Component>(Component.builder(e.getKey()).build(), 1))
.findFirst()
.orElse(null);
}

@Override
public boolean hasOutgoingDependencies() {
return !reads.isEmpty();
}

@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((name == null) ? 0 : name.hashCode());
return result;
}

@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
Component other = (Component) obj;
if (name == null) {
if (other.name != null)
return false;
}
else if (!name.equals(other.name))
return false;
return true;
}

}
Original file line number Diff line number Diff line change
@@ -75,6 +75,10 @@ public void addWhitelistedPackage(PackagePattern pattern) {
.removeIf(r -> pattern.matches(r.getKey()));
}
}

public Iterable<Component.Builder> getComponents() {
return componentsByName.values();
}
}

private final Components components;
@@ -142,4 +146,8 @@ public boolean isWhitelisted(String packageName) {
.findFirst()
.isPresent();
}

public Iterable<Component> getComponents() {
return components;
}
}
Original file line number Diff line number Diff line change
@@ -16,12 +16,17 @@
package org.moditect.deptective.internal.model;

/**
* Describes the relationship beween two components.
* Describes the relationship between two components.
*
* @author Gunnar Morling
*/
public enum ReadKind {
ALLOWED,
DISALLOWED,

/**
* The relationship is part of a cycle involving the two connected components.
*/
CYCLE,
UKNOWN;
}
Original file line number Diff line number Diff line change
@@ -81,6 +81,20 @@ public ReportingPolicy getUnconfiguredPackageReportingPolicy() {
}
}

/**
* Returns the policy for reporting cycles between components.
*/
public ReportingPolicy getCycleReportingPolicy() {
String policy = options.get("deptective.cycle_reporting_policy");

if (policy != null) {
return ReportingPolicy.valueOf(policy.trim().toUpperCase());
}
else {
return ReportingPolicy.ERROR;
}
}

/**
* Returns the task to be performed by the plug-in.
*/
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
/**
* Copyright 2019 The ModiTect authors
*
* 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 org.moditect.deptective.plugintest.cycle;

import static com.google.testing.compile.CompilationSubject.assertThat;
import static org.assertj.core.api.Assertions.assertThat;

import java.util.Optional;

import javax.tools.JavaFileObject;
import javax.tools.StandardLocation;

import org.junit.Test;
import org.moditect.deptective.internal.util.Strings;
import org.moditect.deptective.plugintest.PluginTestBase;
import org.moditect.deptective.plugintest.cycle.abc.Abc;
import org.moditect.deptective.plugintest.cycle.bar.Bar;
import org.moditect.deptective.plugintest.cycle.baz.Baz;
import org.moditect.deptective.plugintest.cycle.def.Def;
import org.moditect.deptective.plugintest.cycle.foo.Foo;
import org.moditect.deptective.plugintest.cycle.qux.Qux;

import com.google.testing.compile.Compilation;
import com.google.testing.compile.Compiler;

public class CycleTest extends PluginTestBase {

@Test
public void shouldDetectCyclesInArchitectureModel() {
Compilation compilation = Compiler.javac()
.withOptions(
"-Xplugin:Deptective",
getConfigFileOption()
)
.compile(
forTestClass(Foo.class),
forTestClass(Bar.class),
forTestClass(Baz.class),
forTestClass(Qux.class),
forTestClass(Abc.class),
forTestClass(Def.class)

);

assertThat(compilation).failed();
assertThat(compilation).hadErrorContaining("Architecture model contains cycle(s) between these components:");
assertThat(compilation).hadErrorContaining(" - bar, baz, foo, qux");
assertThat(compilation).hadErrorContaining(" - abc, def");
}

@Test
public void shouldVisualizeCyclesInArchitectureModel() throws Exception {
Compilation compilation = Compiler.javac()
.withOptions(
"-Xplugin:Deptective",
"-Adeptective.visualize=true",
"-Adeptective.cycle_reporting_policy=WARN",
getConfigFileOption()
)
.compile(
forTestClass(Foo.class),
forTestClass(Bar.class),
forTestClass(Baz.class),
forTestClass(Qux.class),
forTestClass(Abc.class),
forTestClass(Def.class)
);

assertThat(compilation).succeeded();
assertThat(compilation).hadWarningContaining("Architecture model contains cycle(s) between these components:");
assertThat(compilation).hadWarningContaining(" - bar, baz, foo, qux");
assertThat(compilation).hadWarningContaining(" - abc, def");

assertThat(compilation).hadNoteContaining(
"Created DOT file representing the Deptective configuration at mem:///CLASS_OUTPUT/deptective.dot"
);
assertThat(compilation).hadNoteCount(1);

String expectedConfig = Strings.lines(
"digraph \"package dependencies\"",
"{",
" \"abc\";",
" \"bar\";",
" \"baz\";",
" \"def\";",
" \"foo\";",
" \"qux\";",
" subgraph Cycle {",
" edge [color=purple]",
" \"abc\" -> \"def\";",
" \"bar\" -> \"baz\";",
" \"bar\" -> \"qux\";",
" \"baz\" -> \"foo\";",
" \"def\" -> \"abc\";",
" \"foo\" -> \"bar\";",
" \"qux\" -> \"bar\";",
" }",
"}"
);

Optional<JavaFileObject> deptectiveFile = compilation
.generatedFile(StandardLocation.CLASS_OUTPUT, "deptective.dot");
assertThat(deptectiveFile.isPresent()).isTrue();
String generatedConfig = Strings.readToString(deptectiveFile.get().openInputStream());

assertThat(generatedConfig).isEqualTo(expectedConfig);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/**
* Copyright 2019 The ModiTect authors
*
* 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 org.moditect.deptective.plugintest.cycle.abc;

import org.moditect.deptective.plugintest.cycle.def.Def;

public class Abc {

Def def;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/**
* Copyright 2019 The ModiTect authors
*
* 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 org.moditect.deptective.plugintest.cycle.bar;

import org.moditect.deptective.plugintest.cycle.baz.Baz;
import org.moditect.deptective.plugintest.cycle.qux.Qux;

public class Bar {

Baz baz;
Qux qux;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/**
* Copyright 2019 The ModiTect authors
*
* 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 org.moditect.deptective.plugintest.cycle.baz;

import org.moditect.deptective.plugintest.cycle.foo.Foo;

public class Baz {

private Foo foo;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/**
* Copyright 2019 The ModiTect authors
*
* 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 org.moditect.deptective.plugintest.cycle.def;

import org.moditect.deptective.plugintest.cycle.abc.Abc;

public class Def {

Abc abc;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/**
* Copyright 2019 The ModiTect authors
*
* 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 org.moditect.deptective.plugintest.cycle.foo;

import org.moditect.deptective.plugintest.cycle.bar.Bar;

public class Foo {

private final Bar bar = new Bar();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/**
* Copyright 2019 The ModiTect authors
*
* 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 org.moditect.deptective.plugintest.cycle.qux;

import org.moditect.deptective.plugintest.cycle.bar.Bar;

public class Qux {

Bar bar;
}
Original file line number Diff line number Diff line change
@@ -15,6 +15,9 @@
*/
package org.moditect.deptective.plugintest.whitelist.bar;

import org.moditect.deptective.plugintest.whitelist.foo.Foo;

public class Bar {

Foo f;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
{
"components" : [
{
"name" : "foo",
"contains" : [ "org.moditect.deptective.plugintest.cycle.foo" ],
"reads" : [ "bar" ]
},
{
"name" : "bar",
"contains" : [ "org.moditect.deptective.plugintest.cycle.bar" ],
"reads" : [ "baz", "qux" ]
},
{
"name" : "baz",
"contains" : [ "org.moditect.deptective.plugintest.cycle.baz" ],
"reads" : [ "foo" ]
},
{
"name" : "qux",
"contains" : [ "org.moditect.deptective.plugintest.cycle.qux" ],
"reads" : [ "bar" ]
},
{
"name" : "abc",
"contains" : [ "org.moditect.deptective.plugintest.cycle.abc" ],
"reads" : [ "def" ]
},
{
"name" : "def",
"contains" : [ "org.moditect.deptective.plugintest.cycle.def" ],
"reads" : [ "abc" ]
}
]
}

0 comments on commit ff74950

Please sign in to comment.