File: RunSettings.cs
Web Access
Project: src\src\vstest\src\Microsoft.TestPlatform.Common\Microsoft.TestPlatform.Common.csproj (Microsoft.VisualStudio.TestPlatform.Common)
// 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.Generic;
using System.Globalization;
using System.IO;
using System.Xml;

using Microsoft.VisualStudio.TestPlatform.Common.ExtensionFramework.Utilities;
using Microsoft.VisualStudio.TestPlatform.Common.Interfaces;
using Microsoft.VisualStudio.TestPlatform.Common.Logging;
using Microsoft.VisualStudio.TestPlatform.Common.SettingsProvider;
using Microsoft.VisualStudio.TestPlatform.ObjectModel;
using Microsoft.VisualStudio.TestPlatform.ObjectModel.Adapter;
using Microsoft.VisualStudio.TestPlatform.ObjectModel.Logging;
using Microsoft.VisualStudio.TestPlatform.ObjectModel.Utilities;

using CommonResources = Microsoft.VisualStudio.TestPlatform.Common.Resources.Resources;
using ObjectModelCommonResources = Microsoft.VisualStudio.TestPlatform.ObjectModel.Resources.CommonResources;

namespace Microsoft.VisualStudio.TestPlatform.Common;

/// <summary>
/// Used for loading settings for a run.
/// </summary>
public class RunSettings : IRunSettings
{
    /// <summary>
    /// Map of the settings names in the file to their associated settings provider.
    /// </summary>
    private readonly Dictionary<string, LazyExtension<ISettingsProvider, ISettingsProviderCapabilities>> _settings;

    /// <summary>
    /// Used to keep track if settings have been loaded.
    /// </summary>
    private bool _isSettingsLoaded;

    /// <summary>
    /// Initializes a new instance of the <see cref="RunSettings"/> class.
    /// </summary>
    public RunSettings()
    {
        _settings = new Dictionary<string, LazyExtension<ISettingsProvider, ISettingsProviderCapabilities>>();
    }

    /// <summary>
    /// Gets the settings in the form of Xml string.
    /// </summary>
    public string? SettingsXml { get; private set; }

    /// <summary>
    /// Get the settings for the provided settings name.
    /// </summary>
    /// <param name="settingsName">Name of the settings section to get.</param>
    /// <returns>The settings provider for the settings or null if one was not found.</returns>
    public ISettingsProvider? GetSettings(string? settingsName)
    {
        if (settingsName.IsNullOrWhiteSpace())
        {
            throw new ArgumentException(ObjectModelCommonResources.CannotBeNullOrEmpty, nameof(settingsName));
        }

        // Try and lookup the settings provider.
        _settings.TryGetValue(settingsName, out var provider);

        // If a provider was found, return it.
        return provider?.Value;
    }

    /// <summary>
    /// Load the settings from the provided xml string.
    /// </summary>
    /// <param name="settings">xml string</param>
    public void LoadSettingsXml(string settings)
    {
        if (StringUtilities.IsNullOrWhiteSpace(settings))
        {
            throw new ArgumentException(ObjectModelCommonResources.CannotBeNullOrEmpty, settings);
        }

        using var stringReader = new StringReader(settings);
        using var reader = XmlReader.Create(stringReader, XmlRunSettingsUtilities.ReaderSettings);
        ValidateAndSaveSettings(reader);
    }

    /// <summary>
    /// Initialize settings providers with the settings xml.
    /// </summary>
    /// <param name="settings"> The settings xml string. </param>
    public void InitializeSettingsProviders(string settings)
    {
        using var stringReader = new StringReader(settings);
        using var reader = XmlReader.Create(stringReader, XmlRunSettingsUtilities.ReaderSettings);
        ReadRunSettings(reader);
    }

    /// <summary>
    /// Validate the runsettings checking that it is well formed.
    /// This would throw XML exception on failure.
    /// </summary>
    /// <param name="reader">A xml reader instance.</param>
    private void ValidateAndSaveSettings(XmlReader reader)
    {
        try
        {
            var dom = new XmlDocument();
            dom.Load(reader);
            using var writer = new StringWriter(CultureInfo.InvariantCulture);
            dom.Save(writer);
            SettingsXml = writer.ToString();
        }
        catch (Exception e)
        {
            throw new SettingsException(
                string.Format(
                    CultureInfo.CurrentCulture,
                    CommonResources.RunSettingsParseError,
                    e.Message),
                e);
        }
    }

    /// <summary>
    /// Reads test run settings from XmlReader
    /// </summary>
    /// <param name="reader"></param>
    private void ReadRunSettings(XmlReader reader)
    {
        // If settings have already been loaded, throw.
        if (_isSettingsLoaded)
        {
            throw new InvalidOperationException(CommonResources.RunSettingsAlreadyLoaded);
        }

        _isSettingsLoaded = true;

        try
        {
            // Read to the root element.
            XmlReaderUtilities.ReadToRootNode(reader);

            // Read to the first section.
            reader.ReadToNextElement();

            // Lookup the settings provider for each of the elements.
            var settingsExtensionManager = SettingsProviderExtensionManager.Create();
            while (!reader.EOF)
            {
                LoadSection(reader, settingsExtensionManager);
                reader.SkipToNextElement();
            }
        }
        catch (SettingsException)
        {
            throw;
        }
        catch (Exception e)
        {
            throw new SettingsException(
                string.Format(
                    CultureInfo.CurrentCulture,
                    CommonResources.RunSettingsParseError,
                    e.Message),
                e);
        }
    }

    /// <summary>
    /// Loads the section for the current element of the reader.
    /// </summary>
    /// <param name="reader">Reader to load the section from.</param>
    /// <param name="settingsExtensionManager">Settings extension manager to get the provider from.</param>
    private void LoadSection(XmlReader reader, SettingsProviderExtensionManager settingsExtensionManager)
    {
        ValidateArg.NotNull(reader, nameof(reader));
        ValidateArg.NotNull(settingsExtensionManager, nameof(settingsExtensionManager));

        // Check for duplicate settings
        if (_settings.ContainsKey(reader.Name))
        {
            TestSessionMessageLogger.Instance.SendMessage(
                TestMessageLevel.Error,
                string.Format(CultureInfo.CurrentCulture, CommonResources.DuplicateSettingsProvided, reader.Name));

            return;
        }

        // Look up the section for this node.
        var provider = settingsExtensionManager.GetSettingsProvider(reader.Name);

        if (provider != null)
        {
            try
            {
                // Have the provider load the settings.
                provider.Value.Load(reader.ReadSubtree());
            }
            catch (Exception e)
            {
                // Setup to throw the exception when the section is requested.
                provider = CreateLazyThrower(
                    string.Format(
                        CultureInfo.CurrentCulture,
                        CommonResources.SettingsProviderInitializationError,
                        provider.Metadata.SettingsName,
                        e.Message),
                    provider.Metadata,
                    e);
            }
        }
        else
        {
            // Setup to throw when this section is requested.
            var metadata = new TestSettingsProviderMetadata(reader.Name);

            var message = string.Format(
                CultureInfo.CurrentCulture,
                CommonResources.SettingsProviderNotFound,
                metadata.SettingsName);

            provider = CreateLazyThrower(message, metadata);
        }

        // Cache the provider instance so it can be looked up later when the section is requested.
        _settings.Add(provider.Metadata.SettingsName, provider);
    }

    /// <summary>
    /// Creates a lazy instance which will throw a SettingsException when the value property is accessed.
    /// </summary>
    /// <param name="message">Message for the exception.</param>
    /// <param name="metadata">Metadata to use for the lazy instance.</param>
    /// <param name="innerException">Inner exception to include in the exception which is thrown.</param>
    /// <returns>Lazy instance setup to throw when the value property is accessed.</returns>
    private static LazyExtension<ISettingsProvider, ISettingsProviderCapabilities> CreateLazyThrower(
        string message,
        ISettingsProviderCapabilities metadata,
        Exception? innerException = null)
    {
        return new LazyExtension<ISettingsProvider, ISettingsProviderCapabilities>(
            () => throw new SettingsException(message, innerException),
            metadata);
    }

}