// Copyright 2016 Twitter. All rights reserved.
//
// 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 com.twitter.heron.statemgr.zookeeper.curator;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.SettableFuture;
import com.google.protobuf.Message;
import org.apache.curator.framework.CuratorFramework;
import org.apache.curator.framework.api.ACLBackgroundPathAndBytesable;
import org.apache.curator.framework.api.BackgroundCallback;
import org.apache.curator.framework.api.BackgroundPathable;
import org.apache.curator.framework.api.CreateBuilder;
import org.apache.curator.framework.api.CuratorEvent;
import org.apache.curator.framework.api.DeleteBuilder;
import org.apache.curator.framework.api.ExistsBuilder;
import org.apache.curator.framework.api.GetDataBuilder;
import org.apache.zookeeper.CreateMode;
import org.apache.zookeeper.Watcher;
import org.apache.zookeeper.data.Stat;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;
import com.twitter.heron.common.basics.Pair;
import com.twitter.heron.spi.common.Config;
import com.twitter.heron.spi.common.Key;
import com.twitter.heron.spi.utils.NetworkUtils;
import com.twitter.heron.statemgr.zookeeper.ZkContext;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.mockito.Matchers.any;
import static org.mockito.Matchers.anyBoolean;
import static org.mockito.Matchers.anyInt;
import static org.mockito.Matchers.anyString;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
/**
* CuratorStateManager Tester.
*/
public class CuratorStateManagerTest {
private static final String CONNECTION_STRING = "connectionString";
private static final String PATH = "/heron/n90/path";
private static final String ROOT_ADDR = "/";
private static final String TOPOLOGY_NAME = "topology";
private static final String TUNNEL_STRING = "tunnelConnectionString";
private Config tunnelingConfig;
private Config config;
@Before
public void before() throws Exception {
Config.Builder builder = Config.newBuilder(true)
.put(Key.STATEMGR_ROOT_PATH, ROOT_ADDR)
.put(Key.TOPOLOGY_NAME, TOPOLOGY_NAME)
.put(Key.STATEMGR_CONNECTION_STRING, CONNECTION_STRING);
// config is used for testing all the methods exception initialize and close
config = builder.build();
// tunneling config is used when testing initialize/close method
tunnelingConfig = builder
.put(ZkContext.IS_INITIALIZE_TREE, true)
.put(ZkContext.IS_TUNNEL_NEEDED, true)
.put(Key.SCHEDULER_IS_SERVICE, false)
.build();
}
@After
public void after() throws Exception {
}
/**
* Test initialize method
* @throws Exception
*/
@Test
public void testInitialize() throws Exception {
CuratorStateManager spyStateManager = spy(new CuratorStateManager());
CuratorFramework mockClient = mock(CuratorFramework.class);
doReturn(mockClient).when(spyStateManager).getCuratorClient();
doReturn(new Pair<String, List<Process>>(TUNNEL_STRING, new ArrayList<Process>()))
.when(spyStateManager).setupZkTunnel(any(NetworkUtils.TunnelConfig.class));
doReturn(true).when(mockClient).blockUntilConnected(anyInt(), any(TimeUnit.class));
spyStateManager.initialize(tunnelingConfig);
// Make sure tunneling is setup correctly
assertTrue(spyStateManager.getConnectionString().equals(TUNNEL_STRING));
// Verify curator client is invoked
verify(mockClient).start();
verify(mockClient).blockUntilConnected(anyInt(), any(TimeUnit.class));
verify(mockClient, times(9)).createContainers(anyString());
// Verify initTree is called
verify(spyStateManager).initTree();
}
/**
* Test close method
* @throws Exception
*/
@Test
public void testClose() throws Exception {
CuratorStateManager spyStateManager = spy(new CuratorStateManager());
CuratorFramework mockClient = mock(CuratorFramework.class);
Process mockProcess1 = mock(Process.class);
Process mockProcess2 = mock(Process.class);
List<Process> tunnelProcesses = new ArrayList<>();
tunnelProcesses.add(mockProcess1);
tunnelProcesses.add(mockProcess2);
doReturn(mockClient).when(spyStateManager).getCuratorClient();
doReturn(new Pair<>(TUNNEL_STRING, tunnelProcesses))
.when(spyStateManager).setupZkTunnel(any(NetworkUtils.TunnelConfig.class));
doReturn(true).when(mockClient).blockUntilConnected(anyInt(), any(TimeUnit.class));
spyStateManager.initialize(tunnelingConfig);
spyStateManager.close();
// verify curator and processes are closed correctly
verify(mockClient).close();
verify(mockProcess1).destroy();
verify(mockProcess2).destroy();
}
/**
* Test nodeExists method
* @throws Exception
*/
@Test
public void testExistNode() throws Exception {
CuratorStateManager spyStateManager = spy(new CuratorStateManager());
CuratorFramework mockClient = mock(CuratorFramework.class);
ExistsBuilder mockExistsBuilder = mock(ExistsBuilder.class);
final String correctPath = "/correct/path";
final String wrongPath = "/wrong/path";
doReturn(mockClient)
.when(spyStateManager).getCuratorClient();
doReturn(true)
.when(mockClient).blockUntilConnected(anyInt(), any(TimeUnit.class));
doReturn(mockExistsBuilder)
.when(mockClient).checkExists();
doReturn(new Stat())
.when(mockExistsBuilder).forPath(correctPath);
doReturn(null)
.when(mockExistsBuilder).forPath(wrongPath);
spyStateManager.initialize(config);
// Verify the result is true when path is correct
ListenableFuture<Boolean> result1 = spyStateManager.nodeExists(correctPath);
verify(mockExistsBuilder).forPath(correctPath);
assertTrue(result1.get());
// Verify the result is false when path is wrong
ListenableFuture<Boolean> result2 = spyStateManager.nodeExists(wrongPath);
verify(mockExistsBuilder).forPath(wrongPath);
assertFalse(result2.get());
}
/**
* test createNode method
* @throws Exception
*/
@Test
public void testCreateNode() throws Exception {
CuratorStateManager spyStateManager = spy(new CuratorStateManager());
CuratorFramework mockClient = mock(CuratorFramework.class);
CreateBuilder mockCreateBuilder = mock(CreateBuilder.class);
// Mockito doesn't support mock type-parametrized class, thus suppress the warning
@SuppressWarnings("rawtypes")
ACLBackgroundPathAndBytesable mockPath = spy(ACLBackgroundPathAndBytesable.class);
final byte[] data = new byte[10];
doReturn(mockClient)
.when(spyStateManager).getCuratorClient();
doReturn(true)
.when(mockClient).blockUntilConnected(anyInt(), any(TimeUnit.class));
doReturn(mockCreateBuilder)
.when(mockClient).create();
doReturn(mockPath)
.when(mockCreateBuilder).withMode(any(CreateMode.class));
spyStateManager.initialize(config);
// Verify the node is created successfully
ListenableFuture<Boolean> result = spyStateManager.createNode(PATH, data, false);
verify(mockCreateBuilder).withMode(any(CreateMode.class));
verify(mockPath).forPath(PATH, data);
assertTrue(result.get());
}
/**
* Test deleteNode method
* @throws Exception
*/
@Test
public void testDeleteNode() throws Exception {
CuratorStateManager spyStateManager = spy(new CuratorStateManager());
CuratorFramework mockClient = mock(CuratorFramework.class);
DeleteBuilder mockDeleteBuilder = mock(DeleteBuilder.class);
// Mockito doesn't support mock type-parametrized class, thus suppress the warning
@SuppressWarnings("rawtypes")
BackgroundPathable mockBackPathable = mock(BackgroundPathable.class);
doReturn(mockClient)
.when(spyStateManager).getCuratorClient();
doReturn(true)
.when(mockClient).blockUntilConnected(anyInt(), any(TimeUnit.class));
doReturn(mockDeleteBuilder)
.when(mockClient).delete();
doReturn(mockBackPathable)
.when(mockDeleteBuilder).withVersion(-1);
spyStateManager.initialize(config);
ListenableFuture<Boolean> result = spyStateManager.deleteExecutionState(PATH);
// Verify the node is deleted correctly
verify(mockDeleteBuilder).withVersion(-1);
assertTrue(result.get());
}
/**
* Test getNodeData method
* @throws Exception
*/
@Test
public void testGetNodeData() throws Exception {
CuratorStateManager spyStateManager = spy(new CuratorStateManager());
final CuratorFramework mockClient = mock(CuratorFramework.class);
GetDataBuilder mockGetBuilder = mock(GetDataBuilder.class);
// Mockito doesn't support mock type-parametrized class, thus suppress the warning
@SuppressWarnings("rawtypes")
BackgroundPathable mockBackPathable = mock(BackgroundPathable.class);
final CuratorEvent mockEvent = mock(CuratorEvent.class);
Message.Builder mockBuilder = mock(Message.Builder.class);
Message mockMessage = mock(Message.class);
final byte[] data = "wy_1989".getBytes();
doReturn(mockMessage)
.when(mockBuilder).build();
doReturn(data)
.when(mockEvent).getData();
doReturn(PATH)
.when(mockEvent).getPath();
doReturn(mockClient)
.when(spyStateManager).getCuratorClient();
doReturn(true)
.when(mockClient).blockUntilConnected(anyInt(), any(TimeUnit.class));
doReturn(mockGetBuilder)
.when(mockClient).getData();
doReturn(mockBackPathable)
.when(mockGetBuilder).usingWatcher(any(Watcher.class));
doAnswer(new Answer<Object>() {
@Override
public Object answer(InvocationOnMock invocationOnMock) throws Throwable {
Object[] objests = invocationOnMock.getArguments();
// the first object is the BackgroundCallback
((BackgroundCallback) objests[0]).processResult(mockClient, mockEvent);
return null;
}
}).when(mockBackPathable).inBackground(any(BackgroundCallback.class));
spyStateManager.initialize(config);
// Verify the data on node is fetched correctly
ListenableFuture<Message> result = spyStateManager.getNodeData(null, PATH, mockBuilder);
assertTrue(result.get().equals(mockMessage));
}
/**
* Test deleteSchedulerLocation method
* @throws Exception
*/
@Test
public void testDeleteSchedulerLocation() throws Exception {
CuratorStateManager spyStateManager = spy(new CuratorStateManager());
CuratorFramework mockClient = mock(CuratorFramework.class);
doReturn(mockClient)
.when(spyStateManager).getCuratorClient();
doReturn(true)
.when(mockClient).blockUntilConnected(anyInt(), any(TimeUnit.class));
spyStateManager.initialize(config);
final SettableFuture<Boolean> fakeResult = SettableFuture.create();
fakeResult.set(false);
doReturn(fakeResult).when(spyStateManager).deleteNode(anyString(), anyBoolean());
ListenableFuture<Boolean> result = spyStateManager.deleteSchedulerLocation(TOPOLOGY_NAME);
assertTrue(result.get());
}
}