/*! ******************************************************************************
*
* Pentaho Data Integration
*
* Copyright (C) 2002-2013 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.blackbox;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.Locale;
import java.util.Scanner;
import org.junit.Assert;
import org.junit.BeforeClass;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import org.junit.runners.Parameterized.Parameters;
import org.pentaho.di.core.CheckResultInterface;
import org.pentaho.di.core.KettleEnvironment;
import org.pentaho.di.core.Result;
import org.pentaho.di.core.exception.KettleException;
import org.pentaho.di.core.logging.KettleLogStore;
import org.pentaho.di.core.logging.LogChannel;
import org.pentaho.di.core.logging.LogChannelInterface;
import org.pentaho.di.core.logging.LogLevel;
import org.pentaho.di.core.util.EnvUtil;
import org.pentaho.di.core.variables.Variables;
import org.pentaho.di.i18n.GlobalMessages;
import org.pentaho.di.trans.Trans;
import org.pentaho.di.trans.TransMeta;
@RunWith( Parameterized.class )
public class BlackBoxTests {
private File transFile;
private List<File> expectedFiles;
private static ArrayList<Object> allTests;
public BlackBoxTests( File transFile, List<File> expectedFiles ) {
this.transFile = transFile;
this.expectedFiles = expectedFiles;
}
@BeforeClass
public static void setupBlackbox() {
Locale.setDefault( Locale.US );
// set the locale to English so that log file comparisons work
GlobalMessages.setLocale( EnvUtil.createLocale( "en-US" ) );
// Keep all log rows for at least 60 minutes as per BaseCluster.java
KettleLogStore.init( 0, 60 );
}
@Parameters
public static Collection<Object[]> getTests() {
allTests = new ArrayList<Object>();
// Traverse the "testfiles" tree to generate the collection
// do not process the output folder, there won't be any tests there
File dir = new File( "testfiles/blackbox/tests" );
assertTrue( dir.exists() );
assertTrue( dir.isDirectory() );
processDirectory( dir );
Object[][] d = new Object[allTests.size()][2];
for ( int i = 0; i < allTests.size(); i++ ) {
Object[] params = (Object[]) allTests.get( i );
d[i][0] = params[0];
d[i][1] = params[1];
}
return Arrays.asList( d );
}
protected static void processDirectory( File dir ) {
File[] files = dir.listFiles();
// recursively process every folder in testfiles/blackbox/tests
if ( files != null ) {
for ( File file : files ) {
if ( file.isDirectory() ) {
processDirectory( file );
}
}
// now process any transformations or jobs we find
for ( File file : files ) {
if ( file.isFile() ) {
String name = file.getName();
if ( name.endsWith( ".ktr" ) && !name.endsWith( "-tmp.ktr" ) ) {
// we found a transformation
// see if we can find an output file
List<File> expected = getExpectedOutputFile( dir, name.substring( 0, name.length() - 4 ) );
Object[] params = { file, expected };
allTests.add( params );
} else if ( name.endsWith( ".kjb" ) ) {
// we found a job
System.out.println( "JOBS NOT YET HANDLED: " + name );
}
}
}
}
}
/**
* Tries to find an output file to match a transformation or job file
*
* @param dir
* The directory to look in
* @param baseName
* Name of the transformation or the job without the extension
* @return list of output files
*/
protected static List<File> getExpectedOutputFile( File dir, String baseName ) {
List<File> files = new ArrayList<File>();
File expected = new File( dir, baseName + ".fail.txt" );
if ( expected.exists() ) {
files.add( expected );
}
for ( String extension : new String[] { ".txt", ".csv", ".xml" } ) {
expected = new File( dir, baseName + ".expected" + extension );
if ( expected.exists() ) {
files.add( expected );
}
// now see if there are perhaps multiple files generated...
//
boolean found = true;
int nr = 0;
while ( found ) {
expected = new File( dir, baseName + ".expected_" + nr + extension );
if ( expected.exists() ) {
files.add( expected );
nr++;
} else {
found = false;
}
}
}
return files;
}
// This is a generic JUnit 4 test that takes no parameters
@Test
public void runTransOrJob() throws Exception {
// Params are:
// File transFile
// List<File> expectedFiles
LogChannelInterface log = new LogChannel( "BlackBoxTest [" + transFile.toString() + "]" );
if ( !transFile.exists() ) {
log.logError( "Transformation does not exist: " + getPath( transFile ) );
addFailure( "Transformation does not exist: " + getPath( transFile ) );
fail( "Transformation does not exist: " + getPath( transFile ) );
}
if ( expectedFiles.isEmpty() ) {
addFailure( "No expected output files found: " + getPath( transFile ) );
fail( "No expected output files found: " + getPath( transFile ) );
}
Result result = runTrans( transFile.getAbsolutePath(), log );
// verify all the expected output files...
//
for ( int i = 0; i < expectedFiles.size(); i++ ) {
File expected = expectedFiles.get( i );
if ( expected.getAbsoluteFile().toString().contains( ".expected" ) ) {
// create a path to the expected output
String actualFile = expected.getAbsolutePath();
actualFile = actualFile.replaceFirst( ".expected_" + i + ".", ".actual_" + i + "." ); // multiple files case
actualFile = actualFile.replaceFirst( ".expected.", ".actual." ); // single file case
File actual = new File( actualFile );
if ( result.getResult() ) {
fileCompare( expected, actual );
}
}
}
// We didn't get a result, so the only expected file should be a ".fail.txt" file
//
if ( !result.getResult() ) {
String logStr = KettleLogStore.getAppender().getBuffer( result.getLogChannelId(), true ).toString();
if ( expectedFiles.size() == 0 ) {
// We haven't got a ".fail.txt" file, so this is a real failure
fail( "Error running " + getPath( transFile ) + ":" + logStr );
}
}
}
public void writeLog( File logFile, String logStr ) {
try {
// document encoding will be important here
OutputStream stream = new FileOutputStream( logFile );
// parse the log file and remove things that will make comparisons hard
int length = logStr.length();
int pos = 0;
String line;
while ( pos < length ) {
int eol = logStr.indexOf( "\r\n", pos );
if ( eol != -1 ) {
line = logStr.substring( pos, eol );
pos = eol + 2;
} else {
eol = logStr.indexOf( "\n", pos );
if ( eol != -1 ) {
line = logStr.substring( pos, eol );
pos = eol + 1;
} else {
// this must be the last line
line = logStr.substring( pos );
pos = length;
}
}
// remove the date/time
line = line.substring( 22 );
// find the subject
String subject = "";
int idx = line.indexOf( " - " );
if ( idx != -1 ) {
subject = line.substring( 0, idx );
}
// skip the version and build numbers
idx = line.indexOf( " : ", idx );
if ( idx != -1 ) {
String details = line.substring( idx + 3 );
// filter out stacktraces
if ( details.startsWith( "\tat " ) ) {
continue;
}
if ( details.startsWith( "\t... " ) ) {
continue;
}
// force the windows EOL characters
stream.write( ( subject + " : " + details + "\r\n" ).getBytes( "UTF-8" ) );
}
}
stream.close();
} catch ( Exception e ) {
addFailure( "Could not write to log file: " + logFile.getAbsolutePath() );
}
}
public String getPath( File file ) {
return getPath( file.getAbsolutePath() );
}
public String getPath( String filepath ) {
int idx = filepath.indexOf( "/testfiles/" );
if ( idx == -1 ) {
idx = filepath.indexOf( "\\testfiles\\" );
}
if ( idx != -1 ) {
return filepath.substring( idx + 1 );
}
return filepath;
}
public void fileCompare( File expected, File actual ) throws IOException {
String failure = "Ouput files is not equals: expected file: %1s, actual file: %2s. Different fragments: ";
failure = String.format( failure, expected.getCanonicalPath(), actual.getCanonicalPath() );
Scanner expSc = null;
Scanner actSc = null;
try {
expSc = new Scanner( expected );
actSc = new Scanner( actual );
int i = 0;
// seems file is same
while ( expSc.hasNext() && actSc.hasNext() ) {
i++;
String expString = expSc.next();
String actString = actSc.next();
Assert.assertEquals( failure + "Fragment number" + i + " is not same", expString, actString );
}
// seems is not
boolean actRemains = expSc.hasNext();
boolean expRemains = actSc.hasNext();
if ( actRemains || expRemains ) {
if ( actRemains ) {
fail( failure + " actual file has excessive fragments: " + actSc.next() );
} else {
fail( failure + " expected file has excessive fragments: " + expSc.next() );
}
}
} finally {
if ( expSc != null ) {
expSc.close();
}
if ( actSc != null ) {
actSc.close();
}
}
}
public Result runTrans( String fileName, LogChannelInterface log ) throws KettleException {
// Bootstrap the Kettle API...
//
KettleEnvironment.init();
TransMeta transMeta = new TransMeta( fileName );
Trans trans = new Trans( transMeta );
Result result;
try {
trans.setLogLevel( LogLevel.ERROR );
result = trans.getResult();
} catch ( Exception e ) {
result = trans.getResult();
String message = "Processing has stopped because of an error: " + getPath( fileName );
addFailure( message );
log.logError( message, e );
fail( message );
return result;
}
try {
trans.initializeVariablesFrom( null );
trans.getTransMeta().setInternalKettleVariables( trans );
trans.setSafeModeEnabled( true );
// see if the transformation checks ok
List<CheckResultInterface> remarks = new ArrayList<CheckResultInterface>();
trans.getTransMeta().checkSteps( remarks, false, null, new Variables(), null, null );
for ( CheckResultInterface remark : remarks ) {
if ( remark.getType() == CheckResultInterface.TYPE_RESULT_ERROR ) {
// add this to the log
addFailure( "Check error: " + getPath( fileName ) + ", " + remark.getErrorCode() );
log.logError( "BlackBoxTest", "Check error: " + getPath( fileName ) + ", " + remark.getErrorCode() );
}
}
// allocate & run the required sub-threads
try {
trans.execute( null );
} catch ( Exception e ) {
addFailure( "Unable to prepare and initialize this transformation: " + getPath( fileName ) );
log.logError( "BlackBoxTest", "Unable to prepare and initialize this transformation: " + getPath( fileName ) );
fail( "Unable to prepare and initialize this transformation: " + getPath( fileName ) );
return null;
}
trans.waitUntilFinished();
result = trans.getResult();
// The result flag is not set to true by a transformation - set it to true if got no errors
// FIXME: Find out if there is a better way to check if a transformation has thrown an error
result.setResult( result.getNrErrors() == 0 );
return result;
} catch ( Exception e ) {
addFailure( "Unexpected error occurred: " + getPath( fileName ) );
log.logError( "BlackBoxTest", "Unexpected error occurred: " + getPath( fileName ), e );
result.setResult( false );
result.setNrErrors( 1 );
fail( "Unexpected error occurred: " + getPath( fileName ) );
return result;
}
}
protected void addFailure( String message ) {
System.err.println( "failure: " + message );
}
}