mirror of
https://github.com/SquidDev-CC/CC-Tweaked
synced 2025-07-14 16:02:57 +00:00

Name a more iconic duo than @SquidDev and over-engineered test frameworks. This uses Minecraft's test core[1] plus a home-grown framework to run tests against computers in-world. The general idea is: - Build a structure in game. - Save the structure to a file. This will be spawned in every time the test is run. - Write some code which asserts the structure behaves in a particular way. This is done in Kotlin (shock, horror), as coroutines give us a nice way to run asynchronous code while still running on the main thread. As with all my testing efforts, I still haven't actually written any tests! It'd be good to go through some of the historic ones and write some tests though. Turtle block placing and computer redstone interactions are probably a good place to start. [1]: https://www.youtube.com/watch?v=vXaWOJTCYNg
120 lines
4.6 KiB
Java
120 lines
4.6 KiB
Java
package dan200.computercraft.ingame.mod;
|
|
|
|
import com.google.common.base.CaseFormat;
|
|
import dan200.computercraft.ingame.api.GameTest;
|
|
import net.minecraft.test.TestFunctionInfo;
|
|
import net.minecraft.test.TestRegistry;
|
|
import net.minecraft.test.TestTrackerHolder;
|
|
import net.minecraftforge.fml.ModList;
|
|
import net.minecraftforge.fml.common.ObfuscationReflectionHelper;
|
|
import net.minecraftforge.forgespi.language.ModFileScanData;
|
|
import org.objectweb.asm.Type;
|
|
|
|
import java.lang.reflect.Constructor;
|
|
import java.lang.reflect.Field;
|
|
import java.lang.reflect.Method;
|
|
import java.lang.reflect.Modifier;
|
|
import java.util.Arrays;
|
|
import java.util.Collection;
|
|
import java.util.Set;
|
|
import java.util.function.Consumer;
|
|
|
|
/**
|
|
* Loads methods annotated with {@link GameTest} and adds them to the {@link TestRegistry}. This involves some horrible
|
|
* reflection hacks, as Proguard makes many methods (and constructors) private.
|
|
*/
|
|
class TestLoader
|
|
{
|
|
private static final Type gameTest = Type.getType( GameTest.class );
|
|
private static final Collection<TestFunctionInfo> testFunctions = ObfuscationReflectionHelper.getPrivateValue( TestRegistry.class, null, "field_229526_a_" );
|
|
private static final Set<String> testClassNames = ObfuscationReflectionHelper.getPrivateValue( TestRegistry.class, null, "field_229527_b_" );
|
|
|
|
public static void setup()
|
|
{
|
|
ModList.get().getAllScanData().stream()
|
|
.flatMap( x -> x.getAnnotations().stream() )
|
|
.filter( x -> x.getAnnotationType().equals( gameTest ) )
|
|
.forEach( TestLoader::loadTest );
|
|
}
|
|
|
|
|
|
private static void loadTest( ModFileScanData.AnnotationData annotation )
|
|
{
|
|
Class<?> klass;
|
|
Method method;
|
|
try
|
|
{
|
|
klass = TestLoader.class.getClassLoader().loadClass( annotation.getClassType().getClassName() );
|
|
|
|
// We don't know the exact signature (could suspend or not), so find something with the correct descriptor instead.
|
|
String methodName = annotation.getMemberName();
|
|
method = Arrays.stream( klass.getMethods() ).filter( x -> (x.getName() + Type.getMethodDescriptor( x )).equals( methodName ) ).findFirst()
|
|
.orElseThrow( () -> new NoSuchMethodException( "No method " + annotation.getClassType().getClassName() + "." + annotation.getMemberName() ) );
|
|
}
|
|
catch( ReflectiveOperationException e )
|
|
{
|
|
throw new RuntimeException( e );
|
|
}
|
|
|
|
String className = CaseFormat.UPPER_CAMEL.to( CaseFormat.LOWER_UNDERSCORE, klass.getSimpleName() );
|
|
String name = className + "." + method.getName().toLowerCase().replace( ' ', '_' );
|
|
|
|
GameTest test = method.getAnnotation( GameTest.class );
|
|
|
|
TestMod.log.info( "Adding test " + name );
|
|
testClassNames.add( className );
|
|
testFunctions.add( createTestFunction(
|
|
test.batch(), name, name,
|
|
test.required(),
|
|
new TestRunner( name, method ),
|
|
test.timeout(),
|
|
test.setup()
|
|
) );
|
|
}
|
|
|
|
private static TestFunctionInfo createTestFunction(
|
|
String batchName,
|
|
String testName,
|
|
String structureName,
|
|
boolean required,
|
|
Consumer<TestTrackerHolder> function,
|
|
int maxTicks,
|
|
long setupTicks
|
|
)
|
|
{
|
|
try
|
|
{
|
|
Constructor<TestFunctionInfo> ctor = TestFunctionInfo.class.getDeclaredConstructor();
|
|
ctor.setAccessible( true );
|
|
|
|
TestFunctionInfo func = ctor.newInstance();
|
|
setFinalField( func, "batchName", batchName );
|
|
setFinalField( func, "testName", testName );
|
|
setFinalField( func, "structureName", structureName );
|
|
setFinalField( func, "required", required );
|
|
setFinalField( func, "function", function );
|
|
setFinalField( func, "maxTicks", maxTicks );
|
|
setFinalField( func, "setupTicks", setupTicks );
|
|
return func;
|
|
}
|
|
catch( ReflectiveOperationException e )
|
|
{
|
|
throw new RuntimeException( e );
|
|
}
|
|
}
|
|
|
|
private static void setFinalField( TestFunctionInfo func, String name, Object value ) throws ReflectiveOperationException
|
|
{
|
|
Field field = TestFunctionInfo.class.getDeclaredField( name );
|
|
if( (field.getModifiers() & Modifier.FINAL) != 0 )
|
|
{
|
|
Field modifiers = Field.class.getDeclaredField( "modifiers" );
|
|
modifiers.setAccessible( true );
|
|
modifiers.set( field, field.getModifiers() & ~Modifier.FINAL );
|
|
}
|
|
|
|
field.setAccessible( true );
|
|
field.set( func, value );
|
|
}
|
|
}
|