Skip to content

Commit dee1074

Browse files
committed
This closes #142
2 parents 6944ac5 + 4f98035 commit dee1074

File tree

7 files changed

+585
-3
lines changed

7 files changed

+585
-3
lines changed

core/src/main/java/org/apache/brooklyn/core/objs/BasicSpecParameter.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,10 @@ private static final class ParseYamlInputs {
186186
private static final String DEFAULT_TYPE = "string";
187187
private static final Map<String, Class<?>> BUILT_IN_TYPES = ImmutableMap.<String, Class<?>>builder()
188188
.put(DEFAULT_TYPE, String.class)
189+
.put("boolean", Boolean.class)
190+
.put("byte", Byte.class)
191+
.put("char", Character.class)
192+
.put("short", Short.class)
189193
.put("integer", Integer.class)
190194
.put("long", Long.class)
191195
.put("float", Float.class)

test-framework/src/main/java/org/apache/brooklyn/test/framework/TestEffectorImpl.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@ public String get() {
101101
* {@inheritDoc}
102102
*/
103103
public void stop() {
104-
ServiceStateLogic.setExpectedState(this, Lifecycle.STOPPING);
104+
ServiceStateLogic.setExpectedState(this, Lifecycle.STOPPED);
105105
sensors().set(SERVICE_UP, false);
106106
}
107107

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
package org.apache.brooklyn.test.framework;
20+
21+
import org.apache.brooklyn.api.entity.ImplementedBy;
22+
import org.apache.brooklyn.config.ConfigKey;
23+
import org.apache.brooklyn.core.config.ConfigKeys;
24+
25+
/**
26+
* Entity that checks if a TCP endpoint is reachable.
27+
*
28+
* For example:
29+
* <pre>
30+
* {@code
31+
* services:
32+
* - type: com.acme.MyEntityUnderTest
33+
* id: entity-under-test
34+
* - type: org.apache.brooklyn.test.framework.TestCase
35+
* name: Tests
36+
* brooklyn.children:
37+
* - type: org.apache.brooklyn.test.framework.TestEndpointReachable
38+
* name: Endpoint reachable
39+
* brooklyn.config:
40+
* targetId: entity-under-test
41+
* timeout: 2m
42+
* endpointSensor: datastore.url
43+
* }
44+
* </pre>
45+
*
46+
* The sensor's value can be in a number of different formats: a string in the form of {@code ip:port}
47+
* or URI format; or a {@link com.google.common.net.HostAndPort}; or a {@link java.net.URI}; or a
48+
* {@link java.net.URL} instance.
49+
*
50+
* Alternatively an explicit endpoint can be used (e.g. constructed from other sensors of
51+
* the target entity):
52+
* <pre>
53+
* {@code
54+
* ...
55+
* - type: org.apache.brooklyn.test.framework.TestEndpointReachable
56+
* name: Endpoint reachable
57+
* brooklyn.config:
58+
* targetId: entity-under-test
59+
* timeout: 2m
60+
* endpoint:
61+
* $brooklyn:formatString:
62+
* - %s:%s"
63+
* - $brooklyn:entity("entity-under-test").attributeWhenReady("host.name")
64+
* - $brooklyn:entity("entity-under-test").attributeWhenReady("https.port")
65+
* }
66+
* </pre>
67+
*
68+
* One can also assert that the given endpoint is not reachable. Here the timeout means that at
69+
* some point within this timeout period, we expect the endpoint to become unreachable. As soon
70+
* as it is unreachable, we return:
71+
*
72+
* <pre>
73+
* {@code
74+
* ...
75+
* - type: org.apache.brooklyn.test.framework.TestEndpointReachable
76+
* name: Endpoint reachable
77+
* brooklyn.config:
78+
* targetId: entity-under-test
79+
* timeout: 2m
80+
* endpointSensor: datastore.url
81+
* assertions:
82+
* reachable: false
83+
* }
84+
* </pre>
85+
*/
86+
@ImplementedBy(value = TestEndpointReachableImpl.class)
87+
public interface TestEndpointReachable extends BaseTest {
88+
89+
ConfigKey<String> ENDPOINT = ConfigKeys.newStringConfigKey(
90+
"endpoint",
91+
"Endpoint (be it URL or host:port) to test, for tcp-reachability; mutually exclusive with 'endpointSensor'");
92+
93+
ConfigKey<Object> ENDPOINT_SENSOR = ConfigKeys.newConfigKey(
94+
Object.class,
95+
"endpointSensor",
96+
"Sensor (or name of sensor) on target that advertises the endpoint (to test for tcp-reachability); mutually exclusive with 'endpoint'");
97+
98+
/**
99+
* A key within the assertions map, to say whether we should assert that the endpoint is reachable or not reachable.
100+
* The value in the map should be a boolean. If absent, defaults to true.
101+
*/
102+
public static final String REACHABLE_KEY = "reachable";
103+
}
Lines changed: 223 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,223 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
package org.apache.brooklyn.test.framework;
20+
21+
import static org.apache.brooklyn.test.framework.TestFrameworkAssertions.getAssertions;
22+
23+
import java.net.URI;
24+
import java.net.URL;
25+
import java.util.Collection;
26+
import java.util.List;
27+
import java.util.Map;
28+
import java.util.Set;
29+
30+
import org.apache.brooklyn.api.entity.Entity;
31+
import org.apache.brooklyn.api.location.Location;
32+
import org.apache.brooklyn.api.sensor.AttributeSensor;
33+
import org.apache.brooklyn.core.entity.Attributes;
34+
import org.apache.brooklyn.core.entity.lifecycle.Lifecycle;
35+
import org.apache.brooklyn.core.entity.lifecycle.ServiceStateLogic;
36+
import org.apache.brooklyn.core.sensor.Sensors;
37+
import org.apache.brooklyn.test.Asserts;
38+
import org.apache.brooklyn.util.core.flags.TypeCoercions;
39+
import org.apache.brooklyn.util.exceptions.Exceptions;
40+
import org.apache.brooklyn.util.guava.Maybe;
41+
import org.apache.brooklyn.util.net.Networking;
42+
import org.apache.brooklyn.util.text.Strings;
43+
import org.apache.brooklyn.util.time.Duration;
44+
import org.slf4j.Logger;
45+
import org.slf4j.LoggerFactory;
46+
47+
import com.google.common.base.Objects;
48+
import com.google.common.base.Supplier;
49+
import com.google.common.collect.ImmutableMap;
50+
import com.google.common.collect.ImmutableSet;
51+
import com.google.common.collect.Lists;
52+
import com.google.common.collect.Sets;
53+
import com.google.common.net.HostAndPort;
54+
55+
/**
56+
* {@inheritDoc}
57+
*/
58+
public class TestEndpointReachableImpl extends TargetableTestComponentImpl implements TestEndpointReachable {
59+
60+
private static final Logger LOG = LoggerFactory.getLogger(TestEndpointReachableImpl.class);
61+
62+
@Override
63+
public void start(Collection<? extends Location> locations) {
64+
if (!getChildren().isEmpty()) {
65+
throw new RuntimeException(String.format("The entity [%s] cannot have child entities", getClass().getName()));
66+
}
67+
ServiceStateLogic.setExpectedState(this, Lifecycle.STARTING);
68+
final String endpoint = getConfig(ENDPOINT);
69+
final Object endpointSensor = getConfig(ENDPOINT_SENSOR);
70+
final Duration timeout = getConfig(TIMEOUT);
71+
final List<Map<String, Object>> assertions = getAssertions(this, ASSERTIONS);
72+
73+
final Entity target = resolveTarget();
74+
75+
if (endpoint == null && endpointSensor == null) {
76+
throw new RuntimeException(String.format("The entity [%s] must be configured with one of endpoint or endpointSensor", getClass().getName()));
77+
} else if (endpoint != null && endpointSensor != null) {
78+
throw new RuntimeException(String.format("The entity [%s] must be configured with only one of endpoint or endpointSensor", getClass().getName()));
79+
}
80+
81+
final Supplier<HostAndPort> supplier = new Supplier<HostAndPort>() {
82+
@Override
83+
public HostAndPort get() {
84+
Object val;
85+
if (endpoint != null) {
86+
val = endpoint;
87+
} else if (endpointSensor instanceof AttributeSensor) {
88+
val = target.sensors().get((AttributeSensor<?>)endpointSensor);
89+
} else if (endpointSensor instanceof CharSequence) {
90+
AttributeSensor<Object> sensor = Sensors.newSensor(Object.class, ((CharSequence)endpointSensor).toString());
91+
val = target.sensors().get(sensor);
92+
} else {
93+
throw new IllegalArgumentException(String.format("The entity [%s] has endpointSensor of invalid type %s [%s]", getClass().getName(), endpointSensor.getClass().getName(), endpointSensor));
94+
}
95+
return (val == null) ? null : toHostAndPort(val);
96+
}
97+
};
98+
if (endpoint != null) {
99+
// fail-fast if have a static endpoint value
100+
supplier.get();
101+
}
102+
103+
try {
104+
Asserts.succeedsEventually(ImmutableMap.of("timeout", timeout), new Runnable() {
105+
@Override
106+
public void run() {
107+
HostAndPort val = supplier.get();
108+
Asserts.assertNotNull(val);
109+
assertSucceeds(assertions, val);
110+
}});
111+
sensors().set(Attributes.SERVICE_UP, true);
112+
ServiceStateLogic.setExpectedState(this, Lifecycle.RUNNING);
113+
114+
} catch (Throwable t) {
115+
LOG.info("{} [{}] test failed", this, endpoint != null ? endpoint : endpointSensor);
116+
sensors().set(Attributes.SERVICE_UP, false);
117+
ServiceStateLogic.setExpectedState(this, Lifecycle.ON_FIRE);
118+
throw Exceptions.propagate(t);
119+
}
120+
}
121+
122+
protected void assertSucceeds(List<Map<String, Object>> assertions, HostAndPort endpoint) {
123+
Maybe<Object> checkReachableMaybe = getOnlyAssertionsValue(assertions, REACHABLE_KEY);
124+
boolean checkReachable = checkReachableMaybe.isAbsentOrNull() || Boolean.TRUE.equals(TypeCoercions.coerce(checkReachableMaybe.get(), Boolean.class));
125+
boolean reachable = Networking.isReachable(endpoint);
126+
Asserts.assertEquals(reachable, checkReachable, endpoint+" "+(reachable ? "" : "not ")+"reachable");
127+
}
128+
129+
/**
130+
* Finds the value for the given key in one of the maps (or {@link Maybe#absent()} if not found).
131+
*
132+
* @throws IllegalArgumentException if multiple conflicts values for the key, or if there are other (unexpected) keys.
133+
*/
134+
protected Maybe<Object> getOnlyAssertionsValue(List<Map<String, Object>> assertions, String key) {
135+
Maybe<Object> result = Maybe.absent();
136+
Set<String> keys = Sets.newLinkedHashSet();
137+
boolean foundConflictingDuplicate = false;
138+
if (assertions != null) {
139+
for (Map<String, Object> assertionMap : assertions) {
140+
if (assertionMap.containsKey(key)) {
141+
Object val = assertionMap.get(REACHABLE_KEY);
142+
if (result.isPresent() && !Objects.equal(result.get(), val)) {
143+
foundConflictingDuplicate = true;
144+
} else {
145+
result = Maybe.of(val);
146+
}
147+
}
148+
keys.addAll(assertionMap.keySet());
149+
}
150+
}
151+
Set<String> unhandledKeys = Sets.difference(keys, ImmutableSet.of(key));
152+
if (foundConflictingDuplicate) {
153+
throw new IllegalArgumentException("Multiple conflicting values for assertion '"+key+"' in "+this);
154+
} else if (unhandledKeys.size() > 0) {
155+
throw new IllegalArgumentException("Unknown assertions "+unhandledKeys+" in "+this);
156+
}
157+
return result;
158+
}
159+
160+
protected HostAndPort toHostAndPort(Object endpoint) {
161+
if (endpoint == null) {
162+
throw new IllegalArgumentException(String.format("The entity [%s] has no endpoint", getClass().getName()));
163+
} else if (endpoint instanceof String) {
164+
return toHostAndPort((String)endpoint);
165+
} else if (endpoint instanceof URI) {
166+
return toHostAndPort(((URI)endpoint).toString());
167+
} else if (endpoint instanceof URL) {
168+
return toHostAndPort(((URL)endpoint).toString());
169+
} else if (endpoint instanceof HostAndPort) {
170+
return (HostAndPort)endpoint;
171+
} else {
172+
throw new IllegalArgumentException(String.format("The entity [%s] has endpoint of invalid type %s [%s]", getClass().getName(), endpoint.getClass().getName(), endpoint));
173+
}
174+
}
175+
176+
protected HostAndPort toHostAndPort(String endpoint) {
177+
if (Strings.isEmpty(endpoint)) {
178+
throw new IllegalArgumentException(String.format("The entity [%s] has no endpoint", getClass().getName()));
179+
}
180+
try {
181+
URI uri = URI.create(endpoint);
182+
int port;
183+
if (uri.getPort() != -1) {
184+
port = uri.getPort();
185+
} else {
186+
if ("http".equalsIgnoreCase(uri.getScheme())) {
187+
port = 80;
188+
} else if ("https".equalsIgnoreCase(uri.getScheme())) {
189+
port = 443;
190+
} else {
191+
throw new IllegalArgumentException(String.format("The entity [%s] with endpoint [%s] has no port", getClass().getName(), endpoint));
192+
}
193+
}
194+
return HostAndPort.fromParts(uri.getHost(), port);
195+
} catch (IllegalArgumentException e) {
196+
// Not a URL; fall-back to host-and-port
197+
}
198+
199+
HostAndPort result = HostAndPort.fromString(endpoint);
200+
if (!result.hasPort()) {
201+
throw new IllegalArgumentException(String.format("The entity [%s] with endpoint [%s] has no port", getClass().getName(), endpoint));
202+
}
203+
return result;
204+
}
205+
206+
/**
207+
* {@inheritDoc}
208+
*/
209+
public void stop() {
210+
ServiceStateLogic.setExpectedState(this, Lifecycle.STOPPED);
211+
sensors().set(Attributes.SERVICE_UP, false);
212+
}
213+
214+
/**
215+
* {@inheritDoc}
216+
*/
217+
public void restart() {
218+
final Collection<Location> locations = Lists.newArrayList(getLocations());
219+
stop();
220+
start(locations);
221+
}
222+
223+
}

test-framework/src/main/java/org/apache/brooklyn/test/framework/TestHttpCallImpl.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@ public Integer get() {
107107
* {@inheritDoc}
108108
*/
109109
public void stop() {
110-
ServiceStateLogic.setExpectedState(this, Lifecycle.STOPPING);
110+
ServiceStateLogic.setExpectedState(this, Lifecycle.STOPPED);
111111
sensors().set(Attributes.SERVICE_UP, false);
112112
}
113113

test-framework/src/main/java/org/apache/brooklyn/test/framework/TestSensorImpl.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ public Object get() {
8686
* {@inheritDoc}
8787
*/
8888
public void stop() {
89-
ServiceStateLogic.setExpectedState(this, Lifecycle.STOPPING);
89+
ServiceStateLogic.setExpectedState(this, Lifecycle.STOPPED);
9090
sensors().set(SERVICE_UP, false);
9191
}
9292

0 commit comments

Comments
 (0)