/* * This program is free software; you can redistribute it and/or modify it under the * terms of the GNU General Public License, version 2 as published by the Free Software * Foundation. * * You should have received a copy of the GNU General Public License along with this * program; if not, you can obtain a copy at http://www.gnu.org/licenses/gpl-2.0.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 General Public License for more details. * * * Copyright 2006 - 2016 Pentaho Corporation. All rights reserved. */ package org.pentaho.reporting.platform.plugin.async; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; 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.platform.api.engine.ILogger; import org.pentaho.platform.api.engine.IPentahoSession; import org.pentaho.platform.api.engine.ISecurityHelper; import org.pentaho.platform.engine.core.audit.MessageTypes; import org.pentaho.platform.engine.security.SecurityHelper; import org.pentaho.reporting.engine.classic.core.ReportInterruptedException; import org.pentaho.reporting.platform.plugin.AuditWrapper; import org.pentaho.reporting.platform.plugin.SimpleReportingComponent; import org.pentaho.reporting.platform.plugin.staging.AsyncJobFileStagingHandler; import org.pentaho.reporting.platform.plugin.staging.IFixedSizeStreamingContent; import java.io.OutputStream; import java.io.Serializable; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Stack; import java.util.UUID; import java.util.concurrent.Callable; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.mockito.Mockito.*; public class PentahoAsyncExecutionAuditTest { private static final String url = "junit url"; private static final String auditId = "auditId"; private static final String sessionId = "sessionId"; private static final String sessionName = "junitName"; private static final UUID uuid = UUID.randomUUID(); private SimpleReportingComponent component = mock( SimpleReportingComponent.class ); private AsyncJobFileStagingHandler handler = mock( AsyncJobFileStagingHandler.class ); private IPentahoSession session = mock( IPentahoSession.class ); private AuditWrapper wrapper = mock( AuditWrapper.class ); private OutputStream outputStream = mock( OutputStream.class ); @Before public void before() throws Exception { when( session.getId() ).thenReturn( sessionId ); when( session.getName() ).thenReturn( sessionName ); when( handler.getStagingContent() ).thenReturn( new AbstractAsyncReportExecution.NullSizeStreamingContent() ); final ISecurityHelper iSecurityHelper = mock( ISecurityHelper.class ); when( iSecurityHelper.runAsUser( any(), any() ) ).thenAnswer( new Answer<Object>() { @Override public Object answer( InvocationOnMock invocation ) throws Throwable { final Object call = ( (Callable) invocation.getArguments()[ 1 ] ).call(); return call; } } ); SecurityHelper.setMockInstance( iSecurityHelper ); } @Test public void testSuccessExecutionAudit() throws Exception { final PentahoAsyncReportExecution execution = new PentahoAsyncReportExecution( url, component, handler, session, auditId, wrapper ); execution.notifyTaskQueued( uuid, Collections.emptyList() ); //this is successful story when( component.execute() ).thenReturn( true ); execution.call(); verify( wrapper, Mockito.times( 1 ) ).audit( eq( sessionId ), eq( sessionName ), eq( url ), startsWith( execution.getClass().getName() ), startsWith( execution.getClass().getName() ), eq( MessageTypes.INSTANCE_START ), eq( auditId ), eq( "" ), eq( (float) 0 ), any( ILogger.class ) ); verify( wrapper, Mockito.times( 1 ) ).audit( eq( sessionId ), eq( sessionName ), eq( url ), startsWith( execution.getClass().getName() ), startsWith( execution.getClass().getName() ), eq( MessageTypes.INSTANCE_END ), eq( auditId ), eq( "" ), anyFloat(), // hope more than 0 any( ILogger.class ) ); } @Test public void testFailedExecutionAudit() throws Exception { final PentahoAsyncReportExecution execution = new PentahoAsyncReportExecution( url, component, handler, session, auditId, wrapper ); execution.notifyTaskQueued( uuid, Collections.emptyList() ); //this is sad story when( component.execute() ).thenReturn( false ); execution.call(); // we always log instance start for every execution attempt verify( wrapper, Mockito.times( 1 ) ).audit( eq( sessionId ), eq( sessionName ), eq( url ), startsWith( execution.getClass().getName() ), startsWith( execution.getClass().getName() ), eq( MessageTypes.INSTANCE_START ), eq( auditId ), eq( "" ), eq( (float) 0 ), any( ILogger.class ) ); // no async reports for this case. verify( wrapper, Mockito.times( 1 ) ).audit( eq( sessionId ), eq( sessionName ), eq( url ), eq( execution.getClass().getName() ), eq( execution.getClass().getName() ), eq( MessageTypes.FAILED ), eq( auditId ), eq( "" ), eq( (float) 0 ), any( ILogger.class ) ); } /** * We need a special wrapper that will be able to get id from one thread (created for report execution) and made this * value accessible in contest of this junit test thread. * * @throws Exception */ @Test @SuppressWarnings( "unchecked" ) public void testInstanceIdIsSet() throws Exception { final CountDownLatch latch = new CountDownLatch( 1 ); final ThreadSpyAuditWrapper wrapper = new ThreadSpyAuditWrapper( latch ); final String expected = UUID.randomUUID().toString(); final PentahoAsyncReportExecution execution = new PentahoAsyncReportExecution( url, component, handler, session, expected, wrapper ); final PentahoAsyncExecutor<IAsyncReportState> executor = new PentahoAsyncExecutor<>( 2, 0 ); executor.addTask( execution, session ); latch.await(); synchronized ( latch ) { assertEquals( expected, wrapper.capturedId ); } } /** * This is quite intricate test, but make attempt to inspect thread pool executor as much detailed as possible. * * @throws Exception */ @Test public void testCancellationStatusTest() throws Exception { final int CAPACITY = 4; final int NUMBER_OF_THREADS = CAPACITY + 1; // controlled execution: final CountDownLatch startLatch = new CountDownLatch( 1 ); final CountDownLatch waitSubmitted = new CountDownLatch( CAPACITY ); final CountDownLatch finalBlock = new CountDownLatch( CAPACITY ); // this is one more hack - we waiting on a handler's method that will be called in finally block // this way synchronizing on catch() block was executed. final Answer<OutputStream> handlerAnswer = invocation -> { finalBlock.countDown(); return outputStream; }; when( handler.getStagingOutputStream() ).thenAnswer( handlerAnswer ); // hello java 8! final Answer answer = invocation -> { waitSubmitted.countDown(); startLatch.await( 10, TimeUnit.SECONDS ); throw new ReportInterruptedException( "Bang" ); }; final Stack<UUID> tasksIds = new Stack<>(); final PentahoAsyncExecutor<IAsyncReportState> executor = new PentahoAsyncExecutor<>( CAPACITY, 0 ); // add one more excessive execution UUID id; for ( int i = 0; i < NUMBER_OF_THREADS; i++ ) { final SimpleReportingComponent component = mock( SimpleReportingComponent.class ); // every time component.execute() called - we do await on answer object // we wait on a latch exactly number of submitted threads when( component.execute() ).thenAnswer( answer ); final PentahoAsyncReportExecution execution = new PentahoAsyncReportExecution( url, component, handler, session, auditId, wrapper ); id = executor.addTask( execution, session ); // we save id's in order they would submitted assertNotNull( id ); tasksIds.push( id ); } // wait for all working tasks will hang on monitor 'startLatch' // at least we have to have 4 tasks waited for a startLatch waitSubmitted.await( 10, TimeUnit.SECONDS ); final IAsyncReportState state5 = executor.getReportState( tasksIds.peek(), session ); assertEquals( "4 running, one waiting", AsyncExecutionStatus.QUEUED, state5.getStatus() ); // now collect all futures to a one place List<ListenableFuture<IFixedSizeStreamingContent>> futures = new ArrayList<>(); final List<IAsyncReportState> states = new ArrayList<>(); do { // this is a cheat: we know this futures really ListenableFuture. // if you have class cast here... will require additional code to // wait fot their completition, see code below... futures.add( (ListenableFuture<IFixedSizeStreamingContent>) executor.getFuture( tasksIds.peek(), session ) ); states.add( executor.getReportState( tasksIds.pop(), session ) ); } while ( !tasksIds.isEmpty() ); assertEquals( "we have 5: 4 running and one waiting future", NUMBER_OF_THREADS, futures.size() ); // we manually explore futures statuses int done = 0; int cancelled = 0; // we have 5 running and no cancelled (one running is queued) for ( final ListenableFuture<IFixedSizeStreamingContent> item : futures ) { if ( item.isCancelled() ) { cancelled++; } else if ( item.isDone() ) { done++; } } assertEquals( "none done since all waiting on a latch", 0, done ); assertEquals( "still none cancelled", 0, cancelled ); // simulate on-logout call - will attempt to cancel all tasks. executor.onLogout( session ); // again - do a detailed insepction done = 0; cancelled = 0; // we have 5 running and no cancelled (one running is queued) for ( final ListenableFuture<IFixedSizeStreamingContent> item : futures ) { if ( item.isCancelled() ) { cancelled++; } else if ( item.isDone() ) { done++; } } assertEquals( "none done", 0, done ); assertEquals( "cancelled all", NUMBER_OF_THREADS, cancelled ); // after all futures seems to be cancelled - let try to complete them. final ListenableFuture<List<IFixedSizeStreamingContent>> all = Futures.successfulAsList( futures ); // now - release all - if someone was not cancelled - it make attempt to execute successfully. startLatch.countDown(); // wait for if any is still pending all.get(); // wait here before ALL threads come into final block finalBlock.await( 10, TimeUnit.SECONDS ); verify( wrapper, atLeast( CAPACITY ) ).audit( eq( sessionId ), eq( sessionName ), eq( url ), eq( PentahoAsyncReportExecution.class.getName() ), eq( PentahoAsyncReportExecution.class.getName() ), eq( MessageTypes.CANCELLED ), eq( auditId ), eq( "" ), anyFloat(), any( ILogger.class ) ); } /** * This is almost a copy of #testCancellationStatusTest above, but now we have one 'scheduled' execution. */ @Test public void testScheduledExecutionsNotGetCancelled() throws Exception { /* SecurityHelper.setMockInstance( mock( ISecurityHelper.class ) );*/ // we still have 4 threads capacity final int CAPACITY = 4; // but now one of the threads is scheduled final int NUMBER_OF_THREADS = CAPACITY + 2; // controlled execution: final CountDownLatch startLatch = new CountDownLatch( 1 ); final CountDownLatch waitSubmitted = new CountDownLatch( CAPACITY ); final CountDownLatch finalBlock = new CountDownLatch( NUMBER_OF_THREADS - 1 ); final Answer<OutputStream> handlerAnswer = invocation -> { finalBlock.countDown(); return outputStream; }; when( handler.getStagingOutputStream() ).thenAnswer( handlerAnswer ); final int[] doThrow = { 0 }; // hello java 8! final Answer answer = invocation -> { waitSubmitted.countDown(); startLatch.await( 10, TimeUnit.SECONDS ); synchronized ( this ) { if ( doThrow[ 0 ] < NUMBER_OF_THREADS - 1 ) { doThrow[ 0 ]++; throw new ReportInterruptedException( "Bang" ); } else { return true; } } }; final Stack<UUID> tasksIds = new Stack<>(); final PentahoAsyncExecutor<IAsyncReportState> executor = new PentahoAsyncExecutor<IAsyncReportState>( CAPACITY, 0 ) { @Override protected Callable<Serializable> getWriteToJcrTask( final IFixedSizeStreamingContent result, final IAsyncReportExecution runningTask ) { return () -> null; } }; UUID id; for ( int i = 0; i < NUMBER_OF_THREADS; i++ ) { final SimpleReportingComponent component = mock( SimpleReportingComponent.class ); when( component.execute() ).thenAnswer( answer ); final PentahoAsyncReportExecution execution = new PentahoAsyncReportExecution( url, component, handler, session, auditId, wrapper ); id = executor.addTask( execution, session ); assertNotNull( id ); tasksIds.push( id ); } waitSubmitted.await( 10, TimeUnit.SECONDS ); // remember - retrieve but does not remove final IAsyncReportState state5 = executor.getReportState( tasksIds.peek(), session ); assertEquals( "4 running, one waiting", AsyncExecutionStatus.QUEUED, state5.getStatus() ); // what we doing now - is attempt to schedule a peek of queue. // this is creates 'queued + scheduled' execution executor.schedule( tasksIds.peek(), session ); final List<ListenableFuture<IFixedSizeStreamingContent>> futures = new ArrayList<>(); do { futures.add( (ListenableFuture<IFixedSizeStreamingContent>) executor.getFuture( tasksIds.pop(), session ) ); } while ( !tasksIds.isEmpty() ); assertEquals( "we have running and waiting futures", NUMBER_OF_THREADS, futures.size() ); int done = 0; int cancelled = 0; for ( final ListenableFuture<IFixedSizeStreamingContent> item : futures ) { if ( item.isCancelled() ) { cancelled++; } else if ( item.isDone() ) { done++; } } assertEquals( "none done since all waiting on a latch", 0, done ); assertEquals( "still none cancelled", 0, cancelled ); // simulate on-logout call - will attempt to cancel all tasks. // remember - now we have one scheduled execution - so it should survive onLogout call executor.onLogout( session ); final ListenableFuture<List<IFixedSizeStreamingContent>> all = Futures.successfulAsList( futures ); startLatch.countDown(); all.get(); // again - do a detailed inspection done = 0; cancelled = 0; // we have 5 running and no cancelled (one running is queued) for ( final ListenableFuture<IFixedSizeStreamingContent> item : futures ) { if ( item.isCancelled() ) { cancelled++; } else if ( item.isDone() ) { done++; } } assertEquals( "scheduled is done:", 1, done ); assertEquals( "have cancelled all, BUT SCHEDULED!", NUMBER_OF_THREADS - 1, cancelled ); finalBlock.await( 10, TimeUnit.SECONDS ); //All started verify( wrapper, times( 6 ) ).audit( eq( sessionId ), eq( sessionName ), eq( url ), startsWith( PentahoAsyncReportExecution.class.getName() ), startsWith( PentahoAsyncReportExecution.class.getName() ), eq( MessageTypes.INSTANCE_START ), eq( auditId ), eq( "" ), anyFloat(), any( ILogger.class ) ); verify( wrapper, atLeast( CAPACITY ) ).audit( eq( sessionId ), eq( sessionName ), eq( url ), startsWith( PentahoAsyncReportExecution.class.getName() ), startsWith( PentahoAsyncReportExecution.class.getName() ), eq( MessageTypes.CANCELLED ), eq( auditId ), eq( "" ), anyFloat(), any( ILogger.class ) ); // this is ONE sucesfull execution for scheduled task since it was not cancelled!!! verify( wrapper, times( 1 ) ).audit( eq( sessionId ), eq( sessionName ), eq( url ), startsWith( PentahoAsyncReportExecution.class.getName() ), startsWith( PentahoAsyncReportExecution.class.getName() ), eq( MessageTypes.INSTANCE_END ), eq( auditId ), eq( "" ), anyFloat(), any( ILogger.class ) ); } private static class ThreadSpyAuditWrapper extends AuditWrapper { volatile String capturedId; private final CountDownLatch latch; ThreadSpyAuditWrapper( final CountDownLatch latch ) { this.latch = latch; } @Override public void audit( final String instanceId, final String userId, String actionName, final String objectType, final String processId, final String messageType, final String message, final String value, final float duration, final ILogger logger ) { synchronized ( latch ) { latch.countDown(); capturedId = ReportListenerThreadHolder.getRequestId(); } } } }