From e9df67f46293283c4dab1bdf6f3cebe2660097e0 Mon Sep 17 00:00:00 2001 From: NorbiPeti Date: Wed, 19 May 2021 01:40:15 +0200 Subject: [PATCH] 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 --- TechbloxModdingAPI/App/Game.cs | 2 +- TechbloxModdingAPI/Block.cs | 7 +- TechbloxModdingAPI/Blocks/BlockEngine.cs | 14 +- .../Blocks/BlockEventsEngine.cs | 3 +- TechbloxModdingAPI/Blocks/BlockTests.cs | 142 ++++++++++++------ TechbloxModdingAPI/Blocks/BlueprintEngine.cs | 4 +- TechbloxModdingAPI/Blocks/Wire.cs | 4 +- TechbloxModdingAPI/Player.cs | 2 +- TechbloxModdingAPI/Players/PlayerEngine.cs | 2 +- .../Tests/TechbloxModdingAPIPluginTest.cs | 4 +- TechbloxModdingAPI/Tests/TestRoot.cs | 112 ++++++-------- .../Tests/TestValueAttribute.cs | 18 +++ 12 files changed, 193 insertions(+), 121 deletions(-) create mode 100644 TechbloxModdingAPI/Tests/TestValueAttribute.cs diff --git a/TechbloxModdingAPI/App/Game.cs b/TechbloxModdingAPI/App/Game.cs index 252171d..8866165 100644 --- a/TechbloxModdingAPI/App/Game.cs +++ b/TechbloxModdingAPI/App/Game.cs @@ -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; } diff --git a/TechbloxModdingAPI/Block.cs b/TechbloxModdingAPI/Block.cs index ca66f3a..bab3291 100644 --- a/TechbloxModdingAPI/Block.cs +++ b/TechbloxModdingAPI/Block.cs @@ -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. /// + [TestValue(null)] public string Label { get => BlockEngine.GetBlockInfoViewComponent(this).textLabelComponent?.text; diff --git a/TechbloxModdingAPI/Blocks/BlockEngine.cs b/TechbloxModdingAPI/Blocks/BlockEngine.cs index b06194f..b8eece6 100644 --- a/TechbloxModdingAPI/Blocks/BlockEngine.cs +++ b/TechbloxModdingAPI/Blocks/BlockEngine.cs @@ -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(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(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(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)); } } diff --git a/TechbloxModdingAPI/Blocks/BlockEventsEngine.cs b/TechbloxModdingAPI/Blocks/BlockEventsEngine.cs index 3b77075..a1ecd7f 100644 --- a/TechbloxModdingAPI/Blocks/BlockEventsEngine.cs +++ b/TechbloxModdingAPI/Blocks/BlockEventsEngine.cs @@ -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)); } } \ No newline at end of file diff --git a/TechbloxModdingAPI/Blocks/BlockTests.cs b/TechbloxModdingAPI/Blocks/BlockTests.cs index 5cd32d6..f69315e 100644 --- a/TechbloxModdingAPI/Blocks/BlockTests.cs +++ b/TechbloxModdingAPI/Blocks/BlockTests.cs @@ -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(BlockIDs.TextBlock, Unity.Mathematics.float3.zero + 1); }, "Block.PlaceNew() raised an exception: ", "Block.PlaceNew() completed without issue."); - if (!Assert.NotNull(textBlock, "Block.PlaceNew() 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(); }, "Block.Specialize() raised an exception: ", "Block.Specialize() completed without issue."); - if (!Assert.NotNull(b, "Block.Specialize() 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().Keys.Select(ushort.Parse).OrderBy(id => id) + .SequenceEqual(Enum.GetValues(typeof(BlockIDs)).Cast().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(); }, "Block.Specialize() raised an exception: ", "Block.Specialize() completed without issue."); - if (!Assert.NotNull(b, "Block.Specialize() 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(); }, "Block.Specialize() raised an exception: ", "Block.Specialize() completed without issue."); - if (!Assert.NotNull(b, "Block.Specialize() 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 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)[] + { + //(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 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(); + 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() diff --git a/TechbloxModdingAPI/Blocks/BlueprintEngine.cs b/TechbloxModdingAPI/Blocks/BlueprintEngine.cs index a6f65f3..eece947 100644 --- a/TechbloxModdingAPI/Blocks/BlueprintEngine.cs +++ b/TechbloxModdingAPI/Blocks/BlueprintEngine.cs @@ -75,7 +75,7 @@ namespace TechbloxModdingAPI.Blocks int count = selectedBlocksInGroup.Count(); var ret = new Block[count]; for (uint i = 0; i < count; i++) - ret[i] = new Block(selectedBlocksInGroup.Get(i)); + ret[i] = Block.New(selectedBlocksInGroup.Get(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; } diff --git a/TechbloxModdingAPI/Blocks/Wire.cs b/TechbloxModdingAPI/Blocks/Wire.cs index 289dee7..6cbfdb9 100644 --- a/TechbloxModdingAPI/Blocks/Wire.cs +++ b/TechbloxModdingAPI/Blocks/Wire.cs @@ -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; } diff --git a/TechbloxModdingAPI/Player.cs b/TechbloxModdingAPI/Player.cs index 01b5c24..a5e5dc5 100644 --- a/TechbloxModdingAPI/Player.cs +++ b/TechbloxModdingAPI/Player.cs @@ -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; } diff --git a/TechbloxModdingAPI/Players/PlayerEngine.cs b/TechbloxModdingAPI/Players/PlayerEngine.cs index f0c8a11..b4df628 100644 --- a/TechbloxModdingAPI/Players/PlayerEngine.cs +++ b/TechbloxModdingAPI/Players/PlayerEngine.cs @@ -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; diff --git a/TechbloxModdingAPI/Tests/TechbloxModdingAPIPluginTest.cs b/TechbloxModdingAPI/Tests/TechbloxModdingAPIPluginTest.cs index d9cfedb..6b78fa8 100644 --- a/TechbloxModdingAPI/Tests/TechbloxModdingAPIPluginTest.cs +++ b/TechbloxModdingAPI/Tests/TechbloxModdingAPIPluginTest.cs @@ -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 d) => { UnityEngine.Camera.main.fieldOfView = d; }, diff --git a/TechbloxModdingAPI/Tests/TestRoot.cs b/TechbloxModdingAPI/Tests/TestRoot.cs index 9085470..2ac7c62 100644 --- a/TechbloxModdingAPI/Tests/TestRoot.cs +++ b/TechbloxModdingAPI/Tests/TestRoot.cs @@ -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(); - 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(); - 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(); - 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(); + 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 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(); } diff --git a/TechbloxModdingAPI/Tests/TestValueAttribute.cs b/TechbloxModdingAPI/Tests/TestValueAttribute.cs new file mode 100644 index 0000000..48794c9 --- /dev/null +++ b/TechbloxModdingAPI/Tests/TestValueAttribute.cs @@ -0,0 +1,18 @@ +using System; + +namespace TechbloxModdingAPI.Tests +{ + [AttributeUsage(AttributeTargets.Property)] + public class TestValueAttribute : Attribute + { + public object PossibleValue { get; } + + /// + /// + /// When set, the property test accepts the specified value in addition to the test input.
+ /// Useful if setting the property isn't always possible. + /// + ///
+ public TestValueAttribute(object possibleValue) => PossibleValue = possibleValue; + } +} \ No newline at end of file