Files
Garry Newman d95169d8fa Unit Test Cleanup (#5064)
* Move all unit tests to Engine/Tests
* Fix Margin.EdgeSubtract
* Fix Capsule.Contains
* EnvironmentVariables.Remove if it's null
* Fixed ray trace never returning startedsolid
* Fix HistoryList.Navigate on empty list
* Fix GameObject.WorldPosition accepting NaNs
* Fix flex: initial expanding to the wrong grow/shrink
* Translation.TryConvert shouldn't throw on invalid enum strings
* Skip sound file tests on machines with no audio device
2026-06-12 13:23:50 +01:00

437 lines
12 KiB
C#

using Sandbox.Internal;
using System.Collections.Generic;
namespace SceneTests.GameObjects;
[TestClass]
[DoNotParallelize]
public class CloneTest : SceneTest
{
TypeLibrary TypeLibrary;
private TypeLibrary _oldTypeLibrary;
[TestInitialize]
public void TestInitialize()
{
// Replace TypeLibrary / NodeLibrary with mocked ones, store the originals
_oldTypeLibrary = Game.TypeLibrary;
TypeLibrary = new Sandbox.Internal.TypeLibrary();
TypeLibrary.AddAssembly( typeof( ModelRenderer ).Assembly, false );
TypeLibrary.AddAssembly( typeof( ComponentWithPrefabSceneReference ).Assembly, false );
JsonUpgrader.UpdateUpgraders( TypeLibrary );
Game.TypeLibrary = TypeLibrary;
}
[TestCleanup]
public void Cleanup()
{
// Make sure our mocked TypeLibrary doesn't leak out, restore old ones
Game.TypeLibrary = _oldTypeLibrary;
}
/// <summary>
/// When cloning a GameObject that has child GameObjects that are being destroyed, they shouldn't be serialized, or cloned.
/// </summary>
[TestMethod]
public void DestroyBCloneASameFrame()
{
var scene = new Scene();
using var sceneScope = scene.Push();
var a = scene.CreateObject();
var b = scene.CreateObject();
b.SetParent( a );
// Make sure we have one child on a
Assert.AreEqual( 1, a.Children.Count );
// Mark b as destroyed, won't actually be destroyed until next game tick
b.Destroy();
// Clone a, we shouldn't have any children because we are destroying b
var c = a.Clone();
Assert.AreEqual( 0, c.Children.Count );
}
/// <summary>
/// When cloning something, you're meant to be able to start it as disabled.
/// </summary>
[TestMethod]
public void CloneAsDisabled()
{
var scene = new Scene();
using var sceneScope = scene.Push();
var a = scene.CreateObject();
var b = a.Clone( new CloneConfig()
{
StartEnabled = false
} );
Assert.IsFalse( b.Enabled );
}
[TestMethod]
public void ComponentNotClonedFlag()
{
var scene = new Scene();
using var sceneScope = scene.Push();
var a = scene.CreateObject();
var comp = a.AddComponent<ComponentWithOnStartOnAwake>();
comp.Flags |= ComponentFlags.NotCloned;
var clone = a.Clone();
Assert.IsNull( clone.GetComponent<ComponentWithOnStartOnAwake>() );
}
/// <summary>
/// When cloning something, you're meant to be able to start it as disabled.
/// </summary>
[TestMethod]
public void CloneAsDisabled_Overloads()
{
var scene = new Scene();
using var sceneScope = scene.Push();
var a = scene.CreateObject();
{
var b = a.Clone( Transform.Zero, null, false );
Assert.IsFalse( b.Enabled );
}
{
var b = a.Clone( Transform.Zero, a, false );
Assert.IsFalse( b.Enabled );
}
}
[TestMethod]
public void CloneAsDisabled_DoesNotCallOnAwakeAndOnStart()
{
var scene = new Scene();
using var sceneScope = scene.Push();
var go = scene.CreateObject();
go.AddComponent<ComponentWithOnStartOnAwake>();
var clonedGo = go.Clone( new CloneConfig()
{
StartEnabled = false
} );
Assert.IsFalse( clonedGo.Enabled );
var clonedComp = clonedGo.GetComponent<ComponentWithOnStartOnAwake>( true );
Assert.IsNotNull( clonedComp );
// need to tick to make sure all callbacks have a change to trigger
scene.GameTick();
Assert.IsFalse( clonedComp.WasAwakeCalled );
Assert.IsFalse( clonedComp.WasStartCalled );
}
[TestMethod]
public void CloneAsEnabled_DoesCallOnAwakeAndOnStart()
{
var scene = new Scene();
using var sceneScope = scene.Push();
var go = scene.CreateObject();
go.AddComponent<ComponentWithOnStartOnAwake>();
var clonedGo = go.Clone( new CloneConfig()
{
StartEnabled = true
} );
Assert.IsTrue( clonedGo.Enabled );
var clonedComp = clonedGo.GetComponent<ComponentWithOnStartOnAwake>( true );
Assert.IsNotNull( clonedComp );
// need to tick to make sure all callbacks have a change to trigger
scene.GameTick();
Assert.IsTrue( clonedComp.WasAwakeCalled );
Assert.IsTrue( clonedComp.WasStartCalled );
}
/// <summary>
/// When cloning a prefab, that has a PrefabScene property that references the prefab itself, we should be able to clone it.
/// The cloned PrefabScene property should still point to the PrefabScene.
/// </summary>
[TestMethod]
[DataRow( false )]
[DataRow( true )]
public void CloneSelfReferencingPrefabSceneProperty( bool breakBeforeClone )
{
// Load the nested prefab into the resource register so the prefab can find it.
var pfile = new PrefabFile();
pfile.RegisterWeakResourceId( "prefabwithselfreference.prefab" );
pfile.LoadFromJson( PrefabWithSelfReference );
Game.Resources.Register( pfile );
try
{
var prefab = SceneUtility.GetPrefabScene( pfile );
var scene = new Scene();
using var sceneScope = scene.Push();
GameObject gameObjectToClone = prefab;
// We also want to test cloning broken-from-prefab objects
if ( breakBeforeClone )
{
// This clone is part of the setup and not part of the actual test
gameObjectToClone = prefab.Clone();
gameObjectToClone.BreakFromPrefab();
}
var prefabInstance = gameObjectToClone.Clone();
Assert.IsNotNull( prefabInstance.GetComponent<ComponentWithPrefabSceneReference>() );
Assert.IsNotNull( prefab.GetComponent<ComponentWithPrefabSceneReference>().PrefabRef );
Assert.AreEqual( prefab.GetComponent<ComponentWithPrefabSceneReference>().PrefabRef, prefabInstance.GetComponent<ComponentWithPrefabSceneReference>().PrefabRef );
}
finally
{
Game.Resources.Unregister( pfile );
}
}
static readonly string PrefabWithSelfReference = """
{
"RootObject": {
"__guid": "1390a3b8-bbc3-4c73-9e18-2edc5b9c2587",
"Flags": 0,
"Name": "prefabwithselfreference",
"Enabled": true,
"Components": [
{
"__type": "ComponentWithPrefabSceneReference",
"__guid": "f1982f17-e2d2-4ee0-b24f-07558f309b78",
"PrefabRef": {
"_type": "gameobject",
"prefab": "prefabwithselfreference.prefab"
}
}
],
"Children": [],
"__variables": [],
"__properties": {
"FixedUpdateFrequency": 50,
"MaxFixedUpdates": 5,
"NetworkFrequency": 30,
"NetworkInterpolation": true,
"PhysicsSubSteps": 1,
"ThreadedAnimation": true,
"TimeScale": 1,
"UseFixedUpdate": true,
"Metadata": {},
"NavMesh": {
"Enabled": false,
"IncludeStaticBodies": true,
"IncludeKeyframedBodies": true,
"EditorAutoUpdate": true,
"AgentHeight": 64,
"AgentRadius": 16,
"AgentStepSize": 18,
"AgentMaxSlope": 40,
"ExcludedBodies": "",
"IncludedBodies": ""
}
}
},
"ShowInMenu": false,
"MenuPath": null,
"MenuIcon": null,
"DontBreakAsTemplate": false,
"ResourceVersion": 1,
"__version": 1
}
""";
public class ComponentWithPrefabSceneReference : Component
{
[Sandbox.Property]
public PrefabScene PrefabRef { get; set; }
}
/// <summary>
/// When cloning a prefab, that has a GameObject property that references the root of the prefab and therefore the prefab itself, we should be able to clone it.
/// To match old cloning behvaiour, the cloned property should still reference the cloned root GameObject and no longer the PrefabScene.
/// </summary>
[TestMethod]
[DataRow( false )]
[DataRow( true )]
public void ClonePrefabRootReferencingGameObjectProperty( bool breakBeforeClone )
{
// Load the nested prefab into the resource register so the prefab can find it.
var pfile = new PrefabFile();
pfile.RegisterWeakResourceId( "prefabwithRootreference.prefab" );
pfile.LoadFromJson( PrefabWithRootReference );
Game.Resources.Register( pfile );
try
{
var prefab = SceneUtility.GetPrefabScene( pfile );
var scene = new Scene();
using var sceneScope = scene.Push();
GameObject gameObjectToClone = prefab;
// We also want to test cloning broken-from-prefab objects
if ( breakBeforeClone )
{
// This clone is part of the setup and not part of the actual test
gameObjectToClone = prefab.Clone();
gameObjectToClone.BreakFromPrefab();
}
var prefabInstance = gameObjectToClone.Clone();
Assert.IsNotNull( prefabInstance.GetComponent<ComponentWithPrefabRootReference>() );
Assert.IsNotNull( prefab.GetComponent<ComponentWithPrefabRootReference>().PrefabRootRef );
Assert.AreNotEqual( prefab.GetComponent<ComponentWithPrefabRootReference>().PrefabRootRef, prefabInstance.GetComponent<ComponentWithPrefabRootReference>().PrefabRootRef );
Assert.IsInstanceOfType( prefabInstance.GetComponent<ComponentWithPrefabRootReference>().PrefabRootRef, typeof( GameObject ) );
}
finally
{
Game.Resources.Unregister( pfile );
}
}
public class ComponentWithPrefabRootReference : Component
{
[Sandbox.Property]
public GameObject PrefabRootRef { get; set; }
}
static readonly string PrefabWithRootReference = """
{
"RootObject": {
"__guid": "1390a3b8-bbc3-4c73-9e18-2edc5b9c2587",
"Flags": 0,
"Name": "prefabwithRootreference",
"Enabled": true,
"Components": [
{
"__type": "ComponentWithPrefabRootReference",
"__guid": "f1982f17-e2d2-4ee0-b24f-07558f309b78",
"PrefabRootRef": {
"_type": "gameobject",
"prefab": "prefabwithRootreference.prefab"
}
}
],
"Children": [],
"__variables": [],
"__properties": {
"FixedUpdateFrequency": 50,
"MaxFixedUpdates": 5,
"NetworkFrequency": 30,
"NetworkInterpolation": true,
"PhysicsSubSteps": 1,
"ThreadedAnimation": true,
"TimeScale": 1,
"UseFixedUpdate": true,
"Metadata": {},
"NavMesh": {
"Enabled": false,
"IncludeStaticBodies": true,
"IncludeKeyframedBodies": true,
"EditorAutoUpdate": true,
"AgentHeight": 64,
"AgentRadius": 16,
"AgentStepSize": 18,
"AgentMaxSlope": 40,
"ExcludedBodies": "",
"IncludedBodies": ""
}
}
},
"ShowInMenu": false,
"MenuPath": null,
"MenuIcon": null,
"DontBreakAsTemplate": false,
"ResourceVersion": 1,
"__version": 1
}
""";
/// <summary>
/// We would like to clone some types by copy.
/// But we can only do that if they don't have any reference types in them.
/// </summary>
[TestMethod]
public void ReflectionQueryCache_IsTypeClonableByCopy()
{
Assert.IsTrue( ReflectionQueryCache.IsTypeCloneableByCopy( typeof( int ) ) );
Assert.IsTrue( ReflectionQueryCache.IsTypeCloneableByCopy( typeof( Vector3 ) ) );
Assert.IsTrue( ReflectionQueryCache.IsTypeCloneableByCopy( typeof( ValueTypeWithValueTypeProperty ) ) );
Assert.IsFalse( ReflectionQueryCache.IsTypeCloneableByCopy( typeof( List<> ) ) );
Assert.IsFalse( ReflectionQueryCache.IsTypeCloneableByCopy( typeof( GameObject ) ) );
Assert.IsFalse( ReflectionQueryCache.IsTypeCloneableByCopy( typeof( Component ) ) );
Assert.IsFalse( ReflectionQueryCache.IsTypeCloneableByCopy( typeof( EmptyReferenceType ) ) );
Assert.IsFalse( ReflectionQueryCache.IsTypeCloneableByCopy( typeof( ReferenceTypeWithReferenceTypeProperty ) ) );
Assert.IsFalse( ReflectionQueryCache.IsTypeCloneableByCopy( typeof( ValueTypeWithReferenceTypeProperty ) ) );
Assert.IsFalse( ReflectionQueryCache.IsTypeCloneableByCopy( typeof( ValueTypeWithCollectionProperty ) ) );
}
}
public class EmptyReferenceType
{
}
public class ReferenceTypeWithReferenceTypeProperty
{
public EmptyReferenceType Reference;
}
public struct ValueTypeWithReferenceTypeProperty
{
public EmptyReferenceType Reference;
}
public struct ValueTypeWithValueTypeProperty
{
public Vector3 Value;
}
public struct ValueTypeWithCollectionProperty
{
public List<int> Collection;
}
public class ComponentWithOnStartOnAwake : Component
{
public bool WasAwakeCalled = false;
public bool WasStartCalled = false;
protected override void OnAwake()
{
base.OnAwake();
WasAwakeCalled = true;
}
protected override void OnStart()
{
base.OnStart();
WasStartCalled = true;
}
}