/**
* Copyright (c) 2002-2012 "Neo Technology,"
* Network Engine for Objects in Lund AB [http://neotechnology.com]
*
* This file is part of Neo4j.
*
* Neo4j is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* 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 Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.neo4j.consistency.report;
import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Proxy;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.List;
import org.hamcrest.Description;
import org.hamcrest.Matcher;
import org.junit.Rule;
import org.junit.Test;
import org.junit.internal.matchers.TypeSafeMatcher;
import org.junit.rules.TestRule;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import org.junit.runners.Suite;
import org.junit.runners.model.Statement;
import org.mockito.ArgumentCaptor;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;
import org.neo4j.consistency.RecordType;
import org.neo4j.consistency.checking.ComparativeRecordChecker;
import org.neo4j.consistency.checking.RecordCheck;
import org.neo4j.consistency.store.DiffRecordAccess;
import org.neo4j.consistency.store.RecordAccess;
import org.neo4j.consistency.store.RecordReference;
import org.neo4j.kernel.impl.nioneo.store.AbstractBaseRecord;
import org.neo4j.kernel.impl.nioneo.store.DynamicRecord;
import org.neo4j.kernel.impl.nioneo.store.NeoStoreRecord;
import org.neo4j.kernel.impl.nioneo.store.NodeRecord;
import org.neo4j.kernel.impl.nioneo.store.PropertyBlock;
import org.neo4j.kernel.impl.nioneo.store.PropertyIndexRecord;
import org.neo4j.kernel.impl.nioneo.store.PropertyRecord;
import org.neo4j.kernel.impl.nioneo.store.RelationshipRecord;
import org.neo4j.kernel.impl.nioneo.store.RelationshipTypeRecord;
import static org.mockito.Matchers.any;
import static org.mockito.Matchers.argThat;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.verifyZeroInteractions;
@RunWith(Suite.class)
@Suite.SuiteClasses({ConsistencyReporterTest.TestAllReportMessages.class,
ConsistencyReporterTest.TestReportLifecycle.class})
public class ConsistencyReporterTest
{
public static class TestReportLifecycle
{
@Test
public void shouldSummarizeStatisticsAfterCheck()
{
// given
ConsistencySummaryStatistics summary = mock( ConsistencySummaryStatistics.class );
ConsistencyReporter.ReportHandler handler = new ConsistencyReporter.ReportHandler(
mock( ConsistencyLogger.class ), summary, RecordType.PROPERTY, new PropertyRecord( 0 ) );
// when
handler.updateSummary();
// then
verify( summary ).update( RecordType.PROPERTY, 0, 0 );
verifyNoMoreInteractions( summary );
}
@Test
public void shouldOnlySummarizeStatisticsWhenAllReferencesAreChecked()
{
// given
ConsistencySummaryStatistics summary = mock( ConsistencySummaryStatistics.class );
ConsistencyReporter.ReportHandler handler = new ConsistencyReporter.ReportHandler(
mock( ConsistencyLogger.class ), summary, RecordType.PROPERTY, new PropertyRecord( 0 ) );
ConsistencyReport.PropertyConsistencyReport report =
(ConsistencyReport.PropertyConsistencyReport) Proxy
.newProxyInstance( ConsistencyReport.PropertyConsistencyReport.class.getClassLoader(),
new Class[]{ConsistencyReport.PropertyConsistencyReport.class},
handler );
@SuppressWarnings("unchecked")
RecordReference<PropertyRecord> reference = mock( RecordReference.class );
@SuppressWarnings("unchecked")
ComparativeRecordChecker<PropertyRecord, PropertyRecord, ConsistencyReport.PropertyConsistencyReport>
checker = mock( ComparativeRecordChecker.class );
handler.forReference( report, reference, checker );
@SuppressWarnings("unchecked")
ArgumentCaptor<PendingReferenceCheck<PropertyRecord>> captor =
(ArgumentCaptor) ArgumentCaptor.forClass( PendingReferenceCheck.class );
verify( reference ).dispatch( captor.capture() );
PendingReferenceCheck pendingRefCheck = captor.getValue();
// when
handler.updateSummary();
// then
verifyZeroInteractions( summary );
// when
pendingRefCheck.skip();
// then
verify( summary ).update( RecordType.PROPERTY, 0, 0 );
verifyNoMoreInteractions( summary );
}
}
@RunWith(Parameterized.class)
public static class TestAllReportMessages implements Answer
{
@Test
@SuppressWarnings("unchecked")
public void shouldLogInconsistency() throws Exception
{
// given
ConsistencyLogger logger = mock( ConsistencyLogger.class );
ConsistencyReport.Reporter reporter = new ConsistencyReporter(
logger, mock( DiffRecordAccess.class ), new ConsistencySummaryStatistics() );
// when
reportMethod.invoke( reporter, parameters( reportMethod ) );
// then
if ( method.getAnnotation( ConsistencyReport.Warning.class ) == null )
{
if ( reportMethod.getName().endsWith( "Change" ) )
{
verify( logger ).error( any( RecordType.class ),
any( AbstractBaseRecord.class ), any( AbstractBaseRecord.class ),
argThat( hasExpectedFormat() ), any( Object[].class ) );
}
else
{
verify( logger ).error( any( RecordType.class ),
any( AbstractBaseRecord.class ),
argThat( hasExpectedFormat() ), any( Object[].class ) );
}
}
else
{
if ( reportMethod.getName().endsWith( "Change" ) )
{
verify( logger ).warning( any( RecordType.class ),
any( AbstractBaseRecord.class ), any( AbstractBaseRecord.class ),
argThat( hasExpectedFormat() ), any( Object[].class ) );
}
else
{
verify( logger ).warning( any( RecordType.class ),
any( AbstractBaseRecord.class ),
argThat( hasExpectedFormat() ), any( Object[].class ) );
}
}
}
private final Method reportMethod;
private final Method method;
public TestAllReportMessages( Method reportMethod, Method method )
{
this.reportMethod = reportMethod;
this.method = method;
}
@Parameterized.Parameters
public static List<Object[]> methods()
{
ArrayList<Object[]> methods = new ArrayList<Object[]>();
for ( Method reporterMethod : ConsistencyReport.Reporter.class.getMethods() )
{
Type[] parameterTypes = reporterMethod.getGenericParameterTypes();
ParameterizedType checkerParameter = (ParameterizedType) parameterTypes[parameterTypes.length - 1];
Class reportType = (Class) checkerParameter.getActualTypeArguments()[1];
for ( Method method : reportType.getMethods() )
{
if ( !method.getName().equals( "forReference" ) )
{
methods.add( new Object[]{reporterMethod, method} );
}
}
}
return methods;
}
@Rule
public final TestRule logFailure = new TestRule()
{
@Override
public Statement apply( final Statement base, org.junit.runner.Description description )
{
return new Statement()
{
@Override
public void evaluate() throws Throwable
{
try
{
base.evaluate();
}
catch ( Throwable failure )
{
System.err.println( "Failure in " + TestAllReportMessages.this + ": " + failure );
throw failure;
}
}
};
}
};
@Override
public String toString()
{
return String.format( "report.%s( %s{ reporter.%s(); } )",
reportMethod.getName(), signatureOf( reportMethod ), method.getName() );
}
private static String signatureOf( Method reportMethod )
{
if ( reportMethod.getParameterTypes().length == 2 )
{
return "record, RecordCheck( reporter )";
}
else
{
return "oldRecord, newRecord, RecordCheck( reporter )";
}
}
private Object[] parameters( Method method )
{
Class<?>[] parameterTypes = method.getParameterTypes();
Object[] parameters = new Object[parameterTypes.length];
for ( int i = 0; i < parameters.length; i++ )
{
parameters[i] = parameter( parameterTypes[i] );
}
return parameters;
}
private Object parameter( Class<?> type )
{
if ( type == RecordType.class )
{
return RecordType.STRING_PROPERTY;
}
if ( type == RecordCheck.class )
{
return mockChecker();
}
if ( type == NodeRecord.class )
{
return new NodeRecord( 0, 1, 2 );
}
if ( type == RelationshipRecord.class )
{
return new RelationshipRecord( 0, 1, 2, 3 );
}
if ( type == PropertyRecord.class )
{
return new PropertyRecord( 0 );
}
if ( type == PropertyIndexRecord.class )
{
return new PropertyIndexRecord( 0 );
}
if ( type == PropertyBlock.class )
{
return new PropertyBlock();
}
if ( type == RelationshipTypeRecord.class )
{
return new RelationshipTypeRecord( 0 );
}
if ( type == DynamicRecord.class )
{
return new DynamicRecord( 0 );
}
if ( type == NeoStoreRecord.class )
{
return new NeoStoreRecord();
}
throw new IllegalArgumentException( type.getName() );
}
@SuppressWarnings("unchecked")
private RecordCheck mockChecker()
{
RecordCheck checker = mock( RecordCheck.class );
doAnswer( this ).when( checker ).check( any( AbstractBaseRecord.class ),
any( ConsistencyReport.class ),
any( RecordAccess.class ) );
doAnswer( this ).when( checker ).checkChange( any( AbstractBaseRecord.class ),
any( AbstractBaseRecord.class ),
any( ConsistencyReport.class ),
any( DiffRecordAccess.class ) );
return checker;
}
@Override
public Object answer( InvocationOnMock invocation ) throws Throwable
{
Object[] arguments = invocation.getArguments();
return method.invoke( arguments[arguments.length - 2], parameters( method ) );
}
}
private static Matcher<String> hasExpectedFormat()
{
return new TypeSafeMatcher<String>()
{
@Override
public boolean matchesSafely( String item )
{
return item.trim().split( " " ).length > 1;
}
@Override
public void describeTo( Description description )
{
description.appendText( "message of valid format" );
}
};
}
}