Commit 0610378d authored by Greg Turnquist's avatar Greg Turnquist Committed by Dave Syer

Resolves #127: Prevent duplicate report outcomes

The collection of outcomes is a list. Sometimes a race condition causes to instances
of the same outcome to get added to the list shown in the report. By replacing this
with a set and propery equals/hashCode, duplicates are prevented from appearing
in the report.

I added test cases to prove that that POJO is properly managed inside a Set and also
to show that duplicates don't appear in the final report.
parent c71322a0
...@@ -16,13 +16,7 @@ ...@@ -16,13 +16,7 @@
package org.springframework.boot.autoconfigure; package org.springframework.boot.autoconfigure;
import java.util.ArrayList; import java.util.*;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.SortedMap;
import java.util.TreeMap;
import org.springframework.beans.factory.BeanFactory; import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.NoSuchBeanDefinitionException; import org.springframework.beans.factory.NoSuchBeanDefinitionException;
...@@ -40,9 +34,7 @@ import org.springframework.context.annotation.Condition; ...@@ -40,9 +34,7 @@ import org.springframework.context.annotation.Condition;
public class AutoConfigurationReport { public class AutoConfigurationReport {
private static final String BEAN_NAME = "autoConfigurationReport"; private static final String BEAN_NAME = "autoConfigurationReport";
private final SortedMap<String, ConditionAndOutcomes> outcomes = new TreeMap<String, ConditionAndOutcomes>(); private final SortedMap<String, ConditionAndOutcomes> outcomes = new TreeMap<String, ConditionAndOutcomes>();
private AutoConfigurationReport parent; private AutoConfigurationReport parent;
/** /**
...@@ -115,7 +107,7 @@ public class AutoConfigurationReport { ...@@ -115,7 +107,7 @@ public class AutoConfigurationReport {
*/ */
public static class ConditionAndOutcomes implements Iterable<ConditionAndOutcome> { public static class ConditionAndOutcomes implements Iterable<ConditionAndOutcome> {
private List<ConditionAndOutcome> outcomes = new ArrayList<ConditionAndOutcome>(); private Set<ConditionAndOutcome> outcomes = new HashSet<ConditionAndOutcome>();
public void add(Condition condition, ConditionOutcome outcome) { public void add(Condition condition, ConditionOutcome outcome) {
this.outcomes.add(new ConditionAndOutcome(condition, outcome)); this.outcomes.add(new ConditionAndOutcome(condition, outcome));
...@@ -135,7 +127,7 @@ public class AutoConfigurationReport { ...@@ -135,7 +127,7 @@ public class AutoConfigurationReport {
@Override @Override
public Iterator<ConditionAndOutcome> iterator() { public Iterator<ConditionAndOutcome> iterator() {
return Collections.unmodifiableList(this.outcomes).iterator(); return Collections.unmodifiableSet(this.outcomes).iterator();
} }
} }
...@@ -146,7 +138,6 @@ public class AutoConfigurationReport { ...@@ -146,7 +138,6 @@ public class AutoConfigurationReport {
public static class ConditionAndOutcome { public static class ConditionAndOutcome {
private final Condition condition; private final Condition condition;
private final ConditionOutcome outcome; private final ConditionOutcome outcome;
public ConditionAndOutcome(Condition condition, ConditionOutcome outcome) { public ConditionAndOutcome(Condition condition, ConditionOutcome outcome) {
...@@ -161,6 +152,31 @@ public class AutoConfigurationReport { ...@@ -161,6 +152,31 @@ public class AutoConfigurationReport {
public ConditionOutcome getOutcome() { public ConditionOutcome getOutcome() {
return this.outcome; return this.outcome;
} }
@Override
public boolean equals(Object o) {
if (this == o)
return true;
if (o == null || getClass() != o.getClass())
return false;
if (this.outcome == null || this.outcome.getMessage() == null) {
return false;
}
ConditionAndOutcome that = (ConditionAndOutcome) o;
if (that.getOutcome() == null || this.getOutcome().getMessage() == null) {
return false;
}
return this.getOutcome().getMessage().equals(that.getOutcome().getMessage());
}
@Override
public int hashCode() {
return outcome != null && outcome.getMessage() != null ? outcome.getMessage().hashCode() : 0;
}
} }
} }
...@@ -16,8 +16,10 @@ ...@@ -16,8 +16,10 @@
package org.springframework.boot.autoconfigure; package org.springframework.boot.autoconfigure;
import java.util.HashSet;
import java.util.Iterator; import java.util.Iterator;
import java.util.Map; import java.util.Map;
import java.util.Set;
import org.junit.Before; import org.junit.Before;
import org.junit.Test; import org.junit.Test;
...@@ -29,17 +31,19 @@ import org.springframework.beans.factory.support.DefaultListableBeanFactory; ...@@ -29,17 +31,19 @@ import org.springframework.beans.factory.support.DefaultListableBeanFactory;
import org.springframework.boot.autoconfigure.AutoConfigurationReport.ConditionAndOutcome; import org.springframework.boot.autoconfigure.AutoConfigurationReport.ConditionAndOutcome;
import org.springframework.boot.autoconfigure.AutoConfigurationReport.ConditionAndOutcomes; import org.springframework.boot.autoconfigure.AutoConfigurationReport.ConditionAndOutcomes;
import org.springframework.boot.autoconfigure.condition.ConditionOutcome; import org.springframework.boot.autoconfigure.condition.ConditionOutcome;
import org.springframework.boot.autoconfigure.web.MultipartAutoConfiguration;
import org.springframework.boot.autoconfigure.web.WebMvcAutoConfiguration; import org.springframework.boot.autoconfigure.web.WebMvcAutoConfiguration;
import org.springframework.context.annotation.AnnotationConfigApplicationContext; import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Condition; import org.springframework.context.annotation.Condition;
import org.springframework.context.annotation.Import; import org.springframework.context.annotation.Import;
import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.*;
import static org.hamcrest.Matchers.not; import static org.junit.Assert.assertEquals;
import static org.hamcrest.Matchers.nullValue;
import static org.hamcrest.Matchers.sameInstance;
import static org.junit.Assert.assertThat; import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue;
import static org.mockito.BDDMockito.given; import static org.mockito.BDDMockito.given;
import static org.mockito.Matchers.eq;
import static org.mockito.Mockito.mock;
/** /**
* Tests for {@link AutoConfigurationReport}. * Tests for {@link AutoConfigurationReport}.
...@@ -88,8 +92,7 @@ public class AutoConfigurationReportTests { ...@@ -88,8 +92,7 @@ public class AutoConfigurationReportTests {
@Test @Test
public void parent() throws Exception { public void parent() throws Exception {
this.beanFactory.setParentBeanFactory(new DefaultListableBeanFactory()); this.beanFactory.setParentBeanFactory(new DefaultListableBeanFactory());
AutoConfigurationReport.get((ConfigurableListableBeanFactory) this.beanFactory AutoConfigurationReport.get((ConfigurableListableBeanFactory) this.beanFactory.getParentBeanFactory());
.getParentBeanFactory());
assertThat(this.report, assertThat(this.report,
sameInstance(AutoConfigurationReport.get(this.beanFactory))); sameInstance(AutoConfigurationReport.get(this.beanFactory)));
assertThat(this.report, not(nullValue())); assertThat(this.report, not(nullValue()));
...@@ -98,12 +101,15 @@ public class AutoConfigurationReportTests { ...@@ -98,12 +101,15 @@ public class AutoConfigurationReportTests {
@Test @Test
public void recordConditionEvaluations() throws Exception { public void recordConditionEvaluations() throws Exception {
given(this.outcome1.getMessage()).willReturn("Message 1");
given(this.outcome2.getMessage()).willReturn("Message 2");
given(this.outcome3.getMessage()).willReturn("Message 3");
this.report.recordConditionEvaluation("a", this.condition1, this.outcome1); this.report.recordConditionEvaluation("a", this.condition1, this.outcome1);
this.report.recordConditionEvaluation("a", this.condition2, this.outcome2); this.report.recordConditionEvaluation("a", this.condition2, this.outcome2);
this.report.recordConditionEvaluation("b", this.condition3, this.outcome3); this.report.recordConditionEvaluation("b", this.condition3, this.outcome3);
Map<String, ConditionAndOutcomes> map = this.report Map<String, ConditionAndOutcomes> map = this.report.getConditionAndOutcomesBySource();
.getConditionAndOutcomesBySource();
assertThat(map.size(), equalTo(2)); assertThat(map.size(), equalTo(2));
Iterator<ConditionAndOutcome> iterator = map.get("a").iterator(); Iterator<ConditionAndOutcome> iterator = map.get("a").iterator();
ConditionAndOutcome conditionAndOutcome = iterator.next(); ConditionAndOutcome conditionAndOutcome = iterator.next();
...@@ -146,16 +152,102 @@ public class AutoConfigurationReportTests { ...@@ -146,16 +152,102 @@ public class AutoConfigurationReportTests {
@Test @Test
@SuppressWarnings("resource") @SuppressWarnings("resource")
public void springBootConditionPopulatesReport() throws Exception { public void springBootConditionPopulatesReport() throws Exception {
AutoConfigurationReport report = AutoConfigurationReport AutoConfigurationReport report = AutoConfigurationReport.get(new AnnotationConfigApplicationContext(
.get(new AnnotationConfigApplicationContext(Config.class) Config.class).getBeanFactory());
.getBeanFactory());
assertThat(report.getConditionAndOutcomesBySource().size(), not(equalTo(0))); assertThat(report.getConditionAndOutcomesBySource().size(), not(equalTo(0)));
} }
@Test
public void testDuplicateConditionAndOutcomes() {
Condition condition1 = mock(Condition.class);
ConditionOutcome conditionOutcome1 = mock(ConditionOutcome.class);
given(conditionOutcome1.getMessage()).willReturn("This is message 1");
Condition condition2 = mock(Condition.class);
ConditionOutcome conditionOutcome2 = mock(ConditionOutcome.class);
given(conditionOutcome2.getMessage()).willReturn("This is message 2");
Condition condition3 = mock(Condition.class);
ConditionOutcome conditionOutcome3 = mock(ConditionOutcome.class);
given(conditionOutcome3.getMessage()).willReturn("This is message 2"); // identical in value to #2
ConditionAndOutcome outcome1 = new ConditionAndOutcome(condition1,
conditionOutcome1);
assertThat(outcome1, equalTo(outcome1));
ConditionAndOutcome outcome2 = new ConditionAndOutcome(condition2,
conditionOutcome2);
assertThat(outcome1, not(equalTo(outcome2)));
ConditionAndOutcome outcome3 = new ConditionAndOutcome(condition3,
conditionOutcome3);
assertThat(outcome2, equalTo(outcome3));
Set<ConditionAndOutcome> set = new HashSet<ConditionAndOutcome>();
set.add(outcome1);
set.add(outcome2);
set.add(outcome3);
assertThat(set.size(), equalTo(2));
ConditionAndOutcomes outcomes = new ConditionAndOutcomes();
outcomes.add(condition1, conditionOutcome1);
outcomes.add(condition2, conditionOutcome2);
outcomes.add(condition3, conditionOutcome3);
int i = 0;
Iterator<ConditionAndOutcome> iterator = outcomes.iterator();
while (iterator.hasNext()) {
i++;
iterator.next();
}
assertThat(i, equalTo(2));
}
@Test
public void duplicateOutcomes() {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(
DuplicateConfig.class);
AutoConfigurationReport report = AutoConfigurationReport.get(context.getBeanFactory());
String autoconfigKey = "org.springframework.boot.autoconfigure.web.MultipartAutoConfiguration";
assertThat(report.getConditionAndOutcomesBySource().keySet(),
hasItem(autoconfigKey));
ConditionAndOutcomes conditionAndOutcomes = report.getConditionAndOutcomesBySource().get(
autoconfigKey);
Iterator<ConditionAndOutcome> iterator = conditionAndOutcomes.iterator();
int i = 0;
boolean foundConditionalOnClass = false;
boolean foundConditionalOnBean = false;
while (iterator.hasNext()) {
ConditionAndOutcome conditionAndOutcome = iterator.next();
if (conditionAndOutcome.getOutcome().getMessage().contains(
"@ConditionalOnClass classes found: javax.servlet.Servlet,org.springframework.web.multipart.support.StandardServletMultipartResolver")) {
foundConditionalOnClass = true;
}
if (conditionAndOutcome.getOutcome().getMessage().contains(
"@ConditionalOnBean (types: javax.servlet.MultipartConfigElement; SearchStrategy: all) found no beans")) {
foundConditionalOnBean = true;
}
i++;
}
assertThat(i, equalTo(2));
assertTrue(foundConditionalOnClass);
assertTrue(foundConditionalOnBean);
}
@Configurable @Configurable
@Import(WebMvcAutoConfiguration.class) @Import(WebMvcAutoConfiguration.class)
static class Config { static class Config {
} }
@Configurable
@Import(MultipartAutoConfiguration.class)
static class DuplicateConfig {
}
} }
...@@ -216,4 +216,5 @@ public abstract class AbstractJpaAutoConfigurationTests { ...@@ -216,4 +216,5 @@ public abstract class AbstractJpaAutoConfigurationTests {
static class CustomJpaTransactionManager extends JpaTransactionManager { static class CustomJpaTransactionManager extends JpaTransactionManager {
} }
} }
...@@ -18,7 +18,6 @@ package org.springframework.boot.context.embedded; ...@@ -18,7 +18,6 @@ package org.springframework.boot.context.embedded;
import java.io.FileWriter; import java.io.FileWriter;
import java.io.IOException; import java.io.IOException;
import java.net.SocketException;
import java.net.URI; import java.net.URI;
import java.net.URISyntaxException; import java.net.URISyntaxException;
import java.nio.charset.Charset; import java.nio.charset.Charset;
...@@ -99,7 +98,7 @@ public abstract class AbstractEmbeddedServletContainerFactoryTests { ...@@ -99,7 +98,7 @@ public abstract class AbstractEmbeddedServletContainerFactoryTests {
this.container = factory this.container = factory
.getEmbeddedServletContainer(exampleServletRegistration()); .getEmbeddedServletContainer(exampleServletRegistration());
this.container.start(); this.container.start();
this.thrown.expect(SocketException.class); this.thrown.expect(IOException.class);
getResponse("http://localhost:8080/hello"); getResponse("http://localhost:8080/hello");
} }
...@@ -110,7 +109,7 @@ public abstract class AbstractEmbeddedServletContainerFactoryTests { ...@@ -110,7 +109,7 @@ public abstract class AbstractEmbeddedServletContainerFactoryTests {
.getEmbeddedServletContainer(exampleServletRegistration()); .getEmbeddedServletContainer(exampleServletRegistration());
this.container.start(); this.container.start();
this.container.stop(); this.container.stop();
this.thrown.expect(SocketException.class); this.thrown.expect(IOException.class);
getResponse("http://localhost:8080/hello"); getResponse("http://localhost:8080/hello");
} }
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment