File: TestProperty\TestProperty.cs
Web Access
Project: src\src\vstest\src\Microsoft.TestPlatform.ObjectModel\Microsoft.TestPlatform.ObjectModel.csproj (Microsoft.VisualStudio.TestPlatform.ObjectModel)
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Runtime.Serialization;

using Microsoft.VisualStudio.TestPlatform.Utilities;

namespace Microsoft.VisualStudio.TestPlatform.ObjectModel;

public delegate bool ValidateValueCallback(object? value);

[DataContract]
public class TestProperty : IEquatable<TestProperty>
{
    private static readonly ConcurrentDictionary<string, Type> TypeCache = new();

    private static bool DisableFastJson { get; set; } = FeatureFlag.Instance.IsSet(FeatureFlag.VSTEST_DISABLE_FASTER_JSON_SERIALIZATION);

    private Type _valueType;

    //public static Stopwatch

    /// <summary>
    /// Initializes a new instance of the <see cref="TestProperty"/> class.
    /// </summary>
#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable.
    private TestProperty()
#pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable.
    {
        // Default constructor for Serialization.
    }

    private TestProperty(string id, string label, string category, string description, Type valueType, ValidateValueCallback? validateValueCallback, TestPropertyAttributes attributes)
    {
        ValidateArg.NotNullOrEmpty(id, nameof(id));
        ValidateArg.NotNull(label, nameof(label));
        ValidateArg.NotNull(category, nameof(category));
        ValidateArg.NotNull(description, nameof(description));
        ValidateArg.NotNull(valueType, nameof(valueType));

        // If the type of property is unexpected, then fail as otherwise we will not be to serialize it over the wcf channel and serialize it in db.
        if (valueType == typeof(KeyValuePair<string, string>[]))
        {
            ValueType = "System.Collections.Generic.KeyValuePair`2[[System.String],[System.String]][]";
        }
        else if (valueType == typeof(string)
                 || valueType == typeof(Uri)
                 || valueType == typeof(string[])
                 || valueType.AssemblyQualifiedName!.Contains("System.Private")
                 || valueType.AssemblyQualifiedName.Contains("mscorlib"))
        {
            // This comparison is a check to ensure assembly information is not embedded in data.
            // Use type.FullName instead of type.AssemblyQualifiedName since the internal assemblies
            // are different in desktop and coreclr. Thus AQN in coreclr includes System.Private.CoreLib which
            // is not available on the desktop.
            // Note that this doesn't handle generic types. Such types will fail during serialization.
            ValueType = valueType.FullName!;
        }
        else if (valueType.IsValueType)
        {
            // In case of custom types, let the assembly qualified name be available to help
            // deserialization on the client.
            ValueType = valueType.AssemblyQualifiedName;
        }
        else
        {
            throw new ArgumentException(string.Format(CultureInfo.CurrentCulture, Resources.Resources.UnexpectedTypeOfProperty, valueType, id));
        }

        Id = id;
        Label = label;
        Category = category;
        Description = description;
        ValidateValueCallback = validateValueCallback;
        Attributes = attributes;
        _valueType = valueType;
    }

    /// <summary>
    /// Gets or sets the Id for the property.
    /// </summary>
    [DataMember]
    public string Id { get; set; }

    /// <summary>
    /// Gets or sets a label for the property.
    /// </summary>
    [DataMember]
    public string Label { get; set; }

    /// <summary>
    /// Gets or sets a category for the property.
    /// </summary>
    [DataMember]
    public string Category { get; set; }

    /// <summary>
    /// Gets or sets a description for the property.
    /// </summary>
    [DataMember]
    public string Description { get; set; }

    /// <summary>
    /// Gets the callback for validation of property value.
    /// </summary>
    /// <remarks>This property is not required at the client side.</remarks>
    [IgnoreDataMember]
    public ValidateValueCallback? ValidateValueCallback { get; }

    /// <summary>
    /// Gets or sets the attributes for this property.
    /// </summary>
    [DataMember]
    public TestPropertyAttributes Attributes { get; set; }

    /// <summary>
    /// Gets or sets a string representation of the type for value.
    /// </summary>
    [DataMember]
    public string ValueType { get; set; }

    #region IEquatable

    /// <inheritdoc/>
    public override int GetHashCode()
    {
        return Id.GetHashCode();
    }

    /// <inheritdoc/>
    public override bool Equals(object? obj)
    {
        return Equals(obj as TestProperty);
    }

    /// <inheritdoc/>
    public bool Equals(TestProperty? other)
    {
        return (other != null) && (Id == other.Id);
    }

    #endregion IEquatable

    /// <inheritdoc/>
    public override string ToString()
    {
        return Id;
    }

    /// <summary>
    /// Gets the valueType.
    /// </summary>
    /// <remarks>Only works for the valueType that is in the currently executing assembly or in Mscorlib.dll. The default valueType is of string valueType.</remarks>
    /// <returns>The valueType of the test property</returns>
    public Type GetValueType()
    {
        _valueType ??= GetType(ValueType);

        return _valueType;
    }

    private Type GetType(string typeName)
    {
        ValidateArg.NotNull(typeName, nameof(typeName));

        if (!DisableFastJson && TypeCache.TryGetValue(typeName, out var t))
        {
            return t;
        }

        Type? type = null;

        try
        {
            // This only works for the type is in the currently executing assembly or in Mscorlib.dll.
            type = Type.GetType(typeName);

            if (!DisableFastJson)
            {
                if (type != null)
                {
                    TypeCache.TryAdd(typeName, type);
                    return type;
                }
            }

            // Try 2.0 version as discovery returns version of 4.0 for all cases
            type ??= Type.GetType(typeName.Replace("Version=4.0.0.0", "Version=2.0.0.0"));

            // For UAP the type namespace for System.Uri,System.TimeSpan and System.DateTimeOffset differs from the desktop version.
            if (type == null && typeName.StartsWith("System.Uri"))
            {
                type = typeof(Uri);
            }
            else if (type == null && typeName.StartsWith("System.TimeSpan"))
            {
                type = typeof(TimeSpan);
            }
            else if (type == null && typeName.StartsWith("System.DateTimeOffset"))
            {
                type = typeof(DateTimeOffset);
            }
            else if (type == null && typeName.StartsWith("System.Int16"))
            {
                // For LineNumber property - Int is required
                type = typeof(Int16);
            }
            else if (type == null && typeName.StartsWith("System.Int32"))
            {
                type = typeof(Int32);
            }
            else if (type == null && typeName.StartsWith("System.Int64"))
            {
                type = typeof(Int64);
            }
        }
        catch (Exception)
        {
#if FullCLR
            // Try to see if the typeName contains Windows Phone PKT in that case load it from
            // desktop side
            if (typeName.Contains(s_windowsPhonePKT))
            {
                type = GetType(typeName.Replace(s_windowsPhonePKT, s_visualStudioPKT));
            }

            if (type == null)
            {
                System.Diagnostics.Debug.Fail("The test property type " + typeName + " of property " + Id + "is not supported.");
#else
            System.Diagnostics.Debug.WriteLine("The test property type " + typeName + " of property " + Id + "is not supported.");
#endif
#if FullCLR
            }
#endif
        }
        finally
        {
            // default is of string type.
            type ??= typeof(string);
        }

        if (!DisableFastJson)
        {
            TypeCache.TryAdd(typeName, type);
        }
        return type;
    }

    private static readonly Dictionary<string, KeyValuePair<TestProperty, HashSet<Type>>> Properties = new();

#if FullCLR
    private static string s_visualStudioPKT = "b03f5f7f11d50a3a";
    private static string s_windowsPhonePKT = "7cec85d7bea7798e";
#endif

    public static void ClearRegisteredProperties()
    {
        lock (Properties)
        {
            Properties.Clear();
        }
    }

    public static TestProperty? Find(string id)
    {
        ValidateArg.NotNull(id, nameof(id));

        TestProperty? result = null;

        lock (Properties)
        {
            if (Properties.TryGetValue(id, out var propertyTypePair))
            {
                result = propertyTypePair.Key;
            }
        }

        return result;
    }

    public static TestProperty Register(string id, string label, Type valueType, Type owner)
    {
        ValidateArg.NotNullOrEmpty(id, nameof(id));
        ValidateArg.NotNull(label, nameof(label));
        ValidateArg.NotNull(valueType, nameof(valueType));
        ValidateArg.NotNull(owner, nameof(owner));

        return Register(id, label, string.Empty, string.Empty, valueType, null, TestPropertyAttributes.None, owner);
    }

    public static TestProperty Register(string id, string label, Type valueType, TestPropertyAttributes attributes, Type owner)
    {
        ValidateArg.NotNullOrEmpty(id, nameof(id));
        ValidateArg.NotNull(label, nameof(label));
        ValidateArg.NotNull(valueType, nameof(valueType));
        ValidateArg.NotNull(owner, nameof(owner));

        return Register(id, label, string.Empty, string.Empty, valueType, null, attributes, owner);
    }

    public static TestProperty Register(string id, string label, string category, string description, Type valueType, ValidateValueCallback? validateValueCallback, TestPropertyAttributes attributes, Type owner)
    {
        ValidateArg.NotNullOrEmpty(id, nameof(id));
        ValidateArg.NotNull(label, nameof(label));
        ValidateArg.NotNull(category, nameof(category));
        ValidateArg.NotNull(description, nameof(description));
        ValidateArg.NotNull(valueType, nameof(valueType));
        ValidateArg.NotNull(owner, nameof(owner));

        TestProperty result;

        lock (Properties)
        {
            if (Properties.TryGetValue(id, out var propertyTypePair))
            {
                // verify the data valueType is valid
                if (propertyTypePair.Key.ValueType == valueType.AssemblyQualifiedName
                    || propertyTypePair.Key.ValueType == valueType.FullName
                    || propertyTypePair.Key._valueType == valueType)
                {
                    // add the owner to set of owners for this GraphProperty object
                    propertyTypePair.Value.Add(owner);
                    result = propertyTypePair.Key;
                }
                else
                {
                    // not the data valueType we expect, throw an exception
                    string message = string.Format(
                        CultureInfo.CurrentCulture,
                        Resources.Resources.Exception_RegisteredTestPropertyHasDifferentValueType,
                        id,
                        valueType.ToString(),
                        propertyTypePair.Key.ValueType);

                    throw new InvalidOperationException(message);
                }
            }
            else
            {
                // create a new TestProperty object
                result = new TestProperty(id, label, category, description, valueType, validateValueCallback, attributes);

                // setup the data pair used to track owners of this GraphProperty
                propertyTypePair = new KeyValuePair<TestProperty, HashSet<Type>>(result, new HashSet<Type>());
                propertyTypePair.Value.Add(owner);

                // add to the dictionary
                Properties[id] = propertyTypePair;
            }
        }

        return result;
    }

    public static bool TryUnregister(string id, out KeyValuePair<TestProperty, HashSet<Type>> propertyTypePair)
    {
        ValidateArg.NotNullOrEmpty(id, nameof(id));

        lock (Properties)
        {
            if (Properties.TryGetValue(id, out propertyTypePair))
            {
                return Properties.Remove(id);
            }
        }
        return false;
    }

    [SuppressMessage("Style", "IDE0060:Remove unused parameter", Justification = "Part of the public API")]
    public object GetRealObject(StreamingContext context)
    {
        var registeredProperty = Find(Id);
        registeredProperty ??= Register(
                Id,
                Label,
                Category,
                Description,
                GetValueType(),
                ValidateValueCallback,
                Attributes,
                typeof(TestObject));

        return registeredProperty;
    }
}