Use Block.New everywhere, testing *every block property*

Fixed prefab update for nonexistent blocks
Removed Type from block placed/removed event args
Added test to check the block ID enum (whether it has any extra or missing IDs)
Added test to place every block on the ID enum
Added test to set and verify each property of each block type (type-specific properties are also set when they can be through the API)
Added support for enumerator test methods with exception handling
This commit is contained in:
Norbi Peti 2021-05-19 01:40:15 +02:00
parent 70b322583a
commit e9df67f462
No known key found for this signature in database
GPG key ID: DBA4C4549A927E56
12 changed files with 195 additions and 123 deletions

View file

@ -445,7 +445,7 @@ namespace TechbloxModdingAPI.App
Block[] blocks = new Block[blockEGIDs.Length]; Block[] blocks = new Block[blockEGIDs.Length];
for (int b = 0; b < blockEGIDs.Length; b++) for (int b = 0; b < blockEGIDs.Length; b++)
{ {
blocks[b] = new Block(blockEGIDs[b]); blocks[b] = Block.New(blockEGIDs[b]);
} }
return blocks; return blocks;
} }

View file

@ -11,6 +11,7 @@ using Unity.Mathematics;
using Gamecraft.Blocks.GUI; using Gamecraft.Blocks.GUI;
using TechbloxModdingAPI.Blocks; using TechbloxModdingAPI.Blocks;
using TechbloxModdingAPI.Tests;
using TechbloxModdingAPI.Utility; using TechbloxModdingAPI.Utility;
namespace TechbloxModdingAPI namespace TechbloxModdingAPI
@ -101,10 +102,9 @@ namespace TechbloxModdingAPI
{CommonExclusiveGroups.DAMPEDSPRING_BLOCK_GROUP, id => new DampedSpring(id)}, {CommonExclusiveGroups.DAMPEDSPRING_BLOCK_GROUP, id => new DampedSpring(id)},
{CommonExclusiveGroups.TEXT_BLOCK_GROUP, id => new TextBlock(id)}, {CommonExclusiveGroups.TEXT_BLOCK_GROUP, id => new TextBlock(id)},
{CommonExclusiveGroups.TIMER_BLOCK_GROUP, id => new Timer(id)} {CommonExclusiveGroups.TIMER_BLOCK_GROUP, id => new Timer(id)}
};/*.SelectMany(kv => kv.Value.Select(v => (Key: v, Value: kv.Key))) };
.ToDictionary(kv => kv.Key, kv => kv.Value);*/
private static Block New(EGID egid) internal static Block New(EGID egid)
{ {
return GroupToConstructor.ContainsKey(egid.groupID) return GroupToConstructor.ContainsKey(egid.groupID)
? GroupToConstructor[egid.groupID](egid) ? GroupToConstructor[egid.groupID](egid)
@ -299,6 +299,7 @@ namespace TechbloxModdingAPI
/// The text displayed on the block if applicable, or null. /// The text displayed on the block if applicable, or null.
/// Setting it is temporary to the session, it won't be saved. /// Setting it is temporary to the session, it won't be saved.
/// </summary> /// </summary>
[TestValue(null)]
public string Label public string Label
{ {
get => BlockEngine.GetBlockInfoViewComponent<TextLabelEntityViewStruct>(this).textLabelComponent?.text; get => BlockEngine.GetBlockInfoViewComponent<TextLabelEntityViewStruct>(this).textLabelComponent?.text;

View file

@ -1,3 +1,4 @@
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
@ -60,7 +61,7 @@ namespace TechbloxModdingAPI.Blocks
var ret = new Block[cubes.count]; var ret = new Block[cubes.count];
for (int i = 0; i < cubes.count; i++) for (int i = 0; i < cubes.count; i++)
ret[i] = new Block(cubes[i]); ret[i] = Block.New(cubes[i]);
return ret; return ret;
} }
@ -103,7 +104,14 @@ namespace TechbloxModdingAPI.Blocks
var prefabAssetIDOpt = entitiesDB.QueryEntityOptional<PrefabAssetIDComponent>(block); var prefabAssetIDOpt = entitiesDB.QueryEntityOptional<PrefabAssetIDComponent>(block);
uint prefabAssetID = prefabAssetIDOpt uint prefabAssetID = prefabAssetIDOpt
? prefabAssetIDOpt.Get().prefabAssetID ? prefabAssetIDOpt.Get().prefabAssetID
: throw new BlockException("Prefab asset ID not found!"); //Set by the game : uint.MaxValue;
if (prefabAssetID == uint.MaxValue)
{
if (entitiesDB.QueryEntityOptional<DBEntityStruct>(block)) //The block exists
throw new BlockException("Prefab asset ID not found for block " + block); //Set by the game
return;
}
uint prefabId = uint prefabId =
PrefabsID.GetOrCreatePrefabID((ushort) prefabAssetID, material, 1, flipped); PrefabsID.GetOrCreatePrefabID((ushort) prefabAssetID, material, 1, flipped);
entitiesDB.QueryEntityOrDefault<GFXPrefabEntityStructGPUI>(block).prefabID = prefabId; entitiesDB.QueryEntityOrDefault<GFXPrefabEntityStructGPUI>(block).prefabID = prefabId;
@ -239,7 +247,7 @@ namespace TechbloxModdingAPI.Blocks
{ {
var conn = array[index]; var conn = array[index];
if (conn.machineRigidBodyId == sbid) if (conn.machineRigidBodyId == sbid)
set.Add(new Block(conn.ID)); set.Add(Block.New(conn.ID));
} }
} }

View file

@ -47,9 +47,8 @@ namespace TechbloxModdingAPI.Blocks
public struct BlockPlacedRemovedEventArgs public struct BlockPlacedRemovedEventArgs
{ {
public EGID ID; public EGID ID;
public BlockIDs Type;
private Block block; private Block block;
public Block Block => block ?? (block = new Block(ID)); public Block Block => block ?? (block = Block.New(ID));
} }
} }

View file

@ -1,9 +1,13 @@
using System; using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using Gamecraft.Wires; using DataLoader;
using Svelto.Tasks;
using Unity.Mathematics; using Unity.Mathematics;
using TechbloxModdingAPI; using TechbloxModdingAPI.App;
using TechbloxModdingAPI.Tests; using TechbloxModdingAPI.Tests;
using TechbloxModdingAPI.Utility; using TechbloxModdingAPI.Utility;
@ -19,62 +23,116 @@ namespace TechbloxModdingAPI.Blocks
[APITestCase(TestType.Game)] //At least one block must be placed for simulation to work [APITestCase(TestType.Game)] //At least one block must be placed for simulation to work
public static void TestPlaceNew() public static void TestPlaceNew()
{ {
Block newBlock = Block.PlaceNew(BlockIDs.Cube, Unity.Mathematics.float3.zero); Block newBlock = Block.PlaceNew(BlockIDs.Cube, float3.zero);
Assert.NotNull(newBlock.Id, "Newly placed block is missing Id. This should be populated when the block is placed.", "Newly placed block Id is not null, block successfully placed."); Assert.NotNull(newBlock.Id, "Newly placed block is missing Id. This should be populated when the block is placed.", "Newly placed block Id is not null, block successfully placed.");
} }
[APITestCase(TestType.EditMode)] [APITestCase(TestType.EditMode)]
public static void TestInitProperty() public static void TestInitProperty()
{ {
Block newBlock = Block.PlaceNew(BlockIDs.Cube, Unity.Mathematics.float3.zero + 2); Block newBlock = Block.PlaceNew(BlockIDs.Cube, float3.zero + 2);
if (!Assert.CloseTo(newBlock.Position, (Unity.Mathematics.float3.zero + 2), $"Newly placed block at {newBlock.Position} is expected at {Unity.Mathematics.float3.zero + 2}.", "Newly placed block position matches.")) return; if (!Assert.CloseTo(newBlock.Position, (float3.zero + 2), $"Newly placed block at {newBlock.Position} is expected at {Unity.Mathematics.float3.zero + 2}.", "Newly placed block position matches.")) return;
//Assert.Equal(newBlock.Exists, true, "Newly placed block does not exist, possibly because Sync() skipped/missed/failed.", "Newly placed block exists, Sync() successful."); //Assert.Equal(newBlock.Exists, true, "Newly placed block does not exist, possibly because Sync() skipped/missed/failed.", "Newly placed block exists, Sync() successful.");
} }
/*[APITestCase(TestType.EditMode)] [APITestCase(TestType.EditMode)]
public static void TestTextBlock() public static void TestBlockIDCoverage()
{ {
TextBlock textBlock = null; // Note: the assignment operation is a lambda, which slightly confuses the compiler Assert.Equal(
Assert.Errorless(() => { textBlock = Block.PlaceNew<TextBlock>(BlockIDs.TextBlock, Unity.Mathematics.float3.zero + 1); }, "Block.PlaceNew<TextBlock>() raised an exception: ", "Block.PlaceNew<TextBlock>() completed without issue."); FullGameFields._dataDb.GetValues<CubeListData>().Keys.Select(ushort.Parse).OrderBy(id => id)
if (!Assert.NotNull(textBlock, "Block.PlaceNew<TextBlock>() returned null, possibly because it failed silently.", "Specialized TextBlock is not null.")) return; .SequenceEqual(Enum.GetValues(typeof(BlockIDs)).Cast<ushort>().OrderBy(id => id)
if (!Assert.NotNull(textBlock.Text, "TextBlock.Text is null, possibly because it failed silently.", "TextBlock.Text is not null.")) return; .Except(new[] {(ushort) BlockIDs.Invalid})), true,
if (!Assert.NotNull(textBlock.TextBlockId, "TextBlock.TextBlockId is null, possibly because it failed silently.", "TextBlock.TextBlockId is not null.")) return; "Block ID enum is different than the known block types, update needed.",
"Block ID enum matches the known block types.");
} }
[APITestCase(TestType.EditMode)] [APITestCase(TestType.EditMode)]
public static void TestMotor() public static void TestBlockIDs()
{ {
Block newBlock = Block.PlaceNew(BlockIDs.MotorS, Unity.Mathematics.float3.zero + 1); float3 pos = new float3();
Motor b = null; // Note: the assignment operation is a lambda, which slightly confuses the compiler foreach (BlockIDs id in Enum.GetValues(typeof(BlockIDs)))
Assert.Errorless(() => { b = newBlock.Specialise<Motor>(); }, "Block.Specialize<Motor>() raised an exception: ", "Block.Specialize<Motor>() completed without issue."); {
if (!Assert.NotNull(b, "Block.Specialize<Motor>() returned null, possibly because it failed silently.", "Specialized Motor is not null.")) return; if (id == BlockIDs.Invalid) continue;
if (!Assert.CloseTo(b.Torque, 75f, $"Motor.Torque {b.Torque} does not equal default value, possibly because it failed silently.", "Motor.Torque close enough to default.")) return; try
if (!Assert.CloseTo(b.TopSpeed, 30f, $"Motor.TopSpeed {b.TopSpeed} does not equal default value, possibly because it failed silently.", "Motor.Torque is close enough to default.")) return; {
if (!Assert.Equal(b.Reverse, false, $"Motor.Reverse {b.Reverse} does not equal default value, possibly because it failed silently.", "Motor.Reverse is default.")) return; Block.PlaceNew(id, pos);
pos += 0.2f;
}
catch (Exception e)
{ //Only print failed case
Assert.Fail($"Failed to place block type {id}: {e}");
return;
}
}
Assert.Pass("Placing all possible block types succeeded.");
} }
[APITestCase(TestType.EditMode)] [APITestCase(TestType.EditMode)]
public static void TestPiston() public static IEnumerator<TaskContract> TestBlockProperties()
{ //Uses the result of the previous test case
var blocks = Game.CurrentGame().GetBlocksInGame();
for (var index = 0; index < blocks.Length; index++)
{ {
Block newBlock = Block.PlaceNew(BlockIDs.PneumaticPiston, Unity.Mathematics.float3.zero + 1); if (index % 50 == 0) yield return Yield.It; //The material or flipped status can only be changed 130 times per submission
Piston b = null; // Note: the assignment operation is a lambda, which slightly confuses the compiler var block = blocks[index];
Assert.Errorless(() => { b = newBlock.Specialise<Piston>(); }, "Block.Specialize<Piston>() raised an exception: ", "Block.Specialize<Piston>() completed without issue."); if (!block.Exists) continue;
if (!Assert.NotNull(b, "Block.Specialize<Piston>() returned null, possibly because it failed silently.", "Specialized Piston is not null.")) return; foreach (var property in block.GetType().GetProperties())
if (!Assert.CloseTo(b.MaximumExtension, 1.01f, $"Piston.MaximumExtension {b.MaximumExtension} does not equal default value, possibly because it failed silently.", "Piston.MaximumExtension is close enough to default.")) return; {
if (!Assert.CloseTo(b.MaximumForce, 1.0f, $"Piston.MaximumForce {b.MaximumForce} does not equal default value, possibly because it failed silently.", "Piston.MaximumForce is close enough to default.")) return; //Includes specialised block properties
if (property.SetMethod == null) continue;
var testValues = new (Type, object, Predicate<object>)[]
{
//(type, default value, predicate or null for equality)
(typeof(long), 3, null),
(typeof(int), 4, null),
(typeof(double), 5.2f, obj => Math.Abs((double) obj - 5.2f) < float.Epsilon),
(typeof(float), 5.2f, obj => Math.Abs((float) obj - 5.2f) < float.Epsilon),
(typeof(bool), true, obj => (bool) obj),
(typeof(string), "Test", obj => (string) obj == "Test"), //String equality check
(typeof(float3), (float3) 2, obj => math.all((float3) obj - 2 < (float3) float.Epsilon)),
(typeof(BlockColor), new BlockColor(BlockColors.Aqua, 2), null),
(typeof(float4), (float4) 5, obj => math.all((float4) obj - 5 < (float4) float.Epsilon))
};
var propType = property.PropertyType;
if (!propType.IsValueType) continue;
(object valueToUse, Predicate<object> predicateToUse) = (null, null);
foreach (var (type, value, predicate) in testValues)
{
if (type.IsAssignableFrom(propType))
{
valueToUse = value;
predicateToUse = predicate ?? (obj => Equals(obj, value));
break;
}
} }
[APITestCase(TestType.EditMode)] if (propType.IsEnum)
public static void TestServo()
{ {
Block newBlock = Block.PlaceNew(BlockIDs.ServoAxle, Unity.Mathematics.float3.zero + 1); var values = propType.GetEnumValues();
Servo b = null; // Note: the assignment operation is a lambda, which slightly confuses the compiler valueToUse = values.GetValue(values.Length / 2);
Assert.Errorless(() => { b = newBlock.Specialise<Servo>(); }, "Block.Specialize<Servo>() raised an exception: ", "Block.Specialize<Servo>() completed without issue."); predicateToUse = val => Equals(val, valueToUse);
if (!Assert.NotNull(b, "Block.Specialize<Servo>() returned null, possibly because it failed silently.", "Specialized Servo is not null.")) return; }
if (!Assert.CloseTo(b.MaximumAngle, 180f, $"Servo.MaximumAngle {b.MaximumAngle} does not equal default value, possibly because it failed silently.", "Servo.MaximumAngle is close enough to default.")) return;
if (!Assert.CloseTo(b.MinimumAngle, -180f, $"Servo.MinimumAngle {b.MinimumAngle} does not equal default value, possibly because it failed silently.", "Servo.MinimumAngle is close enough to default.")) return; if (valueToUse == null)
if (!Assert.CloseTo(b.MaximumForce, 60f, $"Servo.MaximumForce {b.MaximumForce} does not equal default value, possibly because it failed silently.", "Servo.MaximumForce is close enough to default.")) return; {
}*/ Assert.Fail($"Property {block.GetType().Name}.{property.Name} has an unknown type {propType}, test needs fixing.");
yield break;
}
property.SetValue(block, valueToUse);
object got = property.GetValue(block);
var attr = property.GetCustomAttribute<TestValueAttribute>();
if (!predicateToUse(got) && (attr == null || !Equals(attr.PossibleValue, got)))
{
Assert.Fail($"Property {block.GetType().Name}.{property.Name} value {got} does not equal {valueToUse} for block {block}.");
yield break;
}
}
}
Assert.Pass("Setting all possible properties of all registered API block types succeeded.");
}
[APITestCase(TestType.EditMode)] [APITestCase(TestType.EditMode)]
public static void TestDampedSpring() public static void TestDampedSpring()

View file

@ -75,7 +75,7 @@ namespace TechbloxModdingAPI.Blocks
int count = selectedBlocksInGroup.Count<EGID>(); int count = selectedBlocksInGroup.Count<EGID>();
var ret = new Block[count]; var ret = new Block[count];
for (uint i = 0; i < count; i++) for (uint i = 0; i < count; i++)
ret[i] = new Block(selectedBlocksInGroup.Get<EGID>(i)); ret[i] = Block.New(selectedBlocksInGroup.Get<EGID>(i));
selectedBlocksInGroup.FastClear(); selectedBlocksInGroup.FastClear();
return ret; return ret;
} }
@ -222,7 +222,7 @@ namespace TechbloxModdingAPI.Blocks
new object[] {playerID, blueprintData, entitySerialization, entitiesDB, entityFactory}); new object[] {playerID, blueprintData, entitySerialization, entitiesDB, entityFactory});
var blocks = new Block[placedBlocks.count]; var blocks = new Block[placedBlocks.count];
for (int i = 0; i < blocks.Length; i++) for (int i = 0; i < blocks.Length; i++)
blocks[i] = new Block(placedBlocks[i]); blocks[i] = Block.New(placedBlocks[i]);
return blocks; return blocks;
} }

View file

@ -48,7 +48,7 @@ namespace TechbloxModdingAPI.Blocks
WireEntityStruct wire = signalEngine.MatchPortToWire(port, end.Id, out exists); WireEntityStruct wire = signalEngine.MatchPortToWire(port, end.Id, out exists);
if (exists) if (exists)
{ {
return new Wire(new Block(wire.sourceBlockEGID), end, wire.sourcePortUsage, endPort); return new Wire(Block.New(wire.sourceBlockEGID), end, wire.sourcePortUsage, endPort);
} }
return null; return null;
} }
@ -67,7 +67,7 @@ namespace TechbloxModdingAPI.Blocks
WireEntityStruct wire = signalEngine.MatchPortToWire(port, start.Id, out exists); WireEntityStruct wire = signalEngine.MatchPortToWire(port, start.Id, out exists);
if (exists) if (exists)
{ {
return new Wire(start, new Block(wire.destinationBlockEGID), startPort, wire.destinationPortUsage); return new Wire(start, Block.New(wire.destinationBlockEGID), startPort, wire.destinationPortUsage);
} }
return null; return null;
} }

View file

@ -387,7 +387,7 @@ namespace TechbloxModdingAPI
{ {
var egid = playerEngine.GetThingLookedAt(Id, maxDistance); var egid = playerEngine.GetThingLookedAt(Id, maxDistance);
return egid != EGID.Empty && egid.groupID != CommonExclusiveGroups.SIMULATION_BODIES_GROUP return egid != EGID.Empty && egid.groupID != CommonExclusiveGroups.SIMULATION_BODIES_GROUP
? new Block(egid) ? Block.New(egid)
: null; : null;
} }

View file

@ -469,7 +469,7 @@ namespace TechbloxModdingAPI.Players
for (int j = 0; j < blocks.count; j++) for (int j = 0; j < blocks.count; j++)
{ {
var egid = pointer[j]; var egid = pointer[j];
ret[j] = new Block(egid); ret[j] = Block.New(egid);
} }
return ret; return ret;

View file

@ -324,9 +324,9 @@ namespace TechbloxModdingAPI.Tests
GameClient.SetDebugInfo("InstalledMods", InstalledMods); GameClient.SetDebugInfo("InstalledMods", InstalledMods);
Block.Placed += (sender, args) => Block.Placed += (sender, args) =>
Logging.MetaDebugLog("Placed block " + args.Type + " with ID " + args.ID); Logging.MetaDebugLog("Placed block " + args.Block);
Block.Removed += (sender, args) => Block.Removed += (sender, args) =>
Logging.MetaDebugLog("Removed block " + args.Type + " with ID " + args.ID); Logging.MetaDebugLog("Removed block " + args.Block);
/* /*
CommandManager.AddCommand(new SimpleCustomCommandEngine<float>((float d) => { UnityEngine.Camera.main.fieldOfView = d; }, CommandManager.AddCommand(new SimpleCustomCommandEngine<float>((float d) => { UnityEngine.Camera.main.fieldOfView = d; },

View file

@ -149,71 +149,59 @@ namespace TechbloxModdingAPI.Tests
Game currentGame = Game.CurrentGame(); Game currentGame = Game.CurrentGame();
// in-game tests // in-game tests
yield return new WaitForSecondsEnumerator(5).Continue(); // wait for game to finish loading yield return new WaitForSecondsEnumerator(5).Continue(); // wait for game to finish loading
var testTypesToRun = new[]
{
TestType.Game,
TestType.SimulationMode,
TestType.EditMode
};
for (var index = 0; index < testTypesToRun.Length; index++)
{
foreach (Type t in testTypes) foreach (Type t in testTypes)
{ {
foreach (MethodBase m in t.GetMethods()) foreach (MethodBase m in t.GetMethods())
{ {
APITestCaseAttribute a = m.GetCustomAttribute<APITestCaseAttribute>(); APITestCaseAttribute a = m.GetCustomAttribute<APITestCaseAttribute>();
if (a != null && a.TestType == TestType.Game) if (a == null || a.TestType != testTypesToRun[index]) continue;
{
object ret = null;
try try
{ {
m.Invoke(null, new object[0]); ret = m.Invoke(null, new object[0]);
} }
catch (Exception e) catch (Exception e)
{ {
Assert.Fail($"Game test '{m}' raised an exception: {e.ToString()}"); Assert.Fail($"{a.TestType} test '{m}' raised an exception: {e}");
} }
if (ret is IEnumerator<TaskContract> enumerator)
{ //Support enumerator methods with added exception handling
bool cont;
do
{ //Can't use yield return in a try block...
try
{ //And with Continue() exceptions aren't caught
cont = enumerator.MoveNext();
}
catch (Exception e)
{
Assert.Fail($"{a.TestType} test '{m}' raised an exception: {e}");
cont = false;
}
yield return Yield.It;
} while (cont);
}
yield return Yield.It; yield return Yield.It;
} }
} }
}
if (index + 1 < testTypesToRun.Length) //Don't toggle on the last test
currentGame.ToggleTimeMode(); currentGame.ToggleTimeMode();
yield return new WaitForSecondsEnumerator(5).Continue(); yield return new WaitForSecondsEnumerator(5).Continue();
// simulation tests
foreach (Type t in testTypes)
{
foreach (MethodBase m in t.GetMethods())
{
APITestCaseAttribute a = m.GetCustomAttribute<APITestCaseAttribute>();
if (a != null && a.TestType == TestType.SimulationMode)
{
try
{
m.Invoke(null, new object[0]);
}
catch (Exception e)
{
Assert.Fail($"Simulation test '{m}' raised an exception: {e.ToString()}");
}
yield return Yield.It;
}
}
}
currentGame.ToggleTimeMode();
yield return new WaitForSecondsEnumerator(5).Continue();
// build tests
foreach (Type t in testTypes)
{
foreach (MethodBase m in t.GetMethods())
{
APITestCaseAttribute a = m.GetCustomAttribute<APITestCaseAttribute>();
if (a != null && a.TestType == TestType.EditMode)
{
try
{
m.Invoke(null, new object[0]);
}
catch (Exception e)
{
Assert.Fail($"Build test '{m}' raised an exception: {e.ToString()}");
}
yield return Yield.It;
}
}
} }
// exit game // exit game
yield return new WaitForSecondsEnumerator(5).Continue();
yield return ReturnToMenu().Continue(); yield return ReturnToMenu().Continue();
} }

View file

@ -0,0 +1,18 @@
using System;
namespace TechbloxModdingAPI.Tests
{
[AttributeUsage(AttributeTargets.Property)]
public class TestValueAttribute : Attribute
{
public object PossibleValue { get; }
/// <summary>
/// <param name="possibleValue">
/// When set, the property test accepts the specified value in addition to the test input.<br />
/// Useful if setting the property isn't always possible.
/// </param>
/// </summary>
public TestValueAttribute(object possibleValue) => PossibleValue = possibleValue;
}
}