/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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.apache.brooklyn.feed.function;
import static org.testng.Assert.assertEquals;
import static org.testng.Assert.assertNotEquals;
import static org.testng.Assert.assertTrue;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.atomic.AtomicInteger;
import javax.annotation.Nullable;
import org.apache.brooklyn.api.entity.EntityLocal;
import org.apache.brooklyn.api.entity.EntitySpec;
import org.apache.brooklyn.api.location.Location;
import org.apache.brooklyn.api.sensor.AttributeSensor;
import org.apache.brooklyn.api.sensor.Feed;
import org.apache.brooklyn.api.sensor.SensorEvent;
import org.apache.brooklyn.api.sensor.SensorEventListener;
import org.apache.brooklyn.core.entity.EntityInternal;
import org.apache.brooklyn.core.entity.EntityInternal.FeedSupport;
import org.apache.brooklyn.core.sensor.Sensors;
import org.apache.brooklyn.core.test.BrooklynAppUnitTestSupport;
import org.apache.brooklyn.core.test.entity.TestEntity;
import org.apache.brooklyn.feed.function.FunctionFeed;
import org.apache.brooklyn.feed.function.FunctionFeedTest;
import org.apache.brooklyn.feed.function.FunctionPollConfig;
import org.apache.brooklyn.test.Asserts;
import org.apache.brooklyn.test.EntityTestUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.testng.Assert;
import org.testng.annotations.AfterMethod;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test;
import org.apache.brooklyn.location.localhost.LocalhostMachineProvisioningLocation;
import com.google.common.base.Function;
import com.google.common.base.Functions;
import com.google.common.base.Predicates;
import com.google.common.base.Suppliers;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;
import com.google.common.util.concurrent.Callables;
public class FunctionFeedTest extends BrooklynAppUnitTestSupport {
private static final Logger log = LoggerFactory.getLogger(FunctionFeedTest.class);
final static AttributeSensor<String> SENSOR_STRING = Sensors.newStringSensor("aString", "");
final static AttributeSensor<Integer> SENSOR_INT = Sensors.newIntegerSensor("aLong", "");
private Location loc;
private EntityLocal entity;
private FunctionFeed feed;
@BeforeMethod(alwaysRun=true)
@Override
public void setUp() throws Exception {
super.setUp();
loc = new LocalhostMachineProvisioningLocation();
entity = app.createAndManageChild(EntitySpec.create(TestEntity.class));
app.start(ImmutableList.of(loc));
}
@AfterMethod(alwaysRun=true)
@Override
public void tearDown() throws Exception {
if (feed != null) feed.stop();
super.tearDown();
}
@Test
public void testPollsFunctionRepeatedlyToSetAttribute() throws Exception {
feed = FunctionFeed.builder()
.entity(entity)
.poll(new FunctionPollConfig<Integer,Integer>(SENSOR_INT)
.period(1)
.callable(new IncrementingCallable())
//.onSuccess((Function<Object,Integer>)(Function)Functions.identity()))
)
.build();
Asserts.succeedsEventually(new Runnable() {
@Override
public void run() {
Integer val = entity.getAttribute(SENSOR_INT);
assertTrue(val != null && val > 2, "val=" + val);
}
});
}
@Test
public void testFeedDeDupe() throws Exception {
testPollsFunctionRepeatedlyToSetAttribute();
entity.addFeed(feed);
log.info("Feed 0 is: "+feed);
Feed feed0 = feed;
testPollsFunctionRepeatedlyToSetAttribute();
entity.addFeed(feed);
log.info("Feed 1 is: "+feed);
Feed feed1 = feed;
Assert.assertFalse(feed1==feed0);
FeedSupport feeds = ((EntityInternal)entity).feeds();
Assert.assertEquals(feeds.getFeeds().size(), 1, "Wrong feed count: "+feeds.getFeeds());
// a couple extra checks, compared to the de-dupe test in other *FeedTest classes
Feed feedAdded = Iterables.getOnlyElement(feeds.getFeeds());
Assert.assertTrue(feedAdded==feed1);
Assert.assertFalse(feedAdded==feed0);
}
@Test
public void testFeedDeDupeIgnoresSameObject() throws Exception {
testPollsFunctionRepeatedlyToSetAttribute();
entity.addFeed(feed);
assertFeedIsPolling();
entity.addFeed(feed);
assertFeedIsPollingContinuously();
}
@Test
public void testCallsOnSuccessWithResultOfCallable() throws Exception {
feed = FunctionFeed.builder()
.entity(entity)
.poll(new FunctionPollConfig<Integer, Integer>(SENSOR_INT)
.period(1)
.callable(Callables.returning(123))
.onSuccess(new AddOneFunction()))
.build();
EntityTestUtils.assertAttributeEqualsEventually(entity, SENSOR_INT, 124);
}
@Test
public void testCallsOnExceptionWithExceptionFromCallable() throws Exception {
final String errMsg = "my err msg";
feed = FunctionFeed.builder()
.entity(entity)
.poll(new FunctionPollConfig<Object, String>(SENSOR_STRING)
.period(1)
.callable(new ExceptionCallable(errMsg))
.onException(new ToStringFunction()))
.build();
Asserts.succeedsEventually(new Runnable() {
@Override
public void run() {
String val = entity.getAttribute(SENSOR_STRING);
assertTrue(val != null && val.contains(errMsg), "val=" + val);
}
});
}
@Test
public void testCallsOnFailureWithResultOfCallable() throws Exception {
feed = FunctionFeed.builder()
.entity(entity)
.poll(new FunctionPollConfig<Integer, Integer>(SENSOR_INT)
.period(1)
.callable(Callables.returning(1))
.checkSuccess(Predicates.alwaysFalse())
.onSuccess(new AddOneFunction())
.onFailure(Functions.constant(-1)))
.build();
EntityTestUtils.assertAttributeEqualsEventually(entity, SENSOR_INT, -1);
}
@Test
public void testCallsOnExceptionWhenCheckSuccessIsFalseButNoFailureHandler() throws Exception {
feed = FunctionFeed.builder()
.entity(entity)
.poll(new FunctionPollConfig<Integer, Integer>(SENSOR_INT)
.period(1)
.callable(Callables.returning(1))
.checkSuccess(Predicates.alwaysFalse())
.onSuccess(new AddOneFunction())
.onException(Functions.constant(-1)))
.build();
EntityTestUtils.assertAttributeEqualsEventually(entity, SENSOR_INT, -1);
}
@Test
public void testSharesFunctionWhenMultiplePostProcessors() throws Exception {
final IncrementingCallable incrementingCallable = new IncrementingCallable();
final List<Integer> ints = new CopyOnWriteArrayList<Integer>();
final List<String> strings = new CopyOnWriteArrayList<String>();
entity.subscriptions().subscribe(entity, SENSOR_INT, new SensorEventListener<Integer>() {
@Override public void onEvent(SensorEvent<Integer> event) {
ints.add(event.getValue());
}});
entity.subscriptions().subscribe(entity, SENSOR_STRING, new SensorEventListener<String>() {
@Override public void onEvent(SensorEvent<String> event) {
strings.add(event.getValue());
}});
feed = FunctionFeed.builder()
.entity(entity)
.poll(new FunctionPollConfig<Integer, Integer>(SENSOR_INT)
.period(10)
.callable(incrementingCallable))
.poll(new FunctionPollConfig<Integer, String>(SENSOR_STRING)
.period(10)
.callable(incrementingCallable)
.onSuccess(new ToStringFunction()))
.build();
Asserts.succeedsEventually(new Runnable() {
@Override
public void run() {
assertEquals(ints.subList(0, 2), ImmutableList.of(0, 1));
assertTrue(strings.size()>=2, "wrong strings list: "+strings);
assertEquals(strings.subList(0, 2), ImmutableList.of("0", "1"), "wrong strings list: "+strings);
}});
}
@Test
@SuppressWarnings("unused")
public void testFunctionPollConfigBuilding() throws Exception {
FunctionPollConfig<Integer, Integer> typeFromCallable = FunctionPollConfig.forSensor(SENSOR_INT)
.period(1)
.callable(Callables.returning(1))
.onSuccess(Functions.constant(-1));
FunctionPollConfig<Integer, Integer> typeFromSupplier = FunctionPollConfig.forSensor(SENSOR_INT)
.period(1)
.supplier(Suppliers.ofInstance(1))
.onSuccess(Functions.constant(-1));
FunctionPollConfig<Integer, Integer> usingConstructor = new FunctionPollConfig<Integer, Integer>(SENSOR_INT)
.period(1)
.supplier(Suppliers.ofInstance(1))
.onSuccess(Functions.constant(-1));
FunctionPollConfig<Integer, Integer> usingConstructorWithFailureOrException = new FunctionPollConfig<Integer, Integer>(SENSOR_INT)
.period(1)
.supplier(Suppliers.ofInstance(1))
.onFailureOrException(Functions.<Integer>constant(null));
}
private void assertFeedIsPolling() {
final Integer val = entity.getAttribute(SENSOR_INT);
Asserts.succeedsEventually(new Runnable() {
@Override
public void run() {
assertNotEquals(val, entity.getAttribute(SENSOR_INT));
}
});
}
private void assertFeedIsPollingContinuously() {
Asserts.succeedsContinually(new Runnable() {
@Override
public void run() {
assertFeedIsPolling();
}
});
}
private static class IncrementingCallable implements Callable<Integer> {
private final AtomicInteger next = new AtomicInteger(0);
@Override public Integer call() {
return next.getAndIncrement();
}
}
private static class AddOneFunction implements Function<Integer, Integer> {
@Override public Integer apply(@Nullable Integer input) {
return (input != null) ? (input + 1) : null;
}
}
private static class ExceptionCallable implements Callable<Void> {
private final String msg;
ExceptionCallable(String msg) {
this.msg = msg;
}
@Override public Void call() {
throw new RuntimeException(msg);
}
}
public static class ToStringFunction implements Function<Object, String> {
@Override public String apply(@Nullable Object input) {
return (input != null) ? (input.toString()) : null;
}
}
}