/*! ******************************************************************************
*
* Pentaho Data Integration
*
* Copyright (C) 2002-2016 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.steps.switchcase;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.mockito.Matchers.any;
import static org.mockito.Mockito.*;
import java.io.File;
import java.io.IOException;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Queue;
import java.util.Set;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.mockito.Mockito;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;
import org.pentaho.di.core.QueueRowSet;
import org.pentaho.di.core.RowSet;
import org.pentaho.di.core.database.DatabaseMeta;
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.logging.LoggingObjectInterface;
import org.pentaho.di.core.row.RowMetaInterface;
import org.pentaho.di.core.row.ValueMetaInterface;
import org.pentaho.di.trans.step.StepMeta;
import org.pentaho.di.trans.step.StepMetaInterface;
import org.pentaho.di.trans.steps.dummytrans.DummyTransMeta;
import org.pentaho.di.trans.steps.mock.StepMockHelper;
import org.pentaho.metastore.api.IMetaStore;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;
public class SwitchCaseTest {
private StepMockHelper<SwitchCaseMeta, SwitchCaseData> mockHelper;
private static Boolean EMPTY_STRING_AND_NULL_ARE_DIFFERENT = false;
@Before
public void setUp() throws Exception {
mockHelper =
new StepMockHelper<SwitchCaseMeta, SwitchCaseData>(
"Switch Case", SwitchCaseMeta.class, SwitchCaseData.class );
when( mockHelper.logChannelInterfaceFactory.create( any(), any( LoggingObjectInterface.class ) ) ).thenReturn(
mockHelper.logChannelInterface );
when( mockHelper.trans.isRunning() ).thenReturn( true );
}
@After
public void tearDown() throws Exception {
mockHelper.cleanUp();
}
/**
* PDI 6900. Test that process row works correctly. Simulate step workload when input and output row sets already
* created and mapped to specified case values.
*
* @throws KettleException
*/
@Test
public void testProcessRow() throws KettleException {
SwitchCaseCustom krasavez = new SwitchCaseCustom( mockHelper );
krasavez.first = false;
// create two output row sets
RowSet rowSetOne = new QueueRowSet();
RowSet rowSetTwo = new QueueRowSet();
// this row set should contain only '3'.
krasavez.data.outputMap.put( 3, rowSetOne );
krasavez.data.outputMap.put( 3, rowSetTwo );
// this row set contains nulls only
RowSet rowSetNullOne = new QueueRowSet();
RowSet rowSetNullTwo = new QueueRowSet();
krasavez.data.nullRowSetSet.add( rowSetNullOne );
krasavez.data.nullRowSetSet.add( rowSetNullTwo );
// this row set contains all expect null or '3'
RowSet def = new QueueRowSet();
krasavez.data.defaultRowSetSet.add( def );
// generate some data (see method implementation)
// expected: 5 times null,
// expected 1*2 = 2 times 3
// expected 5*2 + 5 = 15 rows generated
// expected 15 - 5 - 2 = 8 rows go to default.
// expected one empty string at the end
// System.out.println( krasavez.getInputDataOverview() );
// 1, 1, null, 2, 2, null, 3, 3, null, 4, 4, null, 5, 5, null,""
krasavez.generateData( 1, 5, 2 );
// call method under test
krasavez.processRow();
assertEquals( "First row set collects 2 rows", 2, rowSetOne.size() );
assertEquals( "Second row set collects 2 rows", 2, rowSetTwo.size() );
assertEquals( "First null row set collects 5 rows", 6, rowSetNullOne.size() );
assertEquals( "Second null row set collects 5 rows", 6, rowSetNullTwo.size() );
assertEquals( "Default row set collects the rest of rows", 8, def.size() );
// now - check the data is correct in every row set:
assertEquals( "First row set contains only 3: ", true, isRowSetContainsValue(
rowSetOne, new Object[] { 3 }, new Object[] { } ) );
assertEquals( "Second row set contains only 3: ", true, isRowSetContainsValue(
rowSetTwo, new Object[] { 3 }, new Object[] { } ) );
assertEquals( "First null row set contains only null: ", true, isRowSetContainsValue(
rowSetNullOne, new Object[] { null }, new Object[] { } ) );
assertEquals( "Second null row set contains only null: ", true, isRowSetContainsValue(
rowSetNullTwo, new Object[] { null }, new Object[] { } ) );
assertEquals( "Default row set do not contains null or 3, but other", true, isRowSetContainsValue(
def, new Object[] { 1, 2, 4, 5 }, new Object[] { 3, null } ) );
}
private boolean isRowSetContainsValue( RowSet rowSet, Object[] allowed, Object[] illegal ) {
boolean ok = true;
Set<Object> yes = new HashSet<Object>();
yes.addAll( Arrays.asList( allowed ) );
Set<Object> no = new HashSet<Object>();
no.addAll( Arrays.asList( illegal ) );
for ( int i = 0; i < rowSet.size(); i++ ) {
Object[] row = rowSet.getRow();
Object val = row[0];
ok = yes.contains( val ) && !no.contains( val );
if ( !ok ) {
// this is not ok now
return false;
}
}
return ok;
}
/**
* PDI-6900 Check that SwichCase step can correctly set up input values to output rowsets.
*
* @throws KettleException
* @throws URISyntaxException
* @throws ParserConfigurationException
* @throws SAXException
* @throws IOException
*/
@Test
public void testCreateOutputValueMapping() throws KettleException, URISyntaxException,
ParserConfigurationException, SAXException, IOException {
SwitchCaseCustom krasavez = new SwitchCaseCustom( mockHelper );
// load step info value-case mapping from xml.
List<DatabaseMeta> emptyList = new ArrayList<DatabaseMeta>();
krasavez.meta.loadXML( loadStepXmlMetadata( "SwitchCaseTest.xml" ), emptyList, mock( IMetaStore.class ) );
KeyToRowSetMap expectedNN = new KeyToRowSetMap();
Set<RowSet> nulls = new HashSet<RowSet>();
// create real steps for all targets
List<SwitchCaseTarget> list = krasavez.meta.getCaseTargets();
for ( SwitchCaseTarget item : list ) {
StepMetaInterface smInt = new DummyTransMeta();
StepMeta stepMeta = new StepMeta( item.caseTargetStepname, smInt );
item.caseTargetStep = stepMeta;
// create and put row set for this
RowSet rw = new QueueRowSet();
krasavez.map.put( item.caseTargetStepname, rw );
// null values goes to null rowset
if ( item.caseValue != null ) {
expectedNN.put( item.caseValue, rw );
} else {
nulls.add( rw );
}
}
// create default step
StepMetaInterface smInt = new DummyTransMeta();
StepMeta stepMeta = new StepMeta( krasavez.meta.getDefaultTargetStepname(), smInt );
krasavez.meta.setDefaultTargetStep( stepMeta );
RowSet rw = new QueueRowSet();
krasavez.map.put( krasavez.meta.getDefaultTargetStepname(), rw );
krasavez.createOutputValueMapping();
// inspect step output data:
Set<RowSet> ones = krasavez.data.outputMap.get( "1" );
assertEquals( "Output map for 1 values contains 2 row sets", 2, ones.size() );
Set<RowSet> twos = krasavez.data.outputMap.get( "2" );
assertEquals( "Output map for 2 values contains 1 row sets", 1, twos.size() );
assertEquals( "Null row set contains 2 items: ", 2, krasavez.data.nullRowSetSet.size() );
assertEquals( "We have at least one default rowset", 1, krasavez.data.defaultRowSetSet.size() );
// check that rowsets data is correct:
Set<RowSet> rowsets = expectedNN.get( "1" );
for ( RowSet rowset : rowsets ) {
assertTrue( "Output map for 1 values contains expected row set", ones.contains( rowset ) );
}
rowsets = expectedNN.get( "2" );
for ( RowSet rowset : rowsets ) {
assertTrue( "Output map for 2 values contains expected row set", twos.contains( rowset ) );
}
for ( RowSet rowset : krasavez.data.nullRowSetSet ) {
assertTrue( "Output map for null values contains expected row set", nulls.contains( rowset ) );
}
// we have already check that there is only one item.
for ( RowSet rowset : krasavez.data.defaultRowSetSet ) {
assertTrue( "Output map for default case contains expected row set", rowset.equals( rw ) );
}
}
/**
* Load local xml data for case-value mapping, step info.
*
* @return
* @throws URISyntaxException
* @throws ParserConfigurationException
* @throws SAXException
* @throws IOException
*/
private static Node loadStepXmlMetadata( String fileName ) throws URISyntaxException, ParserConfigurationException, SAXException, IOException {
String PKG = SwitchCaseTest.class.getPackage().getName().replace( ".", "/" );
PKG = PKG + "/";
URL url = SwitchCaseTest.class.getClassLoader().getResource( PKG + fileName );
File file = new File( url.toURI() );
DocumentBuilderFactory dbFactory = DocumentBuilderFactory.newInstance();
DocumentBuilder dBuilder = dbFactory.newDocumentBuilder();
Document doc = dBuilder.parse( file );
NodeList nList = doc.getElementsByTagName( "step" );
return nList.item( 0 );
}
@Test
public void processRow_NullsArePutIntoDefaultWhenNotSpecified() throws Exception {
SwitchCaseCustom step = new SwitchCaseCustom( mockHelper );
step.meta.loadXML( loadStepXmlMetadata( "SwitchCaseTest_PDI-12671.xml" ), Collections.<DatabaseMeta>emptyList(), mock( IMetaStore.class ) );
List<RowSet> outputRowSets = new LinkedList<RowSet>();
for ( SwitchCaseTarget item : step.meta.getCaseTargets() ) {
StepMetaInterface smInt = new DummyTransMeta();
item.caseTargetStep = new StepMeta( item.caseTargetStepname, smInt );
RowSet rw = new QueueRowSet();
step.map.put( item.caseTargetStepname, rw );
outputRowSets.add( rw );
}
// create a default step
StepMetaInterface smInt = new DummyTransMeta();
StepMeta stepMeta = new StepMeta( step.meta.getDefaultTargetStepname(), smInt );
step.meta.setDefaultTargetStep( stepMeta );
RowSet defaultRowSet = new QueueRowSet();
step.map.put( step.meta.getDefaultTargetStepname(), defaultRowSet );
step.input.add( new Object[] { null } );
step.processRow();
assertEquals( 1, defaultRowSet.size() );
for ( RowSet rowSet : outputRowSets ) {
assertEquals( 0, rowSet.size() );
}
assertNull( defaultRowSet.getRow()[0] );
}
/**
* Switch case step ancestor with overridden methods to have ability to simulate normal transformation execution.
*
*/
private static class SwitchCaseCustom extends SwitchCase {
Queue<Object[]> input = new LinkedList<Object[]>();
RowMetaInterface rowMetaInterface;
// we will use real data and meta.
SwitchCaseData data = new SwitchCaseData();
SwitchCaseMeta meta = new SwitchCaseMeta();
Map<String, RowSet> map = new HashMap<String, RowSet>();
SwitchCaseCustom( StepMockHelper<SwitchCaseMeta, SwitchCaseData> mockHelper ) throws KettleValueException {
super( mockHelper.stepMeta, mockHelper.stepDataInterface, 0, mockHelper.transMeta, mockHelper.trans );
// this.mockHelper = mockHelper;
init( meta, data );
// call to convert value will returns same value.
data.valueMeta = mock( ValueMetaInterface.class );
when( data.valueMeta.convertData( any( ValueMetaInterface.class ), any() ) ).thenAnswer(
new Answer<Object>() {
@Override
public Object answer( InvocationOnMock invocation ) throws Throwable {
Object[] objArr = invocation.getArguments();
return ( objArr != null && objArr.length > 1 ) ? objArr[1] : null;
}
} );
// same when call to convertDataFromString
when( data.valueMeta.convertDataFromString( Mockito.anyString(), any( ValueMetaInterface.class ),
Mockito.anyString(), Mockito.anyString(), Mockito.anyInt() ) ).thenAnswer(
//CHECKSTYLE:Indentation:OFF
new Answer<Object>() {
public Object answer( InvocationOnMock invocation ) throws Throwable {
Object[] objArr = invocation.getArguments();
return ( objArr != null && objArr.length > 1 ) ? objArr[0] : null;
}
} );
// null-check
when( data.valueMeta.isNull( any() ) ).thenAnswer( new Answer<Object>() {
@Override
public Object answer( InvocationOnMock invocation ) throws Throwable {
Object[] objArr = invocation.getArguments();
Object obj = objArr[0];
if ( obj == null ) {
return true;
}
if ( EMPTY_STRING_AND_NULL_ARE_DIFFERENT ) {
return false;
}
// If it's a string and the string is empty, it's a null value as well
//
if ( obj instanceof String ) {
if ( ( (String) obj ).length() == 0 ) {
return true;
}
}
return false;
}
} );
}
/**
* used for input row generation
*
* @param start
* @param finish
* @param copy
*/
void generateData( int start, int finish, int copy ) {
input.clear();
for ( int i = start; i <= finish; i++ ) {
for ( int j = 0; j < copy; j++ ) {
input.add( new Object[] { i } );
}
input.add( new Object[] { null } );
}
input.add( new Object[] { "" } );
}
/**
* useful to see generated data as String
*
* @return
*/
@SuppressWarnings( "unused" )
public String getInputDataOverview() {
StringBuilder sb = new StringBuilder();
for ( Object[] row : input ) {
sb.append( row[0] + ", " );
}
return sb.toString();
}
/**
* mock step data processing
*/
@Override
public Object[] getRow() throws KettleException {
return input.poll();
}
/**
* simulate concurrent execution
*
* @throws KettleException
*/
public void processRow() throws KettleException {
boolean run = false;
do {
run = processRow( meta, data );
} while ( run );
}
@Override
public RowSet findOutputRowSet( String targetStep ) throws KettleStepException {
return map.get( targetStep );
}
@Override
public RowMetaInterface getInputRowMeta() {
if ( rowMetaInterface == null ) {
rowMetaInterface = getDynamicRowMetaInterface();
}
return rowMetaInterface;
}
private RowMetaInterface getDynamicRowMetaInterface() {
RowMetaInterface inputRowMeta = mock( RowMetaInterface.class );
return inputRowMeta;
}
}
}