/*! ******************************************************************************
*
* Pentaho Data Integration
*
* Copyright (C) 2002-2017 by Pentaho : http://www.pentaho.com
*
*******************************************************************************
*
* 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.pentaho.di.trans.step;
import static org.hamcrest.CoreMatchers.containsString;
import static org.mockito.Matchers.any;
import static org.mockito.Mockito.*;
import static org.junit.Assert.*;
import java.io.IOException;
import java.net.ServerSocket;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicBoolean;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.runners.MockitoJUnitRunner;
import org.mockito.stubbing.Answer;
import org.pentaho.di.core.BlockingRowSet;
import org.pentaho.di.core.QueueRowSet;
import org.pentaho.di.core.ResultFile;
import org.pentaho.di.core.RowMetaAndData;
import org.pentaho.di.core.RowSet;
import org.pentaho.di.core.SingleRowRowSet;
import org.pentaho.di.core.exception.KettleException;
import org.pentaho.di.core.exception.KettleStepException;
import org.pentaho.di.core.exception.KettleValueException;
import org.pentaho.di.core.fileinput.NonAccessibleFileObject;
import org.pentaho.di.core.logging.LogChannelInterface;
import org.pentaho.di.core.logging.LoggingObjectInterface;
import org.pentaho.di.core.row.RowMeta;
import org.pentaho.di.core.row.RowMetaInterface;
import org.pentaho.di.core.row.ValueMetaInterface;
import org.pentaho.di.core.row.value.ValueMetaBase;
import org.pentaho.di.core.row.value.ValueMetaInteger;
import org.pentaho.di.core.row.value.ValueMetaString;
import org.pentaho.di.trans.BasePartitioner;
import org.pentaho.di.trans.steps.mock.StepMockHelper;
import org.pentaho.di.www.SocketRepository;
@RunWith ( MockitoJUnitRunner.class )
public class BaseStepTest {
private StepMockHelper<StepMetaInterface, StepDataInterface> mockHelper;
@Mock RowHandler rowHandler;
@Before
public void setup() {
mockHelper =
new StepMockHelper<>( "BASE STEP", StepMetaInterface.class,
StepDataInterface.class );
when( mockHelper.logChannelInterfaceFactory.create( any(), any( LoggingObjectInterface.class ) ) ).thenReturn(
mockHelper.logChannelInterface );
}
@After
public void tearDown() {
mockHelper.cleanUp();
}
/**
* This test checks that data from one non-partitioned step copies to 2 partitioned steps right.
*
* @throws KettleException
* @see {@link <a href="http://jira.pentaho.com/browse/PDI-12211">http://jira.pentaho.com/browse/PDI-12211<a>}
*/
@Test
public void testBaseStepPutRowLocalSpecialPartitioning() throws KettleException {
List<StepMeta> stepMetas = new ArrayList<StepMeta>();
stepMetas.add( mockHelper.stepMeta );
stepMetas.add( mockHelper.stepMeta );
StepPartitioningMeta stepPartitioningMeta = spy( new StepPartitioningMeta() );
BasePartitioner partitioner = mock( BasePartitioner.class );
when( mockHelper.logChannelInterfaceFactory.create( any(), any( LoggingObjectInterface.class ) ) ).thenAnswer(
new Answer<LogChannelInterface>() {
@Override
public LogChannelInterface answer( InvocationOnMock invocation ) throws Throwable {
( (BaseStep) invocation.getArguments()[ 0 ] ).getLogLevel();
return mockHelper.logChannelInterface;
}
} );
when( mockHelper.trans.isRunning() ).thenReturn( true );
when( mockHelper.transMeta.findNextSteps( any( StepMeta.class ) ) ).thenReturn( stepMetas );
when( mockHelper.stepMeta.getStepPartitioningMeta() ).thenReturn( stepPartitioningMeta );
when( stepPartitioningMeta.getPartitioner() ).thenReturn( partitioner );
when( partitioner.getNrPartitions() ).thenReturn( 2 );
Object object0 = "name0";
ValueMetaInterface meta0 = new ValueMetaBase( object0.toString() );
Object object1 = "name1";
ValueMetaInterface meta2 = new ValueMetaBase( object1.toString() );
RowMetaInterface rowMeta0 = new RowMeta();
rowMeta0.addValueMeta( meta0 );
Object[] objects0 = { object0 };
RowMetaInterface rowMeta1 = new RowMeta();
rowMeta1.addValueMeta( meta2 );
Object[] objects1 = { object1 };
when( stepPartitioningMeta.getPartition( rowMeta0, objects0 ) ).thenReturn( 0 );
when( stepPartitioningMeta.getPartition( rowMeta1, objects1 ) ).thenReturn( 1 );
BlockingRowSet[] rowSet =
{ new BlockingRowSet( 2 ), new BlockingRowSet( 2 ), new BlockingRowSet( 2 ), new BlockingRowSet( 2 ) };
List<RowSet> outputRowSets = new ArrayList<RowSet>();
outputRowSets.addAll( Arrays.asList( rowSet ) );
BaseStep baseStep =
new BaseStep( mockHelper.stepMeta, mockHelper.stepDataInterface, 0, mockHelper.transMeta, mockHelper.trans );
baseStep.setStopped( false );
baseStep.setRepartitioning( StepPartitioningMeta.PARTITIONING_METHOD_SPECIAL );
baseStep.setOutputRowSets( outputRowSets );
baseStep.putRow( rowMeta0, objects0 );
baseStep.putRow( rowMeta1, objects1 );
assertEquals( object0, baseStep.getOutputRowSets().get( 0 ).getRow()[ 0 ] );
assertEquals( object1, baseStep.getOutputRowSets().get( 1 ).getRow()[ 0 ] );
assertEquals( object0, baseStep.getOutputRowSets().get( 2 ).getRow()[ 0 ] );
assertEquals( object1, baseStep.getOutputRowSets().get( 3 ).getRow()[ 0 ] );
}
@Test
public void testBaseStepGetLogLevelWontThrowNPEWithNullLog() {
when( mockHelper.logChannelInterfaceFactory.create( any(), any( LoggingObjectInterface.class ) ) ).thenAnswer(
new Answer<LogChannelInterface>() {
@Override
public LogChannelInterface answer( InvocationOnMock invocation ) throws Throwable {
( (BaseStep) invocation.getArguments()[ 0 ] ).getLogLevel();
return mockHelper.logChannelInterface;
}
} );
new BaseStep( mockHelper.stepMeta, mockHelper.stepDataInterface, 0, mockHelper.transMeta, mockHelper.trans )
.getLogLevel();
}
@Test
public void testStepListenersConcurrentModification() throws InterruptedException {
// Create a base step
final BaseStep baseStep =
new BaseStep( mockHelper.stepMeta, mockHelper.stepDataInterface, 0, mockHelper.transMeta, mockHelper.trans );
// Create thread to dynamically add listeners
final AtomicBoolean done = new AtomicBoolean( false );
Thread addListeners = new Thread() {
@Override
public void run() {
while ( !done.get() ) {
baseStep.addStepListener( mock( StepListener.class ) );
synchronized ( done ) {
done.notify();
}
}
}
};
// Mark start and stop while listeners are being added
try {
addListeners.start();
// Allow a few listeners to be added
synchronized ( done ) {
while ( baseStep.getStepListeners().size() < 20 ) {
done.wait();
}
}
baseStep.markStart();
// Allow more listeners to be added
synchronized ( done ) {
while ( baseStep.getStepListeners().size() < 100 ) {
done.wait();
}
}
baseStep.markStop();
} finally {
// Close addListeners thread
done.set( true );
addListeners.join();
}
}
@Test
public void resultFilesMapIsSafeForConcurrentModification() throws Exception {
final BaseStep step =
new BaseStep( mockHelper.stepMeta, mockHelper.stepDataInterface, 0, mockHelper.transMeta, mockHelper.trans );
final AtomicBoolean complete = new AtomicBoolean( false );
final int FILES_AMOUNT = 10 * 1000;
Thread filesProducer = new Thread( new Runnable() {
@Override public void run() {
try {
for ( int i = 0; i < FILES_AMOUNT; i++ ) {
step.addResultFile( new ResultFile( 0, new NonAccessibleFileObject( Integer.toString( i ) ), null, null ) );
try {
Thread.sleep( 1 );
} catch ( Exception e ) {
fail( e.getMessage() );
}
}
} finally {
complete.set( true );
}
}
} );
filesProducer.start();
try {
while ( !complete.get() ) {
for ( Map.Entry<String, ResultFile> entry : step.getResultFiles().entrySet() ) {
entry.getKey();
}
}
} finally {
filesProducer.join();
}
}
@Test
public void outputRowMetasAreNotSharedAmongSeveralStreams() throws Exception {
RowSet rs1 = new SingleRowRowSet();
RowSet rs2 = new SingleRowRowSet();
when( mockHelper.trans.isRunning() ).thenReturn( true );
BaseStep baseStep =
new BaseStep( mockHelper.stepMeta, mockHelper.stepDataInterface, 0, mockHelper.transMeta, mockHelper.trans );
baseStep.setStopped( false );
baseStep.setRepartitioning( StepPartitioningMeta.PARTITIONING_METHOD_NONE );
baseStep.setOutputRowSets( Arrays.asList( rs1, rs2 ) );
for ( RowSet rowSet : baseStep.getOutputRowSets() ) {
assertNull( "RowMeta should be null, since no calls were done", rowSet.getRowMeta() );
}
RowMetaInterface rowMeta = new RowMeta();
rowMeta.addValueMeta( new ValueMetaString( "string" ) );
rowMeta.addValueMeta( new ValueMetaInteger( "integer" ) );
baseStep.putRow( rowMeta, new Object[] { "a", 1 } );
RowMetaInterface meta1 = rs1.getRowMeta();
RowMetaInterface meta2 = rs2.getRowMeta();
assertNotNull( meta1 );
assertNotNull( meta2 );
// content is same
for ( ValueMetaInterface meta : meta1.getValueMetaList() ) {
assertTrue( meta.getName(), meta2.exists( meta ) );
}
// whereas instances differ
assertFalse( meta1 == meta2 );
}
@Test
public void testBuildLog() throws KettleValueException {
BaseStep testObject = new BaseStep( mockHelper.stepMeta, mockHelper.stepDataInterface, 0, mockHelper.transMeta,
mockHelper.trans );
Date startDate = new Date( (long) 123 );
Date endDate = new Date( (long) 125 );
RowMetaAndData result = testObject.buildLog( "myStepName", 13, 123, 234, 345, 456, 567, startDate, endDate );
assertNotNull( result );
assertEquals( 9, result.size() );
assertEquals( ValueMetaInterface.TYPE_STRING, result.getValueMeta( 0 ).getType() );
assertEquals( "myStepName", result.getString( 0, "default" ) );
assertEquals( ValueMetaInterface.TYPE_NUMBER, result.getValueMeta( 1 ).getType() );
assertEquals( new Double( 13.0 ), Double.valueOf( result.getNumber( 1, 0.1 ) ) );
assertEquals( ValueMetaInterface.TYPE_NUMBER, result.getValueMeta( 2 ).getType() );
assertEquals( new Double( 123 ), Double.valueOf( result.getNumber( 2, 0.1 ) ) );
assertEquals( ValueMetaInterface.TYPE_NUMBER, result.getValueMeta( 3 ).getType() );
assertEquals( new Double( 234 ), Double.valueOf( result.getNumber( 3, 0.1 ) ) );
assertEquals( ValueMetaInterface.TYPE_NUMBER, result.getValueMeta( 4 ).getType() );
assertEquals( new Double( 345 ), Double.valueOf( result.getNumber( 4, 0.1 ) ) );
assertEquals( ValueMetaInterface.TYPE_NUMBER, result.getValueMeta( 5 ).getType() );
assertEquals( new Double( 456 ), Double.valueOf( result.getNumber( 5, 0.1 ) ) );
assertEquals( ValueMetaInterface.TYPE_NUMBER, result.getValueMeta( 6 ).getType() );
assertEquals( new Double( 567 ), Double.valueOf( result.getNumber( 6, 0.1 ) ) );
assertEquals( ValueMetaInterface.TYPE_DATE, result.getValueMeta( 7 ).getType() );
assertEquals( startDate, result.getDate( 7, Calendar.getInstance().getTime() ) );
assertEquals( ValueMetaInterface.TYPE_DATE, result.getValueMeta( 8 ).getType() );
assertEquals( endDate, result.getDate( 8, Calendar.getInstance().getTime() ) );
}
@Test
public void testCleanupRemoteSteps() {
RemoteStep remoteStepMock = mock( RemoteStep.class );
BaseStep.cleanupRemoteSteps( Collections.singletonList( remoteStepMock ) );
verify( remoteStepMock ).cleanup();
}
@Test
public void testCleanup() throws IOException {
BaseStep baseStep =
new BaseStep( mockHelper.stepMeta, mockHelper.stepDataInterface, 0, mockHelper.transMeta, mockHelper.trans );
ServerSocket serverSocketMock = mock( ServerSocket.class );
doReturn( 0 ).when( serverSocketMock ).getLocalPort();
baseStep.setServerSockets( Collections.singletonList( serverSocketMock ) );
SocketRepository socketRepositoryMock = mock( SocketRepository.class );
baseStep.setSocketRepository( socketRepositoryMock );
baseStep.cleanup();
verify( socketRepositoryMock ).releaseSocket( 0 );
}
@Test
public void testCleanupWithInexistentRemoteSteps() throws IOException {
BaseStep baseStep =
spy( new BaseStep( mockHelper.stepMeta, mockHelper.stepDataInterface, 0, mockHelper.transMeta,
mockHelper.trans ) );
ServerSocket serverSocketMock = mock( ServerSocket.class );
doReturn( 0 ).when( serverSocketMock ).getLocalPort();
baseStep.setServerSockets( Collections.singletonList( serverSocketMock ) );
SocketRepository socketRepositoryMock = mock( SocketRepository.class );
baseStep.setSocketRepository( socketRepositoryMock );
RemoteStep inputStep = mock( RemoteStep.class );
doReturn( Collections.singletonList( inputStep ) ).when( baseStep ).getRemoteInputSteps();
RemoteStep outputStep = mock( RemoteStep.class );
doReturn( Collections.singletonList( outputStep ) ).when( baseStep ).getRemoteOutputSteps();
baseStep.cleanup();
verify( inputStep ).cleanup();
verify( outputStep ).cleanup();
verify( socketRepositoryMock ).releaseSocket( 0 );
}
@Test
public void getRowWithRowHandler() throws KettleException {
BaseStep baseStep =
new BaseStep( mockHelper.stepMeta, mockHelper.stepDataInterface,
0, mockHelper.transMeta, mockHelper.trans );
baseStep.setRowHandler( rowHandler );
baseStep.getRow();
verify( rowHandler, times( 1 ) ).getRow();
}
@Test
public void putRowWithRowHandler() throws KettleException {
BaseStep baseStep =
new BaseStep( mockHelper.stepMeta, mockHelper.stepDataInterface,
0, mockHelper.transMeta, mockHelper.trans );
baseStep.setRowHandler( rowHandler );
RowMetaInterface rowMetaInterface = mock( RowMetaInterface.class );
Object[] objects = new Object[] { "foo", "bar" };
baseStep.putRow( rowMetaInterface, objects );
verify( rowHandler, times( 1 ) ).putRow( rowMetaInterface, objects );
}
@Test
public void putErrorWithRowHandler() throws KettleException {
BaseStep baseStep =
new BaseStep( mockHelper.stepMeta, mockHelper.stepDataInterface,
0, mockHelper.transMeta, mockHelper.trans );
baseStep.setRowHandler( rowHandler );
RowMetaInterface rowMetaInterface = mock( RowMetaInterface.class );
Object[] objects = new Object[] { "foo", "bar" };
baseStep.putError( rowMetaInterface, objects, 3l, "desc",
"field1,field2", "errorCode" );
verify( rowHandler, times( 1 ) ).putError(
rowMetaInterface, objects, 3l, "desc",
"field1,field2", "errorCode" );
}
@Test
public void putGetFromPutToDefaultRowHandlerMethods() throws KettleException {
BaseStep baseStep =
new BaseStep( mockHelper.stepMeta, mockHelper.stepDataInterface,
0, mockHelper.transMeta, mockHelper.trans );
baseStep.setRowHandler( rowHandlerWithDefaultMethods() );
RowMetaInterface rowMetaInterface = mock( RowMetaInterface.class );
Object[] objects = new Object[] { "foo", "bar" };
try {
baseStep.putRowTo( rowMetaInterface, objects, new QueueRowSet() );
fail( "Expected default exception for putRowTo" );
} catch ( UnsupportedOperationException uoe ) {
assertThat( uoe.getMessage(), containsString( this.getClass().getName() ) );
}
try {
baseStep.getRowFrom( new QueueRowSet() );
fail( "Expected default exception for getRowFrom" );
} catch ( UnsupportedOperationException uoe ) {
assertThat( uoe.getMessage(), containsString( this.getClass().getName() ) );
}
}
private RowHandler rowHandlerWithDefaultMethods() {
return new RowHandler() {
@Override public Object[] getRow() throws KettleException {
return new Object[ 0 ];
}
@Override public void putRow( RowMetaInterface rowMeta, Object[] row ) throws KettleStepException {
}
@Override public void putError( RowMetaInterface rowMeta, Object[] row, long nrErrors, String errorDescriptions,
String fieldNames, String errorCodes ) throws KettleStepException {
}
};
}
}