VR4Medical/ICI/Library/PackageCache/com.unity.xr.interaction.toolkit@42ef3600567b/Tests/Editor/PackageScriptsTests.cs
2025-07-29 13:45:50 +03:00

430 lines
22 KiB
C#

using System;
using System.Collections;
using System.Linq;
using System.Reflection;
using NUnit.Framework;
using Unity.XR.CoreUtils.Collections;
using UnityEngine;
using UnityEngine.TestTools;
namespace UnityEditor.XR.Interaction.Toolkit.Editor.Tests
{
/// <summary>
/// This class contains tests to help identify potential property and method accessibility issues and
/// potential namespace issues. It is left to the discretion of the developer to add any violations to
/// appropriate exception list or to address the violation in the script.
/// </summary>
[TestFixture]
class PackageScriptsTests
{
// Instructions to developers modifying exceptions lists:
// - Use "All" to apply to all package versions.
// - Use "3.0" or "3.1" etc. to apply the exception to a specific major.minor version.
// This is used for new internal properties added in a 3.x patch that should be changed to public in 3.y.
static readonly (string, string)[] s_NamespaceExceptionList =
{
("All", "UnityEditor.XR.Interaction.Toolkit.Utilities.EditorComponentLocatorUtility"),
};
static readonly (string, string)[] s_InternalPropertyExceptionList =
{
("All", "Property UnityEngine.XR.Interaction.Toolkit.Filtering.XRTargetFilter.evaluators has { internal get; }."),
("All", "Property UnityEngine.XR.Interaction.Toolkit.Filtering.XRTargetFilter.isProcessing has { internal get; private set; }."),
("All", "Property UnityEngine.XR.Interaction.Toolkit.Filtering.XRTargetFilter.linkedInteractors has { internal get; }."),
("All", "Property UnityEngine.XR.Interaction.Toolkit.Inputs.Interactions.SectorInteraction.pressPointOrDefault has { internal get; }."),
("All", "Property UnityEngine.XR.Interaction.Toolkit.Inputs.XRInputModalityManager.leftInputMode has { internal get; }."),
("All", "Property UnityEngine.XR.Interaction.Toolkit.Inputs.XRInputModalityManager.rightInputMode has { internal get; }."),
("All", "Property UnityEngine.XR.Interaction.Toolkit.Interactors.XRInteractionGroup.hasRegisteredStartingMembers has { internal get; private set; }."),
("All", "Property UnityEngine.XR.Interaction.Toolkit.Interactors.XRInteractionGroup.isRegisteredWithInteractionManager has { internal get; }."),
("All", "Property UnityEngine.XR.Interaction.Toolkit.Transformers.XRDualGrabFreeTransformer.lastInteractorAttachPose has { internal get; private set; }."),
("All", "Property UnityEngine.XR.Interaction.Toolkit.UI.TrackedDeviceEventData.pressWorldPosition has { internal get; internal set; }."),
};
static readonly (string, string)[] s_InternalMethodExceptionList =
{
("All", "Method UnityEngine.XR.Interaction.Toolkit.AffordanceSystem.Theme.Audio.AudioAffordanceTheme.ValidateTheme has internal."),
("All", "Method UnityEngine.XR.Interaction.Toolkit.AffordanceSystem.Theme.BaseAffordanceTheme`1.ValidateTheme has internal."),
("All", "Method UnityEngine.XR.Interaction.Toolkit.AR.DragGesture.Reinitialize has internal."),
("All", "Method UnityEngine.XR.Interaction.Toolkit.AR.Gesture`1.Cancel has internal."),
("All", "Method UnityEngine.XR.Interaction.Toolkit.AR.Gesture`1.Reinitialize has internal."),
("All", "Method UnityEngine.XR.Interaction.Toolkit.AR.Gesture`1.Update has internal."),
("All", "Method UnityEngine.XR.Interaction.Toolkit.AR.PinchGesture.Reinitialize has internal."),
("All", "Method UnityEngine.XR.Interaction.Toolkit.AR.TapGesture.Reinitialize has internal."),
("All", "Method UnityEngine.XR.Interaction.Toolkit.AR.TwistGesture.Reinitialize has internal."),
("All", "Method UnityEngine.XR.Interaction.Toolkit.AR.TwoFingerDragGesture.Reinitialize has internal."),
("All", "Method UnityEngine.XR.Interaction.Toolkit.Filtering.XRTargetFilter.RegisterEvaluatorHandlers has internal."),
("All", "Method UnityEngine.XR.Interaction.Toolkit.Filtering.XRTargetFilter.UnregisterEvaluatorHandlers has internal."),
("All", "Method UnityEngine.XR.Interaction.Toolkit.Inputs.Simulation.SimulatedDeviceLifecycleManager.AddDevices has internal."),
("All", "Method UnityEngine.XR.Interaction.Toolkit.Inputs.Simulation.SimulatedDeviceLifecycleManager.ApplyControllerState has internal."),
("All", "Method UnityEngine.XR.Interaction.Toolkit.Inputs.Simulation.SimulatedDeviceLifecycleManager.ApplyHandState has internal."),
("All", "Method UnityEngine.XR.Interaction.Toolkit.Inputs.Simulation.SimulatedDeviceLifecycleManager.ApplyHMDState has internal."),
("All", "Method UnityEngine.XR.Interaction.Toolkit.Inputs.Simulation.SimulatedDeviceLifecycleManager.RemoveDevices has internal."),
("All", "Method UnityEngine.XR.Interaction.Toolkit.Inputs.Simulation.SimulatedDeviceLifecycleManager.SwitchDeviceMode has internal."),
("All", "Method UnityEngine.XR.Interaction.Toolkit.Interactors.Visuals.XRInteractorLineVisual.UpdateLineVisual has internal."),
("All", "Method UnityEngine.XR.Interaction.Toolkit.Interactors.XRInteractionGroup.RemoveMissingMembersFromStartingOverridesMap has internal."),
("All", "Method UnityEngine.XR.Interaction.Toolkit.Locomotion.Comfort.TunnelingVignetteController.PreviewInEditor has internal."),
("All", "Method UnityEngine.XR.Interaction.Toolkit.Locomotion.LocomotionMediator.TryEndLocomotion has internal."),
("All", "Method UnityEngine.XR.Interaction.Toolkit.Locomotion.LocomotionMediator.TryPrepareLocomotion has internal."),
("All", "Method UnityEngine.XR.Interaction.Toolkit.Locomotion.LocomotionMediator.TryStartLocomotion has internal."),
("All", "Method UnityEngine.XR.Interaction.Toolkit.Locomotion.LocomotionProvider.OnLocomotionEnd has internal."),
("All", "Method UnityEngine.XR.Interaction.Toolkit.Locomotion.LocomotionProvider.OnLocomotionStart has internal."),
("All", "Method UnityEngine.XR.Interaction.Toolkit.Transformers.ARTransformer.MigratePlaneClassifications has internal."),
("All", "Method UnityEngine.XR.Interaction.Toolkit.XRControllerRecording.SetFrameDependentData has internal."),
};
static readonly (string, string)[] s_PrivatePropertyExceptionList =
{
("All", "Property UnityEngine.XR.Interaction.Toolkit.Inputs.Simulation.XRDeviceSimulator.targetedDeviceInput has { private get; private set; }."),
("All", "Property UnityEngine.XR.Interaction.Toolkit.Interactables.XRGrabInteractable.isTransformDirty has { private get; private set; }."),
};
static readonly (string, string)[] s_EditorExceptionList =
{
("All", "Field UnityEditor.XR.Interaction.Toolkit.Locomotion.Movement.GrabMoveProviderEditor.m_Reference has private instead of protected."),
("All", "Field UnityEditor.XR.Interaction.Toolkit.Locomotion.Movement.GrabMoveProviderEditor.m_SingletonActionBindings has private instead of protected."),
};
/// <summary>
/// <c>major.minor</c> version of com.unity.xr.interaction.toolkit.
/// </summary>
string m_MajorMinorVersion;
[OneTimeSetUp]
public void SetUp()
{
var assembly = Assembly.Load("Unity.XR.Interaction.Toolkit");
Assert.That(assembly, Is.Not.Null);
PackageManager.PackageInfo packageInfo = PackageManager.PackageInfo.FindForAssembly(assembly);
Assert.That(packageInfo, Is.Not.Null);
// Parse the major.minor version
Assert.That(packageInfo.version, Is.Not.Null);
Assert.That(packageInfo.version, Is.Not.Empty);
var secondDotIndex = packageInfo.version.IndexOf('.', packageInfo.version.IndexOf('.') + 1);
Assert.That(secondDotIndex, Is.GreaterThan(0));
m_MajorMinorVersion = packageInfo.version.Substring(0, secondDotIndex);
}
[UnityTest]
public IEnumerator NamespaceMatchesAssembly()
{
var outputList = new HashSetList<string>();
PopulateWrongNamespaceTypes(Assembly.Load("Unity.XR.Interaction.Toolkit"), "UnityEngine.XR.Interaction.Toolkit", outputList);
PopulateWrongNamespaceTypes(Assembly.Load("Unity.XR.Interaction.Toolkit.Editor"), "UnityEditor.XR.Interaction.Toolkit", outputList);
FilterExceptions(outputList, s_NamespaceExceptionList);
Assert.That(outputList, Is.Empty, $"Contains {outputList.Count} incorrect namespaces that have not been excluded in {nameof(PackageScriptsTests)}.cs:\n" + string.Join("\n", outputList) + "\n");
yield return null;
}
[UnityTest]
public IEnumerator LimitInternalProperties()
{
var outputList = new HashSetList<string>();
PopulateInternalProperties(Assembly.Load("Unity.XR.Interaction.Toolkit"), outputList);
PopulateInternalProperties(Assembly.Load("Unity.XR.Interaction.Toolkit.Editor"), outputList);
FilterExceptions(outputList, s_InternalPropertyExceptionList);
Assert.That(outputList, Is.Empty, $"Contains {outputList.Count} internal properties that have not been excluded in {nameof(PackageScriptsTests)}.cs:\n" + string.Join("\n", outputList) + "\n");
yield return null;
}
[UnityTest]
public IEnumerator LimitInternalMethods()
{
var outputList = new HashSetList<string>();
PopulateInternalMethods(Assembly.Load("Unity.XR.Interaction.Toolkit"), outputList);
PopulateInternalMethods(Assembly.Load("Unity.XR.Interaction.Toolkit.Editor"), outputList);
FilterExceptions(outputList, s_InternalMethodExceptionList);
Assert.That(outputList, Is.Empty, $"Contains {outputList.Count} internal methods that have not been excluded in {nameof(PackageScriptsTests)}.cs:\n" + string.Join("\n", outputList) + "\n");
yield return null;
}
[UnityTest]
public IEnumerator LimitPrivateProperties()
{
var outputList = new HashSetList<string>();
PopulatePrivateGetSetProperties(Assembly.Load("Unity.XR.Interaction.Toolkit"), outputList);
PopulatePrivateGetSetProperties(Assembly.Load("Unity.XR.Interaction.Toolkit.Editor"), outputList);
FilterExceptions(outputList, s_PrivatePropertyExceptionList);
Assert.That(outputList, Is.Empty, $"Contains {outputList.Count} private properties that have not been excluded in {nameof(PackageScriptsTests)}.cs:\n" + string.Join("\n", outputList) + "\n");
yield return null;
}
[UnityTest]
public IEnumerator EditorSerializedPropertyAndContentFieldsShouldBeProtected()
{
var outputList = new HashSetList<string>();
PopulateWrongSerializedPropertyAndContentFields(Assembly.Load("Unity.XR.Interaction.Toolkit.Editor"), outputList);
FilterExceptions(outputList, s_EditorExceptionList);
Assert.That(outputList, Is.Empty, $"Contains {outputList.Count} non-protected SerializedProperty/GUIContent fields that have not been excluded in {nameof(PackageScriptsTests)}.cs:\n" + string.Join("\n", outputList) + "\n");
yield return null;
}
static void PopulateWrongNamespaceTypes(Assembly assembly, string rootNamespace, HashSetList<string> outputList)
{
if (assembly == null)
Assert.Fail($"Could not load assembly.");
foreach (var type in assembly.GetTypes())
{
if (type.FullName != null && type.Namespace != null)
{
if (!type.Namespace.StartsWith(rootNamespace))
outputList.Add(type.FullName);
}
}
}
static void PopulateInternalProperties(Assembly assembly, HashSetList<string> outputList)
{
if (assembly == null)
Assert.Fail($"Could not load assembly.");
var publicClasses = assembly.GetExportedTypes()
.Where(type => type.IsClass && type.IsPublic && !type.IsInterface)
.OrderBy(type => type.FullName)
.ToArray();
foreach (var type in publicClasses)
{
// Get all properties that have internal get and/or set methods.
// The main purpose of this test is for finding properties that should be public but are internal.
// The goal is to minimize the number of internal properties, so each should be evaluated and added
// to the exception list if it is determined that it should remain internal.
var properties = type.GetProperties(BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly)
.Where(property => property.PropertyType.IsPublic || property.PropertyType.IsNestedPublic)
.Where(property =>
property.GetMethod is { IsAssembly: true } ||
property.SetMethod is { IsAssembly: true })
.OrderBy(property => property.Name)
.ToArray();
foreach (var property in properties)
{
outputList.Add(GetStringOutput(type, property));
}
}
}
static void PopulateInternalMethods(Assembly assembly, HashSetList<string> outputList)
{
if (assembly == null)
Assert.Fail($"Could not load assembly.");
var publicClasses = assembly.GetExportedTypes()
.Where(type => type.IsClass && type.IsPublic && !type.IsInterface)
.OrderBy(type => type.FullName)
.ToArray();
foreach (var type in publicClasses)
{
// Get all methods that have internal protection.
// The main purpose of this test is for finding methods that should be public/protected/private protected but are internal.
// The goal is to minimize the number of internal methods, so each should be evaluated and added
// to the exception list if it is determined that it should remain internal.
var methods = type.GetMethods(BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly)
.Where(method => method.IsAssembly)
.Where(method =>
!method.Name.StartsWith("add_") &&
!method.Name.StartsWith("remove_") &&
!method.Name.StartsWith("get_") &&
!method.Name.StartsWith("set_") &&
!method.Name.StartsWith("Internal") &&
!method.Name.EndsWith("Internal"))
.OrderBy(method => method.Name)
.ToArray();
foreach (var method in methods)
{
outputList.Add(GetStringOutput(type, method));
}
}
}
static void PopulatePrivateGetSetProperties(Assembly assembly, HashSetList<string> outputList)
{
if (assembly == null)
Assert.Fail($"Could not load assembly.");
var publicClasses = assembly.GetExportedTypes()
.Where(type => type.IsClass && type.IsPublic && !type.IsInterface)
.OrderBy(type => type.FullName)
.ToArray();
foreach (var type in publicClasses)
{
// Get all properties that have both private get and set methods.
// The main purpose of this test is for finding properties that should be public but are private,
// especially those that are wrapping a SerializeField field that were introduced in patch versions.
// Exclude explicit interface implementation methods by checking !IsFinal.
var properties = type.GetProperties(BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly)
.Where(property =>
property.GetMethod is { IsPrivate: true, IsFinal: false } &&
property.SetMethod is { IsPrivate: true, IsFinal: false })
.OrderBy(property => property.Name)
.ToArray();
foreach (var property in properties)
{
outputList.Add(GetStringOutput(type, property));
}
}
}
static void PopulateWrongSerializedPropertyAndContentFields(Assembly assembly, HashSetList<string> outputList)
{
if (assembly == null)
Assert.Fail($"Could not load assembly.");
var editorClasses = assembly.GetExportedTypes()
.Where(type => type.IsSubclassOf(typeof(UnityEditor.Editor)))
.OrderBy(type => type.FullName)
.ToArray();
foreach (var type in editorClasses)
{
// Skip if the type is Obsolete.
if (type.GetCustomAttribute<ObsoleteAttribute>() != null)
continue;
// Get all SerializedProperty fields that are private or internal.
// The main purpose of this test is for finding fields that should be protected instead,
// especially those that were introduced in patch versions.
var fields = type.GetFields(BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly)
.Where(field =>
(field.FieldType == typeof(SerializedProperty)) &&
field is { IsPublic: false, IsFamily: false })
.OrderBy(property => property.Name)
.ToArray();
foreach (var field in fields)
{
outputList.Add(GetStringOutput(type, field));
}
// Get all GUIContent fields in nested Contents classes that are private or internal.
var nestedTypes = type.GetNestedTypes(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.DeclaredOnly)
.ToArray();
foreach (var nestedType in nestedTypes)
{
// Get the nested type's fields that are of type GUIContent.
var nestedFields = nestedType.GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.DeclaredOnly)
.Where(field =>
field.FieldType == typeof(GUIContent) &&
field is { IsPublic: false, IsFamily: false })
.OrderBy(property => property.Name)
.ToArray();
foreach (var nestedField in nestedFields)
{
outputList.Add(GetStringOutput(nestedType, nestedField));
}
}
}
}
static string GetAccessibility(MethodBase info)
{
if (info.IsPublic)
return "public";
if (info.IsFamily)
return "protected";
if (info.IsPrivate)
return "private";
if (info.IsAssembly)
return "internal";
if (info.IsFamilyOrAssembly)
return "protected internal";
if (info.IsFamilyAndAssembly)
return "private protected";
return "unknown";
}
static string GetAccessibility(FieldInfo info)
{
if (info.IsPublic)
return "public";
if (info.IsFamily)
return "protected";
if (info.IsPrivate)
return "private";
if (info.IsAssembly)
return "internal";
if (info.IsFamilyOrAssembly)
return "protected internal";
if (info.IsFamilyAndAssembly)
return "private protected";
return "unknown";
}
static string GetStringOutput(Type type, PropertyInfo property)
{
if (property.GetMethod != null && property.SetMethod != null)
return $"Property {type.FullName}.{property.Name} has {{ {GetAccessibility(property.GetMethod)} get; {GetAccessibility(property.SetMethod)} set; }}.";
if (property.GetMethod != null)
return $"Property {type.FullName}.{property.Name} has {{ {GetAccessibility(property.GetMethod)} get; }}.";
if (property.SetMethod != null)
return $"Property {type.FullName}.{property.Name} has {{ {GetAccessibility(property.SetMethod)} set; }}.";
return null;
}
static string GetStringOutput(Type type, FieldInfo field)
{
return $"Field {type.FullName}.{field.Name} has {GetAccessibility(field)} instead of protected.";
}
static string GetStringOutput(Type type, MethodInfo method)
{
return $"Method {type.FullName}.{method.Name} has {GetAccessibility(method)}.";
}
void FilterExceptions(HashSetList<string> outputList, (string, string)[] exceptionList)
{
// Many types are not defined without these #if guards defined, so avoid
// failing the test due to "Stale exception" warnings due to the missing method on this platform.
#if ENABLE_VR || UNITY_GAMECORE
var hasStaleException = false;
foreach (var exception in exceptionList)
{
if (exception.Item1 == "All" || exception.Item1 == m_MajorMinorVersion)
{
// Ignore AR types for now since those are optionally included with AR Foundation,
// so this avoids failing the test when that package is not installed.
if (!outputList.Contains(exception.Item2) && !exception.Item2.Contains(".AR"))
{
Debug.LogWarning($"Stale exception not found in source list: {exception.Item2}");
hasStaleException = true;
}
}
}
Assert.That(hasStaleException, Is.False, "Stale exception not found in source list. See warnings.");
#endif
outputList.ExceptWith(exceptionList.Where(e => e.Item1 == "All").Select(e => e.Item2));
outputList.ExceptWith(exceptionList.Where(e => e.Item1 == m_MajorMinorVersion).Select(e => e.Item2));
}
}
}