/*
* This program is free software; you can redistribute it and/or modify it under the
* terms of the GNU Lesser General Public License, version 2.1 as published by the Free Software
* Foundation.
*
* You should have received a copy of the GNU Lesser General Public License along with this
* program; if not, you can obtain a copy at http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html
* or from the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
* See the GNU Lesser General Public License for more details.
*
* Copyright (c) 2000 - 2017 Pentaho Corporation and Contributors...
* All rights reserved.
*/
package org.pentaho.reporting.engine.classic.core.testsupport.gold;
import junit.framework.Assert;
import junit.framework.AssertionFailedError;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.custommonkey.xmlunit.Diff;
import org.custommonkey.xmlunit.XMLAssert;
import org.junit.Before;
import org.pentaho.reporting.engine.classic.core.AttributeNames;
import org.pentaho.reporting.engine.classic.core.ClassicEngineBoot;
import org.pentaho.reporting.engine.classic.core.ClassicEngineCoreModule;
import org.pentaho.reporting.engine.classic.core.DefaultReportEnvironment;
import org.pentaho.reporting.engine.classic.core.MasterReport;
import org.pentaho.reporting.engine.classic.core.ReportProcessingException;
import org.pentaho.reporting.engine.classic.core.designtime.compat.CompatibilityUpdater;
import org.pentaho.reporting.engine.classic.core.layout.output.ReportProcessor;
import org.pentaho.reporting.engine.classic.core.modules.output.pageable.base.PageableReportProcessor;
import org.pentaho.reporting.engine.classic.core.modules.output.pageable.xml.XmlPageOutputProcessor;
import org.pentaho.reporting.engine.classic.core.modules.output.pageable.xml.internal.XmlPageOutputProcessorMetaData;
import org.pentaho.reporting.engine.classic.core.modules.output.table.base.FlowReportProcessor;
import org.pentaho.reporting.engine.classic.core.modules.output.table.base.StreamReportProcessor;
import org.pentaho.reporting.engine.classic.core.modules.output.table.xml.XmlTableOutputProcessor;
import org.pentaho.reporting.engine.classic.core.modules.output.table.xml.internal.XmlTableOutputProcessorMetaData;
import org.pentaho.reporting.engine.classic.core.testsupport.DebugJndiContextFactoryBuilder;
import org.pentaho.reporting.engine.classic.core.testsupport.DebugReportRunner;
import org.pentaho.reporting.engine.classic.core.testsupport.font.LocalFontRegistry;
import org.pentaho.reporting.libraries.base.config.ModifiableConfiguration;
import org.pentaho.reporting.libraries.base.util.DebugLog;
import org.pentaho.reporting.libraries.base.util.FilesystemFilter;
import org.pentaho.reporting.libraries.base.util.IOUtils;
import org.pentaho.reporting.libraries.base.util.MemoryByteArrayOutputStream;
import org.pentaho.reporting.libraries.base.util.StopWatch;
import org.pentaho.reporting.libraries.resourceloader.Resource;
import org.pentaho.reporting.libraries.resourceloader.ResourceException;
import org.pentaho.reporting.libraries.resourceloader.ResourceManager;
import javax.naming.spi.NamingManager;
import java.io.BufferedInputStream;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.FilenameFilter;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.TimeZone;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
public class GoldTestBase {
public enum ReportProcessingMode {
current( "gold" ), legacy( "gold-legacy" ), migration( "gold-migrated" );
private String target;
ReportProcessingMode( final String target ) {
this.target = target;
}
public String getGoldDirectoryName() {
return target;
}
}
protected static class TestThreadFactory implements ThreadFactory {
final AtomicInteger threadNumber = new AtomicInteger( 1 );
public TestThreadFactory() {
}
public Thread newThread( final Runnable r ) {
final Thread t = new Thread( r );
t.setName( "Golden-Sample: " + getClass().getName() + "-" + threadNumber.getAndAdd( 1 ) );
t.setDaemon( true );
t.setPriority( 3 );
return t;
}
}
private class ExecuteReportRunner implements Runnable {
private File reportFile;
private File goldTemplate;
private ReportProcessingMode processingMode;
private List<Throwable> errors;
private ExecuteReportRunner( final File reportFile, final File goldTemplate, final List<Throwable> errors,
final ReportProcessingMode processingMode ) {
this.reportFile = reportFile;
this.goldTemplate = goldTemplate;
this.processingMode = processingMode;
this.errors = errors;
}
public void run() {
try {
System.out.printf( "Processing %s in mode=%s%n", reportFile, processingMode );
GoldTestBase.this.run( reportFile, goldTemplate, processingMode );
System.out.printf( "Finished %s in mode=%s%n", reportFile, processingMode );
} catch ( AssertionError e ) {
errors.add( e );
} catch ( Throwable t ) {
String message = String.format( "Failed to process %s in mode %s", reportFile, processingMode ); // NON-NLS
errors.add( new AssertionError( message, t ) );
}
}
}
private LocalFontRegistry localFontRegistry;
public GoldTestBase() {
}
private static File checkMarkerExists( final String filename ) {
final File file = new File( filename );
if ( file.canRead() ) {
return file;
}
return null;
}
public static File findMarker() {
final ArrayList<String> positions = new ArrayList<String>();
positions.add( "target/test-classes/test-gold/marker.properties" );
for ( final String pos : positions ) {
final File file = checkMarkerExists( pos );
if ( file != null ) {
return file.getAbsoluteFile().getParentFile();
}
}
throw new IllegalStateException( "Cannot find marker, please run from the correct directory" );
}
public static MasterReport parseReport( final Object file ) throws ResourceException {
final ResourceManager manager = new ResourceManager();
manager.registerDefaults();
final Resource resource = manager.createDirectly( file, MasterReport.class );
return (MasterReport) resource.getResource();
}
public static File locateGoldenSampleReport( final String name ) {
final FilesystemFilter filesystemFilter = new FilesystemFilter( name, "Reports" );
final File marker = findMarker();
final String[] directories = new String[] { "reports", "reports-4.0" };
for ( int i = 0; i < directories.length; i++ ) {
final String directory = directories[i];
final File reports = new File( marker, directory );
final File[] files = reports.listFiles( filesystemFilter );
final HashSet<String> fileSet = new HashSet<String>();
if ( files != null ) {
for ( final File file : files ) {
final String s = file.getName().toLowerCase();
if ( fileSet.add( s ) == false ) {
// the toy systems MacOS X and Windows use case-insensitive file systems and completely
// mess up when there are two files with what they consider the same name.
throw new IllegalStateException( "There is a golden sample with the same Windows/Mac "
+ "filename in the directory. Make sure your files are unique and lowercase." );
}
}
for ( final File file : files ) {
if ( file.isDirectory() ) {
continue;
}
return file;
}
}
}
return null;
}
@Before
public void setUp() throws Exception {
Locale.setDefault( Locale.US );
TimeZone.setDefault( TimeZone.getTimeZone( "UTC" ) );
// enforce binary compatibility for the xml-files so that comparing them can be faster.
ClassicEngineBoot.getInstance().start();
if ( NamingManager.hasInitialContextFactoryBuilder() == false ) {
NamingManager.setInitialContextFactoryBuilder( new DebugJndiContextFactoryBuilder() );
}
localFontRegistry = new LocalFontRegistry();
localFontRegistry.initialize();
}
protected MasterReport tuneForTesting( final MasterReport report ) throws Exception {
final ModifiableConfiguration configuration = report.getReportConfiguration();
configuration.setConfigProperty( DefaultReportEnvironment.ENVIRONMENT_KEY + "::internal::report.date",
"2011-04-07T15:00:00.000+0000" );
configuration.setConfigProperty( DefaultReportEnvironment.ENVIRONMENT_TYPE + "::internal::report.date",
"java.util.Date" );
return report;
}
protected MasterReport tuneForLegacyMode( final MasterReport report ) {
report.setCompatibilityLevel( ClassicEngineBoot.computeVersionId( 3, 8, 0 ) );
return report;
}
protected MasterReport tuneForMigrationMode( final MasterReport report ) {
final CompatibilityUpdater updater = new CompatibilityUpdater();
updater.performUpdate( report );
report.setAttribute( AttributeNames.Internal.NAMESPACE, AttributeNames.Internal.COMAPTIBILITY_LEVEL, null );
return report;
}
protected MasterReport tuneForCurrentMode( final MasterReport report ) {
report.setAttribute( AttributeNames.Internal.NAMESPACE, AttributeNames.Internal.COMAPTIBILITY_LEVEL, null );
return report;
}
protected void run( final File file, final File gold, final ReportProcessingMode mode ) throws Exception {
final MasterReport originalReport = parseReport( file );
final MasterReport tunedReport = tuneForTesting( originalReport );
MasterReport report = postProcess( tunedReport, file );
if ( mode == ReportProcessingMode.legacy ) {
report = tuneForLegacyMode( report );
} else if ( mode == ReportProcessingMode.migration ) {
report = tuneForMigrationMode( report );
} else {
report = tuneForCurrentMode( report );
}
final String fileName = IOUtils.getInstance().stripFileExtension( file.getName() );
handleXmlContent( executePageable( report ), new File( gold, fileName + "-page.xml" ) );
handleXmlContent( executeTableStream( report ), new File( gold, fileName + "-table-stream.xml" ) );
handleXmlContent( executeTableFlow( report ), new File( gold, fileName + "-table-flow.xml" ) );
handleXmlContent( executeTablePage( report ), new File( gold, fileName + "-table-page.xml" ) );
}
protected MasterReport postProcess( final MasterReport originalReport, final File file ) throws Exception {
return postProcess( originalReport );
}
protected MasterReport postProcess( final MasterReport originalReport ) throws Exception {
return originalReport;
}
protected void handleXmlContent( final byte[] reportOutput, final File goldSample ) throws Exception {
final byte[] goldData;
final InputStream goldInput = new BufferedInputStream( new FileInputStream( goldSample ) );
final MemoryByteArrayOutputStream goldByteStream =
new MemoryByteArrayOutputStream( Math.min( 1024 * 1024, (int) goldSample.length() ), 1024 * 1024 );
try {
IOUtils.getInstance().copyStreams( goldInput, goldByteStream );
goldData = goldByteStream.toByteArray();
if ( Arrays.equals( goldData, reportOutput ) ) {
return;
}
} finally {
goldInput.close();
}
final Reader reader = new InputStreamReader( new ByteArrayInputStream( goldData ), "UTF-8" );
final ByteArrayInputStream inputStream = new ByteArrayInputStream( reportOutput );
final Reader report = new InputStreamReader( inputStream, "UTF-8" );
try {
XMLAssert.assertXMLEqual( "File " + goldSample + " failed", new Diff( reader, report ), true );
} catch ( AssertionFailedError afe ) {
debugOutput( reportOutput, goldSample );
throw afe;
} finally {
reader.close();
}
}
private void debugOutput( final byte[] reportOutput, final File goldSample ) throws IOException {
try {
File testOutputFile = DebugReportRunner.createTestOutputFile();
final FileOutputStream w =
new FileOutputStream( new File( testOutputFile, "gold-failure-" + goldSample.getName() ) );
try {
w.write( reportOutput );
} finally {
w.close();
}
} catch ( IOException ioe ) {
// ignored ..
DebugLog.log( "Failed to write debug-output", ioe );
}
}
protected byte[] executeTablePage( final MasterReport report ) throws IOException, ReportProcessingException {
final MemoryByteArrayOutputStream outputStream = new MemoryByteArrayOutputStream();
try {
final XmlTableOutputProcessor outputProcessor =
new XmlTableOutputProcessor( outputStream, new XmlTableOutputProcessorMetaData(
XmlTableOutputProcessorMetaData.PAGINATION_FULL, localFontRegistry ) );
final ReportProcessor streamReportProcessor = new PageableReportProcessor( report, outputProcessor );
try {
streamReportProcessor.processReport();
} finally {
streamReportProcessor.close();
}
} finally {
outputStream.close();
}
return ( outputStream.toByteArray() );
}
protected byte[] executeTableFlow( final MasterReport report ) throws IOException, ReportProcessingException {
final MemoryByteArrayOutputStream outputStream = new MemoryByteArrayOutputStream();
try {
final XmlTableOutputProcessor outputProcessor =
new XmlTableOutputProcessor( outputStream, new XmlTableOutputProcessorMetaData(
XmlTableOutputProcessorMetaData.PAGINATION_MANUAL, localFontRegistry ) );
final ReportProcessor streamReportProcessor = new FlowReportProcessor( report, outputProcessor );
try {
streamReportProcessor.processReport();
} finally {
streamReportProcessor.close();
}
} finally {
outputStream.close();
}
return ( outputStream.toByteArray() );
}
protected byte[] executePageable( final MasterReport report ) throws IOException, ReportProcessingException {
final MemoryByteArrayOutputStream outputStream = new MemoryByteArrayOutputStream();
try {
final XmlPageOutputProcessor outputProcessor =
new XmlPageOutputProcessor( outputStream, new XmlPageOutputProcessorMetaData( localFontRegistry ) );
final PageableReportProcessor streamReportProcessor = new PageableReportProcessor( report, outputProcessor );
try {
streamReportProcessor.processReport();
} finally {
streamReportProcessor.close();
}
} finally {
outputStream.close();
}
return ( outputStream.toByteArray() );
}
protected byte[] executeTableStream( final MasterReport report ) throws IOException, ReportProcessingException {
final MemoryByteArrayOutputStream outputStream = new MemoryByteArrayOutputStream();
try {
final XmlTableOutputProcessor outputProcessor =
new XmlTableOutputProcessor( outputStream, new XmlTableOutputProcessorMetaData(
XmlTableOutputProcessorMetaData.PAGINATION_NONE, localFontRegistry ) );
final ReportProcessor streamReportProcessor = new StreamReportProcessor( report, outputProcessor );
try {
streamReportProcessor.processReport();
} finally {
streamReportProcessor.close();
}
} finally {
outputStream.close();
}
return ( outputStream.toByteArray() );
}
protected void initializeTestEnvironment() throws Exception {
}
protected void runAllGoldReports() throws Exception {
if ( "true".equals( ClassicEngineBoot.getInstance().getGlobalConfig().getConfigProperty(
ClassicEngineCoreModule.COMPLEX_TEXT_CONFIG_OVERRIDE_KEY ) ) ) {
Assert.fail( "Dont run GoldenSample tests with the new layout system. These tests are not platform independent." );
}
final int numThreads =
Math.max( 1, ClassicEngineBoot.getInstance().getExtendedConfig().getIntProperty(
"org.pentaho.reporting.engine.classic.core.testsupport.gold.MaxWorkerThreads",
Math.max( 1, Runtime.getRuntime().availableProcessors() - 1 ) ) );
StopWatch w = new StopWatch();
w.start();
try {
if ( numThreads == 1 ) {
runAllGoldReportsSerial();
} else {
runAllGoldReportsInParallel( numThreads );
}
} finally {
System.out.println( w.toString() );
}
}
protected void runAllGoldReportsSerial() throws Exception {
initializeTestEnvironment();
List<Throwable> errors = Collections.synchronizedList( new ArrayList<Throwable>() );
List<ExecuteReportRunner> reports = new ArrayList<ExecuteReportRunner>();
reports.addAll( collectReports( "reports", ReportProcessingMode.legacy, errors ) );
reports.addAll( collectReports( "reports", ReportProcessingMode.migration, errors ) );
reports.addAll( collectReports( "reports", ReportProcessingMode.current, errors ) );
reports.addAll( collectReports( "reports-4.0", ReportProcessingMode.migration, errors ) );
reports.addAll( collectReports( "reports-4.0", ReportProcessingMode.current, errors ) );
for ( ExecuteReportRunner report : reports ) {
report.run();
}
if ( errors.isEmpty() == false ) {
Log log = LogFactory.getLog( GoldTestBase.class );
for ( Throwable throwable : errors ) {
log.error( "Failed", throwable );
}
Assert.fail();
}
System.out.println( findMarker() );
}
protected void runAllGoldReportsInParallel( int threads ) throws Exception {
initializeTestEnvironment();
final List<Throwable> errors = Collections.synchronizedList( new ArrayList<Throwable>() );
final ExecutorService threadPool =
new ThreadPoolExecutor( threads, threads, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>(),
new TestThreadFactory(), new ThreadPoolExecutor.AbortPolicy() );
List<ExecuteReportRunner> reports = new ArrayList<ExecuteReportRunner>();
reports.addAll( collectReports( "reports", ReportProcessingMode.legacy, errors ) );
reports.addAll( collectReports( "reports", ReportProcessingMode.migration, errors ) );
reports.addAll( collectReports( "reports", ReportProcessingMode.current, errors ) );
reports.addAll( collectReports( "reports-4.0", ReportProcessingMode.migration, errors ) );
reports.addAll( collectReports( "reports-4.0", ReportProcessingMode.current, errors ) );
for ( ExecuteReportRunner report : reports ) {
threadPool.submit( report );
}
threadPool.shutdown();
while ( threadPool.isTerminated() == false ) {
threadPool.awaitTermination( 5, TimeUnit.MINUTES );
}
if ( errors.isEmpty() == false ) {
Log log = LogFactory.getLog( GoldTestBase.class );
for ( Throwable throwable : errors ) {
log.error( "Failed", throwable );
}
Assert.fail();
}
}
private List<ExecuteReportRunner> collectReports( final String sourceDirectoryName, final ReportProcessingMode mode,
final List<Throwable> errors ) throws Exception {
final File marker = findMarker();
final File reports = new File( marker, sourceDirectoryName );
final File gold = new File( marker, mode.getGoldDirectoryName() );
final FilenameFilter filter = createReportFilter();
final File[] files = reports.listFiles( filter );
if ( files == null ) {
throw new IOException( "IO-Error while listing files for '" + reports + "'" );
}
final HashSet<String> fileSet = new HashSet<String>();
for ( final File file : files ) {
final String s = file.getName().toLowerCase();
if ( fileSet.add( s ) == false ) {
// the toy systems MacOS X and Windows use case-insensitive file systems and completely
// mess up when there are two files with what they consider the same name.
throw new IllegalStateException( "There is a golden sample with the same Windows/Mac "
+ "filename in the directory. Make sure your files are unique and lowercase." );
}
}
List<ExecuteReportRunner> retval = new ArrayList<ExecuteReportRunner>();
for ( final File file : files ) {
if ( file.isDirectory() ) {
continue;
}
try {
retval.add( new ExecuteReportRunner( file, gold, errors, mode ) );
} catch ( Throwable re ) {
throw new Exception( "Failed at " + file, re );
}
}
return retval;
}
protected void runSingleGoldReport( final String file, final ReportProcessingMode mode ) throws Exception {
initializeTestEnvironment();
final File marker = findMarker();
final File gold = new File( marker, mode.getGoldDirectoryName() );
try {
final File reportFile = findReport( file );
System.out.printf( "Processing %s in mode=%s%n", file, mode );
run( reportFile, gold, mode );
System.out.printf( "Finished %s in mode=%s%n", file, mode );
} catch ( Throwable re ) {
throw new Exception( "Failed at " + file, re );
}
System.out.println( marker );
}
private File findReport( final String file ) throws FileNotFoundException {
final File marker = findMarker();
final File reports = new File( marker, "reports" );
final File reportFile = new File( reports, file );
if ( reportFile.exists() ) {
return reportFile;
}
final File reports4 = new File( marker, "reports-4.0" );
final File reportFile4 = new File( reports4, file );
if ( reportFile4.exists() ) {
return reportFile4;
}
throw new FileNotFoundException( file );
}
protected FilesystemFilter createReportFilter() {
return new FilesystemFilter( new String[] { ".prpt", ".report", ".xml" }, "Reports", false );
}
}