Game updates improvements

* Added a set of tables which define game updates and their targets.
These definitions replace the old enumerate type. Added a set of
triggers which automatically create specific update tables, insert
missing entries, etc... when game update types are being manipulated.

* Removed manual insertion of game updates from empire creation
function and universe generator.

* Added registration of core update targets (i.e. planets and empires),
updated all existing game update processing functions and added type
registrations

* Created Maven project for game updates control components, moved
existing components from the -simple project, rewritten most of what
they contained, added new components for server-side update batch
processing
This commit is contained in:
Emmanuel BENOîT 2012-02-03 16:25:03 +01:00
parent ba6a1e2b41
commit 56eddcc4f0
93 changed files with 4004 additions and 578 deletions

View file

@ -0,0 +1,62 @@
package com.deepclone.lw.beans.updates;
import java.util.Collection;
import java.util.HashSet;
import com.deepclone.lw.interfaces.game.updates.UpdateBatchProcessor;
/**
* A dummy batch processor used in game update tests
*
* @author <a href="mailto:tseeker@legacyworlds.com">E. Benoît</a>
*/
class DummyBatchProcessor
implements UpdateBatchProcessor
{
/** The name that will be returned by {@link #getUpdateType()} */
private final String updateType;
/** Ticks for which the {@link #processBatch(long)} was called */
private final HashSet< Long > ticks = new HashSet< Long >( );
/**
* Set the update type
*
* @param updateType
* the update type
*/
public DummyBatchProcessor( String updateType )
{
this.updateType = updateType;
}
/**
* @return whatever type name was set when the instance was created
*/
@Override
public String getUpdateType( )
{
return this.updateType;
}
/** Adds the specified tick identifier to the set of ticks */
@Override
public void processBatch( long tickId )
{
this.ticks.add( tickId );
}
/** @return the set of ticks for which {@link #processBatch(long)} was called */
Collection< Long > getTicks( )
{
return this.ticks;
}
}

View file

@ -0,0 +1,91 @@
package com.deepclone.lw.beans.updates;
import com.deepclone.lw.interfaces.game.updates.GameUpdateProcessor;
/**
* A mock game update processor
*
* <p>
* This mock component keeps track of which of its methods have been called. It can also simulate
* runtime errors during tick processing.
*
* @author <a href="mailto:tseeker@legacyworlds.com">E. Benoît</a>
*
*/
public class MockGameUpdateProcessor
implements GameUpdateProcessor
{
private boolean tryLockCalled = false;
private boolean unlockCalled = false;
private boolean endCycleCalled = false;
private boolean executeCalled = false;
private boolean failExecute = false;
public boolean wasTryLockCalled( )
{
return this.tryLockCalled;
}
public boolean wasUnlockCalled( )
{
return this.unlockCalled;
}
public boolean wasEndCycleCalled( )
{
return this.endCycleCalled;
}
public boolean wasExecuteCalled( )
{
return this.executeCalled;
}
public void setFailExecute( boolean failExecute )
{
this.failExecute = failExecute;
}
@Override
public boolean tryLock( )
{
this.tryLockCalled = true;
return true;
}
@Override
public void unlock( )
{
this.unlockCalled = true;
}
@Override
public boolean endPreviousCycle( )
{
this.endCycleCalled = true;
return false;
}
@Override
public void executeUpdateCycle( )
{
this.executeCalled = true;
if ( this.failExecute ) {
throw new RuntimeException( "fail" );
}
}
}

View file

@ -0,0 +1,41 @@
package com.deepclone.lw.beans.updates;
import java.util.HashMap;
import com.deepclone.lw.interfaces.game.updates.UpdateBatchProcessor;
/**
* A mock processor registry
*
* @author <a href="mailto:tseeker@legacyworlds.com">E. Benoît</a>
*/
class MockRegistry
implements ServerProcessorRegistry
{
/** The map of "processors" to return */
private final HashMap< String , UpdateBatchProcessor > processors = new HashMap< String , UpdateBatchProcessor >( );
/**
* Add a processor to the registry
*
* @param processor
* the processor to add
*/
public void put( UpdateBatchProcessor processor )
{
this.processors.put( processor.getUpdateType( ) , processor );
}
@Override
public UpdateBatchProcessor getProcessorFor( String type )
{
return this.processors.get( type );
}
}

View file

@ -0,0 +1,32 @@
package com.deepclone.lw.beans.updates;
import com.deepclone.lw.interfaces.game.updates.UpdatesDAO;
import com.deepclone.lw.sqld.sys.GameUpdateResult;
/**
* Mock updates DAO used in tests
*
* @author <a href="mailto:tseeker@legacyworlds.com">E. Benoît</a>
*/
class MockUpdatesDAO
implements UpdatesDAO
{
/** The index to read at the next call to {@link #processUpdates(long)} */
public int index = 0;
/** The values to return as the update type to process */
public String[] values = new String[] { };
@Override
public GameUpdateResult processUpdates( long tickId )
{
if ( this.index >= this.values.length ) {
return new GameUpdateResult( );
}
return new GameUpdateResult( this.values[ this.index++ ] );
}
}

View file

@ -0,0 +1,240 @@
package com.deepclone.lw.beans.updates;
import static org.junit.Assert.*;
import java.sql.Timestamp;
import java.util.Date;
import org.junit.Before;
import org.junit.Test;
import com.deepclone.lw.interfaces.game.updates.UpdateBatchProcessor;
import com.deepclone.lw.interfaces.sys.MaintenanceData;
import com.deepclone.lw.testing.MockLogger;
import com.deepclone.lw.testing.MockSystemStatus;
import com.deepclone.lw.testing.MockTransactionManager;
/**
* Tests for {@link GameUpdateProcessorBean}
*
* @author <a href="mailto:tseeker@legacyworlds.com">E. Benoît</a>
*/
public class TestGameUpdateProcessorBean
{
/** The mock logger */
private MockLogger logger;
/** Mock updates access interface */
private MockUpdatesDAO updatesDAO;
/** Mock processor registry */
private MockRegistry registry;
/** Mock system status component */
private MockSystemStatus system;
/** The instance under test */
private GameUpdateProcessorBean gup;
/**
* A fake batch processor which actually enables maintenance mode.
*
* @author <a href="mailto:tseeker@legacyworlds.com">E. Benoît</a>
*/
private static class MaintenanceEnabler
implements UpdateBatchProcessor
{
/** Mock system status component */
private MockSystemStatus system;
/**
* Set the mock system status component
*
* @param system
* the mock system status component
*/
MaintenanceEnabler( MockSystemStatus system )
{
this.system = system;
}
@Override
public String getUpdateType( )
{
return "maintenance";
}
@Override
public void processBatch( long tickId )
{
this.system.setMaintenance( new MaintenanceData( new Timestamp( new Date( ).getTime( ) ) , new Timestamp(
new Date( ).getTime( ) ) , "no reason" ) );
}
}
@Before
public void setUp( )
{
this.logger = new MockLogger( );
this.updatesDAO = new MockUpdatesDAO( );
this.updatesDAO.values = new String[] {
null
};
this.system = new MockSystemStatus( );
this.registry = new MockRegistry( );
this.registry.put( new MaintenanceEnabler( this.system ) );
this.gup = new GameUpdateProcessorBean( );
this.gup.setLogger( this.logger );
this.gup.setUpdatesDAO( this.updatesDAO );
this.gup.setRegistry( this.registry );
this.gup.setTransactionManager( new MockTransactionManager( ) );
this.gup.setSystemStatus( this.system );
}
/**
* Try locking the processor
*/
@Test
public void testLocking( )
{
assertTrue( this.gup.tryLock( ) );
assertFalse( this.gup.tryLock( ) );
}
/** Try unlocking the processor */
@Test
public void testUnlocking( )
{
this.gup.tryLock( );
this.gup.unlock( );
assertTrue( this.gup.tryLock( ) );
}
/** Try unlocking the processor when it's not locked */
@Test( expected = IllegalStateException.class )
public void testUnlockingWithNoLock( )
{
this.gup.unlock( );
}
/**
* {@link GameUpdateProcessorBean#endPreviousCycle()} returns <code>false</code> when there is
* no "stuck" tick
*/
@Test
public void testEndCycleNoStuckTick( )
{
assertFalse( this.gup.endPreviousCycle( ) );
}
/**
* {@link GameUpdateProcessorBean#endPreviousCycle()} returns <code>true</code> when maintenance
* mode is enabled, but the update is not processed
*/
@Test
public void testEndCycleMaintenance( )
{
this.system.setCSTBehaviour( -2 );
assertTrue( this.gup.endPreviousCycle( ) );
assertEquals( 0 , this.updatesDAO.index );
}
/**
* {@link GameUpdateProcessorBean#endPreviousCycle()} returns <code>true</code> when there is a
* stuck tick, and the update is processed
*/
@Test
public void testEndCycle( )
{
this.system.setCSTBehaviour( 0 );
assertTrue( this.gup.endPreviousCycle( ) );
assertEquals( 1 , this.updatesDAO.index );
}
/**
* {@link GameUpdateProcessorBean#executeUpdateCycle()} does not start a tick if there was a
* stuck tick, but the previous tick is processed.
*/
@Test
public void testProcessWithStuckTick( )
{
this.system.setCSTBehaviour( 0 );
this.gup.executeUpdateCycle( );
assertFalse( this.system.wasStartTickCalled( ) );
assertEquals( 1 , this.updatesDAO.index );
}
/**
* {@link GameUpdateProcessorBean#executeUpdateCycle()} does not start a tick if maintenance
* mode was enabled from the start
*/
@Test
public void testProcessWithStuckTickAndMaintenance( )
{
this.system.setCSTBehaviour( -2 );
this.gup.executeUpdateCycle( );
assertFalse( this.system.wasStartTickCalled( ) );
assertEquals( 0 , this.updatesDAO.index );
}
/**
* {@link GameUpdateProcessorBean#executeUpdateCycle()} does not start a tick if maintenance
* mode is enabled between the check for stuck ticks and the new tick's start.
*/
@Test
public void testProcessWithMaintenance( )
{
this.system.setSTBehaviour( -2 );
this.gup.executeUpdateCycle( );
assertTrue( this.system.wasStartTickCalled( ) );
assertEquals( 0 , this.updatesDAO.index );
}
/**
* {@link GameUpdateProcessorBean#executeUpdateCycle()} throws a runtime exception if there
* seems to be a tick running when the new tick is supposed to start.
*/
@Test( expected = RuntimeException.class )
public void testProcessWithBadState( )
{
this.system.setSTBehaviour( -1 );
this.gup.executeUpdateCycle( );
}
/**
* {@link GameUpdateProcessorBean#executeUpdateCycle()} stops processing mid-way if maintenance
* mode becomes enabled
*/
@Test
public void testProcessWithMaintenanceEnabledMidWay( )
{
this.updatesDAO.values = new String[] {
null , "maintenance" , null
};
this.gup.executeUpdateCycle( );
assertTrue( this.system.wasStartTickCalled( ) );
assertEquals( 2 , this.updatesDAO.index );
}
}

View file

@ -0,0 +1,93 @@
package com.deepclone.lw.beans.updates;
import static org.junit.Assert.*;
import org.junit.Before;
import org.junit.Test;
import com.deepclone.lw.interfaces.sys.Ticker.Frequency;
import com.deepclone.lw.testing.MockTicker;
/**
* Tests for {@link GameUpdateTaskBean}
*
* @author <a href="mailto:tseeker@legacyworlds.com">E. Benoît</a>
*/
public class TestGameUpdateTaskBean
{
/** The mock ticker */
private MockTicker ticker;
/** The mock game update processor */
private MockGameUpdateProcessor processor;
/** The game update task */
private GameUpdateTaskBean gut;
/**
* Initialise the mock components and the game update task instance
*/
@Before
public void setUp( )
{
this.ticker = new MockTicker( Frequency.MINUTE , "Game update" , GameUpdateTaskBean.class );
this.processor = new MockGameUpdateProcessor( );
this.gut = new GameUpdateTaskBean( );
this.gut.setTicker( this.ticker );
this.gut.setGameUpdateProcessor( this.processor );
}
/**
* Check the component's initialisation
*/
@Test
public void testInitialisation( )
{
this.gut.afterPropertiesSet( );
assertTrue( this.processor.wasTryLockCalled( ) );
assertTrue( this.processor.wasUnlockCalled( ) );
assertTrue( this.processor.wasEndCycleCalled( ) );
assertFalse( this.processor.wasExecuteCalled( ) );
}
/**
* Check the component's run() method
*/
@Test
public void testNormalRun( )
{
this.gut.run( );
assertTrue( this.processor.wasTryLockCalled( ) );
assertTrue( this.processor.wasUnlockCalled( ) );
assertFalse( this.processor.wasEndCycleCalled( ) );
assertTrue( this.processor.wasExecuteCalled( ) );
}
/**
* Make sure the component is unlocked even if the execution fails
*/
@Test
public void testFailedRun( )
{
this.processor.setFailExecute( true );
try {
this.gut.run( );
fail( "Mock processor failed to fail" );
} catch ( RuntimeException e ) {
// EMPTY
}
assertTrue( this.processor.wasTryLockCalled( ) );
assertTrue( this.processor.wasUnlockCalled( ) );
assertFalse( this.processor.wasEndCycleCalled( ) );
assertTrue( this.processor.wasExecuteCalled( ) );
}
}

View file

@ -0,0 +1,86 @@
package com.deepclone.lw.beans.updates;
import static org.junit.Assert.*;
import org.junit.Before;
import org.junit.Test;
/**
* Tests for {@link GameUpdateTransaction}
*
* @author <a href="mailto:tseeker@legacyworlds.com">E. Benoît</a>
*/
public class TestGameUpdateTransaction
{
/** The mock updates DAO */
private MockUpdatesDAO updatesDAO;
/** The dummy batch processor used in the tests */
private DummyBatchProcessor processor;
/** The mock processor registry */
private MockRegistry registry;
/** The game update transaction used as a "guinea pig" */
private GameUpdateTransaction transaction;
/** Set up the mock objects and the guinea pig transaction instance */
@Before
public void setUp( )
{
this.updatesDAO = new MockUpdatesDAO( );
this.registry = new MockRegistry( );
this.registry.put( this.processor = new DummyBatchProcessor( "test" ) );
this.transaction = new GameUpdateTransaction( this.updatesDAO , this.registry , 1 );
}
/** Test return value when the stored procedure says that no more updates are to be processed */
@Test
public void testWhenFinished( )
{
assertTrue( this.transaction.doInTransaction( null ) );
}
/** Test return value when the stored procedure says that more updates are to be processed */
@Test
public void testWhenHasMore( )
{
this.updatesDAO.values = new String[] {
null
};
assertFalse( this.transaction.doInTransaction( null ) );
}
/** Test what happens when the stored procedure indicates the need for local processing */
@Test
public void testLocalProcessing( )
{
this.updatesDAO.values = new String[] {
"test"
};
assertFalse( this.transaction.doInTransaction( null ) );
assertEquals( 1 , this.processor.getTicks( ).size( ) );
assertTrue( this.processor.getTicks( ).contains( 1L ) );
}
/**
* Test what happens when the stored procedure indicates the need for local processing but the
* specified processor does not exist
*/
@Test( expected = UnsupportedUpdateException.class )
public void testMissingProcessor( )
{
this.updatesDAO.values = new String[] {
"missing processor"
};
this.transaction.doInTransaction( null );
}
}

View file

@ -0,0 +1,62 @@
package com.deepclone.lw.beans.updates;
import static org.junit.Assert.*;
import org.junit.Before;
import org.junit.Test;
import org.springframework.beans.factory.BeanInitializationException;
/**
* Tests for {@link ServerProcessorRegistryBean}
*
* @author <a href="mailto:tseeker@legacyworlds.com">E. Benoît</a>
*/
public class TestServerProcessorRegistryBean
{
/** The "guinea pig" instance */
private ServerProcessorRegistryBean registry;
@Before
public void setUp( )
{
this.registry = new ServerProcessorRegistryBean( );
this.registry.postProcessAfterInitialization( new DummyBatchProcessor( "test" ) , "test" );
}
/** The registry returns <code>null</code> when the type has no processor */
@Test
public void testMissingReturnsNull( )
{
assertNull( this.registry.getProcessorFor( "does not exist" ) );
}
/** The registry returns the processor when there is one */
@Test
public void testExistingReturnsProcessor( )
{
assertNotNull( this.registry.getProcessorFor( "test" ) );
}
/** The registry ignores objects which are not batch processors */
@Test
public void testIgnoresOtherObjects( )
{
this.registry.postProcessAfterInitialization( "test" , "test" );
}
/** The registry crashes when two batch processors are defined for the same type */
@Test( expected = BeanInitializationException.class )
public void testFailsIfDuplicate( )
{
this.registry.postProcessAfterInitialization( new DummyBatchProcessor( "test" ) , "test" );
}
}

View file

@ -0,0 +1,34 @@
package com.deepclone.lw.testing;
import java.util.HashMap;
import com.deepclone.lw.interfaces.eventlog.Logger;
import com.deepclone.lw.interfaces.eventlog.SystemLogger;
/**
* A mock Logger component
*
* @author <a href="mailto:tseeker@legacyworlds.com">E. Benoît</a>
*
*/
public class MockLogger
implements Logger
{
/** Map of existing system loggers */
private final HashMap< String , MockSystemLogger > loggers = new HashMap< String , MockSystemLogger >( );
@Override
public SystemLogger getSystemLogger( String component )
{
MockSystemLogger logger = this.loggers.get( component );
if ( logger == null ) {
this.loggers.put( component , logger = new MockSystemLogger( ) );
}
return logger;
}
}

View file

@ -0,0 +1,62 @@
package com.deepclone.lw.testing;
import com.deepclone.lw.cmd.admin.logs.LogLevel;
import com.deepclone.lw.interfaces.eventlog.SystemLogger;
/**
* A mock logger for components
*
* @author <a href="mailto:tseeker@legacyworlds.com">E. Benoît</a>
*/
public class MockSystemLogger
implements SystemLogger
{
/** How many messages were logged */
private int counter = 0;
/** @return the amount of messages that were logged */
public int getCounter( )
{
return counter;
}
/**
* Set the message counter
*
* @param counter
* the counter's new value
*/
public void setCounter( int counter )
{
this.counter = counter;
}
@Override
public SystemLogger log( LogLevel level , String message )
{
this.counter++;
return this;
}
@Override
public SystemLogger log( LogLevel level , String message , Throwable exception )
{
this.counter++;
return this;
}
@Override
public SystemLogger flush( )
{
return this;
}
}

View file

@ -0,0 +1,148 @@
package com.deepclone.lw.testing;
import com.deepclone.lw.interfaces.sys.MaintenanceData;
import com.deepclone.lw.interfaces.sys.MaintenanceStatusException;
import com.deepclone.lw.interfaces.sys.SystemStatus;
import com.deepclone.lw.interfaces.sys.TickStatusException;
/**
* A mock system status component
*
* @author <a href="mailto:tseeker@legacyworlds.com">E. Benoît</a>
*/
public class MockSystemStatus
implements SystemStatus
{
/** Maintenance data to return when {@link #checkMaintenance()} is called */
private MaintenanceData maintenance = null;
/** Value that determines the behaviour of {@link #startTick()} */
private long stValue = 0;
/** Value that determines the behaviour of {@link #checkStuckTick()} */
private long cstValue = -1;
/** Was {@link #startTick()} called? */
private boolean startTickCalled = false;
/**
* Set the maintenance data to return when {@link #checkMaintenance()} is called
*
* @param maintenance
* the data to return from {@link #checkMaintenance()}
*/
public void setMaintenance( MaintenanceData maintenance )
{
this.maintenance = maintenance;
}
/**
* Set the value that determines the behaviour of {@link #checkStuckTick()}
*
* <ul>
* <li>-2 will cause a {@link MaintenanceStatusException},
* <li>-1 will cause it to return <code>null</code>,
* <li>any other value will be returned.
* </ul>
*
* @param cstValue
* the value
*/
public void setCSTBehaviour( long cstValue )
{
this.cstValue = cstValue;
}
/**
* Set the value that determines the behaviour of {@link #startTick()}
*
* <ul>
* <li>-2 will cause a {@link MaintenanceStatusException},
* <li>-1 will cause a {@link TickStatusException},
* <li>any other value will be returned.
* </ul>
*
* @param cstValue
* the value
*/
public void setSTBehaviour( long stValue )
{
this.stValue = stValue;
}
/**
* @return <code>true</code> if {@link #startTick()} was called.
*/
public boolean wasStartTickCalled( )
{
return startTickCalled;
}
@Override
public MaintenanceData checkMaintenance( )
{
return this.maintenance;
}
@Override
public void startMaintenance( int adminId , String reason , int duration )
throws MaintenanceStatusException
{
// EMPTY - ignored
}
@Override
public void updateMaintenance( int adminId , int durationFromNow )
throws MaintenanceStatusException
{
// EMPTY - ignored
}
@Override
public void endMaintenance( int adminId )
throws MaintenanceStatusException
{
// EMPTY - ignored
}
@Override
public long startTick( )
throws TickStatusException , MaintenanceStatusException
{
this.startTickCalled = true;
if ( this.stValue == -1 ) {
throw new TickStatusException( );
}
if ( this.stValue == -2 ) {
throw new MaintenanceStatusException( );
}
return this.stValue;
}
@Override
public Long checkStuckTick( )
throws MaintenanceStatusException
{
if ( this.cstValue == -1 ) {
return null;
}
if ( this.cstValue == -2 ) {
throw new MaintenanceStatusException( );
}
return this.cstValue;
}
}

View file

@ -0,0 +1,82 @@
package com.deepclone.lw.testing;
import com.deepclone.lw.interfaces.sys.Ticker;
import static org.junit.Assert.*;
/**
* A mock ticker component
*
* <p>
* This mock component can be used to make sure that another component registers some task as
* expected.
*
* @author <a href="mailto:tseeker@legacyworlds.com">E. Benoît</a>
*
*/
public class MockTicker
implements Ticker
{
/** The task's expected frequency */
private final Frequency expectedFrequency;
/** The task's expected name */
private final String expectedName;
/** The task's expected type */
private final Class< ? > expectedType;
/**
* Initialise the mock ticker's expectations
*
* @param expectedFrequency
* the task's expected frequency
* @param expectedName
* the task's expected name
* @param expectedType
* the task's expected type
*/
public MockTicker( Frequency expectedFrequency , String expectedName , Class< ? > expectedType )
{
this.expectedFrequency = expectedFrequency;
this.expectedName = expectedName;
this.expectedType = expectedType;
}
@Override
public void registerTask( Frequency frequency , String name , Runnable task )
{
assertEquals( this.expectedFrequency , frequency );
assertEquals( this.expectedName , name );
assertTrue( this.expectedType.isInstance( task ) );
}
@Override
public void pause( )
throws IllegalStateException
{
// EMPTY - ignored
}
@Override
public void unpause( )
throws IllegalStateException
{
// EMPTY - ignored
}
@Override
public boolean isActive( )
{
return true;
}
}

View file

@ -0,0 +1,43 @@
package com.deepclone.lw.testing;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionDefinition;
import org.springframework.transaction.TransactionException;
import org.springframework.transaction.TransactionStatus;
/**
* A mock transaction manager
*
* @author <a href="mailto:tseeker@legacyworlds.com">E. Benoît</a>
*/
public class MockTransactionManager
implements PlatformTransactionManager
{
@Override
public TransactionStatus getTransaction( TransactionDefinition definition )
throws TransactionException
{
return null;
}
@Override
public void commit( TransactionStatus status )
throws TransactionException
{
// EMPTY
}
@Override
public void rollback( TransactionStatus status )
throws TransactionException
{
// EMPTY
}
}