/**
* 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.checking.full;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.HashMap;
import java.util.Map;
import org.junit.Rule;
import org.junit.Test;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;
import org.neo4j.consistency.ConsistencyCheckSettings;
import org.neo4j.consistency.checking.CheckDecorator;
import org.neo4j.consistency.checking.PrimitiveRecordCheck;
import org.neo4j.consistency.checking.RecordCheck;
import org.neo4j.consistency.report.ConsistencyLogger;
import org.neo4j.consistency.report.ConsistencyReport;
import org.neo4j.consistency.report.ConsistencySummaryStatistics;
import org.neo4j.consistency.report.PendingReferenceCheck;
import org.neo4j.consistency.store.DiffRecordAccess;
import org.neo4j.consistency.store.RecordAccess;
import org.neo4j.consistency.store.RecordReference;
import org.neo4j.graphdb.DynamicRelationshipType;
import org.neo4j.graphdb.GraphDatabaseService;
import org.neo4j.graphdb.Node;
import org.neo4j.graphdb.factory.GraphDatabaseSettings;
import org.neo4j.helpers.progress.ProgressMonitorFactory;
import org.neo4j.kernel.configuration.Config;
import org.neo4j.kernel.configuration.ConfigurationDefaults;
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.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 org.neo4j.kernel.impl.nioneo.store.StoreAccess;
import org.neo4j.test.GraphStoreFixture;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.fail;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verifyZeroInteractions;
import static org.mockito.Mockito.withSettings;
import static org.neo4j.helpers.collection.MapUtil.stringMap;
import static org.neo4j.test.Property.property;
import static org.neo4j.test.Property.set;
public class ExecutionOrderIntegrationTest
{
@Rule
public final GraphStoreFixture fixture = new GraphStoreFixture()
{
@Override
protected void generateInitialData( GraphDatabaseService graphDb )
{
// TODO: create bigger sample graph here
org.neo4j.graphdb.Transaction tx = graphDb.beginTx();
try
{
Node node1 = set( graphDb.createNode() );
Node node2 = set( graphDb.createNode(), property( "key", "value" ) );
node1.createRelationshipTo( node2, DynamicRelationshipType.withName( "C" ) );
tx.success();
}
finally
{
tx.finish();
}
}
};
private static final boolean LOG_DUPLICATES = false;
@Test
public void shouldRunSameChecksInMultiPassAsInSingleSingleThreadedPass() throws Exception
{
// given
StoreAccess store = fixture.storeAccess();
DiffRecordAccess access = FullCheck.recordAccess( store );
FullCheck singlePass = new FullCheck( config( TaskExecutionOrder.SINGLE_THREADED ),
ProgressMonitorFactory.NONE );
FullCheck multiPass = new FullCheck( config( TaskExecutionOrder.MULTI_PASS ),
ProgressMonitorFactory.NONE );
ConsistencySummaryStatistics multiPassSummary = new ConsistencySummaryStatistics();
ConsistencySummaryStatistics singlePassSummary = new ConsistencySummaryStatistics();
ConsistencyLogger logger = mock( ConsistencyLogger.class );
InvocationLog singlePassChecks = new InvocationLog();
InvocationLog multiPassChecks = new InvocationLog();
// when
singlePass.execute( store, new LogDecorator( singlePassChecks ), access, logger, singlePassSummary );
multiPass.execute( store, new LogDecorator( multiPassChecks ), access, logger, multiPassSummary );
// then
verifyZeroInteractions( logger );
assertEquals( "Expected no inconsistencies in single pass.",
0, singlePassSummary.getTotalInconsistencyCount() );
assertEquals( "Expected no inconsistencies in multiple passes.",
0, multiPassSummary.getTotalInconsistencyCount() );
assertSameChecks( singlePassChecks.data, multiPassChecks.data );
if ( singlePassChecks.duplicates.size() != multiPassChecks.duplicates.size() )
{
if ( LOG_DUPLICATES )
{
System.out.printf( "Duplicate checks with single pass: %s, duplicate checks with multiple passes: %s%n",
singlePassChecks.duplicates, multiPassChecks.duplicates );
}
}
}
static Config config( TaskExecutionOrder executionOrder )
{
return new Config( new ConfigurationDefaults( GraphDatabaseSettings.class, ConsistencyCheckSettings.class )
.apply( stringMap(
ConsistencyCheckSettings.consistency_check_execution_order.name(),
executionOrder.name())
) );
}
private static class InvocationLog
{
private final Map<String, Throwable> data = new HashMap<String, Throwable>();
private final Map<String, Integer> duplicates = new HashMap<String, Integer>();
void log( PendingReferenceCheck check, InvocationOnMock invocation )
{
StringBuilder entry = new StringBuilder( invocation.getMethod().getName() ).append( '(' );
entry.append( check );
for ( Object arg : invocation.getArguments() )
{
if ( arg instanceof AbstractBaseRecord )
{
AbstractBaseRecord record = (AbstractBaseRecord) arg;
entry.append( ',' ).append( record.getClass().getSimpleName() )
.append( '[' ).append( record.getLongId() ).append( ']' );
}
}
String message = entry.append( ')' ).toString();
if ( null != data.put( message, new Throwable( message ) ) )
{
Integer cur = duplicates.get( message );
if ( cur == null )
{
cur = 1;
}
duplicates.put( message, cur + 1 );
}
}
}
private static void assertSameChecks( Map<String, Throwable> singlePassChecks,
Map<String, Throwable> multiPassChecks )
{
if ( !singlePassChecks.keySet().equals( multiPassChecks.keySet() ) )
{
Map<String, Throwable> missing = new HashMap<String, Throwable>( singlePassChecks );
Map<String, Throwable> extras = new HashMap<String, Throwable>( multiPassChecks );
missing.keySet().removeAll( multiPassChecks.keySet() );
extras.keySet().removeAll( singlePassChecks.keySet() );
StringWriter diff = new StringWriter();
PrintWriter writer = new PrintWriter( diff );
if ( !missing.isEmpty() )
{
writer.append( "These expected checks were missing:\n" );
for ( Throwable check : missing.values() )
{
writer.append( " " );
check.printStackTrace( writer );
}
}
if ( !extras.isEmpty() )
{
writer.append( "These extra checks were not expected:\n" );
for ( Throwable check : extras.values() )
{
writer.append( " " );
check.printStackTrace( writer );
}
}
fail( diff.toString() );
}
}
private static class LogDecorator implements CheckDecorator
{
private final InvocationLog log;
LogDecorator( InvocationLog log )
{
this.log = log;
}
<REC extends AbstractBaseRecord, REP extends ConsistencyReport<REC, REP>> RecordCheck<REC, REP> logging(
RecordCheck<REC, REP> checker )
{
return new LoggingChecker<REC, REP>( checker, log );
}
@Override
public RecordCheck<NeoStoreRecord, ConsistencyReport.NeoStoreConsistencyReport> decorateNeoStoreChecker(
PrimitiveRecordCheck<NeoStoreRecord, ConsistencyReport.NeoStoreConsistencyReport> checker )
{
return logging( checker );
}
@Override
public RecordCheck<NodeRecord, ConsistencyReport.NodeConsistencyReport> decorateNodeChecker(
PrimitiveRecordCheck<NodeRecord, ConsistencyReport.NodeConsistencyReport> checker )
{
return logging( checker );
}
@Override
public RecordCheck<RelationshipRecord, ConsistencyReport.RelationshipConsistencyReport> decorateRelationshipChecker(
PrimitiveRecordCheck<RelationshipRecord, ConsistencyReport.RelationshipConsistencyReport> checker )
{
return logging( checker );
}
@Override
public RecordCheck<PropertyRecord, ConsistencyReport.PropertyConsistencyReport> decoratePropertyChecker(
RecordCheck<PropertyRecord, ConsistencyReport.PropertyConsistencyReport> checker )
{
return logging( checker );
}
@Override
public RecordCheck<PropertyIndexRecord, ConsistencyReport.PropertyKeyConsistencyReport> decoratePropertyKeyChecker(
RecordCheck<PropertyIndexRecord, ConsistencyReport.PropertyKeyConsistencyReport> checker )
{
return logging( checker );
}
@Override
public RecordCheck<RelationshipTypeRecord, ConsistencyReport.LabelConsistencyReport> decorateLabelChecker(
RecordCheck<RelationshipTypeRecord, ConsistencyReport.LabelConsistencyReport> checker )
{
return logging( checker );
}
}
private static class LoggingChecker<REC extends AbstractBaseRecord, REP extends ConsistencyReport<REC, REP>>
implements RecordCheck<REC, REP>
{
private final RecordCheck<REC, REP> checker;
private final InvocationLog log;
LoggingChecker( RecordCheck<REC, REP> checker, InvocationLog log )
{
this.checker = checker;
this.log = log;
}
@Override
public void check( REC record, REP report, RecordAccess records )
{
checker.check( record, report, new ComparativeLogging( (DiffRecordAccess) records, log ) );
}
@Override
public void checkChange( REC oldRecord, REC newRecord, REP report, DiffRecordAccess records )
{
checker.checkChange( oldRecord, newRecord, report, new ComparativeLogging( records, log ) );
}
}
private static class LoggingReference<T extends AbstractBaseRecord> implements RecordReference<T>
{
private final RecordReference<T> reference;
private final InvocationLog log;
LoggingReference( RecordReference<T> reference, InvocationLog log )
{
this.reference = reference;
this.log = log;
}
@Override
public void dispatch( PendingReferenceCheck<T> reporter )
{
reference.dispatch( mock( (Class<PendingReferenceCheck<T>>) reporter.getClass(),
withSettings().spiedInstance( reporter )
.defaultAnswer( new ReporterSpy<T>( reference, reporter, log ) ) ) );
}
}
private static class ReporterSpy<T extends AbstractBaseRecord> implements Answer<Object>
{
private final RecordReference<T> reference;
private final PendingReferenceCheck<T> reporter;
private final InvocationLog log;
public ReporterSpy( RecordReference<T> reference, PendingReferenceCheck<T> reporter, InvocationLog log )
{
this.reference = reference;
this.reporter = reporter;
this.log = log;
}
@Override
public Object answer( InvocationOnMock invocation ) throws Throwable
{
if ( !(reference instanceof RecordReference.SkippingReference<?>) )
{
log.log( reporter, invocation );
}
return invocation.callRealMethod();
}
}
private static class ComparativeLogging implements DiffRecordAccess
{
private final DiffRecordAccess access;
private final InvocationLog log;
ComparativeLogging( DiffRecordAccess access, InvocationLog log )
{
this.access = access;
this.log = log;
}
private <T extends AbstractBaseRecord> LoggingReference<T> logging( RecordReference<T> actual )
{
return new LoggingReference<T>( actual, log );
}
@Override
public RecordReference<NodeRecord> previousNode( long id )
{
return logging( access.previousNode( id ) );
}
@Override
public RecordReference<RelationshipRecord> previousRelationship( long id )
{
return logging( access.previousRelationship( id ) );
}
@Override
public RecordReference<PropertyRecord> previousProperty( long id )
{
return logging( access.previousProperty( id ) );
}
@Override
public RecordReference<NeoStoreRecord> previousGraph()
{
return logging( access.previousGraph() );
}
@Override
public NodeRecord changedNode( long id )
{
return access.changedNode( id );
}
@Override
public RelationshipRecord changedRelationship( long id )
{
return access.changedRelationship( id );
}
@Override
public PropertyRecord changedProperty( long id )
{
return access.changedProperty( id );
}
@Override
public DynamicRecord changedString( long id )
{
return access.changedString( id );
}
@Override
public DynamicRecord changedArray( long id )
{
return access.changedArray( id );
}
@Override
public RecordReference<NodeRecord> node( long id )
{
return logging( access.node( id ) );
}
@Override
public RecordReference<RelationshipRecord> relationship( long id )
{
return logging( access.relationship( id ) );
}
@Override
public RecordReference<PropertyRecord> property( long id )
{
return logging( access.property( id ) );
}
@Override
public RecordReference<RelationshipTypeRecord> relationshipLabel( int id )
{
return logging( access.relationshipLabel( id ) );
}
@Override
public RecordReference<PropertyIndexRecord> propertyKey( int id )
{
return logging( access.propertyKey( id ) );
}
@Override
public RecordReference<DynamicRecord> string( long id )
{
return logging( access.string( id ) );
}
@Override
public RecordReference<DynamicRecord> array( long id )
{
return logging( access.array( id ) );
}
@Override
public RecordReference<DynamicRecord> relationshipLabelName( int id )
{
return logging( access.relationshipLabelName( id ) );
}
@Override
public RecordReference<DynamicRecord> propertyKeyName( int id )
{
return logging( access.propertyKeyName( id ) );
}
@Override
public RecordReference<NeoStoreRecord> graph()
{
return logging( access.graph() );
}
}
}