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
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.