Browse Source

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
tags/v2.0.0
NorbiPeti 3 years ago
parent
commit
e9df67f462
Signed by: NorbiPeti <szatmari.norbert.peter@gmail.com> GPG Key ID: DBA4C4549A927E56
12 changed files with 193 additions and 121 deletions
  1. +1
    -1
      TechbloxModdingAPI/App/Game.cs
  2. +4
    -3
      TechbloxModdingAPI/Block.cs
  3. +11
    -3
      TechbloxModdingAPI/Blocks/BlockEngine.cs
  4. +1
    -2
      TechbloxModdingAPI/Blocks/BlockEventsEngine.cs
  5. +100
    -42
      TechbloxModdingAPI/Blocks/BlockTests.cs
  6. +2
    -2
      TechbloxModdingAPI/Blocks/BlueprintEngine.cs
  7. +2
    -2
      TechbloxModdingAPI/Blocks/Wire.cs
  8. +1
    -1
      TechbloxModdingAPI/Player.cs
  9. +1
    -1
      TechbloxModdingAPI/Players/PlayerEngine.cs
  10. +2
    -2
      TechbloxModdingAPI/Tests/TechbloxModdingAPIPluginTest.cs
  11. +50
    -62
      TechbloxModdingAPI/Tests/TestRoot.cs
  12. +18
    -0
      TechbloxModdingAPI/Tests/TestValueAttribute.cs

+ 1
- 1
TechbloxModdingAPI/App/Game.cs View File

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


+ 4
- 3
TechbloxModdingAPI/Block.cs View File

@@ -11,6 +11,7 @@ using Unity.Mathematics;
using Gamecraft.Blocks.GUI;

using TechbloxModdingAPI.Blocks;
using TechbloxModdingAPI.Tests;
using TechbloxModdingAPI.Utility;

namespace TechbloxModdingAPI
@@ -101,10 +102,9 @@ namespace TechbloxModdingAPI
{CommonExclusiveGroups.DAMPEDSPRING_BLOCK_GROUP, id => new DampedSpring(id)},
{CommonExclusiveGroups.TEXT_BLOCK_GROUP, id => new TextBlock(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)
? GroupToConstructor[egid.groupID](egid)
@@ -299,6 +299,7 @@ namespace TechbloxModdingAPI
/// The text displayed on the block if applicable, or null.
/// Setting it is temporary to the session, it won't be saved.
/// </summary>
[TestValue(null)]
public string Label
{
get => BlockEngine.GetBlockInfoViewComponent<TextLabelEntityViewStruct>(this).textLabelComponent?.text;


+ 11
- 3
TechbloxModdingAPI/Blocks/BlockEngine.cs View File

@@ -1,3 +1,4 @@
using System;
using System.Collections.Generic;
using System.Linq;

@@ -60,7 +61,7 @@ namespace TechbloxModdingAPI.Blocks

var ret = new Block[cubes.count];
for (int i = 0; i < cubes.count; i++)
ret[i] = new Block(cubes[i]);
ret[i] = Block.New(cubes[i]);
return ret;
}

@@ -103,7 +104,14 @@ namespace TechbloxModdingAPI.Blocks
var prefabAssetIDOpt = entitiesDB.QueryEntityOptional<PrefabAssetIDComponent>(block);
uint prefabAssetID = prefabAssetIDOpt
? 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 =
PrefabsID.GetOrCreatePrefabID((ushort) prefabAssetID, material, 1, flipped);
entitiesDB.QueryEntityOrDefault<GFXPrefabEntityStructGPUI>(block).prefabID = prefabId;
@@ -239,7 +247,7 @@ namespace TechbloxModdingAPI.Blocks
{
var conn = array[index];
if (conn.machineRigidBodyId == sbid)
set.Add(new Block(conn.ID));
set.Add(Block.New(conn.ID));
}
}



+ 1
- 2
TechbloxModdingAPI/Blocks/BlockEventsEngine.cs View File

@@ -47,9 +47,8 @@ namespace TechbloxModdingAPI.Blocks
public struct BlockPlacedRemovedEventArgs
{
public EGID ID;
public BlockIDs Type;
private Block block;

public Block Block => block ?? (block = new Block(ID));
public Block Block => block ?? (block = Block.New(ID));
}
}

+ 100
- 42
TechbloxModdingAPI/Blocks/BlockTests.cs View File

@@ -1,9 +1,13 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;

using Gamecraft.Wires;
using DataLoader;
using Svelto.Tasks;
using Unity.Mathematics;

using TechbloxModdingAPI;
using TechbloxModdingAPI.App;
using TechbloxModdingAPI.Tests;
using TechbloxModdingAPI.Utility;

@@ -19,62 +23,116 @@ namespace TechbloxModdingAPI.Blocks
[APITestCase(TestType.Game)] //At least one block must be placed for simulation to work
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.");
}

[APITestCase(TestType.EditMode)]
public static void TestInitProperty()
{
Block newBlock = Block.PlaceNew(BlockIDs.Cube, Unity.Mathematics.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;
Block newBlock = Block.PlaceNew(BlockIDs.Cube, float3.zero + 2);
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.");
}

/*[APITestCase(TestType.EditMode)]
public static void TestTextBlock()
{
TextBlock textBlock = null; // Note: the assignment operation is a lambda, which slightly confuses the compiler
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.");
if (!Assert.NotNull(textBlock, "Block.PlaceNew<TextBlock>() returned null, possibly because it failed silently.", "Specialized TextBlock is not null.")) return;
if (!Assert.NotNull(textBlock.Text, "TextBlock.Text is null, possibly because it failed silently.", "TextBlock.Text is not null.")) return;
if (!Assert.NotNull(textBlock.TextBlockId, "TextBlock.TextBlockId is null, possibly because it failed silently.", "TextBlock.TextBlockId is not null.")) return;
}

[APITestCase(TestType.EditMode)]
public static void TestMotor()
[APITestCase(TestType.EditMode)]
public static void TestBlockIDCoverage()
{
Block newBlock = Block.PlaceNew(BlockIDs.MotorS, Unity.Mathematics.float3.zero + 1);
Motor b = null; // Note: the assignment operation is a lambda, which slightly confuses the compiler
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 (!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;
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;
Assert.Equal(
FullGameFields._dataDb.GetValues<CubeListData>().Keys.Select(ushort.Parse).OrderBy(id => id)
.SequenceEqual(Enum.GetValues(typeof(BlockIDs)).Cast<ushort>().OrderBy(id => id)
.Except(new[] {(ushort) BlockIDs.Invalid})), true,
"Block ID enum is different than the known block types, update needed.",
"Block ID enum matches the known block types.");
}

[APITestCase(TestType.EditMode)]
public static void TestPiston()
[APITestCase(TestType.EditMode)]
public static void TestBlockIDs()
{
Block newBlock = Block.PlaceNew(BlockIDs.PneumaticPiston, Unity.Mathematics.float3.zero + 1);
Piston b = null; // Note: the assignment operation is a lambda, which slightly confuses the compiler
Assert.Errorless(() => { b = newBlock.Specialise<Piston>(); }, "Block.Specialize<Piston>() raised an exception: ", "Block.Specialize<Piston>() completed without issue.");
if (!Assert.NotNull(b, "Block.Specialize<Piston>() returned null, possibly because it failed silently.", "Specialized Piston is not null.")) return;
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;
float3 pos = new float3();
foreach (BlockIDs id in Enum.GetValues(typeof(BlockIDs)))
{
if (id == BlockIDs.Invalid) continue;
try
{
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)]
public static void TestServo()
{
Block newBlock = Block.PlaceNew(BlockIDs.ServoAxle, Unity.Mathematics.float3.zero + 1);
Servo b = null; // Note: the assignment operation is a lambda, which slightly confuses the compiler
Assert.Errorless(() => { b = newBlock.Specialise<Servo>(); }, "Block.Specialize<Servo>() raised an exception: ", "Block.Specialize<Servo>() completed without issue.");
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 (!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;
}*/
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++)
{
if (index % 50 == 0) yield return Yield.It; //The material or flipped status can only be changed 130 times per submission
var block = blocks[index];
if (!block.Exists) continue;
foreach (var property in block.GetType().GetProperties())
{
//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;
}
}

if (propType.IsEnum)
{
var values = propType.GetEnumValues();
valueToUse = values.GetValue(values.Length / 2);
predicateToUse = val => Equals(val, valueToUse);
}

if (valueToUse == null)
{
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)]
public static void TestDampedSpring()


+ 2
- 2
TechbloxModdingAPI/Blocks/BlueprintEngine.cs View File

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



+ 2
- 2
TechbloxModdingAPI/Blocks/Wire.cs View File

@@ -48,7 +48,7 @@ namespace TechbloxModdingAPI.Blocks
WireEntityStruct wire = signalEngine.MatchPortToWire(port, end.Id, out 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;
}
@@ -67,7 +67,7 @@ namespace TechbloxModdingAPI.Blocks
WireEntityStruct wire = signalEngine.MatchPortToWire(port, start.Id, out 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;
}


+ 1
- 1
TechbloxModdingAPI/Player.cs View File

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


+ 1
- 1
TechbloxModdingAPI/Players/PlayerEngine.cs View File

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

return ret;


+ 2
- 2
TechbloxModdingAPI/Tests/TechbloxModdingAPIPluginTest.cs View File

@@ -324,9 +324,9 @@ namespace TechbloxModdingAPI.Tests

GameClient.SetDebugInfo("InstalledMods", InstalledMods);
Block.Placed += (sender, args) =>
Logging.MetaDebugLog("Placed block " + args.Type + " with ID " + args.ID);
Logging.MetaDebugLog("Placed block " + args.Block);
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; },


+ 50
- 62
TechbloxModdingAPI/Tests/TestRoot.cs View File

@@ -149,71 +149,59 @@ namespace TechbloxModdingAPI.Tests
Game currentGame = Game.CurrentGame();
// in-game tests
yield return new WaitForSecondsEnumerator(5).Continue(); // wait for game to finish loading
foreach (Type t in testTypes)
{
foreach (MethodBase m in t.GetMethods())
{
APITestCaseAttribute a = m.GetCustomAttribute<APITestCaseAttribute>();
if (a != null && a.TestType == TestType.Game)
{
try
{
m.Invoke(null, new object[0]);
}
catch (Exception e)
{
Assert.Fail($"Game test '{m}' raised an exception: {e.ToString()}");
}
yield return Yield.It;
}
}
}
currentGame.ToggleTimeMode();
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)
{
var testTypesToRun = new[]
{
TestType.Game,
TestType.SimulationMode,
TestType.EditMode
};
for (var index = 0; index < testTypesToRun.Length; index++)
{
foreach (Type t in testTypes)
{
foreach (MethodBase m in t.GetMethods())
{
APITestCaseAttribute a = m.GetCustomAttribute<APITestCaseAttribute>();
if (a == null || a.TestType != testTypesToRun[index]) continue;

object ret = null;
try
{
m.Invoke(null, new object[0]);
}
catch (Exception e)
{
Assert.Fail($"Build test '{m}' raised an exception: {e.ToString()}");
}
{
ret = m.Invoke(null, new object[0]);
}
catch (Exception e)
{
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;
}
}
}
}
}

if (index + 1 < testTypesToRun.Length) //Don't toggle on the last test
currentGame.ToggleTimeMode();
yield return new WaitForSecondsEnumerator(5).Continue();
}
// exit game
yield return new WaitForSecondsEnumerator(5).Continue();
yield return ReturnToMenu().Continue();
}



+ 18
- 0
TechbloxModdingAPI/Tests/TestValueAttribute.cs 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;
}
}

Loading…
Cancel
Save