diff --git a/Directory.Build.props b/Directory.Build.props index 20d6f53..d37b252 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -8,6 +8,11 @@ false source;dotnet;nuget;msbuild MIT + + git + https://github.com/dotnet-campus/CUnit + Copyright (c) 2019-2022 dotnet职业技术学院 + https://github.com/dotnet-campus/CUnit diff --git a/LICENSE b/LICENSE index f9aa867..3a03a24 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2021 dotnet职业技术学院 +Copyright (c) 2022 dotnet职业技术学院 Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/MSTest.Extensions.sln b/MSTest.Extensions.sln index 6946feb..8f21f1c 100644 --- a/MSTest.Extensions.sln +++ b/MSTest.Extensions.sln @@ -23,6 +23,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "dotnetCampus.UITest.WPF", " EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "dotnetCampus.UITest.WPF.Demo", "demo\dotnetCampus.UITest.WPF.Demo\dotnetCampus.UITest.WPF.Demo.csproj", "{F1D52FE3-2E23-4C7D-AA64-CAC204B4EBBF}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "dotnetCampus.UITest.WPFTestHelper", "src\dotnetCampus.UITest.WPFTestHelper\dotnetCampus.UITest.WPFTestHelper.csproj", "{A5195542-3263-436B-8850-A9F4C1E36B2B}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -45,6 +47,10 @@ Global {F1D52FE3-2E23-4C7D-AA64-CAC204B4EBBF}.Debug|Any CPU.Build.0 = Debug|Any CPU {F1D52FE3-2E23-4C7D-AA64-CAC204B4EBBF}.Release|Any CPU.ActiveCfg = Release|Any CPU {F1D52FE3-2E23-4C7D-AA64-CAC204B4EBBF}.Release|Any CPU.Build.0 = Release|Any CPU + {A5195542-3263-436B-8850-A9F4C1E36B2B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A5195542-3263-436B-8850-A9F4C1E36B2B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A5195542-3263-436B-8850-A9F4C1E36B2B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A5195542-3263-436B-8850-A9F4C1E36B2B}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -54,6 +60,7 @@ Global {7AB54453-67E4-46BC-A314-EBA72C082E7E} = {DA39C42F-B4EA-43F0-AB8C-40987B63F4D6} {E62C4947-CD69-411D-8749-78C8B2C25ACE} = {1C4D72B5-A7C1-4AAE-A66F-4DDAC5979C28} {F1D52FE3-2E23-4C7D-AA64-CAC204B4EBBF} = {DA39C42F-B4EA-43F0-AB8C-40987B63F4D6} + {A5195542-3263-436B-8850-A9F4C1E36B2B} = {1C4D72B5-A7C1-4AAE-A66F-4DDAC5979C28} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {21BFBA8A-2901-4A78-A722-0DDA95D3773F} diff --git a/README.md b/README.md index 8ed5838..456118a 100644 --- a/README.md +++ b/README.md @@ -74,3 +74,7 @@ There are many ways to contribute to MSTestEnhancer ## License MSTestEnhancer is licensed under the [MIT license](/LICENSE) + +## Thanks + +https://github.com/dotnet/wpf-test/ \ No newline at end of file diff --git a/src/MSTest.Extensions/MSTest.Extensions.csproj b/src/MSTest.Extensions/MSTest.Extensions.csproj index e952657..7957cff 100644 --- a/src/MSTest.Extensions/MSTest.Extensions.csproj +++ b/src/MSTest.Extensions/MSTest.Extensions.csproj @@ -6,11 +6,7 @@ bin\$(Configuration)\$(TargetFramework)\$(AssemblyName).xml MSTestEnhancer dotnet-campus - https://github.com/dotnet-campus/CUnit.git - git true - https://github.com/dotnet-campus/CUnit - Copyright (c) 2018-2021 dotnet职业技术学院 false MSTestEnhancer helps you to write unit tests without naming any method. You can write method contract descriptions instead of writing confusing test method name when writing unit tests. Add some assersion extensions. diff --git a/src/dotnetCampus.UITest.WPF/dotnetCampus.UITest.WPF.csproj b/src/dotnetCampus.UITest.WPF/dotnetCampus.UITest.WPF.csproj index e794be3..5b8793d 100644 --- a/src/dotnetCampus.UITest.WPF/dotnetCampus.UITest.WPF.csproj +++ b/src/dotnetCampus.UITest.WPF/dotnetCampus.UITest.WPF.csproj @@ -6,10 +6,7 @@ true True The UITest framework for WPF - https://github.com/dotnet-campus/CUnit - Copyright (c) 2021 dotnet职业技术学院 false - https://github.com/dotnet-campus/CUnit dotnet;nuget;msbuild;UITest;WPF;MSTest;TestFramework diff --git a/src/dotnetCampus.UITest.WPFTestHelper/ApplicationControl/ApplicationSettings.cs b/src/dotnetCampus.UITest.WPFTestHelper/ApplicationControl/ApplicationSettings.cs new file mode 100644 index 0000000..7f6ed37 --- /dev/null +++ b/src/dotnetCampus.UITest.WPFTestHelper/ApplicationControl/ApplicationSettings.cs @@ -0,0 +1,24 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; + +namespace dotnetCampus.UITest.WPFTestHelper.ApplicationControl +{ + /// + /// Provides configuration information for an . + /// + [Serializable] + public class ApplicationSettings + { + /// + /// The interface used for creation of the AutomatedApplicationImplementation. + /// + public IAutomatedApplicationImplFactory ApplicationImplementationFactory + { + get; + set; + } + } +} diff --git a/src/dotnetCampus.UITest.WPFTestHelper/ApplicationControl/AutomatedApplication.cs b/src/dotnetCampus.UITest.WPFTestHelper/ApplicationControl/AutomatedApplication.cs new file mode 100644 index 0000000..ff5de78 --- /dev/null +++ b/src/dotnetCampus.UITest.WPFTestHelper/ApplicationControl/AutomatedApplication.cs @@ -0,0 +1,387 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Threading; + +namespace dotnetCampus.UITest.WPFTestHelper.ApplicationControl +{ + /// + /// Loads and starts a test application either in the current process or in a new, + /// separate process. + /// + /// + /// + /// The following example shows in-process usage. The code runs the target + /// application in a separate thread within the current process. + /// + /// public void MyTest() + /// { + /// var path = Path.Combine(executionDir, "WpfTestApplication.exe"); + /// var a = new InProcessApplication(new WpfInProcessApplicationSettings + /// { + /// Path = path, + /// InProcessApplicationType = InProcessApplicationType.InProcessSeparateThread, + /// ApplicationImplementationFactory = new WpfInProcessApplicationFactory() + /// }); + /// + /// a.Start(); + /// a.WaitForMainWindow(TimeSpan.FromMilliseconds(5000)); + /// + /// // Perform various tests... + /// + /// a.Close(); + /// } + /// + /// + /// + /// + /// The following example demonstrates out-of-process usage: + /// + /// public void MyTest() + /// { + /// var path = Path.Combine(executionDir, "WpfTestApplication.exe"); + /// var a = new OutOfProcessApplication(new OutOfProcessApplicationSettings + /// { + /// ProcessStartInfo = new ProcessStartInfo(path), + /// ApplicationImplementationFactory = new UIAutomationOutOfProcessApplicationFactory() + /// }); + /// + /// a.Start(); + /// a.WaitForMainWindow(TimeSpan.FromMilliseconds(5000)); + /// + /// // Perform various tests... + /// + /// a.Close(); + /// } + /// + /// + public abstract class AutomatedApplication : MarshalByRefObject + { + #region Constructor + + /// + /// AutomatedApplication objects are instantiated internally. + /// + protected AutomatedApplication() + { + IsApplicationRunning = false; + _eventHandlers = new Dictionary>(); + } + + #endregion Constructor + + #region Events + + /// + /// Notifies listeners that the MainWindow of the test application has opened. + /// + public event EventHandler MainWindowOpened; + + /// + /// Notifies listeners that the test application has exited. + /// + public event EventHandler Exited; + + #endregion Events + + #region Public Properties + + /// + /// The main window of the test application. + /// + /// + /// This is an AutomationElement for an OutOfProcessApplication and a System.Windows.Window + /// for an InProcessApplication. + /// + public object MainWindow + { + get + { + if (ApplicationImplementation != null) + { + return ApplicationImplementation.MainWindow; + } + + return null; + } + } + + #endregion Public Properties + + #region Public Methods + + /// + /// Starts the test application after validating its settings. + /// + /// + /// Refined abstractions are expected to initialize AutomatedAppImp + /// and call AutomatedAppImp.Start(). + /// + public abstract void Start(); + + /// + /// Waits for the test application to display its main window. + /// + /// + /// Blocks execution of the current thread until the window is displayed + /// or until the specified timeout interval elapses. + /// + /// The timeout interval. + public void WaitForMainWindow(TimeSpan timeout) + { + if (!IsApplicationRunning) + { + return; + } + + TimeSpan zero = TimeSpan.FromMilliseconds(0); + TimeSpan delta = TimeSpan.FromMilliseconds(10); + + // must first wait for for AutomatedAppImp to be initialized + while (ApplicationImplementation == null && timeout > zero) + { + Thread.Sleep(10); + timeout -= delta; + } + + if (ApplicationImplementation != null) + { + // then do the wait implementation + ApplicationImplementation.WaitForMainWindow(timeout); + } + } + + /// + /// Waits for the test application to display a window with a specified name. + /// + /// + /// Blocks execution of the current thread until the window is displayed + /// or until the specified timeout interval elapses. + /// + /// The window to wait for. + /// The timeout interval. + public void WaitForWindow(string windowName, TimeSpan timeout) + { + if (!IsApplicationRunning) + { + return; + } + + ApplicationImplementation.WaitForWindow(windowName, timeout); + } + + /// + /// Closes the automated application gracefully. + /// + public virtual void Close() + { + if (ApplicationImplementation != null) + { + ApplicationImplementation.MainWindowOpened -= this.OnMainWindowOpened; + ApplicationImplementation.FocusChanged -= this.OnFocusChanged; + ApplicationImplementation.Close(); + ApplicationImplementation = null; + } + + IsApplicationRunning = false; + } + + /// + /// Waits for the test application to enter an idle state. + /// + /// + /// Blocks execution of the current thread until the window is displayed + /// or until the specified timeout interval elapses. + /// + /// The timeout interval. + public void WaitForInputIdle(TimeSpan timeout) + { + if (!IsApplicationRunning) + { + return; + } + + ApplicationImplementation.WaitForInputIdle(timeout); + } + + /// + /// Adds an event handler for the given AutomatedApplicationEventType. + /// + /// The type of event to listen for. + /// The delegate to be called when the event occurs. + public void AddEventHandler(AutomatedApplicationEventType eventType, Delegate handler) + { + if (_eventHandlers.ContainsKey(eventType)) + { + if (!_eventHandlers[eventType].Contains(handler)) + { + _eventHandlers[eventType].Add(handler); + } + } + else + { + _eventHandlers.Add(eventType, new List()); + _eventHandlers[eventType].Add(handler); + } + } + + /// + /// Removes an event handler for the given AutomatedApplicationEventType. + /// + /// The type of event to remove. + /// The delegate to remove. + public void RemoveEventHandler(AutomatedApplicationEventType eventType, Delegate handler) + { + if (_eventHandlers.ContainsKey(eventType)) + { + if (_eventHandlers[eventType].Contains(handler)) + { + _eventHandlers[eventType].Remove(handler); + } + } + } + + #endregion Public Methods + + #region Internal and Protected Properties + + /// + /// Gets or sets the automated application implementation. + /// + /// + /// This is the 'implementation' following the bridge pattern. + /// + protected IAutomatedApplicationImpl ApplicationImplementation + { + get; + set; + } + + /// + /// Indicates whether the main window of the test application is open. + /// + protected bool IsMainWindowOpened + { + get + { + if (ApplicationImplementation != null) + { + return ApplicationImplementation.IsMainWindowOpened; + } + + return false; + } + } + + /// + /// Indicates whether the test application is running. + /// + protected bool IsApplicationRunning + { + get; + set; + } + + #endregion Internal and Protected Properties + + #region Internal and Protected Methods + + /// + /// Wrapper to signal the MainWindowOpened event + /// + /// The source of the event which is IAutomatedApplicationImpl. + /// An System.EventArgs that contains no event data. + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Security", "CA2109:ReviewVisibleEventHandlers")] + protected void OnMainWindowOpened(object sender, EventArgs e) + { + if (MainWindowOpened != null) + { + MainWindowOpened(this, new AutomatedApplicationEventArgs(this)); + } + + if (_eventHandlers.ContainsKey(AutomatedApplicationEventType.MainWindowOpenedEvent)) + { + foreach (Delegate handler in _eventHandlers[AutomatedApplicationEventType.MainWindowOpenedEvent]) + { + var openHandler = handler as EventHandler; + if (openHandler != null) + { + openHandler(this, new AutomatedApplicationEventArgs(this)); + } + } + } + } + + /// + /// Wrapper to signal the FocusChanged event + /// + /// The source of the event which is IAutomatedApplicationImpl. + /// An System.EventArgs that contains no event data. + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Security", "CA2109:ReviewVisibleEventHandlers")] + protected void OnFocusChanged(object sender, EventArgs e) + { + if (_eventHandlers.ContainsKey(AutomatedApplicationEventType.FocusChangedEvent)) + { + foreach (Delegate handler in _eventHandlers[AutomatedApplicationEventType.FocusChangedEvent]) + { + var focusHandler = handler as EventHandler; + if (focusHandler != null) + { + focusHandler(this, new AutomatedApplicationFocusChangedEventArgs(this, sender)); + } + } + } + } + + /// + /// Wrapper to signal the Exited event + /// + /// The source of the event which is IAutomatedApplicationImpl. + /// An System.EventArgs that contains no event data. + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Security", "CA2109:ReviewVisibleEventHandlers")] + protected void OnExit(object sender, EventArgs e) + { + if (Exited != null) + { + Exited(this, new AutomatedApplicationEventArgs(this)); + } + + if (_eventHandlers.ContainsKey(AutomatedApplicationEventType.ApplicationExitedEvent)) + { + foreach (Delegate handler in _eventHandlers[AutomatedApplicationEventType.ApplicationExitedEvent]) + { + var exitHandler = handler as EventHandler; + if (exitHandler != null) + { + exitHandler(this, new AutomatedApplicationEventArgs(this)); + } + } + } + + if (ApplicationImplementation != null) + { + ApplicationImplementation.Exited -= this.OnExit; + } + + // remove all event handlers + foreach (var kvp in _eventHandlers) + { + kvp.Value.Clear(); + } + } + + #endregion Internal and Protected Methods + + #region Private Fields + + /// + /// Holds a table of general event handlers for AutomatedApplication + /// + private Dictionary> _eventHandlers; + + #endregion Private Fields + } +} diff --git a/src/dotnetCampus.UITest.WPFTestHelper/ApplicationControl/AutomatedApplicationEventArgs.cs b/src/dotnetCampus.UITest.WPFTestHelper/ApplicationControl/AutomatedApplicationEventArgs.cs new file mode 100644 index 0000000..1a310fd --- /dev/null +++ b/src/dotnetCampus.UITest.WPFTestHelper/ApplicationControl/AutomatedApplicationEventArgs.cs @@ -0,0 +1,33 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; + +namespace dotnetCampus.UITest.WPFTestHelper.ApplicationControl +{ + /// + /// Represents the event args passed to AutomatedApplication events. + /// + public class AutomatedApplicationEventArgs : EventArgs + { + /// + /// Constructs an AutomatedApplicationEventArgs instance with the given + /// AutomatedApplication. + /// + /// The AutomatedApplication data to pass to the listeners. + public AutomatedApplicationEventArgs(AutomatedApplication automatedApp) + { + AutomatedApplication = automatedApp; + } + + /// + /// The AutomatedApplication data passed to listeners. + /// + public AutomatedApplication AutomatedApplication + { + get; + set; + } + } +} diff --git a/src/dotnetCampus.UITest.WPFTestHelper/ApplicationControl/AutomatedApplicationEventType.cs b/src/dotnetCampus.UITest.WPFTestHelper/ApplicationControl/AutomatedApplicationEventType.cs new file mode 100644 index 0000000..5e9973f --- /dev/null +++ b/src/dotnetCampus.UITest.WPFTestHelper/ApplicationControl/AutomatedApplicationEventType.cs @@ -0,0 +1,30 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; + +namespace dotnetCampus.UITest.WPFTestHelper.ApplicationControl +{ + /// + /// Specifies the supported AutomatedApplication events. + /// + [Serializable] + public enum AutomatedApplicationEventType + { + /// + /// The test application's main window opened event. + /// + MainWindowOpenedEvent, + + /// + /// The test application's closed event. + /// + ApplicationExitedEvent, + + /// + /// The test application's main window's focus changed event. + /// + FocusChangedEvent + } +} diff --git a/src/dotnetCampus.UITest.WPFTestHelper/ApplicationControl/AutomatedApplicationFocusChangedEventArgs.cs b/src/dotnetCampus.UITest.WPFTestHelper/ApplicationControl/AutomatedApplicationFocusChangedEventArgs.cs new file mode 100644 index 0000000..1e124fe --- /dev/null +++ b/src/dotnetCampus.UITest.WPFTestHelper/ApplicationControl/AutomatedApplicationFocusChangedEventArgs.cs @@ -0,0 +1,38 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace dotnetCampus.UITest.WPFTestHelper.ApplicationControl +{ + /// + /// Represents the event args passed to AutomatedApplication focus changed events. + /// + public class AutomatedApplicationFocusChangedEventArgs : AutomatedApplicationEventArgs + { + /// + /// Initializes a new instance of the AutomatedApplicationFocusChangedEventArgs + /// class. + /// + /// + /// The AutomatedApplication data to pass to the listeners. + /// + /// + /// The new focused element data to pass the listeners. This can be an AutomationElement + /// for an out-of-process scenario or a UIElement for an in-process WPF scenario. + /// + public AutomatedApplicationFocusChangedEventArgs(AutomatedApplication automatedApp, object newFocusedElement) + : base(automatedApp) + { + NewFocusedElement = newFocusedElement; + } + + /// + /// The new focused element passed to the listeners. + /// + public object NewFocusedElement + { + get; + set; + } + } +} diff --git a/src/dotnetCampus.UITest.WPFTestHelper/ApplicationControl/IAutomatedApplicationImpl.cs b/src/dotnetCampus.UITest.WPFTestHelper/ApplicationControl/IAutomatedApplicationImpl.cs new file mode 100644 index 0000000..be90be5 --- /dev/null +++ b/src/dotnetCampus.UITest.WPFTestHelper/ApplicationControl/IAutomatedApplicationImpl.cs @@ -0,0 +1,79 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; + +namespace dotnetCampus.UITest.WPFTestHelper.ApplicationControl +{ + /// + /// Defines the contract for an AutomatedApplication. + /// + /// + /// Represents the 'Implemention' inteface for the AutomatedApplication bridge. As such, + /// this can vary from the public interface of AutomatedApplication. + /// + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "Impl")] + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1711:IdentifiersShouldNotHaveIncorrectSuffix")] + public interface IAutomatedApplicationImpl + { + /// + /// Starts the test application. + /// + void Start(); + + /// + /// Closes the test application. + /// + void Close(); + + /// + /// Waits for the test application's main window to open. + /// + /// The timeout interval. + void WaitForMainWindow(TimeSpan timeout); + + /// + /// Waits for the given window to open. + /// + /// The window id of the window to wait for. + /// The timeout interval. + void WaitForWindow(string windowName, TimeSpan timeout); + + /// + /// Waits for the test application to become idle. + /// + /// The timeout interval. + void WaitForInputIdle(TimeSpan timeout); + + /// + /// Occurs when the test application's main window is opened. + /// + event EventHandler MainWindowOpened; + + /// + /// Occurs when the test application exits. + /// + event EventHandler Exited; + + /// + /// Occurs when focus changes. + /// + event EventHandler FocusChanged; + + /// + /// The test application's main window. + /// + object MainWindow { get; } + + /// + /// The driver of the test application. + /// + object ApplicationDriver { get; } + + /// + /// Indicates whether the test application's main window has opened. + /// + bool IsMainWindowOpened { get; } + } +} diff --git a/src/dotnetCampus.UITest.WPFTestHelper/ApplicationControl/IAutomatedApplicationImplFactory.cs b/src/dotnetCampus.UITest.WPFTestHelper/ApplicationControl/IAutomatedApplicationImplFactory.cs new file mode 100644 index 0000000..48425b6 --- /dev/null +++ b/src/dotnetCampus.UITest.WPFTestHelper/ApplicationControl/IAutomatedApplicationImplFactory.cs @@ -0,0 +1,27 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; + +namespace dotnetCampus.UITest.WPFTestHelper.ApplicationControl +{ + /// + /// Defines the contract for creating an IAutomatedApplicationImpl instance. + /// + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "Impl")] + public interface IAutomatedApplicationImplFactory + { + /// + /// Factory method for creating the IAutomatedApplicationImpl instance + /// to be used by AutomatedApplication. + /// + /// The settings to be passed the the implementation instance. + /// + /// The AppDomain to create the implementation in. This is intended for in-proc scenarios where + /// the AutomatedApplication needs to create the proxy in a separate AppDomain. + /// + /// Returns the application implementation to be used by AutomatedApplication + IAutomatedApplicationImpl Create(ApplicationSettings settings, AppDomain appDomain); + } +} diff --git a/src/dotnetCampus.UITest.WPFTestHelper/ApplicationControl/IOutOfProcessAutomatedApplicationImpl.cs b/src/dotnetCampus.UITest.WPFTestHelper/ApplicationControl/IOutOfProcessAutomatedApplicationImpl.cs new file mode 100644 index 0000000..048d830 --- /dev/null +++ b/src/dotnetCampus.UITest.WPFTestHelper/ApplicationControl/IOutOfProcessAutomatedApplicationImpl.cs @@ -0,0 +1,25 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Diagnostics; + +namespace dotnetCampus.UITest.WPFTestHelper.ApplicationControl +{ + /// + /// Defines the contract for an out of process AutomatedApplication. + /// + /// + /// Represents the 'Implemention' inteface for the AutomatedApplication bridge. As such, + /// this can vary from the public interface of AutomatedApplication. + /// + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "Impl")] + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1711:IdentifiersShouldNotHaveIncorrectSuffix")] + public interface IOutOfProcessAutomatedApplicationImpl : IAutomatedApplicationImpl + { + /// + /// The process associated with the application. + /// + Process Process { get; } + } +} diff --git a/src/dotnetCampus.UITest.WPFTestHelper/ApplicationControl/InProcessApplication.cs b/src/dotnetCampus.UITest.WPFTestHelper/ApplicationControl/InProcessApplication.cs new file mode 100644 index 0000000..fdb2205 --- /dev/null +++ b/src/dotnetCampus.UITest.WPFTestHelper/ApplicationControl/InProcessApplication.cs @@ -0,0 +1,183 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Globalization; +using System.Threading; + +namespace dotnetCampus.UITest.WPFTestHelper.ApplicationControl +{ + /// + /// Represents a test application running in the current process. + /// + /// + /// + /// The following example demonstrates how to use this class. The code runs the target + /// application in a separate thread within the current process. + /// + /// public void MyTest() + /// { + /// var path = Path.Combine(executionDir, "WpfTestApplication.exe"); + /// var a = new InProcessApplication(new WpfInProcessApplicationSettings + /// { + /// Path = path, + /// InProcessApplicationType = InProcessApplicationType.InProcessSeparateThread, + /// ApplicationImplementationFactory = new WpfInProcessApplicationFactory() + /// }); + /// + /// a.Start(); + /// a.WaitForMainWindow(TimeSpan.FromMilliseconds(5000)); + /// + /// // Perform various tests... + /// + /// a.Close(); + /// } + /// + /// + public class InProcessApplication : AutomatedApplication + { + /// + /// Initializes a new instance of an InProcessApplication. + /// + /// The settings used to start the test application. + public InProcessApplication(InProcessApplicationSettings settings) + { + ValidateApplicationSettings(settings); + ApplicationSettings = settings; + } + + #region Public Properties + + /// + /// Access to the UI thread dispatcher. + /// + /// + /// This is used only for the in-proc/separate thread scenario. + /// + + public object ApplicationDriver + { + get + { + if (ApplicationImplementation != null) + { + return ApplicationImplementation.ApplicationDriver; + } + + return null; + } + } + + /// + /// The settings for the test application. + /// + public InProcessApplicationSettings ApplicationSettings + { + get; + protected set; + } + + #endregion Public Properties + + #region Override Members + + /// + /// Creates and starts the test application. + /// + /// + /// Depending on the AutomatedApplicationType + /// this can be on the same thread or on a separate thread. + /// + public override void Start() + { + if (IsApplicationRunning) + { + throw new InvalidOperationException("Cannot start an application instance if it is already running."); + } + + IsApplicationRunning = true; + + if (ApplicationSettings.InProcessApplicationType == InProcessApplicationType.InProcessSeparateThread || + ApplicationSettings.InProcessApplicationType == InProcessApplicationType.InProcessSeparateThreadAndAppDomain) + { + var mainApplicationThread = new Thread(ApplicationStartWorker); + mainApplicationThread.SetApartmentState(ApartmentState.STA); + mainApplicationThread.Start(); + } + else if (ApplicationSettings.InProcessApplicationType == InProcessApplicationType.InProcessSameThread) + { + ApplicationImplementation = ApplicationSettings.ApplicationImplementationFactory.Create(ApplicationSettings, null); + ApplicationImplementation.MainWindowOpened += this.OnMainWindowOpened; + ApplicationImplementation.Exited += this.OnExit; + ApplicationImplementation.FocusChanged += this.OnFocusChanged; + ApplicationImplementation.Start(); + } + else + { + throw new InvalidOperationException(string.Format( + CultureInfo.CurrentCulture, + "Unable to start automated application with AutomatedApplicationType: {0}", + ApplicationSettings.InProcessApplicationType)); + } + } + + /// + /// Validates the settings for an InProcessApplication. + /// + /// The settings to validate. + private void ValidateApplicationSettings(InProcessApplicationSettings settings) + { + if (settings.ApplicationImplementationFactory == null) + { + throw new InvalidOperationException("ApplicationImplementationFactory must be specified."); + } + + if (string.IsNullOrEmpty(settings.Path)) + { + throw new InvalidOperationException("For InProc scenarios, Path cannot be null or empty."); + } + } + + #endregion Override Members + + #region Private Methods + + /// + /// Thread worker that creates the AutomatedApplication implementation and + /// starts the application. If InProcessSeparateThreadAndAppDomain is specified + /// then the AutomatedApplication implementation is created in a new AppDomain. + /// + private void ApplicationStartWorker() + { + AppDomain testAppDomain = null; + // + + + + + + + + + + ApplicationImplementation = ApplicationSettings.ApplicationImplementationFactory.Create(ApplicationSettings, null); + //} + + ApplicationImplementation.MainWindowOpened += this.OnMainWindowOpened; + ApplicationImplementation.Exited += this.OnExit; + ApplicationImplementation.FocusChanged += this.OnFocusChanged; + ApplicationImplementation.Start(); + + if (testAppDomain != null) + { + // run has completed, now unload this appdomain + AppDomain.Unload(testAppDomain); + testAppDomain = null; + ApplicationImplementation = null; + } + } + + #endregion Private Methods + } +} diff --git a/src/dotnetCampus.UITest.WPFTestHelper/ApplicationControl/InProcessApplicationSettings.cs b/src/dotnetCampus.UITest.WPFTestHelper/ApplicationControl/InProcessApplicationSettings.cs new file mode 100644 index 0000000..3229d2c --- /dev/null +++ b/src/dotnetCampus.UITest.WPFTestHelper/ApplicationControl/InProcessApplicationSettings.cs @@ -0,0 +1,33 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; + +namespace dotnetCampus.UITest.WPFTestHelper.ApplicationControl +{ + /// + /// Configures an in-process automated application. + /// + [Serializable] + public class InProcessApplicationSettings : ApplicationSettings + { + /// + /// Path to the application. + /// + public string Path + { + get; + set; + } + + /// + /// The type of application to create. + /// + public InProcessApplicationType InProcessApplicationType + { + get; + set; + } + } +} diff --git a/src/dotnetCampus.UITest.WPFTestHelper/ApplicationControl/InProcessApplicationType.cs b/src/dotnetCampus.UITest.WPFTestHelper/ApplicationControl/InProcessApplicationType.cs new file mode 100644 index 0000000..c85e583 --- /dev/null +++ b/src/dotnetCampus.UITest.WPFTestHelper/ApplicationControl/InProcessApplicationType.cs @@ -0,0 +1,31 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; + +namespace dotnetCampus.UITest.WPFTestHelper.ApplicationControl +{ + /// + /// Defines the Thread and AppDomain properties of an InProcessApplication. + /// + [Serializable] + public enum InProcessApplicationType + { + /// + /// The application runs in-process and on a separate thread. + /// + InProcessSeparateThread = 0, + + /// + /// The application runs in-process, on a separate thread and + /// in a separate AppDomain. + /// + InProcessSeparateThreadAndAppDomain, + + /// + /// The application runs in-process and on the same thread. + /// + InProcessSameThread + } +} diff --git a/src/dotnetCampus.UITest.WPFTestHelper/ApplicationControl/OutOfProcessApplication.cs b/src/dotnetCampus.UITest.WPFTestHelper/ApplicationControl/OutOfProcessApplication.cs new file mode 100644 index 0000000..29abb48 --- /dev/null +++ b/src/dotnetCampus.UITest.WPFTestHelper/ApplicationControl/OutOfProcessApplication.cs @@ -0,0 +1,108 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Diagnostics; + +namespace dotnetCampus.UITest.WPFTestHelper.ApplicationControl +{ + /// + /// Represents a test application running in a new, separate process. + /// + /// + /// + /// The following example demonstrates how to use this class: + /// + /// public void MyTest() + /// { + /// var path = Path.Combine(executionDir, "WpfTestApplication.exe"); + /// var a = new OutOfProcessApplication(new OutOfProcessApplicationSettings + /// { + /// ProcessStartInfo = new ProcessStartInfo(path), + /// ApplicationImplementationFactory = new UIAutomationOutOfProcessApplicationFactory() + /// }); + /// + /// a.Start(); + /// a.WaitForMainWindow(TimeSpan.FromMilliseconds(5000)); + /// + /// // Perform various tests... + /// + /// a.Close(); + /// } + /// + /// + public class OutOfProcessApplication : AutomatedApplication + { + /// + /// Initializes a new instance of an OutOfProcessApplication. + /// + /// The settings used to start the test application. + public OutOfProcessApplication(OutOfProcessApplicationSettings settings) + { + ValidateApplicationSettings(settings); + ApplicationSettings = settings; + } + + /// + /// The settings for the automated application. + /// + public OutOfProcessApplicationSettings ApplicationSettings + { + get; + protected set; + } + + /// + /// Creates and starts the automated application. + /// + public override void Start() + { + if (IsApplicationRunning) + { + throw new InvalidOperationException("Cannot start an application instance if it is already running."); + } + + IsApplicationRunning = true; + ApplicationImplementation = ApplicationSettings.ApplicationImplementationFactory.Create(ApplicationSettings, null); + ApplicationImplementation.MainWindowOpened += this.OnMainWindowOpened; + ApplicationImplementation.FocusChanged += this.OnFocusChanged; + ApplicationImplementation.Exited += this.OnExit; + ApplicationImplementation.Start(); + } + + /// + /// The process associated with the application. + /// + public Process Process + { + get + { + var impl = ApplicationImplementation as IOutOfProcessAutomatedApplicationImpl; + if (impl != null) + { + return impl.Process; + } + + return null; + } + } + + /// + /// Validates the settings for an OutOfProcessApplication. + /// + /// The settings to validate. + private void ValidateApplicationSettings(OutOfProcessApplicationSettings settings) + { + if (settings.ProcessStartInfo == null) + { + throw new InvalidOperationException("For OutOfProc scenario, ProcessStartInfo cannot be null."); + } + + if (settings.ApplicationImplementationFactory == null) + { + throw new InvalidOperationException("ApplicationImplementationFactory must be specified."); + } + } + } +} diff --git a/src/dotnetCampus.UITest.WPFTestHelper/ApplicationControl/OutOfProcessApplicationSettings.cs b/src/dotnetCampus.UITest.WPFTestHelper/ApplicationControl/OutOfProcessApplicationSettings.cs new file mode 100644 index 0000000..296c2c4 --- /dev/null +++ b/src/dotnetCampus.UITest.WPFTestHelper/ApplicationControl/OutOfProcessApplicationSettings.cs @@ -0,0 +1,25 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Diagnostics; + +namespace dotnetCampus.UITest.WPFTestHelper.ApplicationControl +{ + /// + /// Configures an out-of-process automated application. + /// + [Serializable] + public class OutOfProcessApplicationSettings : ApplicationSettings + { + /// + /// The ProcessStartInfo to start a process. + /// + public ProcessStartInfo ProcessStartInfo + { + get; + set; + } + } +} diff --git a/src/dotnetCampus.UITest.WPFTestHelper/ApplicationControl/UIAutomationApplicationImpl.cs b/src/dotnetCampus.UITest.WPFTestHelper/ApplicationControl/UIAutomationApplicationImpl.cs new file mode 100644 index 0000000..19ec921 --- /dev/null +++ b/src/dotnetCampus.UITest.WPFTestHelper/ApplicationControl/UIAutomationApplicationImpl.cs @@ -0,0 +1,277 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Threading; +using System.Windows.Automation; + +namespace dotnetCampus.UITest.WPFTestHelper.ApplicationControl +{ + /// + /// UIAutomation application implementation for Out-of-Process scenario + /// + internal class UIAutomationApplicationImpl : IOutOfProcessAutomatedApplicationImpl + { + internal UIAutomationApplicationImpl(OutOfProcessApplicationSettings settings) + { + IsMainWindowOpened = false; + this.settings = settings; + this.windows = new List(); + + Process = new Process(); + Process.StartInfo = settings.ProcessStartInfo; + Process.EnableRaisingEvents = true; + Process.Exited += this.OnApplicationExit; + + Automation.AddAutomationEventHandler( + WindowPatternIdentifiers.WindowOpenedEvent, + AutomationElement.RootElement, + TreeScope.Subtree, + OnActivated); + + Automation.AddAutomationFocusChangedEventHandler(OnFocusChanged); + } + + #region Properties + + internal AutomationElement MainWindowAutomationElement + { + get + { + if (this.MainWindow != null) + { + return this.MainWindow as AutomationElement; + } + return null; + } + } + + #endregion Properties + + #region IAutomatedApplicationImpl + + /// + /// Gets the process associated with the application. + /// + public Process Process + { + get; + private set; + } + + /// + /// Event fired when the main window AutomationEliement is opened + /// + public event EventHandler MainWindowOpened; + + /// + /// Event fired when the application exits + /// + public event EventHandler Exited; + + /// + /// Event fired when focus is changed + /// + public event EventHandler FocusChanged; + + /// + /// Gets the value indicating whether the main window is opened + /// + public bool IsMainWindowOpened + { + get; + private set; + } + + /// + /// Gets access to the MainWindow object which is an AutomationElement + /// + public object MainWindow + { + get; + private set; + } + + /// + /// UIAutomation always returns null for this property + /// + public object ApplicationDriver + { + get + { + return null; + } + } + + /// + /// Starts the application through System.Diagnostics.Process + /// + public void Start() + { + Process.Start(); + } + + /// + /// Waits for the application to become idle + /// + /// the timeout interval + public void WaitForInputIdle(TimeSpan timeSpan) + { + if (Process != null && MainWindowAutomationElement != null) + { + Process.WaitForInputIdle((int)timeSpan.TotalMilliseconds); + } + } + + /// + /// Blocks execution of the current thread until the main window of the + /// application is displayed or until the specified timeout interval elapses. + /// + /// The timeout interval. + public void WaitForMainWindow(TimeSpan timeout) + { + TimeSpan zero = TimeSpan.FromMilliseconds(0); + TimeSpan delta = TimeSpan.FromMilliseconds(10); + + while (!this.IsMainWindowOpened && timeout > zero) + { + Thread.Sleep(10); + timeout -= delta; + } + } + + /// + /// Waits for the given window to open + /// + /// the window id of the window to wait for + /// the timeout interval + public void WaitForWindow(string windowName, TimeSpan timeout) + { + bool isWindowOpened = false; + AutomationElement windowToWait = null; + + TimeSpan zero = TimeSpan.FromMilliseconds(0); + TimeSpan delta = TimeSpan.FromMilliseconds(10); + + while (!isWindowOpened && timeout > zero) + { + Thread.Sleep(10); + timeout -= delta; + + var elements = AutomationElement.RootElement.FindAll( + TreeScope.Children, + new PropertyCondition( + AutomationElement.AutomationIdProperty, + windowName, + PropertyConditionFlags.IgnoreCase)); + + if (elements != null && elements.Count > 0) + { + windowToWait = elements[0]; + } + + if (windowToWait != null) + { + isWindowOpened = windowToWait.Current.IsEnabled; + } + } + } + + /// + /// Closes the application + /// + public void Close() + { + if (Process != null && !Process.HasExited) + { + // detach event handlers + Automation.RemoveAllEventHandlers(); + + var waitThread = new Thread(WaitForExit); + waitThread.Start(); + + // close process on new thread so exit event handlers can properly + // be called and disposed + var closeThread = new Thread(CloseProcessWorker); + closeThread.Start(); + + waitThread.Join(60000); + } + } + + #endregion IAutomatedApplicationImpl + + #region Private Methods + + private void OnActivated(object sender, EventArgs e) + { + if (this.IsMainWindowOpened) + { + // keep track of windows that are launched so they can all be closed later + var window = sender as AutomationElement; + if (window != this.MainWindowAutomationElement && !windows.Contains(window)) + { + windows.Add(window); + } + + return; + } + + this.MainWindow = AutomationElement.FromHandle(Process.MainWindowHandle); + + if (MainWindowOpened != null) + { + MainWindowOpened(this, e); + } + + this.IsMainWindowOpened = true; + } + + private void OnFocusChanged(object sender, AutomationFocusChangedEventArgs e) + { + if (FocusChanged != null) + { + FocusChanged(sender, e); + } + } + + private void OnApplicationExit(object sender, EventArgs e) + { + Process.Exited -= OnApplicationExit; + if (Exited != null) + { + Exited(this, e); + } + } + + private void CloseProcessWorker() + { + // close any child windows + foreach (AutomationElement elem in windows) + { + var winPattern = elem.GetCurrentPattern(WindowPattern.Pattern) as WindowPattern; + winPattern.Close(); + } + + Process.CloseMainWindow(); + Process.Close(); + } + + private void WaitForExit() + { + Process.WaitForExit(60000); + } + + #endregion Private Methods + + #region Private Fields + + private OutOfProcessApplicationSettings settings; + private List windows; + + #endregion Private Fields + } +} diff --git a/src/dotnetCampus.UITest.WPFTestHelper/ApplicationControl/UIAutomationOutOfProcessApplicationFactory.cs b/src/dotnetCampus.UITest.WPFTestHelper/ApplicationControl/UIAutomationOutOfProcessApplicationFactory.cs new file mode 100644 index 0000000..613390e --- /dev/null +++ b/src/dotnetCampus.UITest.WPFTestHelper/ApplicationControl/UIAutomationOutOfProcessApplicationFactory.cs @@ -0,0 +1,33 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; + +namespace dotnetCampus.UITest.WPFTestHelper.ApplicationControl +{ + /// + /// Factory for a UIAutomation implementation that AutomatedApplication + /// will consume. + /// + public class UIAutomationOutOfProcessApplicationFactory : IAutomatedApplicationImplFactory + { + /// + /// Factory method for creating the IAutomatedApplicationImpl instance + /// to be used by AutomatedApplication. + /// + /// The settings needed to create the specific instance + /// The UIAutomation app proxy does not require initialization on a separate appdomain + /// Returns the application implementation of UIAutomation for an OutOfProcessApplication + public IAutomatedApplicationImpl Create(ApplicationSettings settings, AppDomain appDomain) + { + IAutomatedApplicationImpl appImp = null; + if (settings != null) + { + appImp = new UIAutomationApplicationImpl(settings as OutOfProcessApplicationSettings); + } + + return appImp; + } + } +} diff --git a/src/dotnetCampus.UITest.WPFTestHelper/ApplicationControl/WpfApplicationImpl.cs b/src/dotnetCampus.UITest.WPFTestHelper/ApplicationControl/WpfApplicationImpl.cs new file mode 100644 index 0000000..8e40b82 --- /dev/null +++ b/src/dotnetCampus.UITest.WPFTestHelper/ApplicationControl/WpfApplicationImpl.cs @@ -0,0 +1,524 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Threading; +using System.Windows; +using System.Windows.Automation; +using System.Windows.Input; +using System.Windows.Navigation; +using System.Windows.Threading; + +namespace dotnetCampus.UITest.WPFTestHelper.ApplicationControl +{ + internal class WpfApplicationImpl : MarshalByRefObject, IAutomatedApplicationImpl + { + internal WpfApplicationImpl(WpfInProcessApplicationSettings settings) + { + if (settings == null) + { + throw new ArgumentNullException("settings"); + } + + if (settings.InProcessApplicationType == InProcessApplicationType.InProcessSeparateThreadAndAppDomain) + { + throw new InvalidOperationException("WpfApplicationImpl currently does not implement a version for InProcessSeparateThreadAndAppDomain."); + } + + if (settings.InProcessApplicationType == InProcessApplicationType.InProcessSameThread) + { + if (string.IsNullOrEmpty(settings.WindowClassName)) + { + throw new InvalidOperationException("For InProcessSameThread scenarios, WindowClassName cannot be null or empty."); + } + } + + IsMainWindowOpened = false; + this.settings = settings; + + InitializeApplication(); + } + + #region IAutomatedApplicationImpl + + /// + /// Event fired when the main window is opened + /// + public event EventHandler MainWindowOpened; + + /// + /// Event fired when the application exits + /// + public event EventHandler Exited; + + /// + /// Event fired when focus is changed + /// + public event EventHandler FocusChanged; + + /// + /// Gets the value indicating whether the main window is opened + /// + public bool IsMainWindowOpened + { + get; + private set; + } + + /// + /// Gets access to the MainWindow object which is a System.Windows.Window + /// + public object MainWindow + { + get + { + if (app != null) + { + return app.MainWindow; + } + return null; + } + } + + /// + /// Gets access to the System.Windows.Application + /// + public object ApplicationDriver + { + get + { + return app; + } + } + + /// + /// Starts the application + /// + public void Start() + { + if (window != null) + { + if (settings.InProcessApplicationType == InProcessApplicationType.InProcessSameThread) + { + window.Show(); + window.Activate(); + } + else + { + app.Run(window); + } + } + else if (app != null) + { + MethodInfo methodInfo = app.GetType().GetMethod(InitializeComponentString); + if (methodInfo != null) + { + methodInfo.Invoke(app, null); + } + + app.Run(); + } + + isAppFirstInitialization = false; + } + + /// + /// Waits for the application to become idle + /// + /// the timeout interval + public void WaitForInputIdle(TimeSpan timeSpan) + { + // To keep this thread busy, we'll have to push a frame. + DispatcherFrame frame = new DispatcherFrame(); + + app.Dispatcher.BeginInvoke(DispatcherPriority.ApplicationIdle, new DispatcherOperationCallback( + delegate(object arg) + { + frame.Continue = false; + return null; + }), null); + + // Keep the thread busy processing events until the timeout has expired. + Dispatcher.PushFrame(frame); + } + + /// + /// Blocks execution of the current thread until the main window of the + /// application is displayed or until the specified timeout interval elapses. + /// + /// The timeout interval. + public void WaitForMainWindow(TimeSpan timeout) + { + TimeSpan zero = TimeSpan.FromMilliseconds(0); + TimeSpan delta = TimeSpan.FromMilliseconds(10); + + while (!this.IsMainWindowOpened && timeout > zero) + { + if (settings.InProcessApplicationType == InProcessApplicationType.InProcessSameThread) + { + DispatcherOperations.WaitFor(DispatcherPriority.ApplicationIdle); + } + else + { + Thread.Sleep(10); + } + + timeout -= delta; + } + + // set as active + if (settings.InProcessApplicationType == InProcessApplicationType.InProcessSameThread) + { + app.MainWindow.Activate(); + } + else + { + this.DispatcherInvoke(() => { app.MainWindow.Activate(); }); + } + } + + /// + /// Waits for the given window to activate. + /// + /// + /// Note that this will not work for dialogs that block the thread. + /// + /// The AutomationProperties.AutomationIdProperty value of the window + /// The timeout interval. + public void WaitForWindow(string windowName, TimeSpan timeout) + { + bool isWindowOpened = false; + + TimeSpan zero = TimeSpan.FromMilliseconds(0); + TimeSpan delta = TimeSpan.FromMilliseconds(10); + + while (!isWindowOpened && timeout > zero) + { + DispatcherOperations.WaitFor(TimeSpan.FromMilliseconds(100)); + timeout -= delta; + + if (settings.InProcessApplicationType == InProcessApplicationType.InProcessSameThread) + { + isWindowOpened = WaitForWindowHelper(windowName); + } + else + { + isWindowOpened = DispatcherInvoke(() => { return WaitForWindowHelper(windowName); }); + } + } + } + + /// + /// Closes the application + /// + public void Close() + { + if (app != null) + { + if (settings.InProcessApplicationType == InProcessApplicationType.InProcessSameThread) + { + CloseHelper(); + + DispatcherOperations.WaitFor(DispatcherPriority.ApplicationIdle); + app.Shutdown(); + app = null; + } + else + { + DispatcherInvoke(() => { CloseHelper(); }); + + DispatcherOperations.WaitFor(DispatcherPriority.ApplicationIdle); + app.Dispatcher.InvokeShutdown(); + app = null; + } + } + } + + #endregion IAutomatedApplicationImpl + + #region Private Methods + + private void OnActivated(object sender, EventArgs e) + { + app.MainWindow.PreviewGotKeyboardFocus += OnFocusChanged; + + if (MainWindowOpened != null) + { + MainWindowOpened(this, e); + } + + IsMainWindowOpened = true; + } + + private void OnFocusChanged(object sender, KeyboardFocusChangedEventArgs e) + { + if (FocusChanged != null) + { + FocusChanged(e.NewFocus, e); + } + } + + private void OnExit(object sender, ExitEventArgs e) + { + app.Exit -= OnExit; + + if (Exited != null) + { + // note: ExitEventArgs is not serializable so should not be passed + // to listeners for cross appdomain scenarios + Exited(this, null); + } + } + + [SuppressMessage("Microsoft.Reliability", "CA2001:AvoidCallingProblematicMethods", MessageId = "System.Type.InvokeMember", Scope = "member", Target = "Microsoft.Test.ApplicationControl.WpfApplicationImpl.#InitializeApplication()", Justification = "Private members must be reflected here to init/close Application settings for in-proc scenarios")] + private void InitializeApplication() + { + // look for the Application class + string assemblyPath = settings.Path; + if (Path.GetExtension(settings.Path).Equals(".exe")) + { + assemblyPath = Path.ChangeExtension(settings.Path, ".dll"); + } + + var assemblyName = AssemblyName.GetAssemblyName(assemblyPath); + var assembly = Assembly.Load(assemblyName); + var applicationType = assembly.GetTypes().FirstOrDefault( + (type) => + { + if (type.BaseType == typeof(Application)) + { + return true; + } + return false; + }); + + if (applicationType == null) + { + throw new InvalidOperationException( + string.Format( + CultureInfo.CurrentCulture, + "Assembly: {0}, does not contain an Application class. This must exist in order to start the application under test.", + settings.Path)); + } + + app = (Application)Activator.CreateInstance(applicationType); + app.Activated += OnActivated; + app.Exit += OnExit; + + if (!isAppFirstInitialization) + { + // need to redo the static initialization so Application can run again + // simulates calling Application.ApplicationInit() + typeof(Application).InvokeMember( + ApplicationInitString, + BindingFlags.Static | BindingFlags.NonPublic | BindingFlags.InvokeMethod, + null, + null, + null, + CultureInfo.CurrentCulture); + } + + // + // simulates implementation for: Application.ResourceAssembly = assembly; + // This is needed for cases when the currently executing assembly needs to + // launch an application. + // + typeof(Application).InvokeMember( + AppResourceAssemblyString, + BindingFlags.Static | BindingFlags.NonPublic | BindingFlags.SetField, + null, + null, + new object[] { assembly }, + CultureInfo.CurrentCulture); + + typeof(BaseUriHelper).InvokeMember( + BaseUriResourceAssemblyString, + BindingFlags.Static | BindingFlags.NonPublic | BindingFlags.SetField, + null, + null, + new object[] { assembly }, + CultureInfo.CurrentCulture); + + if (!string.IsNullOrEmpty(settings.WindowClassName)) + { + // if WindowClassName is specified create the specified window + // and make that the main window + var windowType = assembly.GetType(settings.WindowClassName, true); + window = (Window)Activator.CreateInstance(windowType); + window.Activated += OnActivated; + + app.MainWindow = window; + } + } + + private bool WaitForWindowHelper(string windowId) + { + bool isWindowOpened = false; + Window windowToWait = null; + + foreach (Window window in app.Windows) + { + var id = (string)window.GetValue(AutomationProperties.AutomationIdProperty); + if (id != null && id == windowId) + { + windowToWait = window; + } + } + + if (windowToWait != null) + { + isWindowOpened = windowToWait.IsActive; + } + + return isWindowOpened; + } + + [SuppressMessage("Microsoft.Reliability", "CA2001:AvoidCallingProblematicMethods", MessageId = "System.Type.InvokeMember", Scope = "member", Target = "Microsoft.Test.ApplicationControl.WpfApplicationImpl.#CloseHelper()", Justification="Private members must be reflected here to init/close Application settings for in-proc scenarios")] + private void CloseHelper() + { + if (app.MainWindow != null) + { + foreach (Window window in app.Windows) + { + if (window != app.MainWindow) + { + window.Close(); + } + } + + app.Activated -= OnActivated; + app.MainWindow.Activated -= OnActivated; + app.MainWindow.PreviewGotKeyboardFocus -= OnFocusChanged; + app.MainWindow.Close(); + + // resets the Application instance so it can be created again + // simulates Application._appCreatedInThisAppDomain = false; + typeof(Application).InvokeMember( + AppCreatedInThisAppDomainString, + BindingFlags.Static | BindingFlags.NonPublic | BindingFlags.SetField, + null, + null, + new object[] { false }, + CultureInfo.CurrentCulture); + + // clears the internal PreloadedPackages so Application can be reinitialized + // simulates MS.Internal.IO.Packaging.PreloadedPackages.Clear(); + var coreAsm = Assembly.GetAssembly(typeof(UIElement)); + var preloadedPackagesType = coreAsm.GetType(PreloadedPackagesClassNameString, true, true); + preloadedPackagesType.InvokeMember( + PreloadedPackagesClearString, + BindingFlags.Static | BindingFlags.NonPublic | BindingFlags.InvokeMethod, + null, + null, + null, + CultureInfo.CurrentCulture); + } + } + + private void DispatcherInvoke(Action action) + { + app.Dispatcher.Invoke( + DispatcherPriority.Normal, + new DispatcherOperationCallback((arg) => + { + action(); + return null; + }), + null); + } + + private void DispatcherInvoke(Action action, T args) + { + app.Dispatcher.Invoke( + DispatcherPriority.Normal, + new DispatcherOperationCallback((arg) => + { + action((T)arg); + return null; + }), + args); + } + + private R DispatcherInvoke(Func func) + { + R retVal = (R)app.Dispatcher.Invoke( + DispatcherPriority.Normal, + new DispatcherOperationCallback((arg) => + { + return func(); + }), + null); + + return retVal; + } + + private R DispatcherInvoke(Func func, T args) + { + R retVal = (R)app.Dispatcher.Invoke( + DispatcherPriority.Normal, + new DispatcherOperationCallback((arg) => + { + return func((T)arg); + }), + args); + + return retVal; + } + + private R DispatcherInvoke(Func func, T1 arg1, T2 arg2) + { + R retVal = (R)app.Dispatcher.Invoke( + DispatcherPriority.Normal, + new DispatcherOperationCallback((arg) => + { + return func((T1)arg, (T2)arg); + }), + arg1, + arg2); + + return retVal; + } + + #endregion Private Methods + + #region Private Fields + + // System.Windows.Application => private void InitializeComponent() + private const string InitializeComponentString = "InitializeComponent"; + + // System.Windows.Application => private static void ApplicationInit() + private const string ApplicationInitString = "ApplicationInit"; + + // System.Windows.Application => private Assembly _resourceAssembly; + private const string AppResourceAssemblyString = "_resourceAssembly"; + + // BaseUriHelper => private Assembly _resourceAssembly; + private const string BaseUriResourceAssemblyString = "_resourceAssembly"; + + // System.Windows.Application => private static bool _appCreatedInThisAppDomain; + private const string AppCreatedInThisAppDomainString = "_appCreatedInThisAppDomain"; + + // MS.Internal.IO.Packaging.PreloadedPackages full class name + private const string PreloadedPackagesClassNameString = "MS.Internal.IO.Packaging.PreloadedPackages"; + + // MS.Internal.IO.Packaging.PreloadedPackages => internal static void Clear() + private const string PreloadedPackagesClearString = "Clear"; + + /// + /// Flag used to track if System.Windows.Application has already been called + /// + private static bool isAppFirstInitialization = true; + + private WpfInProcessApplicationSettings settings; + private Application app; + private Window window; + + #endregion Private Fields + } +} diff --git a/src/dotnetCampus.UITest.WPFTestHelper/ApplicationControl/WpfInProcessApplicationFactory.cs b/src/dotnetCampus.UITest.WPFTestHelper/ApplicationControl/WpfInProcessApplicationFactory.cs new file mode 100644 index 0000000..a0bf5f5 --- /dev/null +++ b/src/dotnetCampus.UITest.WPFTestHelper/ApplicationControl/WpfInProcessApplicationFactory.cs @@ -0,0 +1,53 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; + +namespace dotnetCampus.UITest.WPFTestHelper.ApplicationControl +{ + /// + /// Factory for a WpfApplication implementation that AutomatedApplication + /// will consume. + /// + public class WpfInProcessApplicationFactory : IAutomatedApplicationImplFactory + { + /// + /// Factory method for creating the IAutomatedApplicationImpl instance + /// to be used by AutomatedApplication. + /// + /// The settings needed to create the specific instance + /// + /// The AppDomain to create the implementation in. This will be null for scenarios + /// where separate AppDomain is not specified. + /// + /// Returns the application implementation of Wpf for an InProcessApplication + public IAutomatedApplicationImpl Create(ApplicationSettings settings, AppDomain appDomain) + { + IAutomatedApplicationImpl appImp = null; + if (settings != null) + { + if (appDomain != null) + { + // appImp = (WpfApplicationImpl)appDomain.CreateInstanceAndUnwrap( + // Assembly.GetExecutingAssembly().GetName().Name, + // typeof(WpfApplicationImpl).FullName, + // false, + // BindingFlags.CreateInstance, + // null, + // new object[] { settings as WpfInProcessApplicationSettings }, + // CultureInfo.InvariantCulture, + // null, + // null); + throw new Exception("Core: AppDomain instantiation was commented out!!! - miguep"); + } + else + { + appImp = new WpfApplicationImpl(settings as WpfInProcessApplicationSettings); + } + } + + return appImp; + } + } +} diff --git a/src/dotnetCampus.UITest.WPFTestHelper/ApplicationControl/WpfInProcessApplicationSettings.cs b/src/dotnetCampus.UITest.WPFTestHelper/ApplicationControl/WpfInProcessApplicationSettings.cs new file mode 100644 index 0000000..dd04a8e --- /dev/null +++ b/src/dotnetCampus.UITest.WPFTestHelper/ApplicationControl/WpfInProcessApplicationSettings.cs @@ -0,0 +1,27 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; + +namespace dotnetCampus.UITest.WPFTestHelper.ApplicationControl +{ + /// + /// Configures a WPF in-process test application. + /// + [Serializable] + public class WpfInProcessApplicationSettings : InProcessApplicationSettings + { + /// + /// The window class to start. + /// + /// + /// This must be the full class name. + /// + public string WindowClassName + { + get; + set; + } + } +} diff --git a/src/dotnetCampus.UITest.WPFTestHelper/AutomationUtilities.cs b/src/dotnetCampus.UITest.WPFTestHelper/AutomationUtilities.cs new file mode 100644 index 0000000..fc1f87e --- /dev/null +++ b/src/dotnetCampus.UITest.WPFTestHelper/AutomationUtilities.cs @@ -0,0 +1,147 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + + +using System.Windows.Automation; + +namespace dotnetCampus.UITest.WPFTestHelper +{ + /// + /// The AutomationUtilities class provides a simple interface to common + /// UI Automation (UIA) operations. + /// The most common class of UIA operations in testing involves discovery of UI elements. + /// + /// + /// This sample discovers and clicks the "Close" button in an "About" dialog box, thus + /// dismissing the "About" dialog box. + /// + /// + /// string aboutDialogName = "About"; + /// string closeButtonName = "Close"; + /// + /// AutomationElementCollection aboutDialogs = AutomationUtilities.FindElementsByName( + /// AutomationElement.RootElement, + /// aboutDialogName); + /// + /// AutomationElementCollection closeButtons = AutomationUtilities.FindElementsByName( + /// aboutDialogs[0], + /// closeButtonName); + /// + /// // You can either invoke the discovered control, through its invoke pattern ... + /// InvokePattern p = + /// closeButtons[0].GetCurrentPattern(InvokePattern.Pattern) as InvokePattern; + /// p.Invoke(); + /// + /// // ... or you can handle the mouse directly and click on the control. + /// System.Windows.Point winPoint = closeButtons[0].GetClickablePoint(); + /// System.Drawing.Point drawingPoint = new System.Drawing.Point((int)winPoint.X, (int)winPoint.Y); + /// Mouse.MoveTo(drawingPoint); + /// Mouse.Click(System.Windows.Input.MouseButton.Left); + /// + /// + /// + public static class AutomationUtilities + { + /// + /// Retrieves the child element with the specified index. + /// + /// The parent element (e.g., a ListBox control). + /// The index of the child element to find. + /// An AutomationElement representing the discovered child element. + public static AutomationElement FindElementByIndex(AutomationElement rootElement, int index) + { + Condition condition = new PropertyCondition( + AutomationElement.IsControlElementProperty, + true); + + AutomationElementCollection found = rootElement.FindAll(TreeScope.Children, condition); + return found[index]; + } + + + /// + /// Retrieves all UIA elements that meet the specified conditions. + /// + /// Parent element, such as an application window, or + /// AutomationElement.RootElement when searching for the application window. + /// Conditions that the returned collection should meet. + /// A UIA element collection. + public static AutomationElementCollection FindElements(AutomationElement rootElement, params Condition[] conditions) + { + Condition condition = new AndCondition(conditions); + + return rootElement.FindAll(TreeScope.Children, condition); + } + + + /// + /// Retrieves a UIA collection of all elements with a given class name. + /// + /// Parent element, such as an application window, or + /// AutomationElement.RootElement when searching for the application window. + /// The class name of the control type to find. + /// A UIA element collection. + public static AutomationElementCollection FindElementsByClassName(AutomationElement rootElement, string className) + { + Condition condition = new PropertyCondition( + AutomationElement.ClassNameProperty, + className); + + return rootElement.FindAll(TreeScope.Children, condition); + } + + + /// + /// Retrieves a UIA collection of all elements of a given control type. + /// + /// Parent element, such as an application window, or + /// AutomationElement.RootElement when searching for the application window. + /// Control type of the control, such as Button. + /// A UIA element collection. + public static AutomationElementCollection FindElementsByControlType(AutomationElement rootElement, ControlType controlType) + { + Condition condition = new PropertyCondition( + AutomationElement.ControlTypeProperty, + controlType); + + return rootElement.FindAll(TreeScope.Element | TreeScope.Children, condition); + } + + + /// + /// Retrieves a UIA collection of all elements with a given UIA identifier. + /// + /// Parent element, such as an application window, or + /// AutomationElement.RootElement when searching for the application window. + /// UIA identifier of the searched element, such as "button1". + /// A UIA element collection. + public static AutomationElementCollection FindElementsById(AutomationElement rootElement, string automationId) + { + Condition condition = new PropertyCondition( + AutomationElement.AutomationIdProperty, + automationId, + PropertyConditionFlags.IgnoreCase); + + return rootElement.FindAll(TreeScope.Element | TreeScope.Children, condition); + } + + + /// + /// Retrieves a UIA collection of all elements with a given name. + /// + /// Parent element, such as an application window, or + /// AutomationElement.RootElement when searching for the application window. + /// Name of the searched element, such as "button1". + /// A UIA element collection. + public static AutomationElementCollection FindElementsByName(AutomationElement rootElement, string name) + { + Condition condition = new PropertyCondition( + AutomationElement.NameProperty, + name, + PropertyConditionFlags.IgnoreCase); + + return rootElement.FindAll(TreeScope.Element | TreeScope.Children, condition); + } + } +} diff --git a/src/dotnetCampus.UITest.WPFTestHelper/CommandLineParsing/Command.cs b/src/dotnetCampus.UITest.WPFTestHelper/CommandLineParsing/Command.cs new file mode 100644 index 0000000..a106b31 --- /dev/null +++ b/src/dotnetCampus.UITest.WPFTestHelper/CommandLineParsing/Command.cs @@ -0,0 +1,77 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + + +using System; + +namespace dotnetCampus.UITest.WPFTestHelper.CommandLineParsing +{ + /// + /// Provides a base class for the functionality that all commands must implement. + /// + /// + /// + /// The following example shows parsing of a command-line such as "Test.exe RUN /verbose /runId=10" + /// into a strongly-typed Command, that can then be excuted. + /// + /// using System; + /// using System.Linq; + /// using Microsoft.Test.CommandLineParsing; + /// + /// public class RunCommand : Command + /// { + /// public bool? Verbose { get; set; } + /// public int? RunId { get; set; } + /// + /// public override void Execute() + /// { + /// Console.WriteLine("RunCommand: Verbose={0} RunId={1}", Verbose, RunId); + /// } + /// } + /// + /// public class Program + /// { + /// public static void Main(string[] args) + /// { + /// if (String.Compare(args[0], "run", StringComparison.InvariantCultureIgnoreCase) == 0) + /// { + /// Command c = new RunCommand(); + /// c.ParseArguments(args.Skip(1)); // or CommandLineParser.ParseArguments(c, args.Skip(1)) + /// c.Execute(); + /// } + /// } + /// } + /// + /// + public abstract class Command + { + /// + /// The name of the command. The base implementation is to strip off the last + /// instance of "Command" from the end of the type name. So "DiscoverCommand" + /// would become "Discover". If the type name does not have the string "Command" in it, + /// then the name of the command is the same as the type name. This behavior can be + /// overridden, but most derived classes are going to be of the form [Command Name] + Command. + /// + public virtual string Name + { + get + { + string typeName = this.GetType().Name; + if (typeName.Contains("Command")) + { + return typeName.Remove(typeName.LastIndexOf("Command", StringComparison.Ordinal)); + } + else + { + return typeName; + } + } + } + + /// + /// Executes the command. + /// + public abstract void Execute(); + } +} diff --git a/src/dotnetCampus.UITest.WPFTestHelper/CommandLineParsing/CommandLineDictionary.cs b/src/dotnetCampus.UITest.WPFTestHelper/CommandLineParsing/CommandLineDictionary.cs new file mode 100644 index 0000000..1ca0ee9 --- /dev/null +++ b/src/dotnetCampus.UITest.WPFTestHelper/CommandLineParsing/CommandLineDictionary.cs @@ -0,0 +1,206 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + + +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using System.Runtime.Serialization; + +namespace dotnetCampus.UITest.WPFTestHelper.CommandLineParsing +{ + /// + /// Represents a dictionary that is aware of command line input patterns. All lookups for keys ignore case. + /// + /// + /// + /// The example below demonstrates parsing a command line such as "Test.exe /verbose /runId=10" + /// + /// CommandLineDictionary d = CommandLineDictionary.FromArguments(args); + /// + /// bool verbose = d.ContainsKey("verbose"); + /// int runId = Int32.Parse(d["runId"]); + /// + /// + /// + /// + /// You can also explicitly provide key and value identifiers for the cases + /// that use other characters (rather than '/' and '=') as key/value identifiers. + /// The example below demonstrates parsing a command line such as "Test.exe -verbose -runId:10" + /// + /// CommandLineDictionary d = CommandLineDictionary.FromArguments(args, '-', ':'); + /// + /// bool verbose = d.ContainsKey("verbose"); + /// int runId = Int32.Parse(d["runId"]); + /// + /// + [Serializable] + public class CommandLineDictionary : Dictionary + { + #region Constructors + + /// + /// Create an empty CommandLineDictionary using the default key/value + /// separators of '/' and '='. + /// + public CommandLineDictionary() + : base(StringComparer.OrdinalIgnoreCase) + { + KeyCharacter = '/'; + ValueCharacter = '='; + } + + /// + /// Creates a dictionary using a serialization info and context. This + /// is used for Xml deserialization and isn't normally called from user code. + /// + /// Data needed to deserialize the dictionary. + /// Describes source and destination of the stream. + protected CommandLineDictionary(SerializationInfo info, StreamingContext context) + : base(info, context) + { + } + + #endregion + + #region Public Members + + /// + /// Initializes a new instance of the CommandLineDictionary class, populating a + /// dictionary with key/value pairs from a command line that supports syntax + /// where options are provided in the form "/key=value". + /// + /// Key/value pairs. + /// + public static CommandLineDictionary FromArguments(IEnumerable arguments) + { + return FromArguments(arguments, '/', '='); + } + + /// + /// Creates a dictionary that is populated with key/value pairs from a command line + /// that supports syntax where options are provided in the form "/key=value". + /// This method supports the ability to specify delimiter characters for options in + /// the command line. + /// + /// Key/value pairs. + /// A character that precedes a key. + /// A character that separates a key from a value. + /// + public static CommandLineDictionary FromArguments(IEnumerable arguments, char keyCharacter, char valueCharacter) + { + CommandLineDictionary cld = new CommandLineDictionary(); + cld.KeyCharacter = keyCharacter; + cld.ValueCharacter = valueCharacter; + foreach (string argument in arguments) + { + cld.AddArgument(argument); + } + + return cld; + } + + #endregion + + #region Override Members + + /// + /// Converts dictionary contents to a command line string of key/value pairs. + /// + /// Command line string. + public override string ToString() + { + string commandline = String.Empty; + foreach (KeyValuePair pair in this) + { + if (!string.IsNullOrEmpty(pair.Value)) + { + commandline += String.Format(CultureInfo.InvariantCulture, "{0}{1}{2}{3} ", KeyCharacter, pair.Key, ValueCharacter, pair.Value); + } + else // There is no value, so we just serialize the key + { + commandline += String.Format(CultureInfo.InvariantCulture, "{0}{1} ", KeyCharacter, pair.Key); + } + } + return commandline.TrimEnd(); + } + + #endregion + + #region Protected Members + + /// + /// Populates a SerializationInfo with data needed to serialize the dictionary. + /// This is used by Xml serialization and isn't normally called from user code. + /// + /// SerializationInfo object to populate. + /// StreamingContext to populate data from. + [SuppressMessage("Microsoft.Security", "CA2123")] + public override void GetObjectData(SerializationInfo info, StreamingContext context) + { + base.GetObjectData(info, context); + } + + #endregion + + #region Private Members + + /// + /// Character to treat as the key character in the value line. + /// If the arguments should be of the form /Foo=Bar, then the + /// key character is /. (Which is the default) + /// + private char KeyCharacter { get; set; } + + /// + /// Character to treat as the value character in the value line. + /// If the arguments should be of the form /Foo=Bar, then the + /// value character is =. (Which is the default) + /// + private char ValueCharacter { get; set; } + + /// + /// Adds the specified argument to the dictionary + /// + /// Key/Value pair argument. + private void AddArgument(string argument) + { + if (argument == null) + { + throw new ArgumentNullException("argument"); + } + + string key; + string value; + + if (argument.StartsWith(KeyCharacter.ToString(), StringComparison.OrdinalIgnoreCase)) + { + string[] splitArg = argument.Substring(1).Split(ValueCharacter); + + //Key is extracted from first element + key = splitArg[0]; + + //Reconstruct the value. We could also do this using substrings. + if (splitArg.Length > 1) + { + value = string.Join("=", splitArg, 1, splitArg.Length - 1); + } + else + { + value = string.Empty; + } + } + else + { + throw new ArgumentException("Unsupported value line argument format.", argument); + } + + Add(key, value); + } + + #endregion + } +} + diff --git a/src/dotnetCampus.UITest.WPFTestHelper/CommandLineParsing/CommandLineParser.cs b/src/dotnetCampus.UITest.WPFTestHelper/CommandLineParsing/CommandLineParser.cs new file mode 100644 index 0000000..503000c --- /dev/null +++ b/src/dotnetCampus.UITest.WPFTestHelper/CommandLineParsing/CommandLineParser.cs @@ -0,0 +1,425 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + + +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Text; + +namespace dotnetCampus.UITest.WPFTestHelper.CommandLineParsing +{ + /// + /// Provides utilities for parsing command-line values. + /// + /// + /// + /// The following example shows parsing of a command-line such as "Test.exe /verbose /runId=10" + /// into a strongly-typed structure. + /// + /// using System; + /// using System.Linq; + /// using Microsoft.Test.CommandLineParsing; + /// + /// public class CommandLineArguments + /// { + /// public bool? Verbose { get; set; } + /// public int? RunId { get; set; } + /// } + /// + /// public class Program + /// { + /// public static void Main(string[] args) + /// { + /// CommandLineArguments a = new CommandLineArguments(); + /// a.ParseArguments(args); // or CommandLineParser.ParseArguments(a, args); + /// + /// Console.WriteLine("Verbose: {0}, RunId: {1}", a.Verbose, a.RunId); + /// } + /// } + /// + /// + /// + /// + /// The following example shows parsing of a command-line such as "Test.exe RUN /verbose /runId=10" + /// into a strongly-typed Command, that can then be excuted. + /// + /// using System; + /// using System.Linq; + /// using Microsoft.Test.CommandLineParsing; + /// + /// public class RunCommand : Command + /// { + /// public bool? Verbose { get; set; } + /// public int? RunId { get; set; } + /// + /// public override void Execute() + /// { + /// Console.WriteLine("RunCommand: Verbose={0} RunId={1}", Verbose, RunId); + /// } + /// } + /// + /// public class Program + /// { + /// public static void Main(string[] args) + /// { + /// if (String.Compare(args[0], "run", StringComparison.InvariantCultureIgnoreCase) == 0) + /// { + /// Command c = new RunCommand(); + /// c.ParseArguments(args.Skip(1)); // or CommandLineParser.ParseArguments(c, args.Skip(1)) + /// c.Execute(); + /// } + /// } + /// } + /// + /// + public static class CommandLineParser + { + #region Constructors + + /// + /// Static constructor. + /// + static CommandLineParser() + { + // The parser will want to convert from value line string arguments into various + // data types on a value. Any type that doesn't have a default TypeConverter that + // can convert from string to it's type needs to have a custom TypeConverter written + // for it, and have it added here. + TypeDescriptor.AddAttributes(typeof(DirectoryInfo), new TypeConverterAttribute(typeof(DirectoryInfoConverter))); + TypeDescriptor.AddAttributes(typeof(FileInfo), new TypeConverterAttribute(typeof(FileInfoConverter))); + } + + #endregion + + #region Public Members + + /// + /// Sets properties on an object from a series of key/value string + /// arguments that are in the form "/PropertyName=Value", where the + /// value is converted from a string into the property type. + /// + /// The object to set properties on. + /// The key/value arguments describing the property names and values to set. + /// + /// Indicates whether the properties were successfully set. Reasons for failure reasons include + /// a property name that does not exist or a value that cannot be converted from a string. + /// + /// Thrown when one of the key/value strings cannot be parsed into a property. + public static void ParseArguments(this object valueToPopulate, IEnumerable args) + { + CommandLineDictionary commandLineDictionary = CommandLineDictionary.FromArguments(args); + + PropertyDescriptorCollection properties = TypeDescriptor.GetProperties(valueToPopulate); + + // Ensure required properties are specified. + foreach (PropertyDescriptor property in properties) + { + // See whether any of the attributes on the property is a RequiredAttribute. + if (property.Attributes.Cast().Any(attribute => attribute is RequiredAttribute)) + { + // If so, and the command line dictionary doesn't contain a key matching + // the property's name, it means that a required property isn't specified. + if (!commandLineDictionary.ContainsKey(property.Name)) + { + throw new ArgumentException("A value for the " + property.Name + " property is required."); + } + } + } + + foreach (KeyValuePair keyValuePair in commandLineDictionary) + { + // Find a property whose name matches the kvp's key, ignoring case. + // We can't just use the indexer because that is case-sensitive. + PropertyDescriptor property = MatchProperty(keyValuePair.Key, properties,valueToPopulate.GetType()); + + // If the value is null/empty and the property is a bool, we + // treat it as a flag, which means its presence means true. + if (String.IsNullOrEmpty(keyValuePair.Value) && + (property.PropertyType == typeof(bool) || property.PropertyType == typeof(bool?))) + { + property.SetValue(valueToPopulate, true); + continue; + } + + object valueToSet; + + // We support a limited set of collection types. Setting a List + // is one of the most flexible types as it supports three different + // interfaces, but the catch is that we don't support the concrete + // Collection type. We can expand it to support Collection + // in the future, but the code will get a bit uglier. + switch (property.PropertyType.Name) + { + case "IEnumerable`1": + case "ICollection`1": + case "IList`1": + case "List`1": + MethodInfo methodInfo = typeof(CommandLineParser).GetMethod("FromCommaSeparatedList", BindingFlags.Static | BindingFlags.NonPublic); + Type[] genericArguments = property.PropertyType.GetGenericArguments(); + MethodInfo genericMethodInfo = methodInfo.MakeGenericMethod(genericArguments); + valueToSet = genericMethodInfo.Invoke(null, new object[] { keyValuePair.Value }); + break; + default: + TypeConverter typeConverter = TypeDescriptor.GetConverter(property.PropertyType); + if (typeConverter == null || !typeConverter.CanConvertFrom(typeof(string))) + { + throw new ArgumentException("Unable to convert from a string to a property of type " + property.PropertyType + "."); + } + valueToSet = typeConverter.ConvertFromInvariantString(keyValuePair.Value); + break; + } + + property.SetValue(valueToPopulate, valueToSet); + } + + return; + } + + /// + /// Match the property to the specified keyName + /// + /// If match cannot be found, throw an argument exception + /// + private static PropertyDescriptor MatchProperty(string keyName, PropertyDescriptorCollection properties, Type targetType) + { + foreach(PropertyDescriptor prop in properties) + { + if(prop.Name.Equals(keyName, StringComparison.OrdinalIgnoreCase)) + { + return prop; + } + } + throw new ArgumentException("A matching public property of name " + keyName + " on type " + targetType + " could not be found."); + } + + /// + /// Prints names and descriptions for properties on the specified component. + /// + /// The component to print usage for. + public static void PrintUsage(object component) + { + IEnumerable properties = TypeDescriptor.GetProperties(component).Cast(); + IEnumerable propertyNames = properties.Select(property => property.Name); + IEnumerable propertyDescriptions = properties.Select(property => property.Description); + IEnumerable lines = FormatNamesAndDescriptions(propertyNames, propertyDescriptions, Console.WindowWidth); + + Console.WriteLine("Possible arguments:"); + foreach (string line in lines) + { + Console.WriteLine(line); + } + } + + /// + /// Prints a general summary of each command. + /// + /// A collection of possible commands. + public static void PrintCommands(IEnumerable commands) + { + // Print out general descriptions for every command. + IEnumerable commandNames = commands.Select(command => command.Name); + IEnumerable commandDescriptions = commands.Select(command => command.GetAttribute().Description); + IEnumerable lines = FormatNamesAndDescriptions(commandNames, commandDescriptions, Console.WindowWidth); + + Console.WriteLine("Possible commands:"); + foreach (string line in lines) + { + Console.WriteLine(line); + } + } + + /// + /// Creates a string that represents key/value arguments for the properties of the + /// specified object. For example, an object with a name (string) of "example" and a + /// priority value (integer) of 1 translates to '/name=example /priority=1'. This + /// can be used to send data structures through the command line. + /// + /// Value to create key/value arguments from. + /// Space-delimited key/value arguments. + public static string ToString(object valueToConvert) + { + IEnumerable properties = TypeDescriptor.GetProperties(valueToConvert).Cast(); + IEnumerable propertiesOnParent = TypeDescriptor.GetProperties(valueToConvert.GetType().BaseType).Cast(); + properties = properties.Except(propertiesOnParent); + CommandLineDictionary commandLineDictionary = new CommandLineDictionary(); + + foreach (PropertyDescriptor property in properties) + { + commandLineDictionary[property.Name] = property.GetValue(valueToConvert).ToString(); + } + + return commandLineDictionary.ToString(); + } + + #endregion + + #region Private Members + + /// + /// Given collections of names and descriptions, returns a set of lines + /// where the description text is wrapped and left aligned. eg: + /// First Name this is a string that wraps around + /// and is left aligned. + /// Second Name this is another string. + /// + /// Collection of name strings. + /// Collection of description strings. + /// Maximum length of formatted lines + /// Formatted lines of text. + private static IEnumerable FormatNamesAndDescriptions(IEnumerable names, IEnumerable descriptions, int maxLineLength) + { + if (names.Count() != descriptions.Count()) + { + throw new ArgumentException("Collection sizes are not equal", "names"); + } + + int namesMaxLength = names.Max(commandName => commandName.Length); + + List lines = new List(); + + for (int i = 0; i < names.Count(); i++) + { + string line = names.ElementAt(i); + line = line.PadRight(namesMaxLength + 2); + + foreach (string wrappedLine in WordWrap(descriptions.ElementAt(i), maxLineLength - namesMaxLength - 3)) + { + line += wrappedLine; + lines.Add(line); + line = new string(' ', namesMaxLength + 2); + } + } + + return lines; + } + + /// + /// Convert a comma separated list to a List of T. There must be a + /// TypeConverter for the collection type that can convert from a string. + /// "1,2,3" => List(int) containing 1, 2, and 3. + /// Commas in the textual representation itself should be escaped with + /// a ----lash, as should backslash itself. + /// + /// Type of objects in the collection. + /// Comma separated list representation. + /// Collection of objects. + private static List FromCommaSeparatedList(this string commaSeparatedList) + { + List collection = new List(); + + TypeConverter typeConverter = TypeDescriptor.GetConverter(typeof(T)); + if (typeConverter.CanConvertFrom(typeof(string))) + { + StringBuilder builder = new StringBuilder(); + bool isEscaped = false; + + foreach (char character in commaSeparatedList) + { + // If we are in escaped mode, add the character and exit escape mode + if (isEscaped) + { + builder.Append(character); + isEscaped = false; + } + // If we see the backslash and are not in escaped mode, go into escaped mode + else if (character == '\\' && !isEscaped) + { + isEscaped = true; + } + // A comma outside of escaped mode is an item separator, convert + // built string to T and add to collection, then zero out the builder + else if (character == ',' && !isEscaped) + { + collection.Add((T)typeConverter.ConvertFromInvariantString(builder.ToString())); + builder.Length = 0; + } + // Otherwise simply add the character + else + { + builder.Append(character); + } + } + + // If builder.Length is non-zero, of course we want to add it. + // If, however, it is zero, it can mean one of two things: + // - There are no items at all, i.e. the commaSeparatedList string + // is null/empty, and we should return an empty collection. + // - The builder just got flushed by a comma, and there is one last + // item in the collection to add that should be typeconverted + // from an empty string. + // collection.Count is always 0 for the former and greater than 0 + // for the later, so we will also add if Count > 0. + if (builder.Length > 0 || collection.Count > 0) + { + collection.Add((T)typeConverter.ConvertFromInvariantString(builder.ToString())); + } + } + + return collection; + } + + /// + /// Gets an attribute on the specified object instance. + /// + /// Type of attribute to get. + /// Object instance to look for attribute on. + /// First instance of the specified attribute. + private static T GetAttribute(this object value) where T : Attribute + { + IEnumerable attributes = TypeDescriptor.GetAttributes(value).Cast(); + return (T)attributes.First(attribute => attribute is T); + } + + /// + /// Word wrap text for a specified maximum line length. + /// + /// Text to word wrap. + /// Maximum length of a line. + /// Collection of lines for the word wrapped text. + private static IEnumerable WordWrap(string text, int maxLineLength) + { + List lines = new List(); + string currentLine = String.Empty; + + foreach (string word in text.Split(' ')) + { + // Whenever adding the word would push us over the maximum + // width, add the current line to the lines collection and + // begin a new line. The new line starts with space padding + // it to be left aligned with the previous line of text from + // this column. + if (currentLine.Length + word.Length > maxLineLength) + { + lines.Add(currentLine); + currentLine = String.Empty; + } + + currentLine += word; + + // Add spaces between words except for when we are at exactly the + // maximum width. + if (currentLine.Length != maxLineLength) + { + currentLine += " "; + } + } + + // Add the remainder of the current line except for when it is + // empty, which is true in the case when we had just started a + // new line. + if (currentLine.Trim() != String.Empty) + { + lines.Add(currentLine); + } + + return lines; + } + + #endregion + } +} diff --git a/src/dotnetCampus.UITest.WPFTestHelper/CommandLineParsing/DirectoryInfoConverter.cs b/src/dotnetCampus.UITest.WPFTestHelper/CommandLineParsing/DirectoryInfoConverter.cs new file mode 100644 index 0000000..5619e6e --- /dev/null +++ b/src/dotnetCampus.UITest.WPFTestHelper/CommandLineParsing/DirectoryInfoConverter.cs @@ -0,0 +1,47 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + + +using System; +using System.ComponentModel; +using System.IO; + +namespace dotnetCampus.UITest.WPFTestHelper.CommandLineParsing +{ + /// + /// Converter that can convert from a string to a DirectoryInfo. + /// + public class DirectoryInfoConverter : TypeConverter + { + /// + /// Converts from a string to a DirectoryInfo. + /// + /// Context. + /// Culture. + /// Value to convert. + /// DirectoryInfo, or null if value was null or non-string. + public override object ConvertFrom(ITypeDescriptorContext context, System.Globalization.CultureInfo culture, object value) + { + if (value is string && value != null) + { + return new DirectoryInfo((string)value); + } + else + { + return null; + } + } + + /// + /// Returns whether this converter can convert an object of the given type to the type of this converter, using the specified context. + /// + /// An ITypeDescriptorContext that provides a format context. + /// A Type that represents the type you want to convert from. + /// True if this converter can perform the conversion; otherwise, False. + public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType) + { + return (sourceType == typeof(string)); + } + } +} diff --git a/src/dotnetCampus.UITest.WPFTestHelper/CommandLineParsing/FileInfoConverter.cs b/src/dotnetCampus.UITest.WPFTestHelper/CommandLineParsing/FileInfoConverter.cs new file mode 100644 index 0000000..dca58dd --- /dev/null +++ b/src/dotnetCampus.UITest.WPFTestHelper/CommandLineParsing/FileInfoConverter.cs @@ -0,0 +1,70 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + + +using System; +using System.ComponentModel; +using System.IO; + +namespace dotnetCampus.UITest.WPFTestHelper.CommandLineParsing +{ + /// + /// Converter that can convert from a string to a FileInfo. + /// + public class FileInfoConverter : TypeConverter + { + /// + /// Converts from a string to a FileInfo. + /// + /// Context. + /// Culture. + /// Value to convert. + /// FileInfo, or null if value was null or non-string. + public override object ConvertFrom(ITypeDescriptorContext context, System.Globalization.CultureInfo culture, object value) + { + if (value is string && value != null) + { + return new FileInfo((string)value); + } + else + { + return null; + } + } + + /// + /// Returns whether this converter can convert an object of the given type to a FileInfo. + /// + /// An ITypeDescriptorContext that provides a format context. + /// A Type that represents the type you want to convert from. + /// True if this converter can perform the conversion; otherwise, False. + public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType) + { + return (sourceType == typeof(string)); + } + + /// + /// Converts from a FileInfo to a string. + /// + public override object ConvertTo(ITypeDescriptorContext context, System.Globalization.CultureInfo culture, object value, Type destinationType) + { + if (value is FileInfo && destinationType == typeof(string)) + { + return ((FileInfo)value).FullName; + } + else + { + return null; + } + } + + /// + /// Returns whether this converter can convert a FileInfo object to the specified type. + /// + public override bool CanConvertTo(ITypeDescriptorContext context, Type destinationType) + { + return (destinationType == typeof(string)); + } + } +} diff --git a/src/dotnetCampus.UITest.WPFTestHelper/CommandLineParsing/RequiredAttribute.cs b/src/dotnetCampus.UITest.WPFTestHelper/CommandLineParsing/RequiredAttribute.cs new file mode 100644 index 0000000..389f54d --- /dev/null +++ b/src/dotnetCampus.UITest.WPFTestHelper/CommandLineParsing/RequiredAttribute.cs @@ -0,0 +1,21 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + + +using System; + +namespace dotnetCampus.UITest.WPFTestHelper.CommandLineParsing +{ + /// + /// Defines whether a property value is required to be specified. + /// + [AttributeUsage(AttributeTargets.Property, Inherited = true, AllowMultiple = false)] + public sealed class RequiredAttribute : Attribute + { + /// + public RequiredAttribute() + { + } + } +} diff --git a/src/dotnetCampus.UITest.WPFTestHelper/DispatcherOperations.cs b/src/dotnetCampus.UITest.WPFTestHelper/DispatcherOperations.cs new file mode 100644 index 0000000..9ca958b --- /dev/null +++ b/src/dotnetCampus.UITest.WPFTestHelper/DispatcherOperations.cs @@ -0,0 +1,119 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + + +using System; +using System.Security; +using System.Security.Permissions; +using System.Windows.Threading; + +namespace dotnetCampus.UITest.WPFTestHelper +{ + /// + /// Helper class for the WPF Dispatcher. This class provides simple and + /// consistent wrappers for common dispatcher operations. + /// + /// + /// + /// + /// // SAMPLE USAGE #1: + /// // Move the mouse to a certain location on the screen. Wait for a popup to appear. + /// // Verify that it appeared. + /// TimeSpan defaultPopupDelay = TimeSpan.FromSeconds(2); + /// Mouse.MoveTo(new System.Drawing.Point(100, 100)); + /// DispatcherOperations.WaitFor(defaultPopupDelay); + /// // verify that the popup showed up. + /// + /// // SAMPLE USAGE #2: + /// // Click on a button and verify that a mouse click event handler gets called. + /// Mouse.MoveTo(new System.Drawing.Point(100, 100)); + /// Mouse.Click(System.Windows.Input.MouseButton.Left); + /// DispatcherOperations.WaitFor(DispatcherPriority.SystemIdle); + /// // verify that the handler has been clicked (e.g. check a isClicked variable) + /// + /// + public static class DispatcherOperations + { + #region Public Methods + + /// + /// This method will wait until all pending DispatcherOperations of a + /// priority higher than the specified priority have been processed. + /// + /// The priority to wait for before continuing. + [System.Security.Permissions.PermissionSet(System.Security.Permissions.SecurityAction.Assert, Name = "FullTrust")] + public static void WaitFor(DispatcherPriority priority) + { + PermissionSet permissions = new PermissionSet(PermissionState.Unrestricted); + permissions.Demand(); + WaitFor(TimeSpan.Zero, priority); + } + + /// + /// This method will wait for the specified TimeSpan, allowing pending + /// DispatcherOperations (such as UI rendering) to continue during that + /// time. This method should be used with caution. Waiting for time is + /// generally discouraged, because it may have an adverse effect on the + /// overall run time of a test suite when the test suite has a large + /// number of tests. + /// + /// Amount of time to wait. + [System.Security.Permissions.PermissionSet(System.Security.Permissions.SecurityAction.Assert, Name = "FullTrust")] + public static void WaitFor(TimeSpan time) + { + PermissionSet permissions = new PermissionSet(PermissionState.Unrestricted); + permissions.Demand(); + WaitFor(time, DispatcherPriority.SystemIdle); + } + + #endregion + + + #region Private Methods + + /// + /// This effectively enables a caller to do all pending UI work before continuing. + /// The method will block until the desired priority has been reached, emptying the + /// Dispatcher queue of all items with a higher priority. + /// + /// Amount of time to wait. + /// The priority to wait for before continuing. + [System.Security.Permissions.PermissionSet(System.Security.Permissions.SecurityAction.Assert, Name = "FullTrust")] + private static void WaitFor(TimeSpan time, DispatcherPriority priority) + { + PermissionSet permissions = new PermissionSet(PermissionState.Unrestricted); + permissions.Demand(); + + // Create a timer for the minimum wait time. + // When the time passes, the Tick handler will be called, + // which allows us to stop the dispatcher frame. + DispatcherTimer timer = new DispatcherTimer(priority); + timer.Tick += new EventHandler(OnDispatched); + timer.Interval = time; + + // Run a dispatcher frame. + DispatcherFrame dispatcherFrame = new DispatcherFrame(false); + timer.Tag = dispatcherFrame; + timer.Start(); + Dispatcher.PushFrame(dispatcherFrame); + } + + /// + /// Dummy SystemIdle dispatcher item. This discontinues the current + /// dispatcher frame so control can return to the caller of WaitFor(). + /// + [System.Security.Permissions.PermissionSet(System.Security.Permissions.SecurityAction.Assert, Name = "FullTrust")] + private static void OnDispatched(object sender, EventArgs args) + { + // Stop the timer now. + DispatcherTimer timer = (DispatcherTimer)sender; + timer.Tick -= new EventHandler(OnDispatched); + timer.Stop(); + DispatcherFrame frame = (DispatcherFrame)timer.Tag; + frame.Continue = false; + } + + #endregion + } +} diff --git a/src/dotnetCampus.UITest.WPFTestHelper/FaultInjection/BuiltInConditions.cs b/src/dotnetCampus.UITest.WPFTestHelper/FaultInjection/BuiltInConditions.cs new file mode 100644 index 0000000..05b41bb --- /dev/null +++ b/src/dotnetCampus.UITest.WPFTestHelper/FaultInjection/BuiltInConditions.cs @@ -0,0 +1,115 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + + +using dotnetCampus.UITest.WPFTestHelper.FaultInjection.Conditions; + +namespace dotnetCampus.UITest.WPFTestHelper.FaultInjection +{ + /// + /// Contains all built-in fault injection conditions. + /// + /// + /// For more information on how to use the BuiltInConditions class, see the class. + /// All fault injection conditions implement the interface. + /// + public static class BuiltInConditions + { + #region Public Members + + /// + /// A built-in condition which triggers a fault every time the faulted method is called. + /// + public static ICondition TriggerOnEveryCall + { + get { return new TriggerOnEveryCall(); } + } + + /// + /// A built-in condition which triggers a fault if the faulted method is called by a specified method. + /// A string in the format: + /// System.Console.WriteLine(string), + /// Namespace<T>.OuterClass<E>.InnerClass<F,G>.MethodName<H>(T, E, F, H, List<H>). + /// + public static ICondition TriggerIfCalledBy(string caller) + { + return new TriggerIfCalledBy(caller); + } + + + /// + /// A built-in condition which triggers a fault if the current call stack contains a specified method. + /// + /// A string in the format: + /// System.Console.WriteLine(string), + /// Namespace<T>.OuterClass<E>.InnerClass<F,G>.MethodName<H>(T, E, F, H, List<H>). + /// + public static ICondition TriggerIfStackContains(string method) + { + return new TriggerIfStackContains(method); + } + + + /// + /// A built-in condition which triggers a fault after every n times the faulted method is called. + /// + /// A positive number. + /// + /// A System.Argument exception is thrown if n is not positive. + /// + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1704")] + public static ICondition TriggerOnEveryNthCall(int n) + { + return new TriggerOnEveryNthCall(n); + } + + + /// + /// A built-in condition which triggers a fault after the first n times the faulted method is called. + /// + /// A positive number. + /// + /// A System.Argument exception is thrown if n is not positive. + /// + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1704")] + public static ICondition TriggerOnNthCall(int n) + { + return new TriggerOnNthCall(n); + } + + + /// + /// A built-in condition which triggers a fault the first time the faulted method is called. + /// + public static ICondition TriggerOnFirstCall + { + get { return new TriggerOnFirstCall(); } + } + + + /// + /// A built-in condition which never triggers a fault. This condition can be used to turn off a fault rule. + /// + public static ICondition NeverTrigger + { + get { return new NeverTrigger(); } + } + + /// + /// A built-in condition which triggers a fault after the faulted method is called n times by the specified caller. + /// + /// A positive number. + /// A string in the format: + /// System.Console.WriteLine(string), + /// Namespace<T>.OuterClass<E>.InnerClass<F,G>.MethodName<H>(T, E, F, H, List<H>). + /// + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1704")] + public static ICondition TriggerOnNthCallBy(int n, string caller) + { + return new TriggerOnNthCallBy(n, caller); + } + + #endregion + } +} diff --git a/src/dotnetCampus.UITest.WPFTestHelper/FaultInjection/BuiltInFaults.cs b/src/dotnetCampus.UITest.WPFTestHelper/FaultInjection/BuiltInFaults.cs new file mode 100644 index 0000000..feabdef --- /dev/null +++ b/src/dotnetCampus.UITest.WPFTestHelper/FaultInjection/BuiltInFaults.cs @@ -0,0 +1,84 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + + +using System; +using dotnetCampus.UITest.WPFTestHelper.FaultInjection.Faults; + +namespace dotnetCampus.UITest.WPFTestHelper.FaultInjection +{ + #region Public Members + + /// + /// Contains all built-in faults. + /// + /// + /// For more information on how to use the BuiltInFaults class, see the class. + /// All fault injection faults implement the interface. + /// + public static class BuiltInFaults + { + /// + /// A built-in fault which returns when triggered. + /// + /// + /// This method can be called when the faulted method has a void return type; + /// it will return null if triggered in a non-void method. + /// + public static IFault ReturnFault() + { + return new ReturnFault(); + } + + /// + /// A built-in fault which returns the specified object when triggered. + /// + /// The object to return. The faulted method will return this object when the fault condition is triggered. + public static IFault ReturnValueFault(object returnValue) + { + return new ReturnValueFault(returnValue); + } + + /// + /// A built-in fault which returns an object constructed according to the specified expression when triggered. + /// + /// A string in the format: + /// (int)3, (double)6.6, (bool)true, Hello World?which means "Hello World", + /// System.Exception(This is a fault?. + /// + + public static IFault ReturnValueRuntimeFault(string returnValueExpression) + { + return new ReturnValueRuntimeFault(returnValueExpression); + } + + /// + /// A built-in fault which throws the specified exception object when triggered. + /// + /// + /// An Exception object constructed by the process that injects the fault. + /// + /// + /// The exception object must be serializable. + /// + public static IFault ThrowExceptionFault(Exception exceptionValue) + { + return new ThrowExceptionFault(exceptionValue); + } + + /// + /// A built-in fault which throws an exception object constructed according to the specified expression when triggered. + /// + /// A string in the format: + /// System.Exception(This is a fault?, + /// CustomizedNameSpace.CustomizedException(Error Message? (int)3, System.Exception(innerException?). + /// + public static IFault ThrowExceptionRuntimeFault(string exceptionExpression) + { + return new ThrowExceptionRuntimeFault(exceptionExpression); + } + + #endregion + } +} diff --git a/src/dotnetCampus.UITest.WPFTestHelper/FaultInjection/BuiltInType.cs b/src/dotnetCampus.UITest.WPFTestHelper/FaultInjection/BuiltInType.cs new file mode 100644 index 0000000..80e0de8 --- /dev/null +++ b/src/dotnetCampus.UITest.WPFTestHelper/FaultInjection/BuiltInType.cs @@ -0,0 +1,38 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + + +namespace dotnetCampus.UITest.WPFTestHelper.FaultInjection +{ + internal static class BuiltInTypeHelper + { + #region Public Methods + + public static string AliasToFullName(string alias) + { + switch (alias) + { + case "bool": return "System.Boolean"; + case "byte": return "System.Byte"; + case "sbyte": return "System.SByte"; + case "char": return "System.Char"; + case "decimal": return "System.Decimal"; + case "double": return "System.Double"; + case "float": return "System.Single"; + case "int": return "System.Int32"; + case "uint": return "System.UInt32"; + case "long": return "System.Int64"; + case "ulong": return "System.UInt64"; + case "object": return "System.Object"; + case "short": return "System.Int16"; + case "ushort": return "System.UInt16"; + case "string": return "System.String"; + case "void": return "System.Void"; + } + return null; + } + + #endregion // Public Methods + } +} diff --git a/src/dotnetCampus.UITest.WPFTestHelper/FaultInjection/CallStack.cs b/src/dotnetCampus.UITest.WPFTestHelper/FaultInjection/CallStack.cs new file mode 100644 index 0000000..2885320 --- /dev/null +++ b/src/dotnetCampus.UITest.WPFTestHelper/FaultInjection/CallStack.cs @@ -0,0 +1,119 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + + +using System; +using System.Diagnostics; +using System.Reflection; +using dotnetCampus.UITest.WPFTestHelper.FaultInjection.SignatureParsing; + +namespace dotnetCampus.UITest.WPFTestHelper.FaultInjection +{ + /// + /// Extracts frame information from a StackTrace object. + /// + /// + /// Calling CallStack[n] (where n is zero-indexed) will return a C#-style method signature for the nth frame. + /// + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1711")] + public class CallStack + { + #region Private Data + + private StackTrace stackTrace; + + #endregion + + #region Constructors + + /// + /// Initializes a new instance of the CallStack class. + /// + /// A stack trace from which to create the CallStack. + public CallStack(StackTrace stackTrace) + { + this.stackTrace = stackTrace; + } + + #endregion + + #region Public Members + + /// + /// Number of Frames in the CallStack. + /// + public int FrameCount + { + get + { + return stackTrace.FrameCount; + } + } + + /// + /// C#-style method signature for the specified frame. + /// + /// frame to evaluate + public String this[int index] + { + get + { + return GetCallStackFunction(index); + } + } + + #endregion + + #region Private Members + + private String GetCallStackFunction(int index) + { + StackFrame stackFrame = stackTrace.GetFrame(index); + + if (stackFrame == null) + { + return null; + } + MethodBase mb = stackFrame.GetMethod(); + ParameterInfo[] paras = mb.GetParameters(); + System.String callingFunc = stackFrame.GetMethod().ToString(); + + string signatureBeforeFunName = MethodSignatureTranslator.GetTypeString(mb.DeclaringType); + + System.String[] temps = callingFunc.Split('('); + if (temps.Length < 2) + { + //error handling + } + temps = temps[0].Split(' '); + if (temps.Length < 2) + { + //error handling + } + temps[0] = temps[1]; + + temps[0] = temps[0].Replace('[', '<'); + temps[0] = temps[0].Replace(']', '>'); + temps[0] = temps[0].Insert(0, signatureBeforeFunName + "."); + temps[0] = temps[0].Insert(temps[0].Length, "("); + + foreach (ParameterInfo p in paras) + { + String typeString = MethodSignatureTranslator.GetTypeString(p.ParameterType); + + temps[0] = temps[0].Insert(temps[0].Length, typeString); + temps[0] = temps[0].Insert(temps[0].Length, ","); + } + if (paras != null && paras.Length > 0) + { + temps[0] = temps[0].Remove(temps[0].Length - 1); + } + temps[0] = temps[0].Insert(temps[0].Length, ")"); + callingFunc = temps[0]; + return callingFunc; + } + + #endregion + } +} diff --git a/src/dotnetCampus.UITest.WPFTestHelper/FaultInjection/ComRegistrar.cs b/src/dotnetCampus.UITest.WPFTestHelper/FaultInjection/ComRegistrar.cs new file mode 100644 index 0000000..e029cb7 --- /dev/null +++ b/src/dotnetCampus.UITest.WPFTestHelper/FaultInjection/ComRegistrar.cs @@ -0,0 +1,145 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + + +using System; +using System.Diagnostics; +using System.Globalization; +using System.IO; +using System.Reflection; +using System.Runtime.InteropServices; +using dotnetCampus.UITest.WPFTestHelper.FaultInjection.Constants; +using Microsoft.Win32; + +namespace dotnetCampus.UITest.WPFTestHelper.FaultInjection +{ + /// + /// Provides facilities for registration and query of the fault injection COM engine. + /// + public static class ComRegistrar + { + #region Private Data + + private static bool registered = false; + + #endregion + + #region Public Members + + /// + /// CLSID of the fault injection COM engine. + /// + public const string Clsid = EngineInfo.Engine_CLSID; + + /// + /// Registers the fault injection COM engine. + /// + /// Path name of engine file. + public static void Register(string enginePathName) + { + if (Registry.ClassesRoot.OpenSubKey(EngineInfo.Engine_RegistryKey) == null) + { + if (String.IsNullOrEmpty(enginePathName)) + { + enginePathName = CalculateEnginePath(); + } + Process regsvr32 = Process.Start("regsvr32", "/s \"" + enginePathName + "\""); + regsvr32.WaitForExit(); + if (regsvr32.ExitCode != 0) + { + enginePathName = Path.GetFullPath(enginePathName); + string errorMessage = null; + switch (regsvr32.ExitCode) + { + case 3: + errorMessage = ApiErrorMessages.RegisterEngineFileNotFound; + break; + case 5: + errorMessage = ApiErrorMessages.RegisterEngineAccessDenied; + break; + default: + errorMessage = ApiErrorMessages.RegisterEngineFailed; + break; + } + throw new FaultInjectionException( + string.Format(CultureInfo.CurrentCulture, errorMessage, regsvr32.ExitCode, enginePathName)); + } + } + } + + /// + /// Unregisters the fault injection COM engine. + /// + public static void Unregister(string enginePathName) + { + if (String.IsNullOrEmpty(enginePathName)) + { + enginePathName = CalculateEnginePath(); + } + Process regsvr32 = Process.Start("regsvr32", " /s /u \"" + enginePathName + "\""); + regsvr32.WaitForExit(); + if (regsvr32.ExitCode != 0) + { + enginePathName = Path.GetFullPath(enginePathName); + string errorMessage = null; + switch (regsvr32.ExitCode) + { + case 3: + errorMessage = ApiErrorMessages.RegisterEngineFileNotFound; + break; + case 5: + errorMessage = ApiErrorMessages.RegisterEngineAccessDenied; + break; + default: + errorMessage = ApiErrorMessages.RegisterEngineFailed; + break; + } + throw new FaultInjectionException( + string.Format(CultureInfo.CurrentCulture, errorMessage, regsvr32.ExitCode, enginePathName)); + } + + } + + /// + /// Supresses auto-registration behaviour of the fault injection API. + /// + public static void SuppressAutoRegister() + { + registered = true; + } + + #endregion + + #region Internal Members + + internal static void AutoRegister() + { + if (registered) { return; } + + Register(null); + registered = true; + } + + #endregion + + #region Private Members + + private static string CalculateEnginePath() + { + string assemblyDir = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location); + //determine if we are in a 32 or 64 bit process + if (Marshal.SizeOf(new IntPtr()) == 8) + { + assemblyDir = Path.Combine(assemblyDir, "FaultInjectionEngine\\x64\\"); + } + else + { + assemblyDir = Path.Combine(assemblyDir, "FaultInjectionEngine\\x86\\"); + } + return Path.Combine(assemblyDir, EngineInfo.FaultEngineFileName); + } + + #endregion + } +} diff --git a/src/dotnetCampus.UITest.WPFTestHelper/FaultInjection/Conditions/NeverTrigger.cs b/src/dotnetCampus.UITest.WPFTestHelper/FaultInjection/Conditions/NeverTrigger.cs new file mode 100644 index 0000000..e5fee69 --- /dev/null +++ b/src/dotnetCampus.UITest.WPFTestHelper/FaultInjection/Conditions/NeverTrigger.cs @@ -0,0 +1,18 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + + +using System; + +namespace dotnetCampus.UITest.WPFTestHelper.FaultInjection.Conditions +{ + [Serializable()] + internal sealed class NeverTrigger : ICondition + { + public bool Trigger(IRuntimeContext context) + { + return false; + } + } +} diff --git a/src/dotnetCampus.UITest.WPFTestHelper/FaultInjection/Conditions/TriggerIfCalledBy.cs b/src/dotnetCampus.UITest.WPFTestHelper/FaultInjection/Conditions/TriggerIfCalledBy.cs new file mode 100644 index 0000000..eea146e --- /dev/null +++ b/src/dotnetCampus.UITest.WPFTestHelper/FaultInjection/Conditions/TriggerIfCalledBy.cs @@ -0,0 +1,29 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + + +using System; +using dotnetCampus.UITest.WPFTestHelper.FaultInjection.SignatureParsing; + +namespace dotnetCampus.UITest.WPFTestHelper.FaultInjection.Conditions +{ + [Serializable()] + internal sealed class TriggerIfCalledBy : ICondition + { + public TriggerIfCalledBy(String aTargetCaller) + { + targetCaller = Signature.ConvertSignature(aTargetCaller); + } + + public bool Trigger(IRuntimeContext context) + { + if (context.Caller == targetCaller) + { + return true; + } + return false; + } + private String targetCaller; + } +} diff --git a/src/dotnetCampus.UITest.WPFTestHelper/FaultInjection/Conditions/TriggerIfStackContains.cs b/src/dotnetCampus.UITest.WPFTestHelper/FaultInjection/Conditions/TriggerIfStackContains.cs new file mode 100644 index 0000000..2df7121 --- /dev/null +++ b/src/dotnetCampus.UITest.WPFTestHelper/FaultInjection/Conditions/TriggerIfStackContains.cs @@ -0,0 +1,36 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + + +using System; +using dotnetCampus.UITest.WPFTestHelper.FaultInjection.SignatureParsing; + +namespace dotnetCampus.UITest.WPFTestHelper.FaultInjection.Conditions +{ + [Serializable()] + internal sealed class TriggerIfStackContains : ICondition + { + public TriggerIfStackContains(String aTargetFunction) + { + targetFunction = Signature.ConvertSignature(aTargetFunction); + } + + public bool Trigger(IRuntimeContext context) + { + for (int i = 0; i < context.CallStack.FrameCount; ++i) + { + if (context.CallStack[i] == null) + { + return false; + } + else if (context.CallStack[i] == targetFunction) + { + return true; + } + } + return false; + } + private String targetFunction; + } +} diff --git a/src/dotnetCampus.UITest.WPFTestHelper/FaultInjection/Conditions/TriggerOnEveryCall.cs b/src/dotnetCampus.UITest.WPFTestHelper/FaultInjection/Conditions/TriggerOnEveryCall.cs new file mode 100644 index 0000000..fbb45a9 --- /dev/null +++ b/src/dotnetCampus.UITest.WPFTestHelper/FaultInjection/Conditions/TriggerOnEveryCall.cs @@ -0,0 +1,18 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + + +using System; + +namespace dotnetCampus.UITest.WPFTestHelper.FaultInjection.Conditions +{ + [Serializable()] + internal sealed class TriggerOnEveryCall : ICondition + { + public bool Trigger(IRuntimeContext context) + { + return true; + } + } +} diff --git a/src/dotnetCampus.UITest.WPFTestHelper/FaultInjection/Conditions/TriggerOnEveryNthCall.cs b/src/dotnetCampus.UITest.WPFTestHelper/FaultInjection/Conditions/TriggerOnEveryNthCall.cs new file mode 100644 index 0000000..57ad26c --- /dev/null +++ b/src/dotnetCampus.UITest.WPFTestHelper/FaultInjection/Conditions/TriggerOnEveryNthCall.cs @@ -0,0 +1,32 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + + +using System; + +namespace dotnetCampus.UITest.WPFTestHelper.FaultInjection.Conditions +{ + [Serializable()] + internal sealed class TriggerOnEveryNthCall : ICondition + { + public TriggerOnEveryNthCall(int nth) + { + if (nth <= 0) + { + throw new ArgumentException("The parameter of TriggerEveryNthCall(int) should be a positive number"); + } + this.n = nth; + } + + public bool Trigger(IRuntimeContext context) + { + if (context.CalledTimes % n == 0) + { + return true; + } + return false; + } + private int n; + } +} diff --git a/src/dotnetCampus.UITest.WPFTestHelper/FaultInjection/Conditions/TriggerOnFirstCall.cs b/src/dotnetCampus.UITest.WPFTestHelper/FaultInjection/Conditions/TriggerOnFirstCall.cs new file mode 100644 index 0000000..113c9e9 --- /dev/null +++ b/src/dotnetCampus.UITest.WPFTestHelper/FaultInjection/Conditions/TriggerOnFirstCall.cs @@ -0,0 +1,24 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + + +using System; + +namespace dotnetCampus.UITest.WPFTestHelper.FaultInjection.Conditions +{ + [Serializable()] + internal sealed class TriggerOnFirstCall : ICondition + { + public bool Trigger(IRuntimeContext context) + { + if (triggered == false) + { + triggered = true; + return true; + } + return false; + } + private bool triggered = false; + } +} diff --git a/src/dotnetCampus.UITest.WPFTestHelper/FaultInjection/Conditions/TriggerOnNthCall.cs b/src/dotnetCampus.UITest.WPFTestHelper/FaultInjection/Conditions/TriggerOnNthCall.cs new file mode 100644 index 0000000..b3ac03c --- /dev/null +++ b/src/dotnetCampus.UITest.WPFTestHelper/FaultInjection/Conditions/TriggerOnNthCall.cs @@ -0,0 +1,32 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + + +using System; + +namespace dotnetCampus.UITest.WPFTestHelper.FaultInjection.Conditions +{ + [Serializable()] + internal sealed class TriggerOnNthCall : ICondition + { + public TriggerOnNthCall(int nth) + { + if (nth <= 0) + { + throw new ArgumentException("The parameter of TriggerOnNthCall(int) should be a postive number"); + } + this.n = nth; + } + + public bool Trigger(IRuntimeContext context) + { + if (context.CalledTimes == n) + { + return true; + } + return false; + } + private int n; + } +} diff --git a/src/dotnetCampus.UITest.WPFTestHelper/FaultInjection/Conditions/TriggerOnNthCallBy.cs b/src/dotnetCampus.UITest.WPFTestHelper/FaultInjection/Conditions/TriggerOnNthCallBy.cs new file mode 100644 index 0000000..29010b0 --- /dev/null +++ b/src/dotnetCampus.UITest.WPFTestHelper/FaultInjection/Conditions/TriggerOnNthCallBy.cs @@ -0,0 +1,40 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + + +using System; +using dotnetCampus.UITest.WPFTestHelper.FaultInjection.SignatureParsing; + +namespace dotnetCampus.UITest.WPFTestHelper.FaultInjection.Conditions +{ + [Serializable()] + internal sealed class TriggerOnNthCallBy : ICondition + { + public TriggerOnNthCallBy(int nth, String aTargetCaller) + { + calledTimes = 0; + n = nth; + if (nth <= 0) + { + throw new ArgumentException("The first parameter of TriggerOnNthCallBy(int, string) should be a postive number"); + } + targetCaller = Signature.ConvertSignature(aTargetCaller); + } + + public bool Trigger(IRuntimeContext context) + { + if (context.Caller == targetCaller) + { + if ((++calledTimes) == n) + { + return true; + } + } + return false; + } + private int calledTimes; + private int n; + private String targetCaller; + } +} diff --git a/src/dotnetCampus.UITest.WPFTestHelper/FaultInjection/Constants/ApiErrorMessages.cs b/src/dotnetCampus.UITest.WPFTestHelper/FaultInjection/Constants/ApiErrorMessages.cs new file mode 100644 index 0000000..1dcc3ab --- /dev/null +++ b/src/dotnetCampus.UITest.WPFTestHelper/FaultInjection/Constants/ApiErrorMessages.cs @@ -0,0 +1,37 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + + +namespace dotnetCampus.UITest.WPFTestHelper.FaultInjection.Constants +{ + internal static class ApiErrorMessages + { + // Error messages for class FaultRule + public const string ConditionNull = "Condition object of a fault rule can't be null."; + public const string FaultNull = "Fault object of a fault rule can't be null."; + + // Error messages for class FaultSession + public const string FaultRulesNullOrEmpty = "Fault rules used to initialize FaultSession instance can't be null or empty."; + public const string FaultRulesConflict = "Each method can attach only one FaultRule in one FaultSession."; + public const string LauncherNull = "Launch delegate can't be null"; + public const string UnableToCreateFileReadWriteMutex = "Unable to create Mutex for file I/O with name \"{0}\"."; + public const string RegisterEngineFailed = "Register fault injection engine file \"{1}\" failed. Regsvr32 return error code 0x{0:X} "; + public const string RegisterEngineAccessDenied = "Register fault injection engine file \"{1}\" failed. You should run fault injection tool as Administrator(e.g. Run cmd or Visual Studio as Administrator), error code 0x{0:X}."; + public const string RegisterEngineFileNotFound = "Cannot find fault injection engine file \"{1}\". Please check if the file exists, error code 0x{0:X}."; + + public const string LogDirectoryNullOrEmpty = "Directory for log files can't be null or empty."; + + // Error messages for FaultRuleLoader + public const string UnableToFindEnvironmentVariable = "Can't find environment variable \"{0}\""; + + // Error messages for faults + public const string ExceptionNull = "The exception object used to initialize ThrowExceptionFault can't be null."; + public const string ExpressionNullOrEmpty = "Expression to specify exception want to be thrown can't be null or empty."; + + // Error messages for Helpers + public const string MethodSignatureNullOrEmpty = "Method signature can't be null or empty."; + public const string InvalidMethodSignature = "Invalid method signature: {0}"; + public const string FaultRulesNullInSerialization = "Can't serialize null FaultRule[] instance"; + } +} diff --git a/src/dotnetCampus.UITest.WPFTestHelper/FaultInjection/Constants/EngineInfo.cs b/src/dotnetCampus.UITest.WPFTestHelper/FaultInjection/Constants/EngineInfo.cs new file mode 100644 index 0000000..16861b0 --- /dev/null +++ b/src/dotnetCampus.UITest.WPFTestHelper/FaultInjection/Constants/EngineInfo.cs @@ -0,0 +1,20 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + + +namespace dotnetCampus.UITest.WPFTestHelper.FaultInjection.Constants +{ + internal static class EngineInfo + { + // FaultSession namespace + public const string NameSpace = "Microsoft.Test.FaultInjection"; + + // CLSID and registry key for profiling callback COM component + public const string Engine_CLSID = "{2EB6DCDB-3250-4D7F-AA42-41B1B84113ED}"; + public const string Engine_RegistryKey = @"CLSID\" + Engine_CLSID; + + // File name for profiling callback COM dll + public const string FaultEngineFileName = "FaultInjectionEngine.dll"; + } +} diff --git a/src/dotnetCampus.UITest.WPFTestHelper/FaultInjection/Constants/EnvironmentVariable.cs b/src/dotnetCampus.UITest.WPFTestHelper/FaultInjection/Constants/EnvironmentVariable.cs new file mode 100644 index 0000000..21b031b --- /dev/null +++ b/src/dotnetCampus.UITest.WPFTestHelper/FaultInjection/Constants/EnvironmentVariable.cs @@ -0,0 +1,21 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + + +namespace dotnetCampus.UITest.WPFTestHelper.FaultInjection.Constants +{ + internal static class EnvironmentVariable + { + // Environment variables we used to pass informations + public const string EnableProfiling = "COR_ENABLE_PROFILING"; + public const string Proflier = "COR_PROFILER"; + public const string RuleRepository = "FAULT_INJECTION_RULE_REPOSITORY"; + public const string MethodFilter = "FAULT_INJECTION_METHOD_FILTER"; + public const string LogDirectory = "FAULT_INJECTION_LOG_DIR"; + public const string LogVerboseLevel = "FAULT_INJECTION_LOG_LEVEL"; + + // This flag is necessary to enable code injection in CLR4 binaries + public const string CLR4Compatibility = "COMPLUS_ProfAPI_ProfilerCompatibilitySetting"; + } +} diff --git a/src/dotnetCampus.UITest.WPFTestHelper/FaultInjection/Constants/FaultDispatcherMessages.cs b/src/dotnetCampus.UITest.WPFTestHelper/FaultInjection/Constants/FaultDispatcherMessages.cs new file mode 100644 index 0000000..c8ed47f --- /dev/null +++ b/src/dotnetCampus.UITest.WPFTestHelper/FaultInjection/Constants/FaultDispatcherMessages.cs @@ -0,0 +1,31 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + + +namespace dotnetCampus.UITest.WPFTestHelper.FaultInjection.Constants +{ + internal static class FaultDispatcherMessages + { + //info + public const string FaultRuleFound = "Function Faulted, Condition & Trigger found"; + public const string ConditionTriggered = "Condition Triggered"; + public const string ConditionNotTriggered = "Condition Not Triggered"; + public const string NoCondition = "No Condition found"; + public const string NoFault = "No Fault found"; + public const string FaultType = "Fault Type: {0}"; + public const string ReturnTypeVoid = "Return Type is void, will directly return"; + + //error + public const string LoadFaultRuleError = "Fatal Error when Loading Fault Rules!"; + public const string NoExceptionAllowedInTriggerAndRetrieve = "Unexpected Exception thrown in ICondition.Trigger() & IFault.Retrieve()"; + public const string UnknownExceptionInTrap = "Exception occurred inside Trap(), refer to InnerException for more info"; + public const string ReturnValueTypeNullError = "Return type '{0}' is Value Type, its value cannot be 'null'"; + public const string ReturnTypeMismatchError = "Return type mismatch, Require '{0}', but get '{1}'"; + + //accurate info + public const string ReturnValueSet = "Return value of type '{0}' is successfully set to '{1}'"; + public const string ExceptionValueSet = "Exception value of type '{0}' is successfully created"; + + } +} diff --git a/src/dotnetCampus.UITest.WPFTestHelper/FaultInjection/Constants/XmlErrorMessages.cs b/src/dotnetCampus.UITest.WPFTestHelper/FaultInjection/Constants/XmlErrorMessages.cs new file mode 100644 index 0000000..9bdbfa5 --- /dev/null +++ b/src/dotnetCampus.UITest.WPFTestHelper/FaultInjection/Constants/XmlErrorMessages.cs @@ -0,0 +1,29 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + + +namespace dotnetCampus.UITest.WPFTestHelper.FaultInjection.Constants +{ + internal static class XmlErrorMessages + { + // Error messages for Expression + public const string ArrayItemTypeError = "Array item type not compatible with array type"; + public const string ConstructorNotFound = "Constructor for class {0} with specific parameter(s) not found."; + public const string ExpressionFormatError = "Expression format error"; + public const string NoParser = "No parser for type {0}"; + public const string TargetAssemblyNotFound = "Target assembly {0} not found"; + public const string TargetTypeNotFound = "Target type {0} not found"; + + // Error messages for XmlFaultSession + public const string ElementError = "{0} element is missing or is too many"; + public const string FileNotFound = "Xml file {0} not found. Try using (SomeType){0}"; + public const string IndexOutOfBound = "Specified test case index {0} out of bound"; + public const string SchemaValidationError = "Schema validation error"; + public const string SchemaValidationErrorHeader = "\r\n\tSchema Validation error: "; + public const string TestCaseNotFound = "Specified test case \"{0}\" not found"; + public const string ThreeItemMessage = "\n at {0} {1} '{2}'"; + public const string TwoItemMessage = "\n at {0} {1}"; + public const string XmlFileError = "Xml File {0} error"; + } +} diff --git a/src/dotnetCampus.UITest.WPFTestHelper/FaultInjection/FaultDispatcher.cs b/src/dotnetCampus.UITest.WPFTestHelper/FaultInjection/FaultDispatcher.cs new file mode 100644 index 0000000..d348e77 --- /dev/null +++ b/src/dotnetCampus.UITest.WPFTestHelper/FaultInjection/FaultDispatcher.cs @@ -0,0 +1,202 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + + +using System; +using System.Diagnostics; +using System.Globalization; +using System.Reflection; +using dotnetCampus.UITest.WPFTestHelper.FaultInjection.Constants; +using dotnetCampus.UITest.WPFTestHelper.FaultInjection.SignatureParsing; + +namespace dotnetCampus.UITest.WPFTestHelper.FaultInjection +{ + /// + /// Calls the appropriate fault for a given faulted method. + /// + /// + /// The FaultDispatcher class contains a static method , which is called by the MSIL code, + /// injected in the faulted method, in order to dispatch to the specific fault. + /// + public static class FaultDispatcher + { + #region Private Data + + static RuntimeContext[] contexts = null; + static object initialized = new Object(); + + #endregion + + #region Public Members + + /// + /// Injected into the prologue of the target method. + /// + /// Exception thrown by fault + /// Value to return from fault + /// + /// + /// Trap creates a RuntimeContext for the current call and evaluates + /// the fault condition's Trigger method. If it evaluates to true + /// the fault's Retrieve method is called. + /// + public static bool Trap(out Exception exceptionValue, out Object returnValue) + { + StackTrace stackTrace = new StackTrace(1); + CallStack callStack = new CallStack(stackTrace); + //stackFrame does not include Trap() + StackFrame stackFrame = stackTrace.GetFrame(0); + String currentFunction = callStack[0]; + + exceptionValue = null; + returnValue = null; + FaultRule[] newRules; + + + if (GetNewRulesAndPrepareContext(out newRules, currentFunction) == false) + { + return false; + } + + RuntimeContext currentContext = null; + FaultRule rule = null; + + + try + { + //Get Fault Rule and Update current RuntimeContext + int len = contexts.Length; + for (int i = 0; i < len; ++i) + { + if (newRules[i].FormalSignature != null && + currentFunction.Equals(newRules[i].FormalSignature) == true) + { + rule = newRules[i]; + currentContext = new RuntimeContext(); + lock (contexts) + { + contexts[i].CalledTimes++; + currentContext.CalledTimes = contexts[i].CalledTimes; + } + currentContext.CallStack = callStack; + currentContext.CallStackTrace = stackTrace; + + break; + } + } + + //Using ICondition and IFault + if (rule == null) + { + return false; + } + bool triggered = false; + try + { + lock (contexts) + { + triggered = rule.Condition.Trigger(currentContext); + if (triggered == true) + { + rule.Fault.Retrieve(currentContext, out (exceptionValue), out returnValue); + } + } + + } + catch (System.Exception e) + { + throw new FaultInjectionException(FaultDispatcherMessages.NoExceptionAllowedInTriggerAndRetrieve, e); + } + if (!triggered) + { + return false; + } + } + catch (System.Exception e) + { + throw new FaultInjectionException(FaultDispatcherMessages.UnknownExceptionInTrap, e); + } + + + // check return-value's type if not to throw exception + if (null == exceptionValue) + { + if (stackFrame.GetMethod() is ConstructorInfo) + { + return true; + } + Type returnTypeOfTrappedMethod = ((MethodInfo)stackFrame.GetMethod()).ReturnType; + return CheckReturnType(returnTypeOfTrappedMethod, returnValue, currentFunction); + } + return true; + + } + + #endregion + + #region Private Members + + private static bool GetNewRulesAndPrepareContext(out FaultRule[] newRules, string currentFunction) + { + try + { + newRules = FaultRuleLoader.Load(); + } + catch (FaultInjectionException e) + { + throw e; + } + catch (Exception e) + { + throw new FaultInjectionException(FaultDispatcherMessages.LoadFaultRuleError, e); + } + if (newRules == null || newRules.Length == 0) + { + return false; + } + + lock(initialized) + { + if (contexts == null) + { + int length = newRules.Length; + contexts = new RuntimeContext[length]; + for (int i = 0; i < length; ++i) + { + contexts[i] = new RuntimeContext(); + } + } + } + + return true; + } + + private static bool CheckReturnType(Type returnTypeOfTrappedMethod, Object returnValue, String currentFunction) + { + // + // If the return type is or List, we simply omit it. + // + if (returnTypeOfTrappedMethod.IsGenericParameter || returnTypeOfTrappedMethod.ContainsGenericParameters) + { + return true; + } + + if (returnTypeOfTrappedMethod != typeof(void)) + { + if (returnValue == null && returnTypeOfTrappedMethod.IsValueType == true) + { + throw new FaultInjectionException(string.Format(CultureInfo.InvariantCulture.NumberFormat, FaultDispatcherMessages.ReturnValueTypeNullError, MethodSignatureTranslator.GetTypeString(returnTypeOfTrappedMethod))); + } + else if (returnValue != null && + returnTypeOfTrappedMethod.IsInstanceOfType(returnValue) == false) + { + throw new FaultInjectionException(string.Format(CultureInfo.InvariantCulture.NumberFormat, FaultDispatcherMessages.ReturnTypeMismatchError, MethodSignatureTranslator.GetTypeString(returnTypeOfTrappedMethod), MethodSignatureTranslator.GetTypeString(returnValue.GetType()))); + } + } + return true; + } + + #endregion + } +} diff --git a/src/dotnetCampus.UITest.WPFTestHelper/FaultInjection/FaultInjectionException.cs b/src/dotnetCampus.UITest.WPFTestHelper/FaultInjection/FaultInjectionException.cs new file mode 100644 index 0000000..3fad21a --- /dev/null +++ b/src/dotnetCampus.UITest.WPFTestHelper/FaultInjection/FaultInjectionException.cs @@ -0,0 +1,42 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + + +using System; +using System.Runtime.Serialization; + +namespace dotnetCampus.UITest.WPFTestHelper.FaultInjection +{ + /// + /// An exception that is thrown when and error in the FaultInjection API occurs. + /// + [Serializable] + public class FaultInjectionException : Exception + { + #region Constructors + + /// + /// Initializes a new instance of the FaultInjectionException class. + /// + public FaultInjectionException() { } + + /// + /// Initializes a new instance of the FaultInjectionException class using the specified message. + /// + public FaultInjectionException(string message) : base(message) { } + + /// + /// Initializes a new instance of the FaultInjectionException class using the specified message and inner + /// exception. + /// + public FaultInjectionException(string message, Exception innerException) : base(message, innerException) { } + + /// + /// Constructor used for serialization purposes. + /// + protected FaultInjectionException(SerializationInfo info, StreamingContext context) : base(info, context) { } + + #endregion + } +} diff --git a/src/dotnetCampus.UITest.WPFTestHelper/FaultInjection/FaultRule.cs b/src/dotnetCampus.UITest.WPFTestHelper/FaultInjection/FaultRule.cs new file mode 100644 index 0000000..07d471f --- /dev/null +++ b/src/dotnetCampus.UITest.WPFTestHelper/FaultInjection/FaultRule.cs @@ -0,0 +1,176 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + + +using System; +using dotnetCampus.UITest.WPFTestHelper.FaultInjection.Conditions; +using dotnetCampus.UITest.WPFTestHelper.FaultInjection.Constants; +using dotnetCampus.UITest.WPFTestHelper.FaultInjection.Faults; +using dotnetCampus.UITest.WPFTestHelper.FaultInjection.SignatureParsing; + +namespace dotnetCampus.UITest.WPFTestHelper.FaultInjection +{ + /// + /// Defines which method to fault, under what conditions the fault will occur, + /// and how the method will fail. See also the class. + /// + /// + /// There can only be one fault rule per target method. + /// + /// + /// + /// The following example creates a new FaultSession with several different + /// FaultRules and launches the application. + /// + /// string sampleAppPath = "SampleApp.exe"; + /// + /// FaultRule[] ruleArray = new FaultRule[] + /// { + /// // Instance method + /// new FaultRule( + /// "SampleApp.TargetType.TargetMethod(string, string)", + /// BuiltInConditions.TriggerOnEveryCall, + /// BuiltInFaults.ReturnValueFault("")), + /// + /// // Constructor + /// new FaultRule( + /// "SampleApp.TargetType.TargetType(string, int)", + /// BuiltInConditions.TriggerOnEveryCall, + /// BuiltInFaults.ThrowExceptionFault(new InvalidOperationException())), + /// + /// // Static method + /// new FaultRule( + /// "static SampleApp.TargetType.StaticTargetMethod()", + /// BuiltInConditions.TriggerOnEveryNthCall(2), + /// BuiltInFaults.ThrowExceptionFault(new InvalidOperationException())), + /// + /// // Generic method + /// new FaultRule( + /// "SampleApp.TargetType.GenericTargetMethod<T>(string)", + /// BuiltInConditions.TriggerOnEveryCall, + /// BuiltInFaults.ReturnFault()), + /// + /// // Property + /// new FaultRule( + /// "SampleApp.TargetType.get_TargetProperty()", + /// BuiltInConditions.TriggerOnEveryNthCall(3), + /// BuiltInFaults.ReturnFault()), + /// + /// // Operator overload + /// new FaultRule( + /// "SampleApp.TargetType.op_Increment(int, int)", + /// BuiltInConditions.TriggerOnEveryCall, + /// BuiltInFaults.ThrowExceptionFault(new InvalidOperationException())) + /// }; + /// + /// FaultSession session = new FaultSession(ruleArray); + /// ProcessStartInfo psi = session.GetProcessStartInfo(sampleAppPath); + /// Process.Start(psi); + /// + /// + [Serializable()] + public sealed class FaultRule + { + #region Private Data + + private string method = null; + private string formalSignature = null; + private ICondition condition = new NeverTrigger(); + private IFault fault = new ReturnFault(); + private int serializationVersion = 0; + + #endregion + + #region Constructors + + /// + /// Initializes a new instance of FaultRule class with the specified method. + /// + /// The signature of the method where the fault should be injected. + public FaultRule(string method) + { + this.formalSignature = Signature.ConvertSignature(method, SignatureStyle.Formal); + this.method = method; + } + + /// + /// Initializes a new instance of FaultRule class with the specified method, condition and fault. + /// + /// The signature of the method where the fault should be injected. + /// The condition that defines when the fault should occur. + /// The fault to be injected. + public FaultRule(string method, ICondition condition, IFault fault) + : this(method) + { + this.Condition = condition; + this.Fault = fault; + } + + #endregion // Constructors + + #region Public Members + + /// + /// The signature of the method where the fault should be injected. + /// + public string MethodSignature + { + get { return method; } + } + + /// + /// The condition that defines when the fault should occur. + /// + public ICondition Condition + { + get { return condition; } + set + { + if (value == null) + { + throw new FaultInjectionException(ApiErrorMessages.ConditionNull); + } + condition = value; + } + } + + /// + /// The fault to be injected. + /// + public IFault Fault + { + get { return fault; } + set + { + if (value == null) + { + throw new FaultInjectionException(ApiErrorMessages.FaultNull); + } + fault = value; + } + } + + #endregion + + #region Internal Members + + // This is the formal signature which is more strict. Users won't see this but internally, we use + // this to specify method . + internal string FormalSignature + { + get { return formalSignature; } + } + + // This property records "version" of this rule from viewpoint of serialization. + // Each time the rule is serialized after some changes, the version number increased. + // We need this when we load rule by deserialization. + internal int SerializationVersion + { + get { return serializationVersion; } + set { serializationVersion = value; } + } + + #endregion + } +} diff --git a/src/dotnetCampus.UITest.WPFTestHelper/FaultInjection/FaultRuleLoader.cs b/src/dotnetCampus.UITest.WPFTestHelper/FaultInjection/FaultRuleLoader.cs new file mode 100644 index 0000000..149be29 --- /dev/null +++ b/src/dotnetCampus.UITest.WPFTestHelper/FaultInjection/FaultRuleLoader.cs @@ -0,0 +1,84 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + + +using System; +using System.Globalization; +using System.IO; +using System.Threading; +using dotnetCampus.UITest.WPFTestHelper.FaultInjection.Constants; + +namespace dotnetCampus.UITest.WPFTestHelper.FaultInjection +{ + internal static class FaultRuleLoader + { + #region Private Data + + private static Mutex fileReadWriteMutex; // Shared with tester and testee process + private static FaultRule[] currentRules; + private static object initializeLock = new object(); + private static object accessCurrentRuleLock = new object(); + + #endregion + + #region Public Members + + public static FaultRule[] Load() + { + string serializationFileName = Environment.GetEnvironmentVariable(EnvironmentVariable.RuleRepository); + if (serializationFileName == null) + { + throw new FaultInjectionException(string.Format(CultureInfo.CurrentCulture, ApiErrorMessages.UnableToFindEnvironmentVariable, EnvironmentVariable.RuleRepository)); + } + + lock (initializeLock) + { + if (fileReadWriteMutex == null) + { + fileReadWriteMutex = new Mutex(false, Path.GetFileName(serializationFileName)); + currentRules = Serializer.DeserializeRules(serializationFileName, fileReadWriteMutex); + + return currentRules; + } + } + + FaultRule[] swapBuffer; + lock (accessCurrentRuleLock) + { + swapBuffer = new FaultRule[currentRules.Length]; + currentRules.CopyTo(swapBuffer, 0); + } + + FaultRule[] loadedRules = Serializer.DeserializeRules(serializationFileName, fileReadWriteMutex); + MergeRuleArray(swapBuffer, loadedRules); + lock (accessCurrentRuleLock) + { + currentRules = swapBuffer; + } + + return currentRules; + } + + #endregion + + #region Private Members + + private static void MergeRuleArray(FaultRule[] current, FaultRule[] loaded) + { + foreach (FaultRule loadedRule in loaded) + { + int i = Array.FindIndex( + current, + delegate(FaultRule ithRule) { return ithRule.FormalSignature == loadedRule.FormalSignature; } + ); + if (i != -1 && current[i].SerializationVersion < loadedRule.SerializationVersion) + { + current[i] = loadedRule; + } + } + } + + #endregion + } +} diff --git a/src/dotnetCampus.UITest.WPFTestHelper/FaultInjection/FaultSession.cs b/src/dotnetCampus.UITest.WPFTestHelper/FaultInjection/FaultSession.cs new file mode 100644 index 0000000..1b08efc --- /dev/null +++ b/src/dotnetCampus.UITest.WPFTestHelper/FaultInjection/FaultSession.cs @@ -0,0 +1,339 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Globalization; +using System.IO; +using System.Threading; +using dotnetCampus.UITest.WPFTestHelper.FaultInjection.Constants; +using dotnetCampus.UITest.WPFTestHelper.FaultInjection.SignatureParsing; + +namespace dotnetCampus.UITest.WPFTestHelper.FaultInjection +{ + /// + /// Maintains information needed for injecting faults into a test application. + /// For general information on fault injection see this page. + /// + /// + /// Users can launch the faulted application by calling GetProcessStartInfo(string) and calling Process.Start() + /// with the returned ProcessStartInfo. + /// + /// + /// + /// The following example creates a new FaultSession with a single FaultRule and launches the application under test. + /// + /// string sampleAppPath = "SampleApp.exe"; + /// + /// FaultRule rule = new FaultRule( + /// "SampleApp.TargetMethod(string, string)", + /// BuiltInConditions.TriggerOnEveryCall, + /// BuiltInFaults.ReturnValueFault("")); + /// + /// FaultSession session = new FaultSession(rule); + /// ProcessStartInfo psi = session.GetProcessStartInfo(sampleAppPath); + /// Process.Start(psi); + /// + /// + /// + /// + /// The following example creates a new FaultSession with multiple FaultRules and launches the application under test. + /// + /// string sampleAppPath = "SampleApp.exe"; + /// + /// FaultRule[] ruleArray = new FaultRule[] + /// { + /// new FaultRule( + /// "SampleApp.TargetMethod(string, string)", + /// BuiltInConditions.TriggerOnEveryCall, + /// BuiltInFaults.ReturnValueFault("")), + /// + /// new FaultRule( + /// "SampleApp.TargetMethod2(string, int)", + /// BuiltInConditions.TriggerOnEveryCall, + /// BuiltInFaults.ReturnValueFault(Int32.MaxValue)), + /// + /// new FaultRule( + /// "static SampleApp.StaticTargetMethod()", + /// BuiltInConditions.TriggerOnEveryNthCall(2), + /// BuiltInFaults.ThrowExceptionFault(new InvalidOperationException())) + /// }; + /// + /// FaultSession session = new FaultSession(ruleArray); + /// ProcessStartInfo psi = session.GetProcessStartInfo(sampleAppPath); + /// Process.Start(psi); + /// + /// + /// + /// The following example demonstrates how to modify a fault rule in an existing session. + /// + /// ... + /// string sampleAppPath = "SampleApp.exe"; + /// FaultSession session = new FaultSession(rule); + /// ... + /// + /// FaultRule foundRule = session.FindRule("SampleApp.TargetMethod(string, string)"); + /// if (foundRule != null) + /// { + /// foundRule.Condition = BuiltInConditions.TriggerOnEveryNthCall(4); + /// session.NotifyRuleChanges(); + /// } + /// ... + /// + /// + public sealed class FaultSession + { + #region Private Data + + private Dictionary ruleDict = new Dictionary(); + // This cache stores binary representation of previously serialized rules + // FaultSession use this cache to decide whether serialization version of an FaultRule + // object should be updated while reserialized + private Dictionary ruleCache = new Dictionary(); + private readonly string serializationFileName; + private readonly Mutex serializationMutex; // Shared with tester and testee process + private readonly string methodFilterFileName; + private string logDirectory = Directory.GetCurrentDirectory(); + + #endregion + + #region Constructors + + /// + /// Initializes a new instance of the FaultSession class with the specified FaultRule objects. + /// + /// FaultRule objects defining how to fault the test application. + /// Two FaultRule objects corresponding to the same method. + public FaultSession(params FaultRule[] rules) + { + ComRegistrar.AutoRegister(); + + AddRulesToDict(rules); + + serializationFileName = Path.Combine(this.logDirectory, DateTime.Now.ToString("yyyyMMddHHmmssff", CultureInfo.CurrentCulture) + ".rul"); + { + string mutexName = Path.GetFileName(serializationFileName); + + // Serialization file will be shared between tester and testee process, they must be synchronized + // by a named global Mutex + bool newMutexCreated; + serializationMutex = new Mutex(false, mutexName, out newMutexCreated); + if (!newMutexCreated) + { + throw new FaultInjectionException( + string.Format(CultureInfo.CurrentCulture, ApiErrorMessages.UnableToCreateFileReadWriteMutex, mutexName)); + } + } + + methodFilterFileName = Path.Combine(this.logDirectory, DateTime.Now.ToString("yyyyMMddHHmmssff", CultureInfo.CurrentCulture) + ".mfi"); + MethodFilterHelper.WriteMethodFilter(methodFilterFileName, rules); + } + + #endregion + + #region Public Members + + /// + /// Finds the rule corresponding to a specified method. + /// + /// The signature of the method. + /// + /// The FaultRule instance corresponding to the method specified. Returns null if no such rule exists. + /// + public FaultRule FindRule(string method) + { + string key = Signature.ConvertSignature(method, SignatureStyle.Formal); + if (ruleDict.ContainsKey(key)) + { + return ruleDict[key]; + } + + return null; + } + + /// + /// Notifies all test applications that fault rules have changed. + /// + public void NotifyRuleChanges() + { + SerializeRules(); + } + + /// + /// Creates a ProcessStartInfo with the appropriate environment variables set + /// for fault injection. + /// + /// The path to the executable to launch. + /// The ProcessStartInfo object for the executable. + public ProcessStartInfo GetProcessStartInfo(string file) + { + ProcessStartInfo psi = new ProcessStartInfo(file); + SerializeRules(); + SetProcessEnvironmentVariables(this, psi); + psi.UseShellExecute = false; + return psi; + } + + /// + /// Directory for all log files written by applications launched by this session. + /// + public string LogDirectory + { + get { return logDirectory; } + set + { + if (value == null || value == string.Empty) + { + throw new FaultInjectionException(ApiErrorMessages.LogDirectoryNullOrEmpty); + } + logDirectory = Path.GetFullPath(value); + if (!Directory.Exists(logDirectory)) + { + Directory.CreateDirectory(logDirectory); + } + } + } + + #endregion + + #region Private Members + + private void AddRulesToDict(FaultRule[] rules) + { + if (rules == null || rules.Length == 0) + { + throw new FaultInjectionException(ApiErrorMessages.FaultRulesNullOrEmpty); + } + foreach (FaultRule rule in rules) + { + if (ruleDict.ContainsKey(rule.FormalSignature)) + { + throw new FaultInjectionException(ApiErrorMessages.FaultRulesConflict); + } + ruleDict.Add(rule.FormalSignature, rule); + } + } + + private void SerializeRules() + { + List rules = new List(); + + foreach (KeyValuePair pair in ruleDict) + { + rules.Add(pair.Value); + + // If the rule doesn't changed since last serialization, the version need not be updated + if (ruleCache.ContainsKey(pair.Key)) + { + byte[] current = Serializer.SerializeRuleToBuffer(pair.Value); + byte[] cached = ruleCache[pair.Key]; + if (ArrayEquals(cached, current)) + { + continue; + } + } + + // Update serialization version, then save it to cache + ++pair.Value.SerializationVersion; + ruleCache[pair.Key] = Serializer.SerializeRuleToBuffer(pair.Value); + } + + Serializer.SerializeRules(serializationFileName, rules.ToArray(), serializationMutex); + } + + #endregion + + #region Static Members + + /// + /// Sets a global fault session. This enables fault injection for all .NET + /// processes launched after the creation of the global session. This functionality is often + /// useful when testing server .NET applications, where one application typically consists of + /// and launches many processes. + /// + public static void SetGlobalFault(FaultSession session) + { + session.NotifyRuleChanges(); + + SetEnvironmentVariable(session, EnvironmentVariableTarget.Process); + SetEnvironmentVariable(session, EnvironmentVariableTarget.Machine); + } + + /// + /// Destroys the global fault session. + /// + public static void ClearGlobalFault() + { + ClearEnvironmentVariable(EnvironmentVariableTarget.Machine); + ClearEnvironmentVariable(EnvironmentVariableTarget.Process); + } + + private static void SetProcessEnvironmentVariables(FaultSession session, ProcessStartInfo processStartInfo) + { + // Enable Profiling Callback implemented by Engine + processStartInfo.EnvironmentVariables.Add(EnvironmentVariable.EnableProfiling, "1"); + processStartInfo.EnvironmentVariables.Add(EnvironmentVariable.Proflier, ComRegistrar.Clsid); + + // Set method filter file name for Engine + processStartInfo.EnvironmentVariables.Add(EnvironmentVariable.MethodFilter, session.methodFilterFileName); + // Set serialization file name for Dispatcher + processStartInfo.EnvironmentVariables.Add(EnvironmentVariable.RuleRepository, session.serializationFileName); + // Set log directory for Engine and Dispatcher + processStartInfo.EnvironmentVariables.Add(EnvironmentVariable.LogDirectory, session.LogDirectory); + + processStartInfo.EnvironmentVariables.Add(EnvironmentVariable.CLR4Compatibility, "EnableV2Profiler"); + } + + private static void SetEnvironmentVariable(FaultSession session, EnvironmentVariableTarget target) + { + // Enable Profiling Callback implemented by Engine + Environment.SetEnvironmentVariable(EnvironmentVariable.EnableProfiling, "1", target); + Environment.SetEnvironmentVariable(EnvironmentVariable.Proflier, ComRegistrar.Clsid, target); + + // Set method filter file name for Engine + Environment.SetEnvironmentVariable(EnvironmentVariable.MethodFilter, session.methodFilterFileName, target); + // Set serialization file name for Dispatcher + Environment.SetEnvironmentVariable(EnvironmentVariable.RuleRepository, session.serializationFileName, target); + // Set log directory for Engine and Dispatcher + Environment.SetEnvironmentVariable(EnvironmentVariable.LogDirectory, session.LogDirectory, target); + + Environment.SetEnvironmentVariable(EnvironmentVariable.CLR4Compatibility, "EnableV2Profiler", target); + } + + private static void ClearEnvironmentVariable(EnvironmentVariableTarget target) + { + // Disable Profiling Callback implemented by Engine + Environment.SetEnvironmentVariable(EnvironmentVariable.EnableProfiling, "0", target); + Environment.SetEnvironmentVariable(EnvironmentVariable.Proflier, string.Empty, target); + // Clear method filter file name + Environment.SetEnvironmentVariable(EnvironmentVariable.MethodFilter, string.Empty, target); + // Clear serialization file name for Dispatcher + Environment.SetEnvironmentVariable(EnvironmentVariable.RuleRepository, string.Empty, target); + // Clear log directory for Engine and Dispatcher + Environment.SetEnvironmentVariable(EnvironmentVariable.LogDirectory, string.Empty, target); + + Environment.SetEnvironmentVariable(EnvironmentVariable.CLR4Compatibility, string.Empty, target); + } + + private static bool ArrayEquals(byte[] lhs, byte[] rhs) + { + if (lhs.Length != rhs.Length) + { + return false; + } + for (int i = 0; i < lhs.Length; ++i) + { + if (lhs[i] != rhs[i]) + { + return false; + } + } + return true; + } + + #endregion + } +} diff --git a/src/dotnetCampus.UITest.WPFTestHelper/FaultInjection/Faults/ReturnFault.cs b/src/dotnetCampus.UITest.WPFTestHelper/FaultInjection/Faults/ReturnFault.cs new file mode 100644 index 0000000..151c3fa --- /dev/null +++ b/src/dotnetCampus.UITest.WPFTestHelper/FaultInjection/Faults/ReturnFault.cs @@ -0,0 +1,19 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + + +using System; + +namespace dotnetCampus.UITest.WPFTestHelper.FaultInjection.Faults +{ + [Serializable()] + internal sealed class ReturnFault : IFault + { + public void Retrieve(IRuntimeContext rtx, out Exception exceptionValue, out object returnValue) + { + exceptionValue = null; + returnValue = null; + } + } +} diff --git a/src/dotnetCampus.UITest.WPFTestHelper/FaultInjection/Faults/ReturnValueFault.cs b/src/dotnetCampus.UITest.WPFTestHelper/FaultInjection/Faults/ReturnValueFault.cs new file mode 100644 index 0000000..c8f3afd --- /dev/null +++ b/src/dotnetCampus.UITest.WPFTestHelper/FaultInjection/Faults/ReturnValueFault.cs @@ -0,0 +1,24 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + + +using System; + +namespace dotnetCampus.UITest.WPFTestHelper.FaultInjection.Faults +{ + [Serializable()] + internal sealed class ReturnValueFault : IFault + { + public ReturnValueFault(object returnValue) + { + this.returnValue = returnValue; + } + public void Retrieve(IRuntimeContext rtx, out Exception exceptionValue, out object returnValue) + { + exceptionValue = null; + returnValue = this.returnValue; + } + private readonly object returnValue; + } +} diff --git a/src/dotnetCampus.UITest.WPFTestHelper/FaultInjection/Faults/ReturnValueRuntimeFault.cs b/src/dotnetCampus.UITest.WPFTestHelper/FaultInjection/Faults/ReturnValueRuntimeFault.cs new file mode 100644 index 0000000..26cce97 --- /dev/null +++ b/src/dotnetCampus.UITest.WPFTestHelper/FaultInjection/Faults/ReturnValueRuntimeFault.cs @@ -0,0 +1,25 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + + +using System; +using dotnetCampus.UITest.WPFTestHelper.FaultInjection.SignatureParsing; + +namespace dotnetCampus.UITest.WPFTestHelper.FaultInjection.Faults +{ + [Serializable(), RuntimeFault] + internal sealed class ReturnValueRuntimeFault : IFault + { + public ReturnValueRuntimeFault(string returnValueExpression) + { + this.returnValueExpression = returnValueExpression; + } + public void Retrieve(IRuntimeContext rtx, out Exception exceptionValue, out object returnValue) + { + exceptionValue = null; + returnValue = Expression.GeneralExpression(returnValueExpression); + } + private readonly string returnValueExpression; + } +} diff --git a/src/dotnetCampus.UITest.WPFTestHelper/FaultInjection/Faults/RuntimeFaultAttribute.cs b/src/dotnetCampus.UITest.WPFTestHelper/FaultInjection/Faults/RuntimeFaultAttribute.cs new file mode 100644 index 0000000..5e711f9 --- /dev/null +++ b/src/dotnetCampus.UITest.WPFTestHelper/FaultInjection/Faults/RuntimeFaultAttribute.cs @@ -0,0 +1,13 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + + +using System; + +namespace dotnetCampus.UITest.WPFTestHelper.FaultInjection.Faults +{ + // Attribute class used by FaultInjector to distinguish normal fault and runtime fault + [AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)] + internal sealed class RuntimeFaultAttribute : Attribute { } +} diff --git a/src/dotnetCampus.UITest.WPFTestHelper/FaultInjection/Faults/ThrowExceptionFault.cs b/src/dotnetCampus.UITest.WPFTestHelper/FaultInjection/Faults/ThrowExceptionFault.cs new file mode 100644 index 0000000..5d4d386 --- /dev/null +++ b/src/dotnetCampus.UITest.WPFTestHelper/FaultInjection/Faults/ThrowExceptionFault.cs @@ -0,0 +1,29 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + + +using System; +using dotnetCampus.UITest.WPFTestHelper.FaultInjection.Constants; + +namespace dotnetCampus.UITest.WPFTestHelper.FaultInjection.Faults +{ + [Serializable()] + internal sealed class ThrowExceptionFault : IFault + { + public ThrowExceptionFault(Exception exceptionValue) + { + if (exceptionValue == null) + { + throw new FaultInjectionException(ApiErrorMessages.ExceptionNull); + } + this.exceptionValue = exceptionValue; + } + public void Retrieve(IRuntimeContext rtx, out Exception exceptionValue, out object returnValue) + { + returnValue = null; + exceptionValue = this.exceptionValue; + } + private readonly Exception exceptionValue; + } +} diff --git a/src/dotnetCampus.UITest.WPFTestHelper/FaultInjection/Faults/ThrowExceptionRuntimeFault.cs b/src/dotnetCampus.UITest.WPFTestHelper/FaultInjection/Faults/ThrowExceptionRuntimeFault.cs new file mode 100644 index 0000000..0a9a4dc --- /dev/null +++ b/src/dotnetCampus.UITest.WPFTestHelper/FaultInjection/Faults/ThrowExceptionRuntimeFault.cs @@ -0,0 +1,25 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + + +using System; +using dotnetCampus.UITest.WPFTestHelper.FaultInjection.SignatureParsing; + +namespace dotnetCampus.UITest.WPFTestHelper.FaultInjection.Faults +{ + [Serializable(), RuntimeFault] + internal sealed class ThrowExceptionRuntimeFault : IFault + { + public ThrowExceptionRuntimeFault(string exceptionExpression) + { + this.exceptionExpression = exceptionExpression; + } + public void Retrieve(IRuntimeContext rtx, out Exception exceptionValue, out object returnValue) + { + returnValue = null; + exceptionValue = (Exception)Expression.GeneralExpression(exceptionExpression); + } + private readonly string exceptionExpression; + } +} diff --git a/src/dotnetCampus.UITest.WPFTestHelper/FaultInjection/ICondition.cs b/src/dotnetCampus.UITest.WPFTestHelper/FaultInjection/ICondition.cs new file mode 100644 index 0000000..986bf52 --- /dev/null +++ b/src/dotnetCampus.UITest.WPFTestHelper/FaultInjection/ICondition.cs @@ -0,0 +1,25 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + + +namespace dotnetCampus.UITest.WPFTestHelper.FaultInjection +{ + + /// + /// Defines the contract for specifying when a fault will be triggered on a method. + /// + /// + /// If the fault condition is not triggered, the faulted method will execute its original code. + /// For more information on how to use a condition, see the class. + /// + public interface ICondition + { + /// + /// Determines whether a fault should be triggered. + /// + /// The runtime context information for this call and the faulted method. + /// Returns true if a fault should be triggered, otherwise returns false. + bool Trigger(IRuntimeContext context); + } +} diff --git a/src/dotnetCampus.UITest.WPFTestHelper/FaultInjection/IFault.cs b/src/dotnetCampus.UITest.WPFTestHelper/FaultInjection/IFault.cs new file mode 100644 index 0000000..0169fb7 --- /dev/null +++ b/src/dotnetCampus.UITest.WPFTestHelper/FaultInjection/IFault.cs @@ -0,0 +1,52 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + + +using System; + +namespace dotnetCampus.UITest.WPFTestHelper.FaultInjection +{ + /// + /// Defines the contract for a fault. + /// + /// + /// For more information on how to use a fault, see the class. + /// + /// + /// + /// Define a custom fault, which returns a random int when triggered. + /// + /// public class ReturnRandomIntFault : IFault + /// { + /// private Random rand; + /// + /// public ReturnRandomIntFault(int seed) + /// { + /// rand = new Random(seed); + /// } + /// + /// public void Retrieve(IRuntimeContext context, out Exception exceptionValue, out object returnValue) + /// { + /// exceptionValue = null; + /// returnValue = rand.Next(); + /// } + /// } + /// + /// + public interface IFault + { + /// + /// Defines the behavior of the fault when triggered. + /// + /// The runtime context information for this call. + /// An output paramter for the exception to be thrown by the faulted method. + /// An output paramter for the value to be returned by the faulted method. + /// + /// Parameter + /// is only checked when returns null. + /// + void Retrieve(IRuntimeContext context, out Exception exceptionValue, out object returnValue); + } + +} diff --git a/src/dotnetCampus.UITest.WPFTestHelper/FaultInjection/IRuntimeContext.cs b/src/dotnetCampus.UITest.WPFTestHelper/FaultInjection/IRuntimeContext.cs new file mode 100644 index 0000000..2ce54b2 --- /dev/null +++ b/src/dotnetCampus.UITest.WPFTestHelper/FaultInjection/IRuntimeContext.cs @@ -0,0 +1,48 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + + +using System; +using System.Diagnostics; + +namespace dotnetCampus.UITest.WPFTestHelper.FaultInjection +{ + /// + /// Defines the contract for information provided by the faulted method. + /// + public interface IRuntimeContext + { + /// + /// The number of times the method is called. + /// + int CalledTimes + { + get; + } + + /// + /// The method's stack trace. + /// + StackTrace CallStackTrace + { + get; + } + + /// + /// An array of C#-style method signatures for each method on the call stack. + /// + CallStack CallStack + { + get; + } + + /// + /// The C#-style method signature of the caller of the faulted method. + /// + String Caller + { + get; + } + } +} diff --git a/src/dotnetCampus.UITest.WPFTestHelper/FaultInjection/MethodFilter.cs b/src/dotnetCampus.UITest.WPFTestHelper/FaultInjection/MethodFilter.cs new file mode 100644 index 0000000..7d9c557 --- /dev/null +++ b/src/dotnetCampus.UITest.WPFTestHelper/FaultInjection/MethodFilter.cs @@ -0,0 +1,35 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + + +using System.IO; +using dotnetCampus.UITest.WPFTestHelper.FaultInjection.SignatureParsing; + +namespace dotnetCampus.UITest.WPFTestHelper.FaultInjection +{ + internal static class MethodFilterHelper + { + #region Public Members + + public static void WriteMethodFilter(string file, FaultRule[] rules) + { + using (Stream stream = File.Open(file, FileMode.Create)) + { + using (StreamWriter writer = new StreamWriter(stream)) + { + foreach (FaultRule rule in rules) + { + if (rule != null) + { + string signature = Signature.ConvertSignature(rule.MethodSignature, SignatureStyle.Com); + writer.WriteLine(signature); + } + } + } + } + } + + #endregion + } +} diff --git a/src/dotnetCampus.UITest.WPFTestHelper/FaultInjection/RuntimeContext.cs b/src/dotnetCampus.UITest.WPFTestHelper/FaultInjection/RuntimeContext.cs new file mode 100644 index 0000000..8ea7e0e --- /dev/null +++ b/src/dotnetCampus.UITest.WPFTestHelper/FaultInjection/RuntimeContext.cs @@ -0,0 +1,102 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + + +using System.Diagnostics; + +namespace dotnetCampus.UITest.WPFTestHelper.FaultInjection +{ + /// + /// Stores information about a faulted method. + /// + public class RuntimeContext : IRuntimeContext + { + #region Private Data + + private int calledTimes = 0; + StackTrace callStackTrace = null; + private CallStack callStack = new CallStack(new StackTrace()); + + #endregion + + #region Contructors + /// + /// Initializes a new instance of the RuntimeContext class. + /// + public RuntimeContext() + { + } + + #endregion + + #region Public Members + + /// + /// The number of times the method has been called. + /// + public int CalledTimes + { + get + { + int times = calledTimes; + return times; + } + set + { + calledTimes = value; + + } + } + + /// + /// The method's stack trace. + /// + public StackTrace CallStackTrace + { + get + { + return callStackTrace; + } + set + { + callStackTrace = value; + } + } + + /// + /// An array of C#-style method signatures for each method on the call stack. + /// + public CallStack CallStack + { + get + { + return callStack; + } + set + { + callStack = value; + } + } + + /// + /// The C#-style method signature of the caller of the faulted method. + /// + public string Caller + { + get + { + if (callStack != null) + { + return callStack[1]; + } + else + { + return null; + } + } + } + + #endregion + } +} diff --git a/src/dotnetCampus.UITest.WPFTestHelper/FaultInjection/Serializer.cs b/src/dotnetCampus.UITest.WPFTestHelper/FaultInjection/Serializer.cs new file mode 100644 index 0000000..06375c8 --- /dev/null +++ b/src/dotnetCampus.UITest.WPFTestHelper/FaultInjection/Serializer.cs @@ -0,0 +1,155 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + + +using System; +using System.Collections.Generic; +using System.IO; +using System.Reflection; +using System.Runtime.Serialization.Formatters.Binary; +using System.Threading; +using dotnetCampus.UITest.WPFTestHelper.FaultInjection.Constants; +#if TESTBUILD_CLR20 +// These includes are for the different Assembly.LoadFrom() overload used to make CLR 2.0 CAS policy work +using System.Security; +using System.Security.Policy; +#endif + +namespace dotnetCampus.UITest.WPFTestHelper.FaultInjection +{ + internal static class Serializer + { + #region Private Data + + private static AssemblyResolver resolver = new AssemblyResolver(); + + #endregion + + #region Public Members + + public static void SerializeRules(string fileName, FaultRule[] rules, Mutex mutex) + { + if (rules == null) + { + throw new FaultInjectionException(ApiErrorMessages.FaultRulesNullInSerialization); + } + + // Recording locations for all assemblies loaded in AppDomain running test code + // We need these information to find types while deserialization + Dictionary locations = new Dictionary(); + foreach (Assembly assembly in AppDomain.CurrentDomain.GetAssemblies()) + { + try + { + locations[assembly.FullName] = assembly.Location; + } + catch (NotSupportedException) + { + // ---- NotSupportedException for dynamic assemblies + } + } + + // Serialize fault rules into a temporary file + BinaryFormatter formatter = new BinaryFormatter(); + string tempFile = Path.Combine(Path.GetDirectoryName(fileName), Path.GetFileName(Path.GetTempFileName())); + using (FileStream stream = File.Open(tempFile, FileMode.Create)) + { + formatter.Serialize(stream, locations); + formatter.Serialize(stream, rules); + } + + // Swap it to serialization file + using (ScopedLock scope = new ScopedLock(mutex)) + { + File.Delete(fileName); + File.Move(tempFile, fileName); + } + } + + public static FaultRule[] DeserializeRules(string fileName, Mutex mutex) + { + BinaryFormatter formatter = new BinaryFormatter(); + using (ScopedLock scope = new ScopedLock(mutex)) + { + using (FileStream stream = File.Open(fileName, FileMode.Open)) + { + // Use assembly locations recorded to handle assembly resolvation + Dictionary locations = (Dictionary)formatter.Deserialize(stream); + resolver.AddAssemblyLocations(locations); + + return (FaultRule[])formatter.Deserialize(stream); + } + } + } + + public static byte[] SerializeRuleToBuffer(FaultRule rule) + { + BinaryFormatter formatter = new BinaryFormatter(); + using (MemoryStream stream = new MemoryStream()) + { + formatter.Serialize(stream, rule); + return stream.GetBuffer(); + } + } + + #endregion + + #region Private Members + + private sealed class ScopedLock : IDisposable + { + private readonly Mutex mutex; + public ScopedLock(Mutex mutex) + { + this.mutex = mutex; + this.mutex.WaitOne(); + } + public void Dispose() + { + this.mutex.ReleaseMutex(); + } + } + // We use this class to resolve assemblies which are visible to API but not AUT + // It do this by find assembly locations recorded while serialize rules + private sealed class AssemblyResolver + { + public AssemblyResolver() + { + // Kicks in when AUT can't find assembly by itself + AppDomain.CurrentDomain.AssemblyResolve += this.AssemblyResolveEventHandler; + } + + public void AddAssemblyLocations(Dictionary newLocations) + { + foreach (KeyValuePair pair in newLocations) + { + this.locations[pair.Key] = pair.Value; + } + } + + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Reliability", "CA2001")] //Assembly.LoadFrom + public Assembly AssemblyResolveEventHandler(object sender, ResolveEventArgs args) + { + string location; + if (locations.TryGetValue(args.Name, out location)) + { +#if TESTBUILD_CLR20 + Evidence evidence = new Evidence(); + evidence.AddHost(new Zone(SecurityZone.MyComputer)); + return Assembly.LoadFrom(location, evidence); +#endif +#if TESTBUILD_CLR40 + return Assembly.LoadFrom(location); +#endif + + } + return null; // Should I do log here? + } + + private Dictionary locations = new Dictionary(); + } + + #endregion + } +} diff --git a/src/dotnetCampus.UITest.WPFTestHelper/FaultInjection/SignatureParsing/Expression.cs b/src/dotnetCampus.UITest.WPFTestHelper/FaultInjection/SignatureParsing/Expression.cs new file mode 100644 index 0000000..7fb61f6 --- /dev/null +++ b/src/dotnetCampus.UITest.WPFTestHelper/FaultInjection/SignatureParsing/Expression.cs @@ -0,0 +1,533 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + + +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Reflection; +using System.Text; +using System.Text.RegularExpressions; +using dotnetCampus.UITest.WPFTestHelper.FaultInjection.Constants; +#if TESTBUILD_CLR20 +// These includes are for the different Assembly.LoadFrom() overload used to make CLR 2.0 CAS policy work +using System.Security; +using System.Security.Policy; +#endif + +namespace dotnetCampus.UITest.WPFTestHelper.FaultInjection.SignatureParsing +{ + /// + /// Parse string expression into value + /// + internal static class Expression + { + #region Private Data + + private static Regex isStruct = new Regex(@"\(\x20*(?.[\w\.<>\[\]]+)\x20*(@\x20*(?[\w\.:\\/=,'\x20]*))?\)\x20*(?.+)", RegexOptions.CultureInvariant); + private static Regex isString = new Regex(@"'(?.*)'", RegexOptions.CultureInvariant); + private static Regex isObject = new Regex(@"(?[\w\.<>\[\]]+)\x20*(\(\x20*(?.*)\x20*\))?(\x20*@\x20*(?[\w\.:\\/=,'\x20]+)\x20*)?", RegexOptions.CultureInvariant); + private static Regex isArray = new Regex(@"(?[\w\.<>\[\]]+)\x20*\[\x20*\]\x20*{\x20*(?.*)\x20*}(\x20*@\x20*(?[\w\.:\\/=,'\x20]+))?\x20*", RegexOptions.CultureInvariant); + + #endregion + + #region Public Members + + /// + /// Parse expression that represents an reference object + /// + /// class name + /// constructor parameter string + /// assembly, optional + /// the object + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1811")] + public static object ObjectExpression(string className, string parameterString, string assembly) + { + Type type; + return ObjectExpression(className, parameterString, assembly, null, out type); + } + + /// + /// Parse expression from string, also return expression value type + /// + /// expression string + /// return value type + /// return value + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1811")] + public static object GeneralExpression(string fullName, out Type type) + { + return GeneralExpression(fullName, null, out type); + } + + /// + /// Parse expression from string + /// + /// expression string + /// return value + public static object GeneralExpression(string FullName) + { + Type type; + return GeneralExpression(FullName, null, out type); + } + + #endregion + + #region Private Members + + /// + /// Load assembly given its name with path + /// + /// assembly name + /// the assembly + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Reliability", "CA2001")] //Assembly.LoadFrom + private static Assembly LoadAssembly(string assemblyName) + { + //consider the form of [assembly name] + assemblyName = assemblyName.Trim(); + Match matchString = isString.Match(assemblyName); + + //is string + if (matchString.Success && matchString.Groups[0].Length == assemblyName.Length) + { + Type outType; + assemblyName = (string)StringExpression(matchString.Groups["String"].ToString(), out outType); + } + + //load assembly using Assembly.Load(FullName) + Assembly targetAssembly; + try + { + targetAssembly = Assembly.Load(assemblyName); + } + catch (ArgumentNullException) + { + targetAssembly = null; + } + catch (ArgumentException) + { + targetAssembly = null; + } + catch (FileNotFoundException) + { + targetAssembly = null; + } + catch (FileLoadException) + { + targetAssembly = null; + } + catch (BadImageFormatException) + { + targetAssembly = null; + } + if (targetAssembly != null) + { + return targetAssembly; + } + + //load assembly using Assembly.Load(AssemblyName) + AssemblyName assemblyRef = new AssemblyName(); + assemblyRef.Name = Path.GetFileNameWithoutExtension(assemblyName); + assemblyRef.CodeBase = Path.GetDirectoryName(assemblyName); + if (string.IsNullOrEmpty(assemblyRef.CodeBase)) + { + assemblyRef.CodeBase = Environment.CurrentDirectory; + } + + try + { + targetAssembly = Assembly.Load(assemblyRef); + } + catch (FileNotFoundException) + { + targetAssembly = null; + } + catch (FileLoadException) + { + targetAssembly = null; + } + catch (BadImageFormatException) + { + targetAssembly = null; + } + if (targetAssembly != null) + { + return targetAssembly; + } + + //use Assembly.LoadFrom(string) if Load(AssemblyName)failed + +#if TESTBUILD_CLR20 + Evidence evidence = new Evidence(); + evidence.AddHost(new Zone(SecurityZone.MyComputer)); + targetAssembly = Assembly.LoadFrom(assemblyName, evidence); +#endif +#if TESTBUILD_CLR40 + targetAssembly = Assembly.LoadFrom(assemblyName); +#endif + if (targetAssembly == null) + { + throw new FaultInjectionException(string.Format(CultureInfo.CurrentCulture, XmlErrorMessages.TargetAssemblyNotFound, assemblyName)); + } + return targetAssembly; + } + + /// + /// Get type from current appDomain or specific assembly, supporting nested class names + /// + /// type name + /// assembly file path + /// alternative assembly name + /// return type + private static Type GetType(string typeName, string assemblyName, string alternativeAssemblyName) + { + //get assemblies + List assemblyList = new List(); + if (!string.IsNullOrEmpty(assemblyName)) + { + assemblyList.Add(LoadAssembly(assemblyName)); + } + else + { + if (!string.IsNullOrEmpty(alternativeAssemblyName)) + { + assemblyList.Add(LoadAssembly(alternativeAssemblyName)); + } + + //get type from current appDomain + assemblyList.AddRange(AppDomain.CurrentDomain.GetAssemblies()); + } + Assembly[] assemblies = assemblyList.ToArray(); + + //Trim white spaces + typeName = typeName.Trim(); + + //Change known buitin type with full name + string fullName = BuiltInTypeHelper.AliasToFullName(typeName); + if (fullName == null) + { + fullName = typeName; + } + + Type type = null; + foreach (Assembly assembly in assemblies) + { + string reflectionTypeName = fullName; + do + { + type = assembly.GetType(reflectionTypeName); + if (type != null) + { + break; + } + int lastPeriod = reflectionTypeName.LastIndexOf('.'); + if (lastPeriod < 0) + { + break; + } + reflectionTypeName = reflectionTypeName.Substring(0, lastPeriod) + '+' + reflectionTypeName.Substring(lastPeriod + 1, reflectionTypeName.Length - lastPeriod - 1); + } while (true); + if (type != null) + { + break; + } + } + if (type == null) + { + if (typeName.StartsWith(EngineInfo.NameSpace, StringComparison.Ordinal)) + { + typeName = typeName.Substring(EngineInfo.NameSpace.Length + ".".Length, typeName.Length - EngineInfo.NameSpace.Length - ".".Length); + } + throw new FaultInjectionException(string.Format(CultureInfo.CurrentCulture, XmlErrorMessages.TargetTypeNotFound, typeName)); + } + return type; + } + + /// + /// String type expression + /// + /// string + /// return value type + /// return value + private static object StringExpression(string stringExpression, out Type type) + { + type = typeof(string); + return stringExpression.Replace("''", "'"); + } + + /// + /// Struct type expression as well as null object expression + /// + /// type name + /// parameter + /// assembly, optional + /// alternative assembly, otional + /// return value type + /// return value + private static object StructExpression(string structName, string parameter, string assembly, string alternativeAssembly, out Type type) + { + //get type + type = GetType(structName, assembly, alternativeAssembly); + + if (type.IsValueType) + { + if (type.IsEnum) + { + //Invoke enums Parse + return Enum.Parse(type, parameter); + } + else + { + //Invoke Parse + MethodInfo parser = type.GetMethod("Parse", new Type[] { typeof(string) }); + if (parser == null) + { + throw new FaultInjectionException(string.Format(CultureInfo.CurrentCulture, XmlErrorMessages.NoParser, structName)); + } + return parser.Invoke(null, new object[] { parameter }); + } + } + else + { + //Return null for reference type, negleting parameter + return null; + } + } + + /// + /// Parameter list splitter, utility + /// + /// the whole string that represents parameter list + /// parameter list + private static string[] ParameterListSplitter(string parameters) + { + parameters = parameters.Trim(); + if (string.IsNullOrEmpty(parameters)) return null; + List parameterList = new List(); + StringBuilder currentParameter = new StringBuilder(); + int bracket = 0; + int sharpBracket = 0; + int bigBracket = 0; + int squaredBracket = 0; + bool quote = true; + for (int i = 0; i < parameters.Length; i++) + { + char c = parameters[i]; + if (c == '(') + { + bracket++; + } + else if (c == ')') + { + bracket--; + } + else if (c == '<') + { + sharpBracket++; + } + else if (c == '>') + { + sharpBracket--; + } + else if (c == '{') + { + bigBracket++; + } + else if (c == '}') + { + bigBracket--; + } + else if (c == '[') + { + squaredBracket++; + } + else if (c == ']') + { + squaredBracket--; + } + else if (c == '\'') + { + if (i + 1 >= parameters.Length || parameters[i + 1] != '\'') + { + quote = !quote; + } + } + //only comma out of (),<>,{} is valid root level separater + if (c == ',' && bracket == 0 && sharpBracket == 0 && bigBracket == 0 && squaredBracket == 0 && quote) + { + parameterList.Add(currentParameter.ToString()); + currentParameter = new StringBuilder(); + } + else + { + currentParameter.Append(c); + } + } + parameterList.Add(currentParameter.ToString()); + return parameterList.ToArray(); + } + + /// + /// Parse expression that represents an reference object + /// + /// class name + /// constructor parameter string + /// assembly, optionaly + /// alternative assembly, optional + /// object type + /// the object + private static object ObjectExpression(string className, string parameterString, string assembly, string alternativeAssembly, out Type type) + { + //get type + type = GetType(className, assembly, alternativeAssembly); + + //merge alternative assembly name + if (string.IsNullOrEmpty(assembly)) + { + assembly = alternativeAssembly; + } + + //get constructor parameters + string[] parameters = ParameterListSplitter(parameterString); + object[] realParameters; + Type[] parameterTypes; + if (parameters == null || parameters.Length == 0) + { + realParameters = null; + parameterTypes = new Type[0]; + } + else + { + //make parameters + List realParameterList = new List(); + List parameterTypeList = new List(); + foreach (string parameter in parameters) + { + Type parameterType; + object realParameter = GeneralExpression(parameter, assembly, out parameterType); + realParameterList.Add(realParameter); + parameterTypeList.Add(parameterType); + } + realParameters = realParameterList.ToArray(); + parameterTypes = parameterTypeList.ToArray(); + } + + //get constructor + ConstructorInfo constructor = type.GetConstructor(parameterTypes); + if (constructor == null) + { + throw new FaultInjectionException(string.Format(CultureInfo.CurrentCulture, XmlErrorMessages.ConstructorNotFound, className)); + } + + //new object + return constructor.Invoke(realParameters); + } + + /// + /// Parse expression that represents an array + /// + /// array type + /// array item list + /// assembly, optional + /// alternative assembly, optional + /// array type + /// return value + private static object ArrayExpression(string className, string parameterString, string assembly, string alternativeAssembly, out Type type) + { + //get type + Type itemType = GetType(className, assembly, alternativeAssembly); + + //merge alternative assembly name + if (string.IsNullOrEmpty(assembly)) + { + assembly = alternativeAssembly; + } + + //get array list + string[] parameters = ParameterListSplitter(parameterString); + Array returnValue; + type = itemType.MakeArrayType(); + if (parameters == null || parameters.Length == 0) + { + returnValue = Array.CreateInstance(itemType, 0); + } + else + { + returnValue = Array.CreateInstance(itemType, parameters.Length); + for (int i = 0; i < parameters.Length; i++) + { + string parameter = parameters[i]; + Type parameterType; + object realParameter = GeneralExpression(parameter, assembly, out parameterType); + if (!(parameterType.Equals(itemType) || parameterType.IsSubclassOf(itemType))) + { + throw new FaultInjectionException(XmlErrorMessages.ArrayItemTypeError); + } + returnValue.SetValue(realParameter, i); + } + } + return returnValue; + } + + /// + /// Parse expression from string + /// + /// expression string + /// alternative assembly name + /// return value type + /// return value + private static object GeneralExpression(string fullName, string alternativeAssembly, out Type type) + { + //Trim white space and unwanted char + fullName = fullName.Trim().Replace("\n", string.Empty); + fullName = fullName.Replace("\r", string.Empty); + fullName = fullName.Replace("\t", string.Empty); + + //does not support null object without type information + /*if (string.Compare(FullName, "null") == 0) + { + return null; + }*/ + Match matchString = isString.Match(fullName); + Match matchStruct = isStruct.Match(fullName); + Match matchObject = isObject.Match(fullName); + Match matchArray = isArray.Match(fullName); + + //is string + if (matchString.Success && matchString.Groups[0].Length == fullName.Length) + { + return StringExpression(matchString.Groups["String"].ToString(), out type); + } + + //is struct or null object + if (matchStruct.Success && matchStruct.Groups[0].Length == fullName.Length) + { + return StructExpression(matchStruct.Groups["StructName"].ToString(), matchStruct.Groups["Parameter"].ToString(), matchStruct.Groups["Assembly"].ToString(), alternativeAssembly, out type); + } + + //is object + else if (matchObject.Success && matchObject.Groups[0].Length == fullName.Length) + { + return ObjectExpression(matchObject.Groups["ClassName"].ToString(), matchObject.Groups["Parameters"].ToString(), matchObject.Groups["Assembly"].ToString(), alternativeAssembly, out type); + } + + //is array + else if (matchArray.Success && matchArray.Groups[0].Length == fullName.Length) + { + return ArrayExpression(matchArray.Groups["ClassName"].ToString(), matchArray.Groups["Parameters"].ToString(), matchArray.Groups["Assembly"].ToString(), alternativeAssembly, out type); + } + + //format error + else + { + throw new FaultInjectionException(XmlErrorMessages.ExpressionFormatError); + } + } + + #endregion + + + } +} + diff --git a/src/dotnetCampus.UITest.WPFTestHelper/FaultInjection/SignatureParsing/MethodSignatureTranslator.cs b/src/dotnetCampus.UITest.WPFTestHelper/FaultInjection/SignatureParsing/MethodSignatureTranslator.cs new file mode 100644 index 0000000..5b47adb --- /dev/null +++ b/src/dotnetCampus.UITest.WPFTestHelper/FaultInjection/SignatureParsing/MethodSignatureTranslator.cs @@ -0,0 +1,93 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + + +using System; +using System.Collections.Generic; +using System.Globalization; + +namespace dotnetCampus.UITest.WPFTestHelper.FaultInjection.SignatureParsing +{ + internal static class MethodSignatureTranslator + { + #region Public Members + + public static String GetGenericParaString(Type genericType, int genericIndex) + { + String strGenericParas = null; + Type[] allGenericTypes = genericType.GetGenericArguments(); + + + int genericNumber = GetGenericNumber(genericType); + + if (allGenericTypes != null && allGenericTypes.Length > 0 && genericNumber > 0) + { + Type[] currentGenericTypes = new Type[genericNumber]; + + strGenericParas = "<"; + for (int i = 0; i < genericNumber; ++i) + { + currentGenericTypes[i] = allGenericTypes[genericIndex + i]; + } + foreach (Type genericPara in currentGenericTypes) + { + strGenericParas = strGenericParas.Insert(strGenericParas.Length, GetTypeString(genericPara) + ","); + } + if (currentGenericTypes.Length > 0) + { + strGenericParas = strGenericParas.Remove(strGenericParas.Length - 1); + } + strGenericParas = strGenericParas.Insert(strGenericParas.Length, ">"); + } + return strGenericParas; + } + + public static int GetGenericNumber(Type type) + { + String[] temp = type.Name.Split('`'); + int genericParaNum = System.Int32.Parse(temp[1], CultureInfo.InvariantCulture); + return genericParaNum; + } + + public static String GetTypeString(Type type) + { + String methodString = null; + Stack stack = new Stack(); + stack.Push(type); + while (type.IsNested == true && !(type.IsGenericType == false && type.FullName == null)) //Eliminate + { + type = type.DeclaringType; + stack.Push(type); + } + + Type outterType = stack.Pop(); + methodString = outterType.ToString().Replace('+','.'); + int genericIndex = 0; + + if (outterType.IsGenericType == true) + { + String[] temp = methodString.Split('['); + methodString = temp[0]; + methodString = methodString.Remove(methodString.LastIndexOf('`')); + methodString = methodString.Insert(methodString.Length, GetGenericParaString(outterType, genericIndex)); + genericIndex += GetGenericNumber(outterType); + } + while (stack.Count > 0) + { + Type currentType = stack.Pop(); + methodString = methodString.Insert(methodString.Length, "." + currentType.Name); + + if (currentType.IsGenericType == true && currentType.Name.Contains("`") == true) + { + methodString = methodString.Remove(methodString.LastIndexOf('`')); + methodString = methodString.Insert(methodString.Length, GetGenericParaString(currentType, genericIndex)); + genericIndex += GetGenericNumber(currentType); + } + } + return methodString; + } + + #endregion + } +} diff --git a/src/dotnetCampus.UITest.WPFTestHelper/FaultInjection/SignatureParsing/Signature.cs b/src/dotnetCampus.UITest.WPFTestHelper/FaultInjection/SignatureParsing/Signature.cs new file mode 100644 index 0000000..6ae31dd --- /dev/null +++ b/src/dotnetCampus.UITest.WPFTestHelper/FaultInjection/SignatureParsing/Signature.cs @@ -0,0 +1,638 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + + +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Text.RegularExpressions; +using dotnetCampus.UITest.WPFTestHelper.FaultInjection.Constants; + +namespace dotnetCampus.UITest.WPFTestHelper.FaultInjection.SignatureParsing +{ + internal static class Signature + { + #region Public Members + + public static string ConvertSignature(string signature) + { + return ConvertSignature(signature, SignatureStyle.Formal); + } + + public static string ConvertSignature(string signature, SignatureStyle style) + { + if (signature == null || signature == string.Empty) + { + throw new FaultInjectionException(ApiErrorMessages.MethodSignatureNullOrEmpty); + } + + try + { + Lex lex = new Lex(signature); + Yacc yacc = new Yacc(lex.LexedSignature, lex.Identifiers); + return yacc.GetSignature(style); + } + catch (FaultInjectionException) + { + throw new FaultInjectionException(string.Format(CultureInfo.CurrentCulture, ApiErrorMessages.InvalidMethodSignature, signature)); + } + } + + #endregion + + #region Private Members + + private sealed class Lex + { + #region Private Data + + private readonly List identifiers = null; + private readonly string lexSignature = null; + private const string separatorPattern = @"[.,<>\[\]\(\)]"; + // "ref", "out", "params" and "static" must be followed with a space in user style signature. + // Thus they must be the last symbol after I have split the input by spaces. + // To ensure this, pattern match them should have a '$' at the end. + private const string staticPattern = @"static$"; // "static" will be lexed as a '!' + private const string refPattern = @"(ref|out)$"; // "ref" and "out" will be lexed as a '&' + private const string paramsPattern = @"params$"; // "params" will be lexed as a '%' + private const string identifierPattern = @"[a-z|A-Z|_]\w*"; + + #endregion + + #region Constructors + + // Do lexical parse in constructor + public Lex(string signature) + { + lexSignature = string.Empty; + identifiers = new List(); + + string[] segments = signature.Split(new char[] { ' ', '\t', '\n' }, StringSplitOptions.RemoveEmptyEntries); + foreach (string segment in segments) + { + string temp = segment; + while (temp != string.Empty) + { + string symbol = null; + if (ParseLexSymbol(separatorPattern, ref temp, out symbol)) + { + lexSignature += symbol; + continue; + } + + if (ParseLexSymbol(staticPattern, ref temp, out symbol)) + { + lexSignature += '!'; + continue; + } + + if (ParseLexSymbol(refPattern, ref temp, out symbol)) + { + lexSignature += '&'; + continue; + } + + if (ParseLexSymbol(paramsPattern, ref temp, out symbol)) + { + lexSignature += '%'; + continue; + } + + if (ParseLexSymbol(identifierPattern, ref temp, out symbol)) + { + if (BuiltInTypeHelper.AliasToFullName(symbol) != null) + { + lexSignature += symbol; + } + else + { + int i = identifiers.IndexOf(symbol); + if (i == -1) + { + identifiers.Add(symbol); + i = identifiers.Count - 1; + } + lexSignature += string.Format(CultureInfo.InvariantCulture, "#{0}#", i); + } + continue; + } + + throw new FaultInjectionException(); + } + } + } + + #endregion + + #region Public Members + + public string[] Identifiers { get { return identifiers.ToArray(); } } + public string LexedSignature { get { return lexSignature; } } + + #endregion + + #region Private Members + + // Parse one lexical symbol by matching head of input string using regex pattern argument. + private bool ParseLexSymbol(string pattern, ref string input, out string symbol) + { + symbol = null; + + Regex r = new Regex("^" + pattern); + Match m = r.Match(input); + if (m.Success) + { + input = input.Remove(0, m.Length); + symbol = m.Value; + } + return (symbol != null); + } + + #endregion + } + + private sealed class Yacc + { + #region Private Data + + private Dictionary identifiers = new Dictionary(); + private readonly Signature signature; + + #endregion + + #region Constructors + + public Yacc(string lexedSignature, string[] identifierArray) + { + for (int i = 0; i < identifierArray.Length; ++i) + { + identifiers.Add(i, identifierArray[i]); + } + signature = new Signature(lexedSignature); + } + + #endregion + + #region Public Members + + public string GetSignature(SignatureStyle style) + { + string result = signature.ToString(style); + + Regex identifiersPattern = new Regex(@"#(?\d+)#"); + Match match = identifiersPattern.Match(result); + while (match.Success) + { + int index = int.Parse(match.Groups["index"].Value, CultureInfo.InvariantCulture); + result = result.Replace(match.Value, identifiers[index]); + match = match.NextMatch(); + } + + return result; + } + + #endregion + + #region Private Members + + // A bunch of classes for grammar elements. To understand them, see the following pseudo-grammar: + // + // Signature = TypeName.MethodName(ParaList) | !TypeName.MethodName(ParaList) + // ~~~~~~~~~ Note: "!" for static method + // ParaList = ParaType,ParaType,ParaType ... ParaType + // ParaType = TypeName | &TypeName | %TypeName + // ~~~~~~~~~~~~~~~~~~~~~ Note: "&" for "ref|out" and "%" for params + // TypeName = PlainTypeName | PlainTypeName[][,,][]... + // PlainTypeName = Name.Name.Name ... Name | C# alias for built-in type + // MethodName = Name | .cctor + // Name = Identifier + // TypeList = TypeName,TypeName,TypeName ... TypeName + // + private sealed class Signature + { + #region Private Data + + internal readonly TypeName declaringType = null; + internal readonly MethodName method = null; + internal readonly ParaList parameters = null; + internal readonly bool isStatic = false; + internal readonly bool isConstructor = false; + + #endregion + + #region Public Members + + public Signature(string signature) + { + Regex regex = new Regex(@"^(?!?)(?.+)\.(?.+)\((?.*)\)$"); + Match match = regex.Match(signature); + + if (!match.Success) + { + throw new FaultInjectionException(); + } + + declaringType = new TypeName(match.Groups["type"].Value); + method = new MethodName(match.Groups["method"].Value); + if (match.Groups["paras"].Value != string.Empty) + { + parameters = new ParaList(match.Groups["paras"].Value); + } + + // Is static method? + isStatic = (match.Groups["static"].Value != string.Empty); + // Is constructor? + if (declaringType.arraySuffix == null) + { + string methodName = method.name.identifier; + string className = declaringType.plainTypeName.className; + if (className == methodName) + { + isConstructor = true; + } + } + } + public string ToString(SignatureStyle style) + { + string methodName = method.ToString(style); + if (isConstructor) + { + if (isStatic) + { + methodName = ".cctor"; + } + else + methodName = ".ctor"; + } + + string result = declaringType.ToString(style) + "." + methodName; + if (style == SignatureStyle.Formal) + { + result += "("; + if (parameters != null) + { + result += parameters.ToString(style); + } + result += ")"; + } + return result; + } + + #endregion + } + + private sealed class ParaList + { + #region Private Data + + internal readonly ParaType[] parameters; + + #endregion + + #region Public Members + + public ParaList(string paraList) + { + string[] paraNames = SplitAtTopLevel(paraList, ','); + parameters = Array.ConvertAll(paraNames, delegate(string name) { return new ParaType(name); }); + int i = Array.FindIndex(parameters, delegate(ParaType para) { return para.paramsFlag; }); + if (i != -1 && i != parameters.Length - 1) + { + throw new FaultInjectionException(); + } + } + + public string ToString(SignatureStyle style) + { + string[] temp = Array.ConvertAll(parameters, delegate(ParaType para) { return para.ToString(style); }); + return ConcatenateList(temp, ","); + } + + #endregion + } + + private sealed class ParaType + { + #region Private Data + + internal readonly bool refFlag = false; + internal readonly bool paramsFlag = false; + internal readonly TypeName parameterTypeName; + + #endregion + + #region Contructors + + public ParaType(string para) + { + refFlag = (para[0] == '&'); + paramsFlag = (para[0] == '%'); + if (refFlag || paramsFlag) + { + para = para.Remove(0, 1); + } + + parameterTypeName = new TypeName(para); + + if (paramsFlag) + { + string arraySuffix = parameterTypeName.arraySuffix; + if (arraySuffix == null || !arraySuffix.EndsWith("[]", StringComparison.Ordinal)) + { + throw new FaultInjectionException(); + } + } + } + + #endregion + + #region Public Members + + public string ToString(SignatureStyle style) + { + string result = parameterTypeName.ToString(style); + if (refFlag) + { + result += "&"; + } + return result; + } + + #endregion + } + + private sealed class TypeName + { + #region Private Data + + internal readonly string arraySuffix; + internal readonly PlainTypeName plainTypeName; + + #endregion + + #region Constructors + + public TypeName(string type) + { + string plainType = type; + + Regex arrayPattern = new Regex(@"\[,*\]$"); + Match match = arrayPattern.Match(plainType); + while (match.Success) + { + plainType = plainType.Substring(0, plainType.Length - match.Length); + if (arraySuffix == null) + { + arraySuffix = string.Empty; + } + arraySuffix = match.Value + arraySuffix; + match = arrayPattern.Match(plainType); + } + plainTypeName = new PlainTypeName(plainType); + } + + #endregion + + #region Public Members + + public string ToString(SignatureStyle style) + { + return plainTypeName.ToString(style) + arraySuffix; + } + + #endregion + } + + private sealed class PlainTypeName + { + #region Private Data + + internal readonly Name[] names; + internal readonly string builtInTypeFullName; + internal readonly string className; + + #endregion + + #region Constructors + + public PlainTypeName(string plainType) + { + builtInTypeFullName = BuiltInTypeHelper.AliasToFullName(plainType); + if (builtInTypeFullName == null) + { + string[] temp = SplitAtTopLevel(plainType, '.'); + names = Array.ConvertAll(temp, delegate(string name) { return new Name(name); }); + className = names[names.Length - 1].identifier; + } + else + { + string[] temp = builtInTypeFullName.Split('.'); + className = temp[temp.Length - 1]; + } + } + + #endregion + + #region Public Membera + + public string ToString(SignatureStyle style) + { + if (builtInTypeFullName != null) + { + return builtInTypeFullName; + } + + string[] temp = Array.ConvertAll(names, delegate(Name name) { return name.ToString(style); }); + return ConcatenateList(temp, "."); + } + + #endregion + } + + private sealed class MethodName + { + #region Private Data + + internal readonly Name name; + + #endregion + + #region Contructors + + public MethodName(string methodName) + { + name = new Name(methodName); + } + + #endregion + + #region Public Members + + public string ToString(SignatureStyle style) + { + if (style == SignatureStyle.Com) + { + return name.identifier; + } + else + return name.ToString(style); + } + + #endregion + } + + private sealed class Name + { + #region Private Data + + internal readonly string identifier; + internal readonly TypeList genericParameters; + + #endregion + + #region Constructors + + public Name(string name) + { + Regex pattern = new Regex(@"^(?#\d+#)(<(?.+)>)?$"); + Match match = pattern.Match(name); + if (!match.Success) + { + throw new FaultInjectionException(); + } + + identifier = match.Groups["identifier"].Value; + + if (match.Groups["typelist"].Value != string.Empty) + { + genericParameters = new TypeList(match.Groups["typelist"].Value); + } + else + { + genericParameters = null; + } + } + + #endregion + + #region Public Members + + public string ToString(SignatureStyle style) + { + string result = identifier; + if (genericParameters != null) + { + if (style == SignatureStyle.Formal) + { + result += "<" + genericParameters.ToString(style) + ">"; + } + else if (style == SignatureStyle.Com) + result += "`" + genericParameters.typeNames.Length.ToString(NumberFormatInfo.InvariantInfo); + } + return result; + } + + #endregion + } + + private sealed class TypeList + { + #region Private Data + + internal readonly TypeName[] typeNames; + + #endregion + + #region Constructors + + public TypeList(string typeList) + { + string[] temp = SplitAtTopLevel(typeList, ','); + typeNames = Array.ConvertAll(temp, delegate(string type) { return new TypeName(type); }); + } + + #endregion + + #region Public Members + + public string ToString(SignatureStyle style) + { + string[] temp = Array.ConvertAll(typeNames, delegate(TypeName type) { return type.ToString(style); }); + return ConcatenateList(temp, ","); + } + + #endregion + } + + // String manipulation methods + private static string[] SplitAtTopLevel(string input, char separator) + { + return SplitAtTopLevel(input, separator, "[(<".ToCharArray(), "])>".ToCharArray()); + } + private static string[] SplitAtTopLevel(string input, char separator, char[] lefts, char[] rights) + { + if (lefts.Length != rights.Length) + { + throw new FaultInjectionException(); + } + + List result = new List(); + + Stack bracketStack = new Stack(); + int begin = 0; + for (int i = 0; i < input.Length; ++i) + { + int leftIndex = Array.IndexOf(lefts, input[i]); + int rightIndex = Array.IndexOf(rights, input[i]); + if (leftIndex != -1) + { + bracketStack.Push(leftIndex); + } + else if (rightIndex != -1) + { + if (bracketStack.Count == 0 || bracketStack.Peek() != rightIndex) + { + throw new FaultInjectionException(); + } + bracketStack.Pop(); + } + else if (input[i] == separator && bracketStack.Count == 0) + { + string item = input.Substring(begin, i - begin); + if (item == string.Empty) + { + throw new FaultInjectionException(); + } + result.Add(item); + begin = i + 1; + } + } + if (begin >= input.Length) + { + throw new FaultInjectionException(); + } + result.Add(input.Substring(begin, input.Length - begin)); + + return result.ToArray(); + } + private static string ConcatenateList(string[] list, string separator) + { + if (list.Length == 0) + { + return string.Empty; + } + string result = list[0]; + for (int i = 1; i < list.Length; ++i) + { + result = result + separator + list[i]; + } + return result; + } + + #endregion + } + + #endregion + } +} diff --git a/src/dotnetCampus.UITest.WPFTestHelper/FaultInjection/SignatureParsing/SignatureStyle.cs b/src/dotnetCampus.UITest.WPFTestHelper/FaultInjection/SignatureParsing/SignatureStyle.cs new file mode 100644 index 0000000..3e66c14 --- /dev/null +++ b/src/dotnetCampus.UITest.WPFTestHelper/FaultInjection/SignatureParsing/SignatureStyle.cs @@ -0,0 +1,19 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + + +namespace dotnetCampus.UITest.WPFTestHelper.FaultInjection.SignatureParsing +{ + internal enum SignatureStyle + { + // Formal signature style used to identify a method + // Full quailfied type name, "<>" for generics, "&" for "ref" and "out" + // An example: NSName1.NSName2.OuterClass.InnerClass.foo(System.String[], int&) + Formal = 0, + // Signature style used for our COM engine to filter method + // Full quailfied type name, no method parameters, only records number of generic parameters for classes + // An example: NSName1.NSName2.OuterClass`2.InnerClass`2.foo + Com = 1 + } +} diff --git a/src/dotnetCampus.UITest.WPFTestHelper/Input/Key.cs b/src/dotnetCampus.UITest.WPFTestHelper/Input/Key.cs new file mode 100644 index 0000000..cfbfe9a --- /dev/null +++ b/src/dotnetCampus.UITest.WPFTestHelper/Input/Key.cs @@ -0,0 +1,1039 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + + + +namespace dotnetCampus.UITest.WPFTestHelper.Input +{ + /// + /// An enumeration of all the possible key values on a keyboard. + /// + public enum Key : int + { + /// + /// No key pressed. + /// + None = 0, + + /// + /// The CANCEL key. + /// + Cancel = 0x03, + + /// + /// The BACKSPACE key. + /// + Back = 0x08, + + /// + /// The TAB key. + /// + Tab = 0x09, + + /// + /// The LineFeed key. + /// + LineFeed, + + /// + /// The CLEAR key. + /// + Clear = 0x0C, + + /// + /// The RETURN key. + /// + Return = 0x0D, + + /// + /// The ENTER key. + /// + Enter = Return, + + /// + /// The SHIFT key. + /// + Shift = 0x10, + + /// + /// The CTRL key. + /// + Ctrl = 0x11, + + /// + /// The ALT key. + /// + Alt = 0x12, + + /// + /// The PAUSE key. + /// + Pause = 0x13, + + /// + /// The CAPS LOCK key. + /// + Capital = 0x14, + + /// + /// The CAPS LOCK key. + /// + CapsLock = Capital, + + /// + /// The IME Kana mode key. + /// + KanaMode = 0x15, + + /// + /// The IME Hangul mode key. + /// + HangulMode = KanaMode, + + /// + /// The IME Junja mode key. + /// + JunjaMode = 0x17, + + /// + /// The IME Final mode key. + /// + FinalMode= 0x18, + + /// + /// The IME Hanja mode key. + /// + HanjaMode = 0x19, + + /// + /// The IME Kanji mode key. + /// + KanjiMode = HanjaMode, + + /// + /// The ESC key. + /// + Escape = 0x1B, + + /// + /// The IME Convert key. + /// + ImeConvert = 0x1C, + + /// + /// The IME NonConvert key. + /// + ImeNonConvert = 0x1D, + + /// + /// The IME Accept key. + /// + ImeAccept = 0x1E, + + /// + /// The IME Mode change request. + /// + ImeModeChange = 0x1F, + + /// + /// The SPACEBAR key. + /// + Space = 0x20, + + /// + /// The PAGE UP key. + /// + Prior = 0x21, + + /// + /// The PAGE UP key. + /// + PageUp = Prior, + + /// + /// The PAGE DOWN key. + /// + Next = 0x22, + + /// + /// The PAGE DOWN key. + /// + PageDown = Next, + + /// + /// The END key. + /// + End = 0x23, + + /// + /// The HOME key. + /// + Home = 0x24, + + /// + /// The LEFT ARROW key. + /// + Left = 0x25, + + /// + /// The UP ARROW key. + /// + Up = 0x26, + + /// + /// The RIGHT ARROW key. + /// + Right = 0x27, + + /// + /// The DOWN ARROW key. + /// + Down = 0x28, + + /// + /// The SELECT key. + /// + Select = 0x29, + + /// + /// The PRINT key. + /// + Print = 0x2A, + + /// + /// The EXECUTE key. + /// + Execute = 0x2B, + + /// + /// The SNAPSHOT key. + /// + Snapshot = 0x2C, + + /// + /// The PRINT SCREEN key. + /// + PrintScreen = Snapshot, + + /// + /// The INS key. + /// + Insert = 0x2D, + + /// + /// The DEL key. + /// + Delete = 0x2E, + + /// + /// The HELP key. + /// + Help = 0x2F, + + /// + /// The 0 key. + /// + D0 = 0x30, + + /// + /// The 1 key. + /// + D1 = 0x31, + + /// + /// The 2 key. + /// + D2 = 0x32, + + /// + /// The 3 key. + /// + D3 = 0x33, + + /// + /// The 4 key. + /// + D4 = 0x34, + + /// + /// The 5 key. + /// + D5 = 0x35, + + /// + /// The 6 key. + /// + D6 = 0x36, + + /// + /// The 7 key. + /// + D7 = 0x37, + + /// + /// The 8 key. + /// + D8 = 0x38, + + /// + /// The 9 key. + /// + D9 = 0x39, + + /// + /// The A key. + /// + A = 0x41, + + /// + /// The B key. + /// + B = 0x42, + + /// + /// The C key. + /// + C = 0x43, + + /// + /// The D key. + /// + D = 0x44, + + /// + /// The E key. + /// + E = 0x45, + + /// + /// The F key. + /// + F = 0x46, + + /// + /// The G key. + /// + G = 0x47, + + /// + /// The H key. + /// + H = 0x48, + + /// + /// The I key. + /// + I = 0x49, + + /// + /// The J key. + /// + J = 0x4A, + + /// + /// The K key. + /// + K = 0x4B, + + /// + /// The L key. + /// + L = 0x4C, + + /// + /// The M key. + /// + M = 0x4D, + + /// + /// The N key. + /// + N = 0x4E, + + /// + /// The O key. + /// + O = 0x4F, + + /// + /// The P key. + /// + P = 0x50, + + /// + /// The Q key. + /// + Q = 0x51, + + /// + /// The R key. + /// + R = 0x52, + + /// + /// The S key. + /// + S = 0x53, + + /// + /// The T key. + /// + T = 0x54, + + /// + /// The U key. + /// + U = 0x55, + + /// + /// The V key. + /// + V = 0x56, + + /// + /// The W key. + /// + W = 0x57, + + /// + /// The X key. + /// + X = 0x58, + + /// + /// The Y key. + /// + Y = 0x59, + + /// + /// The Z key. + /// + Z = 0x5A, + + /// + /// The left Windows logo key (Microsoft Natural Keyboard). + /// + LWin = 0x5B, + + /// + /// The right Windows logo key (Microsoft Natural Keyboard). + /// + RWin = 0x5C, + + /// + /// The Application key (Microsoft Natural Keyboard). + /// + Apps = 0x5D, + + /// + /// The Computer Sleep key. + /// + Sleep = 0x5F, + + /// + /// The 0 key on the numeric keypad. + /// + NumPad0 = 0x60, + + /// + /// The 1 key on the numeric keypad. + /// + NumPad1 = 0x61, + + /// + /// The 2 key on the numeric keypad. + /// + NumPad2 = 0x62, + + /// + /// The 3 key on the numeric keypad. + /// + NumPad3 = 0x63, + + /// + /// The 4 key on the numeric keypad. + /// + NumPad4 = 0x64, + + /// + /// The 5 key on the numeric keypad. + /// + NumPad5 = 0x65, + + /// + /// The 6 key on the numeric keypad. + /// + NumPad6 = 0x66, + + /// + /// The 7 key on the numeric keypad. + /// + NumPad7 = 0x67, + + /// + /// The 8 key on the numeric keypad. + /// + NumPad8 = 0x68, + + /// + /// The 9 key on the numeric keypad. + /// + NumPad9 = 0x69, + + /// + /// The Multiply key. + /// + Multiply = 0x6A, + + /// + /// The Add key. + /// + Add = 0x6B, + + /// + /// The Separator key. + /// + Separator = 0x6C, + + /// + /// The Subtract key. + /// + Subtract = 0x6D, + + /// + /// The Decimal key. + /// + Decimal = 0x6E, + + /// + /// The Divide key. + /// + Divide = 0x6F, + + /// + /// The F1 key. + /// + F1 = 0x70, + + /// + /// The F2 key. + /// + F2 = 0x71, + + /// + /// The F3 key. + /// + F3 = 0x72, + + /// + /// The F4 key. + /// + F4 = 0x73, + + /// + /// The F5 key. + /// + F5 = 0x74, + + /// + /// The F6 key. + /// + F6 = 0x75, + + /// + /// The F7 key. + /// + F7 = 0x76, + + /// + /// The F8 key. + /// + F8 = 0x77, + + /// + /// The F9 key. + /// + F9 = 0x78, + + /// + /// The F10 key. + /// + F10 = 0x79, + + /// + /// The F11 key. + /// + F11 = 0x7A, + + /// + /// The F12 key. + /// + F12 = 0x7B, + + /// + /// The F13 key. + /// + F13 = 0x7C, + + /// + /// The F14 key. + /// + F14 = 0x7D, + + /// + /// The F15 key. + /// + F15 = 0x7E, + + /// + /// The F16 key. + /// + F16 = 0x7F, + + /// + /// The F17 key. + /// + F17 = 0x80, + + /// + /// The F18 key. + /// + F18 = 0x81, + + /// + /// The F19 key. + /// + F19 = 0x82, + + /// + /// The F20 key. + /// + F20 = 0x83, + + /// + /// The F21 key. + /// + F21 = 0x84, + + /// + /// The F22 key. + /// + F22 = 0x85, + + /// + /// The F23 key. + /// + F23 = 0x86, + + /// + /// The F24 key. + /// + F24 = 0x87, + + /// + /// The NUM LOCK key. + /// + NumLock = 0x90, + + /// + /// The SCROLL LOCK key. + /// + Scroll = 0x91, + + /// + /// The left SHIFT key. + /// + LeftShift = 0xA0, + + /// + /// The right SHIFT key. + /// + RightShift = 0xA1, + + /// + /// The left CTRL key. + /// + LeftCtrl = 0xA2, + + /// + /// The right CTRL key. + /// + RightCtrl = 0xA3, + + /// + /// The left ALT key. + /// + LeftAlt = 0xA4, + + /// + /// The right ALT key. + /// + RightAlt = 0xA5, + + /// + /// The Browser Back key. + /// + BrowserBack = 0xA6, + + /// + /// The Browser Forward key. + /// + BrowserForward = 0xA7, + + /// + /// The Browser Refresh key. + /// + BrowserRefresh = 0xA8, + + /// + /// The Browser Stop key. + /// + BrowserStop = 0xA9, + + /// + /// The Browser Search key. + /// + BrowserSearch = 0xAA, + + /// + /// The Browser Favorites key. + /// + BrowserFavorites = 0xAB, + + /// + /// The Browser Home key. + /// + BrowserHome = 0xAC, + + /// + /// The Volume Mute key. + /// + VolumeMute = 0xAD, + + /// + /// The Volume Down key. + /// + VolumeDown = 0xAE, + + /// + /// The Volume Up key. + /// + VolumeUp = 0xAF, + + /// + /// The Media Next Track key. + /// + MediaNextTrack = 0xB0, + + /// + /// The Media Previous Track key. + /// + MediaPreviousTrack = 0xB1, + + /// + /// The Media Stop key. + /// + MediaStop = 0xB2, + + /// + /// The Media Play Pause key. + /// + MediaPlayPause = 0xB3, + + /// + /// The Launch Mail key. + /// + LaunchMail = 0xB4, + + /// + /// The Select Media key. + /// + SelectMedia = 0xB5, + + /// + /// The Launch Application1 key. + /// + LaunchApplication1 = 0xB6, + + /// + /// The Launch Application2 key. + /// + LaunchApplication2 = 0xB7, + + /// + /// The Oem 1 key. ',:' for US + /// + Oem1 = 0xBA, + + /// + /// The Oem Semicolon key. + /// + OemSemicolon = Oem1, + + /// + /// The Oem plus key. '+' any country + /// + OemPlus = 0xBB, + + /// + /// The Oem comma key. ',' any country + /// + OemComma = 0xBC, + + /// + /// The Oem Minus key. '-' any country + /// + OemMinus = 0xBD, + + /// + /// The Oem Period key. '.' any country + /// + OemPeriod = 0xBE, + + /// + /// The Oem 2 key. '/?' for US + /// + Oem2 = 0xBF, + + /// + /// The Oem Question key. + /// + OemQuestion = Oem2, + + /// + /// The Oem 3 key. '`~' for US + /// + Oem3 = 0xC0, + + /// + /// The Oem tilde key. + /// + OemTilde = Oem3, + + /// + /// The ABNT_C1 (Brazilian) key. + /// + AbntC1 = 0xC1, + + /// + /// The ABNT_C2 (Brazilian) key. + /// + AbntC2 = 0xC2, + + /// + /// The Oem 4 key. + /// + Oem4 = 0xDB, + + /// + /// The Oem Open Brackets key. + /// + OemOpenBrackets = Oem4, + + /// + /// The Oem 5 key. + /// + Oem5 = 0xDC, + + /// + /// The Oem Pipe key. + /// + OemPipe = Oem5, + + /// + /// The Oem 6 key. + /// + Oem6 = 0xDD, + + /// + /// The Oem Close Brackets key. + /// + OemCloseBrackets = Oem6, + + /// + /// The Oem 7 key. + /// + Oem7 = 0xDE, + + /// + /// The Oem Quotes key. + /// + OemQuotes = Oem7, + + /// + /// The Oem8 key. + /// + Oem8 = 0xDF, + + /// + /// The Oem 102 key. + /// + Oem102 = 0xE2, + + /// + /// The Oem Backslash key. + /// + OemBackslash = Oem102, + + /// + /// A special key masking the real key being processed by an IME. + /// + ImeProcessed = 0xE5, + + /// + /// A special key masking the real key being processed as a system key. + /// + System, + + /// + /// The OEM_ATTN key. + /// + OemAttn = 0xF0, + + /// + /// The DBE_ALPHANUMERIC key. + /// + DbeAlphanumeric = OemAttn, + + /// + /// The OEM_FINISH key. + /// + OemFinish = 0xF1, + + /// + /// The DBE_KATAKANA key. + /// + DbeKatakana = OemFinish, + + /// + /// The OEM_COPY key. + /// + OemCopy = 0xF2, + + /// + /// The DBE_HIRAGANA key. + /// + DbeHiragana = OemCopy, + + /// + /// The OEM_AUTO key. + /// + OemAuto = 0xF3, + + /// + /// The DBE_SBCSCHAR key. + /// + DbeSbcsChar = OemAuto, + + /// + /// The OEM_ENLW key. + /// + OemEnlw = 0xF4, + + /// + /// The DBE_DBCSCHAR key. + /// + DbeDbcsChar = OemEnlw, + + /// + /// The OEM_BACKTAB key. + /// + OemBackTab = 0xF5, + + /// + /// The DBE_ROMAN key. + /// + DbeRoman = OemBackTab, + + /// + /// The ATTN key. + /// + Attn = 0xF6, + + /// + /// The DBE_NOROMAN key. + /// + DbeNoRoman = Attn, + + /// + /// The CRSEL key. + /// + CrSel = 0xF7, + + /// + /// The DBE_ENTERWORDREGISTERMODE key. + /// + DbeEnterWordRegisterMode = CrSel, + + /// + /// The EXSEL key. + /// + ExSel = 0xF8, + + /// + /// The DBE_ENTERIMECONFIGMODE key. + /// + DbeEnterImeConfigureMode = ExSel, + + /// + /// The ERASE EOF key. + /// + EraseEof = 0xF9, + + /// + /// The DBE_FLUSHSTRING key. + /// + DbeFlushString = EraseEof, + + /// + /// The PLAY key. + /// + Play = 0xFA, + + /// + /// The DBE_CODEINPUT key. + /// + DbeCodeInput = Play, + + /// + /// The ZOOM key. + /// + Zoom = 0xFB, + + /// + /// The DBE_NOCODEINPUT key. + /// + DbeNoCodeInput = Zoom, + + /// + /// A constant reserved for future use. + /// + NoName = 0xFC, + + /// + /// The DBE_DETERMINESTRING key. + /// + DbeDetermineString = NoName, + + /// + /// The PA1 key. + /// + Pa1 = 0xFD, + + /// + /// The DBE_ENTERDLGCONVERSIONMODE key. + /// + DbeEnterDialogConversionMode = Pa1, + + /// + /// The CLEAR key. + /// + OemClear = 0xFE, + + /// + /// Indicates the key is part of a dead-key composition + /// + DeadCharProcessed = 0, + } +} diff --git a/src/dotnetCampus.UITest.WPFTestHelper/Input/Keyboard.cs b/src/dotnetCampus.UITest.WPFTestHelper/Input/Keyboard.cs new file mode 100644 index 0000000..1d55772 --- /dev/null +++ b/src/dotnetCampus.UITest.WPFTestHelper/Input/Keyboard.cs @@ -0,0 +1,418 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.InteropServices; +using System.Threading; + +namespace dotnetCampus.UITest.WPFTestHelper.Input +{ + /// + /// Exposes a simple interface to common keyboard operations, allowing the user to simulate keyboard input. + /// + /// + /// The following code types "Hello, world!" with the specified casing, + /// and then types "hello, capitalized world!" which will be in all caps because + /// the left shift key is being held down. + /// + /// Keyboard.Type("Hello, world!"); + /// Keyboard.Type(Key.Enter); + /// Keyboard.Press(Key.LeftShift); + /// Keyboard.Type("hello, capitalized world!"); + /// Keyboard.Release(Key.LeftShift); + /// + /// + public class Keyboard + { + static Keyboard() + { + KeyBoardKeys = new Dictionary(); + KeyBoardKeys.Add(Key.Cancel, new KeySpec((ushort)Key.Cancel, false, "cancel")); + KeyBoardKeys.Add(Key.Back, new KeySpec((ushort)Key.Back, false, "backspace")); + KeyBoardKeys.Add(Key.Tab, new KeySpec((ushort)Key.Tab, false, "tab")); + KeyBoardKeys.Add(Key.Clear, new KeySpec((ushort)Key.Clear, false, "clear")); + KeyBoardKeys.Add(Key.Return, new KeySpec((ushort)Key.Return, false, "return")); + KeyBoardKeys.Add(Key.Shift, new KeySpec((ushort)Key.Shift, false, "shift")); + KeyBoardKeys.Add(Key.Ctrl, new KeySpec((ushort)Key.Ctrl, false, "ctrl")); + KeyBoardKeys.Add(Key.Alt, new KeySpec((ushort)Key.Alt, false, "alt")); + KeyBoardKeys.Add(Key.Pause, new KeySpec((ushort)Key.Pause, false, "pause")); + KeyBoardKeys.Add(Key.Capital, new KeySpec((ushort)Key.Capital, false, "capital")); + KeyBoardKeys.Add(Key.KanaMode, new KeySpec((ushort)Key.KanaMode, false, "kanamode")); + KeyBoardKeys.Add(Key.JunjaMode, new KeySpec((ushort)Key.JunjaMode, false, "junjamode")); + KeyBoardKeys.Add(Key.FinalMode, new KeySpec((ushort)Key.FinalMode, false, "finalmode")); + KeyBoardKeys.Add(Key.HanjaMode, new KeySpec((ushort)Key.HanjaMode, false, "hanjamode")); + KeyBoardKeys.Add(Key.Escape, new KeySpec((ushort)Key.Escape, false, "esc")); + KeyBoardKeys.Add(Key.ImeConvert, new KeySpec((ushort)Key.ImeConvert, false, "imeconvert")); + KeyBoardKeys.Add(Key.ImeNonConvert, new KeySpec((ushort)Key.ImeNonConvert, false, "imenonconvert")); + KeyBoardKeys.Add(Key.ImeAccept, new KeySpec((ushort)Key.ImeAccept, false, "imeaccept")); + KeyBoardKeys.Add(Key.ImeModeChange, new KeySpec((ushort)Key.ImeAccept, false, "imemodechange")); + KeyBoardKeys.Add(Key.Space, new KeySpec((ushort)Key.Space, false, " ")); + KeyBoardKeys.Add(Key.Prior, new KeySpec((ushort)Key.Prior, true, "prior")); + KeyBoardKeys.Add(Key.Next, new KeySpec((ushort)Key.Next, true, "next")); + KeyBoardKeys.Add(Key.End, new KeySpec((ushort)Key.End, true, "end")); + KeyBoardKeys.Add(Key.Home, new KeySpec((ushort)Key.Home, true, "home")); + KeyBoardKeys.Add(Key.Left, new KeySpec((ushort)Key.Left, true, "left")); + KeyBoardKeys.Add(Key.Up, new KeySpec((ushort)Key.Up, true, "up")); + KeyBoardKeys.Add(Key.Right, new KeySpec((ushort)Key.Right, true, "right")); + KeyBoardKeys.Add(Key.Down, new KeySpec((ushort)Key.Down, true, "down")); + KeyBoardKeys.Add(Key.Select, new KeySpec((ushort)Key.Select, false, "select")); + KeyBoardKeys.Add(Key.Print, new KeySpec((ushort)Key.Print, false, "print")); + KeyBoardKeys.Add(Key.Execute, new KeySpec((ushort)Key.Execute, false, "execute")); + KeyBoardKeys.Add(Key.Snapshot, new KeySpec((ushort)Key.Snapshot, true, "snapshot")); + KeyBoardKeys.Add(Key.Insert, new KeySpec((ushort)Key.Insert, true, "insert")); + KeyBoardKeys.Add(Key.Delete, new KeySpec((ushort)Key.Delete, true, "delete")); + KeyBoardKeys.Add(Key.Help, new KeySpec((ushort)Key.Help, false, "help")); + + KeyBoardKeys.Add(Key.D0, new KeySpec((ushort)Key.D0, false, "0")); + KeyBoardKeys.Add(Key.D1, new KeySpec((ushort)Key.D1, false, "1")); + KeyBoardKeys.Add(Key.D2, new KeySpec((ushort)Key.D2, false, "2")); + KeyBoardKeys.Add(Key.D3, new KeySpec((ushort)Key.D3, false, "3")); + KeyBoardKeys.Add(Key.D4, new KeySpec((ushort)Key.D4, false, "4")); + KeyBoardKeys.Add(Key.D5, new KeySpec((ushort)Key.D5, false, "5")); + KeyBoardKeys.Add(Key.D6, new KeySpec((ushort)Key.D6, false, "6")); + KeyBoardKeys.Add(Key.D7, new KeySpec((ushort)Key.D7, false, "7")); + KeyBoardKeys.Add(Key.D8, new KeySpec((ushort)Key.D8, false, "8")); + KeyBoardKeys.Add(Key.D9, new KeySpec((ushort)Key.D9, false, "9")); + + KeyBoardKeys.Add(Key.A, new KeySpec((ushort)Key.A, false, "a")); + KeyBoardKeys.Add(Key.B, new KeySpec((ushort)Key.B, false, "b")); + KeyBoardKeys.Add(Key.C, new KeySpec((ushort)Key.C, false, "c")); + KeyBoardKeys.Add(Key.D, new KeySpec((ushort)Key.D, false, "d")); + KeyBoardKeys.Add(Key.E, new KeySpec((ushort)Key.E, false, "e")); + KeyBoardKeys.Add(Key.F, new KeySpec((ushort)Key.F, false, "f")); + KeyBoardKeys.Add(Key.G, new KeySpec((ushort)Key.G, false, "g")); + KeyBoardKeys.Add(Key.H, new KeySpec((ushort)Key.H, false, "h")); + KeyBoardKeys.Add(Key.I, new KeySpec((ushort)Key.I, false, "i")); + KeyBoardKeys.Add(Key.J, new KeySpec((ushort)Key.J, false, "j")); + KeyBoardKeys.Add(Key.K, new KeySpec((ushort)Key.K, false, "k")); + KeyBoardKeys.Add(Key.L, new KeySpec((ushort)Key.L, false, "l")); + KeyBoardKeys.Add(Key.M, new KeySpec((ushort)Key.M, false, "m")); + KeyBoardKeys.Add(Key.N, new KeySpec((ushort)Key.N, false, "n")); + KeyBoardKeys.Add(Key.O, new KeySpec((ushort)Key.O, false, "o")); + KeyBoardKeys.Add(Key.P, new KeySpec((ushort)Key.P, false, "p")); + KeyBoardKeys.Add(Key.Q, new KeySpec((ushort)Key.Q, false, "q")); + KeyBoardKeys.Add(Key.R, new KeySpec((ushort)Key.R, false, "r")); + KeyBoardKeys.Add(Key.S, new KeySpec((ushort)Key.S, false, "s")); + KeyBoardKeys.Add(Key.T, new KeySpec((ushort)Key.T, false, "t")); + KeyBoardKeys.Add(Key.U, new KeySpec((ushort)Key.U, false, "u")); + KeyBoardKeys.Add(Key.V, new KeySpec((ushort)Key.V, false, "v")); + KeyBoardKeys.Add(Key.W, new KeySpec((ushort)Key.W, false, "w")); + KeyBoardKeys.Add(Key.X, new KeySpec((ushort)Key.X, false, "x")); + KeyBoardKeys.Add(Key.Y, new KeySpec((ushort)Key.Y, false, "y")); + KeyBoardKeys.Add(Key.Z, new KeySpec((ushort)Key.Z, false, "z")); + + KeyBoardKeys.Add(Key.LWin, new KeySpec((ushort)Key.LWin, true, "lwin")); + KeyBoardKeys.Add(Key.RWin, new KeySpec((ushort)Key.RWin, true, "rwin")); + KeyBoardKeys.Add(Key.Apps, new KeySpec((ushort)Key.Apps, true, "apps")); + KeyBoardKeys.Add(Key.Sleep, new KeySpec((ushort)Key.Sleep, false, "sleep")); + + KeyBoardKeys.Add(Key.NumPad0, new KeySpec((ushort)Key.NumPad0, false, "n0")); + KeyBoardKeys.Add(Key.NumPad1, new KeySpec((ushort)Key.NumPad1, false, "n1")); + KeyBoardKeys.Add(Key.NumPad2, new KeySpec((ushort)Key.NumPad2, false, "n2")); + KeyBoardKeys.Add(Key.NumPad3, new KeySpec((ushort)Key.NumPad3, false, "n3")); + KeyBoardKeys.Add(Key.NumPad4, new KeySpec((ushort)Key.NumPad4, false, "n4")); + KeyBoardKeys.Add(Key.NumPad5, new KeySpec((ushort)Key.NumPad5, false, "n5")); + KeyBoardKeys.Add(Key.NumPad6, new KeySpec((ushort)Key.NumPad6, false, "n6")); + KeyBoardKeys.Add(Key.NumPad7, new KeySpec((ushort)Key.NumPad7, false, "n7")); + KeyBoardKeys.Add(Key.NumPad8, new KeySpec((ushort)Key.NumPad8, false, "n8")); + KeyBoardKeys.Add(Key.NumPad9, new KeySpec((ushort)Key.NumPad9, false, "n9")); + + KeyBoardKeys.Add(Key.Multiply, new KeySpec((ushort)Key.Multiply, false, "*")); + KeyBoardKeys.Add(Key.Add, new KeySpec((ushort)Key.Add, false, "+")); + KeyBoardKeys.Add(Key.Separator, new KeySpec((ushort)Key.Separator, false, "separator")); + KeyBoardKeys.Add(Key.Subtract, new KeySpec((ushort)Key.Subtract, false, "-")); + KeyBoardKeys.Add(Key.Decimal, new KeySpec((ushort)Key.Decimal, false, "decimal")); + KeyBoardKeys.Add(Key.Divide, new KeySpec((ushort)Key.Divide, true, "/")); + + KeyBoardKeys.Add(Key.F1, new KeySpec((ushort)Key.F1, false, "f1")); + KeyBoardKeys.Add(Key.F2, new KeySpec((ushort)Key.F2, false, "f2")); + KeyBoardKeys.Add(Key.F3, new KeySpec((ushort)Key.F3, false, "f3")); + KeyBoardKeys.Add(Key.F4, new KeySpec((ushort)Key.F4, false, "f4")); + KeyBoardKeys.Add(Key.F5, new KeySpec((ushort)Key.F5, false, "f5")); + KeyBoardKeys.Add(Key.F6, new KeySpec((ushort)Key.F6, false, "f6")); + KeyBoardKeys.Add(Key.F7, new KeySpec((ushort)Key.F7, false, "f7")); + KeyBoardKeys.Add(Key.F8, new KeySpec((ushort)Key.F8, false, "f8")); + KeyBoardKeys.Add(Key.F9, new KeySpec((ushort)Key.F9, false, "f9")); + KeyBoardKeys.Add(Key.F10, new KeySpec((ushort)Key.F10, false, "f10")); + KeyBoardKeys.Add(Key.F11, new KeySpec((ushort)Key.F11, false, "f11")); + KeyBoardKeys.Add(Key.F12, new KeySpec((ushort)Key.F12, false, "f12")); + KeyBoardKeys.Add(Key.F13, new KeySpec((ushort)Key.F13, false, "f13")); + KeyBoardKeys.Add(Key.F14, new KeySpec((ushort)Key.F14, false, "f14")); + KeyBoardKeys.Add(Key.F15, new KeySpec((ushort)Key.F15, false, "f15")); + KeyBoardKeys.Add(Key.F16, new KeySpec((ushort)Key.F16, false, "f16")); + KeyBoardKeys.Add(Key.F17, new KeySpec((ushort)Key.F17, false, "f17")); + KeyBoardKeys.Add(Key.F18, new KeySpec((ushort)Key.F18, false, "f18")); + KeyBoardKeys.Add(Key.F19, new KeySpec((ushort)Key.F19, false, "f19")); + KeyBoardKeys.Add(Key.F20, new KeySpec((ushort)Key.F20, false, "f20")); + KeyBoardKeys.Add(Key.F21, new KeySpec((ushort)Key.F21, false, "f21")); + KeyBoardKeys.Add(Key.F22, new KeySpec((ushort)Key.F22, false, "f22")); + KeyBoardKeys.Add(Key.F23, new KeySpec((ushort)Key.F23, false, "f23")); + KeyBoardKeys.Add(Key.F24, new KeySpec((ushort)Key.F24, false, "f24")); + + KeyBoardKeys.Add(Key.NumLock, new KeySpec((ushort)Key.NumLock, true, "numlock")); + KeyBoardKeys.Add(Key.Scroll, new KeySpec((ushort)Key.Scroll, false, "scroll")); + KeyBoardKeys.Add(Key.LeftShift, new KeySpec((ushort)Key.LeftShift, false, "leftshift")); + KeyBoardKeys.Add(Key.RightShift, new KeySpec((ushort)Key.RightShift, false, "rightshift")); + KeyBoardKeys.Add(Key.LeftCtrl, new KeySpec((ushort)Key.LeftCtrl, false, "leftctrl")); + KeyBoardKeys.Add(Key.RightCtrl, new KeySpec((ushort)Key.RightCtrl, true, "rightctrl")); + KeyBoardKeys.Add(Key.LeftAlt, new KeySpec((ushort)Key.LeftAlt, false, "leftalt")); + KeyBoardKeys.Add(Key.RightAlt, new KeySpec((ushort)Key.RightAlt, true, "rightalt")); + + KeyBoardKeys.Add(Key.BrowserBack, new KeySpec((ushort)Key.BrowserBack, false, "browserback")); + KeyBoardKeys.Add(Key.BrowserForward, new KeySpec((ushort)Key.BrowserForward, false, "browserforward")); + KeyBoardKeys.Add(Key.BrowserRefresh, new KeySpec((ushort)Key.BrowserRefresh, false, "browserrefresh")); + KeyBoardKeys.Add(Key.BrowserStop, new KeySpec((ushort)Key.BrowserStop, false, "browserstop")); + KeyBoardKeys.Add(Key.BrowserSearch, new KeySpec((ushort)Key.BrowserSearch, false, "browsersearch")); + KeyBoardKeys.Add(Key.BrowserFavorites, new KeySpec((ushort)Key.BrowserFavorites, false, "BrowserFavorites")); + KeyBoardKeys.Add(Key.BrowserHome, new KeySpec((ushort)Key.BrowserHome, false, "BrowserHome")); + + KeyBoardKeys.Add(Key.VolumeMute, new KeySpec((ushort)Key.VolumeMute, false, "VolumeMute")); + KeyBoardKeys.Add(Key.VolumeDown, new KeySpec((ushort)Key.VolumeDown, false, "VolumeDown")); + KeyBoardKeys.Add(Key.VolumeUp, new KeySpec((ushort)Key.VolumeUp, false, "VolumeUp")); + KeyBoardKeys.Add(Key.MediaNextTrack, new KeySpec((ushort)Key.MediaNextTrack, false, "MediaNextTrack")); + KeyBoardKeys.Add(Key.MediaPreviousTrack, new KeySpec((ushort)Key.MediaPreviousTrack, false, "MediaPreviousTrack")); + KeyBoardKeys.Add(Key.MediaStop, new KeySpec((ushort)Key.MediaStop, false, "MediaStop")); + KeyBoardKeys.Add(Key.MediaPlayPause, new KeySpec((ushort)Key.MediaPlayPause, false, "MediaPlayPause")); + KeyBoardKeys.Add(Key.LaunchMail, new KeySpec((ushort)Key.LaunchMail, false, "LaunchMail")); + KeyBoardKeys.Add(Key.SelectMedia, new KeySpec((ushort)Key.SelectMedia, false, "SelectMedia")); + KeyBoardKeys.Add(Key.LaunchApplication1, new KeySpec((ushort)Key.LaunchApplication1, false, "LaunchApplication1")); + KeyBoardKeys.Add(Key.LaunchApplication2, new KeySpec((ushort)Key.LaunchApplication2, false, "LaunchApplication2")); + + KeyBoardKeys.Add(Key.Oem1, new KeySpec((ushort)Key.Oem1, false, ";")); + KeyBoardKeys.Add(Key.OemPlus, new KeySpec((ushort)Key.OemPlus, false, "+")); + KeyBoardKeys.Add(Key.OemComma, new KeySpec((ushort)Key.OemComma, false, ",")); + KeyBoardKeys.Add(Key.OemMinus, new KeySpec((ushort)Key.OemMinus, false, "-")); + KeyBoardKeys.Add(Key.OemPeriod, new KeySpec((ushort)Key.OemPeriod, false, ".")); + KeyBoardKeys.Add(Key.Oem2, new KeySpec((ushort)Key.Oem2, false, "?")); + KeyBoardKeys.Add(Key.Oem3, new KeySpec((ushort)Key.Oem3, false, "~")); + KeyBoardKeys.Add(Key.AbntC1, new KeySpec((ushort)Key.AbntC1, false, "AbntC1")); + KeyBoardKeys.Add(Key.AbntC2, new KeySpec((ushort)Key.AbntC2, false, "AbntC2")); + KeyBoardKeys.Add(Key.Oem4, new KeySpec((ushort)Key.Oem4, false, "[")); + KeyBoardKeys.Add(Key.Oem5, new KeySpec((ushort)Key.Oem5, false, "|")); + KeyBoardKeys.Add(Key.Oem6, new KeySpec((ushort)Key.Oem6, false, "]")); + KeyBoardKeys.Add(Key.Oem7, new KeySpec((ushort)Key.Oem7, false, "\"")); + KeyBoardKeys.Add(Key.Oem8, new KeySpec((ushort)Key.Oem8, false, "Oem8")); + KeyBoardKeys.Add(Key.Oem102, new KeySpec((ushort)Key.Oem102, false, "\\")); + KeyBoardKeys.Add(Key.ImeProcessed, new KeySpec((ushort)Key.ImeProcessed, false, "ImeProcessed")); + KeyBoardKeys.Add(Key.OemAttn, new KeySpec((ushort)Key.OemAttn, false, "OemAttn")); + KeyBoardKeys.Add(Key.OemFinish, new KeySpec((ushort)Key.OemFinish, false, "OemFinish")); + KeyBoardKeys.Add(Key.OemCopy, new KeySpec((ushort)Key.OemCopy, false, "OemCopy")); + KeyBoardKeys.Add(Key.OemAuto, new KeySpec((ushort)Key.OemAuto, false, "OemAuto")); + KeyBoardKeys.Add(Key.OemEnlw, new KeySpec((ushort)Key.OemEnlw, false, "OemEnlw")); + KeyBoardKeys.Add(Key.OemBackTab, new KeySpec((ushort)Key.OemBackTab, false, "OemBackTab")); + KeyBoardKeys.Add(Key.Attn, new KeySpec((ushort)Key.Attn, false, "Attn")); + KeyBoardKeys.Add(Key.CrSel, new KeySpec((ushort)Key.CrSel, false, "CrSel")); + KeyBoardKeys.Add(Key.ExSel, new KeySpec((ushort)Key.ExSel, false, "ExSel")); + KeyBoardKeys.Add(Key.EraseEof, new KeySpec((ushort)Key.EraseEof, false, "EraseEof")); + KeyBoardKeys.Add(Key.Play, new KeySpec((ushort)Key.Play, false, "Play")); + KeyBoardKeys.Add(Key.Zoom, new KeySpec((ushort)Key.Zoom, false, "Zoom")); + KeyBoardKeys.Add(Key.NoName, new KeySpec((ushort)Key.NoName, false, "NoName")); + KeyBoardKeys.Add(Key.Pa1, new KeySpec((ushort)Key.Pa1, false, "Pa1")); + KeyBoardKeys.Add(Key.OemClear, new KeySpec((ushort)Key.OemClear, false, "OemClear")); + KeyBoardKeys.Add(Key.DeadCharProcessed, new KeySpec((ushort)Key.DeadCharProcessed, false, "DeadCharProcessed")); + } + + #region Public Methods + + /// + /// Presses down a key. + /// + /// The key to press. + public static void Press(Key key) + { + var keySpec = GetKeySpecFromKey(key); + SendKeyboardKey(keySpec.KeyCode, true, keySpec.IsExtended, false); + } + + /// + /// Releases a key. + /// + /// The key to release. + public static void Release(Key key) + { + var keySpec = GetKeySpecFromKey(key); + SendKeyboardKey(keySpec.KeyCode, false, keySpec.IsExtended, false); + } + + /// + /// Resets the system keyboard to a clean state. + /// + public static void Reset() + { + foreach (Key key in Enum.GetValues(typeof(Key))) + { + if ((GetKeyState(key) & KeyStates.Down) > 0) + { + Release(key); + } + } + } + + /// + /// Performs a press-and-release operation for the specified key, which is effectively equivallent to typing. + /// + /// The key to press. + public static void Type(Key key) + { + Press(key); + Release(key); + } + + + /// + /// Types the specified text. + /// + /// + /// Note that a combination of a combination of Key.Shift or Key.Capital and a Unicode point above 0xFE + /// is not considered valid and will not result in the Unicode point being types without + /// applying of the modifier key. + /// + /// The text to type. + public static void Type(string text) + { + foreach (char c in text) + { + // If code point is bigger than 8 bits, we are going for Unicode approach by setting wVk to 0. + if (c > 0xFE) + { + SendKeyboardKey(c, true, false, true); + SendKeyboardKey(c, false, false, true); + } + else + { + // We get the vKey value for the character via a Win32 API. We then use bit masks to pull the + // upper and lower bytes to get the shift state and key information. We then use WPF KeyInterop + // to go from the vKey key info into a System.Windows.Input.Key data structure. This work is + // necessary because Key doesn't distinguish between upper and lower case, so we have to wrap + // the key type inside a shift press/release if necessary. + int vKeyValue = NativeMethods.VkKeyScan(c); + bool keyIsShifted = (vKeyValue & NativeMethods.VKeyShiftMask) == NativeMethods.VKeyShiftMask; + Key key = (Key)(vKeyValue & NativeMethods.VKeyCharMask); + + if (keyIsShifted) + { + Type(key, new Key[] { Key.Shift }); + } + else + { + Type(key); + } + } + } + } + + #endregion Public Methods + + #region Private Methods + + private static KeySpec GetKeySpecFromKey(Key key) + { + KeySpec resultKey; + if (!KeyBoardKeys.TryGetValue(key, out resultKey)) + { + resultKey = new KeySpec(); + } + + return resultKey; + } + + private static void Type(Key key, Key[] modifierKeys) + { + foreach (Key modiferKey in modifierKeys) + { + Press(modiferKey); + } + + Type(key); + + foreach (Key modifierKey in modifierKeys.Reverse()) + { + Release(modifierKey); + } + } + + private static void SendKeyboardKey(ushort key, bool isKeyDown, bool isExtended, bool isUnicode) + { + var input = new NativeMethods.INPUT(); + input.Type = NativeMethods.INPUT_KEYBOARD; + if (!isKeyDown) + { + input.Data.Keyboard.dwFlags |= NativeMethods.KEYEVENTF_KEYUP; + } + + if (isUnicode) + { + input.Data.Keyboard.dwFlags |= NativeMethods.KEYEVENTF_UNICODE; + input.Data.Keyboard.wScan = key; + input.Data.Keyboard.wVk = 0; + } + else + { + input.Data.Keyboard.wScan = 0; + input.Data.Keyboard.wVk = key; + } + + if (isExtended) + { + input.Data.Keyboard.dwFlags |= NativeMethods.KEYEVENTF_EXTENDEDKEY; + } + + input.Data.Keyboard.time = 0; + input.Data.Keyboard.dwExtraInfo = IntPtr.Zero; + + NativeMethods.SendInput(1, new NativeMethods.INPUT[] { input }, Marshal.SizeOf(input)); + Thread.Sleep(100); + } + + private static KeyStates GetKeyState(Key key) + { + var keyStates = KeyStates.None; + var nativeKeyState = NativeMethods.GetKeyState((int)key); + + if ((nativeKeyState & 0x00008000) == 0x00008000) + { + keyStates |= KeyStates.Down; + } + + if ((nativeKeyState & 0x00000001) == 0x00000001) + { + keyStates |= KeyStates.Toggled; + } + + return keyStates; + } + + #endregion Private Methods + + #region Private Data + + private struct KeySpec + { + public ushort KeyCode; + public bool IsExtended; + public string Name; + + public KeySpec(ushort keyCode, bool isExtended, string name) + { + this.KeyCode = keyCode; + this.IsExtended = isExtended; + this.Name = name; + } + } + + private enum KeyStates + { + None = 0, + Down = 1, + Toggled = 2 + } + + private static Dictionary KeyBoardKeys = null; + + #endregion Private Data + } +} diff --git a/src/dotnetCampus.UITest.WPFTestHelper/Input/Mouse.cs b/src/dotnetCampus.UITest.WPFTestHelper/Input/Mouse.cs new file mode 100644 index 0000000..c8ba5f1 --- /dev/null +++ b/src/dotnetCampus.UITest.WPFTestHelper/Input/Mouse.cs @@ -0,0 +1,291 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + + +using System; +using System.ComponentModel; +using System.Runtime.InteropServices; + +namespace dotnetCampus.UITest.WPFTestHelper.Input +{ + /// + /// Exposes a simple interface to common mouse operations, allowing the user to simulate mouse input. + /// + /// The following code moves to screen coordinate 100,100 and left clicks. + /// + /// Mouse.MoveTo(new Point(100, 100)); + /// Mouse.Click(MouseButton.Left); + /// + /// + public static class Mouse + { + #region Public Methods + + /// + /// Clicks a mouse button. + /// + /// The mouse button to click. + public static void Click(MouseButton mouseButton) + { + Down(mouseButton); + Up(mouseButton); + } + + /// + /// Double-clicks a mouse button. + /// + /// The mouse button to click. + public static void DoubleClick(MouseButton mouseButton) + { + Click(mouseButton); + Click(mouseButton); + } + + /// + /// Performs a mouse-down operation for a specified mouse button. + /// + /// The mouse button to use. + public static void Down(MouseButton mouseButton) + { + int additionalData; + var inputFlags = GetInputFlags(mouseButton, false, out additionalData); + SendMouseInput(0, 0, additionalData, inputFlags); + } + + /// + /// Moves the mouse pointer to the specified screen coordinates. + /// + /// The screen coordinates to move to. + public static void MoveTo(System.Drawing.Point point) + { + SendMouseInput(point.X, point.Y, 0, SendMouseInputFlags.Move | SendMouseInputFlags.Absolute); + } + + /// + /// Resets the system mouse to a clean state. + /// + public static void Reset() + { + MoveTo(new System.Drawing.Point(0, 0)); + + if (GetButtonState(MouseButton.Left) == MouseButtonState.Pressed) + { + SendMouseInput(0, 0, 0, SendMouseInputFlags.LeftUp); + } + + if (GetButtonState(MouseButton.Middle) == MouseButtonState.Pressed) + { + SendMouseInput(0, 0, 0, SendMouseInputFlags.MiddleUp); + } + + if (GetButtonState(MouseButton.Right) == MouseButtonState.Pressed) + { + SendMouseInput(0, 0, 0, SendMouseInputFlags.RightUp); + } + + if (GetButtonState(MouseButton.XButton1) == MouseButtonState.Pressed) + { + SendMouseInput(0, 0, (int)NativeMethods.XBUTTON1, SendMouseInputFlags.XUp); + } + + if (GetButtonState(MouseButton.XButton2) == MouseButtonState.Pressed) + { + SendMouseInput(0, 0, (int)NativeMethods.XBUTTON2, SendMouseInputFlags.XUp); + } + } + + /// + /// Simulates scrolling of the mouse wheel up or down. + /// + /// + /// The number of lines to scroll. Use positive numbers to + /// scroll up and negative numbers to scroll down. + /// + public static void Scroll(double lines) + { + int amount = (int)(NativeMethods.WheelDelta * lines); + SendMouseInput(0, 0, amount, SendMouseInputFlags.Wheel); + } + + /// + /// Performs a mouse-up operation for a specified mouse button. + /// + /// The mouse button to use. + public static void Up(MouseButton mouseButton) + { + int additionalData; + var inputFlags = GetInputFlags(mouseButton, true, out additionalData); + SendMouseInput(0, 0, additionalData, inputFlags); + } + + #endregion Public Methods + + #region Private Methods + + /// + /// Sends mouse input. + /// + /// x coordinate + /// y coordinate + /// scroll wheel amount + /// SendMouseInputFlags flags + private static void SendMouseInput(int x, int y, int data, SendMouseInputFlags flags) + { + uint intflags = (uint)flags; + + if ((intflags & (int)SendMouseInputFlags.Absolute) != 0) + { + // Absolute position requires normalized coordinates. + NormalizeCoordinates(ref x, ref y); + intflags |= NativeMethods.MouseeventfVirtualdesk; + } + + // don't coalesce mouse moves - tests expect to see the results immediately + if ((intflags & (int)SendMouseInputFlags.Move) != 0) + { + intflags |= NativeMethods.MOUSEEVENTF_MOVE_NOCOALESCE; + } + + var mi = new NativeMethods.INPUT(); + mi.Type = NativeMethods.INPUT_MOUSE; + mi.Data.Mouse.dx = x; + mi.Data.Mouse.dy = y; + mi.Data.Mouse.mouseData = data; + mi.Data.Mouse.dwFlags = intflags; + mi.Data.Mouse.time = 0; + mi.Data.Mouse.dwExtraInfo = new IntPtr(0); + + if (NativeMethods.SendInput(1, new NativeMethods.INPUT[] { mi }, Marshal.SizeOf(mi)) == 0) + { + throw new Win32Exception(Marshal.GetLastWin32Error()); + } + + if ((intflags & (int)SendMouseInputFlags.Wheel) != 0) + { + // MouseWheel input seems to be getting coalesced by the OS, similar to mouse-move. + // There isn't a NOCOALESCE flag to turn this off, so instead just sleep for + // a short time, hopefully enough to avoid the coalescing. + System.Threading.Thread.Sleep(50); + } + } + + private static SendMouseInputFlags GetInputFlags(MouseButton mouseButton, bool isUp, out int additionalData) + { + SendMouseInputFlags flags; + additionalData = 0; + + if (mouseButton == MouseButton.Left && isUp) + { + flags = SendMouseInputFlags.LeftUp; + } + else if (mouseButton == MouseButton.Left && !isUp) + { + flags = SendMouseInputFlags.LeftDown; + } + else if (mouseButton == MouseButton.Right && isUp) + { + flags = SendMouseInputFlags.RightUp; + } + else if (mouseButton == MouseButton.Right && !isUp) + { + flags = SendMouseInputFlags.RightDown; + } + else if (mouseButton == MouseButton.Middle && isUp) + { + flags = SendMouseInputFlags.MiddleUp; + } + else if (mouseButton == MouseButton.Middle && !isUp) + { + flags = SendMouseInputFlags.MiddleDown; + } + else if (mouseButton == MouseButton.XButton1 && isUp) + { + flags = SendMouseInputFlags.XUp; + additionalData = (int)NativeMethods.XBUTTON1; + } + else if (mouseButton == MouseButton.XButton1 && !isUp) + { + flags = SendMouseInputFlags.XDown; + additionalData = (int)NativeMethods.XBUTTON1; + } + else if (mouseButton == MouseButton.XButton2 && isUp) + { + flags = SendMouseInputFlags.XUp; + additionalData = (int)NativeMethods.XBUTTON2; + } + else if (mouseButton == MouseButton.XButton2 && !isUp) + { + flags = SendMouseInputFlags.XDown; + additionalData = (int)NativeMethods.XBUTTON2; + } + else + { + throw new InvalidOperationException(); + } + + return flags; + } + + private static void NormalizeCoordinates(ref int x, ref int y) + { + int vScreenWidth = NativeMethods.GetSystemMetrics(NativeMethods.SMCxvirtualscreen); + int vScreenHeight = NativeMethods.GetSystemMetrics(NativeMethods.SMCyvirtualscreen); + int vScreenLeft = NativeMethods.GetSystemMetrics(NativeMethods.SMXvirtualscreen); + int vScreenTop = NativeMethods.GetSystemMetrics(NativeMethods.SMYvirtualscreen); + + // Absolute input requires that input is in 'normalized' coords - with the entire + // desktop being (0,0)...(65536,65536). Need to convert input x,y coords to this + // first. + // + // In this normalized world, any pixel on the screen corresponds to a block of values + // of normalized coords - eg. on a 1024x768 screen, + // y pixel 0 corresponds to range 0 to 85.333, + // y pixel 1 corresponds to range 85.333 to 170.666, + // y pixel 2 correpsonds to range 170.666 to 256 - and so on. + // Doing basic scaling math - (x-top)*65536/Width - gets us the start of the range. + // However, because int math is used, this can end up being rounded into the wrong + // pixel. For example, if we wanted pixel 1, we'd get 85.333, but that comes out as + // 85 as an int, which falls into pixel 0's range - and that's where the pointer goes. + // To avoid this, we add on half-a-"screen pixel"'s worth of normalized coords - to + // push us into the middle of any given pixel's range - that's the 65536/(Width*2) + // part of the formula. So now pixel 1 maps to 85+42 = 127 - which is comfortably + // in the middle of that pixel's block. + // The key ting here is that unlike points in coordinate geometry, pixels take up + // space, so are often better treated like rectangles - and if you want to target + // a particular pixel, target its rectangle's midpoint, not its edge. + x = ((x - vScreenLeft) * 65536) / vScreenWidth + 65536 / (vScreenWidth * 2); + y = ((y - vScreenTop) * 65536) / vScreenHeight + 65536 / (vScreenHeight * 2); + } + + private static MouseButtonState GetButtonState(MouseButton mouseButton) + { + var mouseButtonState = MouseButtonState.Released; + + int virtualKeyCode = 0; + switch (mouseButton) + { + case MouseButton.Left: + virtualKeyCode = NativeMethods.VK_LBUTTON; + break; + case MouseButton.Right: + virtualKeyCode = NativeMethods.VK_RBUTTON; + break; + case MouseButton.Middle: + virtualKeyCode = NativeMethods.VK_MBUTTON; + break; + case MouseButton.XButton1: + virtualKeyCode = NativeMethods.VK_XBUTTON1; + break; + case MouseButton.XButton2: + virtualKeyCode = NativeMethods.VK_XBUTTON2; + break; + } + + mouseButtonState = (NativeMethods.GetKeyState(virtualKeyCode) & 0x8000) != 0 ? MouseButtonState.Pressed : MouseButtonState.Released; + return mouseButtonState; + } + + #endregion Private Methods + } +} diff --git a/src/dotnetCampus.UITest.WPFTestHelper/Input/MouseButton.cs b/src/dotnetCampus.UITest.WPFTestHelper/Input/MouseButton.cs new file mode 100644 index 0000000..517ecca --- /dev/null +++ b/src/dotnetCampus.UITest.WPFTestHelper/Input/MouseButton.cs @@ -0,0 +1,39 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + + + +namespace dotnetCampus.UITest.WPFTestHelper.Input +{ + /// + /// Defines values that specify the buttons on a mouse device. + /// + public enum MouseButton + { + /// + /// The left mouse button. + /// + Left = 0, + + /// + /// The middle mouse button. + /// + Middle = 1, + + /// + /// The right mouse button. + /// + Right = 2, + + /// + /// The first extended mouse button. + /// + XButton1 = 3, + + /// + /// The second extended mouse button + /// + XButton2 = 4, + } +} diff --git a/src/dotnetCampus.UITest.WPFTestHelper/Input/MouseButtonState.cs b/src/dotnetCampus.UITest.WPFTestHelper/Input/MouseButtonState.cs new file mode 100644 index 0000000..4ac1e59 --- /dev/null +++ b/src/dotnetCampus.UITest.WPFTestHelper/Input/MouseButtonState.cs @@ -0,0 +1,17 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + + + +namespace dotnetCampus.UITest.WPFTestHelper.Input +{ + /// + /// The state of the mouse button. + /// + internal enum MouseButtonState + { + Released = 0, + Pressed = 1, + } +} diff --git a/src/dotnetCampus.UITest.WPFTestHelper/Input/NativeMethods.cs b/src/dotnetCampus.UITest.WPFTestHelper/Input/NativeMethods.cs new file mode 100644 index 0000000..8d19ea3 --- /dev/null +++ b/src/dotnetCampus.UITest.WPFTestHelper/Input/NativeMethods.cs @@ -0,0 +1,156 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + + +using System; +using System.Runtime.InteropServices; + +namespace dotnetCampus.UITest.WPFTestHelper.Input +{ + internal static class NativeMethods + { + #region Const data + + private const string Gdi32Dll = "GDI32.dll"; + private const string User32Dll = "User32.dll"; + + public const int INPUT_MOUSE = 0; + public const int INPUT_KEYBOARD = 1; + public const int INPUT_HARDWARE = 2; + public const uint KEYEVENTF_EXTENDEDKEY = 0x0001; + public const uint KEYEVENTF_KEYUP = 0x0002; + public const uint KEYEVENTF_UNICODE = 0x0004; + public const uint KEYEVENTF_SCANCODE = 0x0008; + public const uint XBUTTON1 = 0x0001; + public const uint XBUTTON2 = 0x0002; + public const uint MOUSEEVENTF_MOVE = 0x0001; + public const uint MOUSEEVENTF_LEFTDOWN = 0x0002; + public const uint MOUSEEVENTF_LEFTUP = 0x0004; + public const uint MOUSEEVENTF_RIGHTDOWN = 0x0008; + public const uint MOUSEEVENTF_RIGHTUP = 0x0010; + public const uint MOUSEEVENTF_MIDDLEDOWN = 0x0020; + public const uint MOUSEEVENTF_MIDDLEUP = 0x0040; + public const uint MOUSEEVENTF_XDOWN = 0x0080; + public const uint MOUSEEVENTF_XUP = 0x0100; + public const uint MOUSEEVENTF_WHEEL = 0x0800; + public const uint MOUSEEVENTF_MOVE_NOCOALESCE = 0x2000; + public const uint MOUSEEVENTF_VIRTUALDESK = 0x4000; + public const uint MOUSEEVENTF_ABSOLUTE = 0x8000; + + public const int VKeyShiftMask = 0x0100; + public const int VKeyCharMask = 0x00FF; + + public const int VK_LBUTTON = 0x0001; + public const int VK_RBUTTON = 0x0002; + public const int VK_MBUTTON = 0x0004; + public const int VK_XBUTTON1 = 0x0005; + public const int VK_XBUTTON2 = 0x0006; + + public const int SMXvirtualscreen = 76; + public const int SMYvirtualscreen = 77; + public const int SMCxvirtualscreen = 78; + public const int SMCyvirtualscreen = 79; + + public const int MouseeventfVirtualdesk = 0x4000; + public const int WheelDelta = 120; + + #endregion Const data + + #region Structs + + [StructLayout(LayoutKind.Sequential)] + public struct MOUSEINPUT + { + public int dx; + public int dy; + public int mouseData; + public uint dwFlags; + public uint time; + public IntPtr dwExtraInfo; + } + + [StructLayout(LayoutKind.Sequential)] + public struct KEYBDINPUT + { + public ushort wVk; + public ushort wScan; + public uint dwFlags; + public uint time; + public IntPtr dwExtraInfo; + } + + [StructLayout(LayoutKind.Sequential)] + public struct HARDWAREINPUT + { + public uint uMsg; + public ushort wParamL; + public ushort wParamH; + } + + /// + /// The INPUT structure is used by SendInput to store information for synthesizing input events such as keystrokes, mouse movement, and mouse clicks. (see: http://msdn.microsoft.com/en-us/library/ms646270(VS.85).aspx) + /// Declared in Winuser.h, include Windows.h + /// + /// + /// This structure contains information identical to that used in the parameter list of the keybd_event or mouse_event function. + /// Windows 2000/XP: INPUT_KEYBOARD supports nonkeyboard input methods, such as handwriting recognition or voice recognition, as if it were text input by using the KEYEVENTF_UNICODE flag. For more information, see the remarks section of KEYBDINPUT. + /// + public struct INPUT + { + /// + /// Specifies the type of the input event. This member can be one of the following values. + /// InputType.MOUSE - The event is a mouse event. Use the mi structure of the union. + /// InputType.KEYBOARD - The event is a keyboard event. Use the ki structure of the union. + /// InputType.HARDWARE - Windows 95/98/Me: The event is from input hardware other than a keyboard or mouse. Use the hi structure of the union. + /// + public UInt32 Type; + + /// + /// The data structure that contains information about the simulated Mouse, Keyboard or Hardware event. + /// + public MOUSEKEYBDHARDWAREINPUT Data; + } + + /// + /// The combined/overlayed structure that includes Mouse, Keyboard and Hardware Input message data (see: http://msdn.microsoft.com/en-us/library/ms646270(VS.85).aspx) + /// + [StructLayout(LayoutKind.Explicit)] + public struct MOUSEKEYBDHARDWAREINPUT + { + [FieldOffset(0)] + public MOUSEINPUT Mouse; + + [FieldOffset(0)] + public KEYBDINPUT Keyboard; + + [FieldOffset(0)] + public HARDWAREINPUT Hardware; + } + + #endregion Structs + + #region Methods + + [DllImport(User32Dll)] + public static extern short GetKeyState(int nVirtKey); + + [DllImport(User32Dll, CharSet = CharSet.Auto)] + public static extern short VkKeyScan(char ch); + + [DllImport(User32Dll, SetLastError = true)] + public static extern uint SendInput(uint nInputs, INPUT[] pInputs, int cbSize); + + [DllImport(User32Dll, ExactSpelling = true, EntryPoint = "GetSystemMetrics", CharSet = CharSet.Auto)] + public static extern int GetSystemMetrics(int nIndex); + + /// Converts the client-area coordinates of a specified point to screen coordinates. + /// Handle to the window whose client area is used for the conversion. + /// POINT structure that contains the client coordinates to be converted. + /// true if the function succeeds, false otherwise. + [DllImport("user32.dll", EntryPoint = "ClientToScreen", CharSet = CharSet.Auto)] + public static extern bool ClientToScreen(IntPtr hwndFrom, [In, Out] ref System.Drawing.Point pt); + + #endregion Methods + } +} diff --git a/src/dotnetCampus.UITest.WPFTestHelper/Input/SendMouseInputFlags.cs b/src/dotnetCampus.UITest.WPFTestHelper/Input/SendMouseInputFlags.cs new file mode 100644 index 0000000..9d7d5a7 --- /dev/null +++ b/src/dotnetCampus.UITest.WPFTestHelper/Input/SendMouseInputFlags.cs @@ -0,0 +1,28 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + + +using System; + +namespace dotnetCampus.UITest.WPFTestHelper.Input +{ + /// + /// Mouse input flags used by the Native Input struct. + /// + [Flags] + internal enum SendMouseInputFlags + { + Move = 0x0001, + LeftDown = 0x0002, + LeftUp = 0x0004, + RightDown = 0x0008, + RightUp = 0x0010, + MiddleDown = 0x0020, + MiddleUp = 0x0040, + XDown = 0x0080, + XUp = 0x0100, + Wheel = 0x0800, + Absolute = 0x8000, + }; +} diff --git a/src/dotnetCampus.UITest.WPFTestHelper/LeakDetection/MemoryInterop.cs b/src/dotnetCampus.UITest.WPFTestHelper/LeakDetection/MemoryInterop.cs new file mode 100644 index 0000000..891b721 --- /dev/null +++ b/src/dotnetCampus.UITest.WPFTestHelper/LeakDetection/MemoryInterop.cs @@ -0,0 +1,81 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + + +using System; +using System.ComponentModel; +using System.Diagnostics; +using System.Runtime.InteropServices; + +namespace dotnetCampus.UITest.WPFTestHelper.LeakDetection +{ + internal static class MemoryInterop + { + internal static PROCESS_MEMORY_COUNTERS_EX GetCounters(IntPtr hProcess) + { + PROCESS_MEMORY_COUNTERS_EX counters = new PROCESS_MEMORY_COUNTERS_EX(); + counters.cb = Marshal.SizeOf(counters); + if (NativeMethods.GetProcessMemoryInfo(hProcess, out counters, Marshal.SizeOf(counters)) == 0) + { + throw new Win32Exception(); + } + + return counters; + } + + internal static long GetPrivateWorkingSet(Process process) + { + SYSTEM_INFO sysinfo = new SYSTEM_INFO(); + NativeMethods.GetSystemInfo(ref sysinfo); + + int wsInfoLength = (int)(Marshal.SizeOf(new PSAPI_WORKING_SET_INFORMATION()) + + Marshal.SizeOf(new PSAPI_WORKING_SET_BLOCK()) * (process.WorkingSet64 / (sysinfo.dwPageSize))); + IntPtr workingSetPointer = Marshal.AllocHGlobal(wsInfoLength); + + if (NativeMethods.QueryWorkingSet(process.Handle, workingSetPointer, wsInfoLength) == 0) + { + throw new Win32Exception(); + } + + PSAPI_WORKING_SET_INFORMATION workingSet = GenerateWorkingSetArray(workingSetPointer); + Marshal.FreeHGlobal(workingSetPointer); + + return CalculatePrivatePages(workingSet) * sysinfo.dwPageSize; + } + + // Generates an array containing working set information based on a pointer in memory. + private static PSAPI_WORKING_SET_INFORMATION GenerateWorkingSetArray(IntPtr workingSetPointer) + { + int entries = Marshal.ReadInt32(workingSetPointer); + + PSAPI_WORKING_SET_INFORMATION workingSet = new PSAPI_WORKING_SET_INFORMATION(); + workingSet.NumberOfEntries = entries; + workingSet.WorkingSetInfo = new PSAPI_WORKING_SET_BLOCK[entries]; + + for (int i = 0; i < entries; i++) + { + workingSet.WorkingSetInfo[i].Flags = (uint)Marshal.ReadInt32(workingSetPointer, 4 + i * 4); + } + + return workingSet; + } + + // Calculates the number of private pages in memory based on working set information. + private static int CalculatePrivatePages(PSAPI_WORKING_SET_INFORMATION workingSet) + { + int totalPages = workingSet.NumberOfEntries; + int privatePages = 0; + + for (int i = 0; i < totalPages; i++) + { + if (workingSet.WorkingSetInfo[i].Block1.Shared == 0) + { + privatePages++; + } + } + + return privatePages; + } + } +} diff --git a/src/dotnetCampus.UITest.WPFTestHelper/LeakDetection/MemorySnapshot.cs b/src/dotnetCampus.UITest.WPFTestHelper/LeakDetection/MemorySnapshot.cs new file mode 100644 index 0000000..2508c38 --- /dev/null +++ b/src/dotnetCampus.UITest.WPFTestHelper/LeakDetection/MemorySnapshot.cs @@ -0,0 +1,696 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + + +using System; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using System.IO; +using System.Xml; + +namespace dotnetCampus.UITest.WPFTestHelper.LeakDetection +{ + /// + /// Represents a snapshot in time of the memory consumed by a specified OS process. + /// MemorySnapshot objects can be instantiated from a running process or from a file. + ///

MemorySnapshot objects are used for detection of memory leaks.

+ ///
+ /// + /// + /// The following example demonstrates taking two memory snapshots of Notepad and comparing them for leaks. + /// + /// Process p = Process.Start("notepad.exe"); + /// p.WaitForInputIdle(5000); + /// Thread.Sleep(3000); + /// MemorySnapshot s1 = MemorySnapshot.FromProcess(p.Id); + /// + /// // Perform operations that may cause a leak... + /// + /// MemorySnapshot s2 = MemorySnapshot.FromProcess(p.Id); + /// + /// MemorySnapshot diff = s2.CompareTo(s1); + /// if (diff.GdiObjectCount != 0) + /// { + /// s1.ToFile(@"\s1.xml"); + /// s2.ToFile(@"\s2.xml"); + /// Console.WriteLine("Possible GDI handle leak! Review the saved memory snapshots."); + /// } + /// + /// p.CloseMainWindow(); + /// p.Close(); + /// + /// + /// + /// + ///

For more information on memory leak detection in native code, refer to the + /// Memory Leak Detection and Isolation article. The table below provides a relationship between the metrics reported by the different tools: + ///

+ /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + ///
TestApiPerformance CountersProcess ExplorerTask Manager (Windows 7)
- Handles : GDI ObjectsGDI Handles
HandleCountHandles : HandlesHandles
PageFileBytes - -
PageFileBytesPeak - -
Pool Nonpaged Bytes - NonPaged Pool
Pool Paged Bytes - Paged Pool
- ThreadsThreads
- - -
- Handles : USER ObjectsUSER Handles
VirtualBytesVirtual Memory : Virtual Size -
PrivateBytesVirtual Memory : Private BytesCommit Size
WorkingSetPhysical Memory : WorkingSetWorking Set (Memory)
WorkingSetPeakPhysical Memory : Peak Working SetPeak Working Set (Memory)
WorkingSetPrivatePhysical Memory : Working Set : WS PrivateMemory (Private Working Set)
+ ///
+ public class MemorySnapshot + { + #region Public members + + /// + /// Creates a MemorySnapshot instance for the specified OS process. + /// + /// The ID of the process for which to generate the memory snapshot. + /// A MemorySnapshot instance containing memory information for the specified process, + /// at the time of the snapshot. + public static MemorySnapshot FromProcess(int processId) + { + MemorySnapshot memorySnapshot = new MemorySnapshot(); + Process process = Process.GetProcessById(processId); + process.Refresh(); + PROCESS_MEMORY_COUNTERS_EX counters = MemoryInterop.GetCounters(process.Handle); + + // Populate memory statistics. + memorySnapshot.GdiObjectCount = NativeMethods.GetGuiResources(process.Handle, NativeMethods.GR_GDIOBJECTS); + memorySnapshot.HandleCount = process.HandleCount; + memorySnapshot.PageFileBytes = counters.PagefileUsage; + memorySnapshot.PageFilePeakBytes = counters.PeakPagefileUsage; + memorySnapshot.PoolNonpagedBytes = counters.QuotaNonPagedPoolUsage; + memorySnapshot.PoolPagedBytes = counters.QuotaPagedPoolUsage; + memorySnapshot.ThreadCount = process.Threads.Count; + memorySnapshot.UserObjectCount = NativeMethods.GetGuiResources(process.Handle, NativeMethods.GR_USEROBJECTS); + memorySnapshot.VirtualMemoryBytes = process.VirtualMemorySize64; + memorySnapshot.VirtualMemoryPrivateBytes = counters.PrivateUsage; + memorySnapshot.WorkingSetBytes = process.WorkingSet64; + memorySnapshot.WorkingSetPeakBytes = process.PeakWorkingSet64; + memorySnapshot.WorkingSetPrivateBytes = MemoryInterop.GetPrivateWorkingSet(process); + memorySnapshot.Timestamp = DateTime.Now; + + return memorySnapshot; + } + + /// + /// Creates a MemorySnapshot instance from data in the specified file. + /// + /// The path to the memory snapshot file. + /// A MemorySnapshot instance containing memory information recorded in the specified file. + public static MemorySnapshot FromFile(string filePath) + { + MemorySnapshot memorySnapshot = new MemorySnapshot(); + + XmlDocument xmlDoc = new XmlDocument(); + using (Stream s = new FileInfo(filePath).OpenRead()) + { + try + { + xmlDoc.Load(s); + } + catch (XmlException) + { + throw new XmlException("MemorySnapshot file \"" + filePath + "\" could not be loaded."); + } + } + + // Grab memory stats. + XmlNode rootNode = xmlDoc.DocumentElement; + memorySnapshot = Deserialize(rootNode); + + return memorySnapshot; + } + + /// + /// Writes the current MemorySnapshot to a file. + /// + /// The path to the output file. + public void ToFile(string filePath) + { + if (String.IsNullOrEmpty(filePath)) + { + throw new ArgumentNullException("MemorySnapshot.ToFile(): the specified file path \"" + filePath + "\" is null or empty."); + } + + XmlDocument xmlDoc = new XmlDocument(); + XmlNode rootNode = Serialize(xmlDoc); + + xmlDoc.AppendChild(rootNode); + xmlDoc.Save(filePath); + } + + /// + /// Compares the current MemorySnapshot instance to the specified MemorySnapshot to produce a difference. + /// + /// The MemorySnapshot to be compared to. + /// A new MemorySnapshot object representing the difference between the two memory snapshots + /// (i.e. the result of the comparison). + public MemorySnapshot CompareTo(MemorySnapshot memorySnapshot) + { + MemorySnapshot diff = new MemorySnapshot(); + + diff.GdiObjectCount = GdiObjectCount - memorySnapshot.GdiObjectCount; + diff.HandleCount = HandleCount - memorySnapshot.HandleCount; + diff.PageFileBytes = PageFileBytes - memorySnapshot.PageFileBytes; + diff.PageFilePeakBytes = PageFilePeakBytes - memorySnapshot.PageFilePeakBytes; + diff.PoolNonpagedBytes = PoolNonpagedBytes - memorySnapshot.PoolNonpagedBytes; + diff.PoolPagedBytes = PoolPagedBytes - memorySnapshot.PoolPagedBytes; + diff.ThreadCount = ThreadCount - memorySnapshot.ThreadCount; + diff.UserObjectCount = UserObjectCount - memorySnapshot.UserObjectCount; + diff.VirtualMemoryBytes = VirtualMemoryBytes - memorySnapshot.VirtualMemoryBytes; + diff.VirtualMemoryPrivateBytes = VirtualMemoryPrivateBytes - memorySnapshot.VirtualMemoryPrivateBytes; + diff.WorkingSetBytes = WorkingSetBytes - memorySnapshot.WorkingSetBytes; + diff.WorkingSetPeakBytes = WorkingSetPeakBytes - memorySnapshot.WorkingSetPeakBytes; + diff.WorkingSetPrivateBytes = WorkingSetPrivateBytes - memorySnapshot.WorkingSetPrivateBytes; + + return diff; + } + + /// + /// The number of handles to GDI objects in use by the process. For more information see the + /// GetGuiResources function. + /// + /// + /// + ///

This metric is reported as follows by the other tools:

+ /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + ///
ToolMetric
+ /// Performance Countersn/a
+ /// Process ExplorerHandles : GDI Objects
Task Manager (Windows 7)GDI Handles
+ ///
+ [SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "Gdi")] + public long GdiObjectCount { get; private set; } + + /// + /// The total number of handles currently open by the process. This number is equal to the sum of the handles + /// currently open by each thread in this process. + /// + /// + /// + ///

This metric is reported as follows by the other tools:

+ /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + ///
ToolMetric
+ /// Performance CountersHandleCount
+ /// Process ExplorerHandles : Handles
Task Manager (Windows 7)Handles
+ ///
+ public long HandleCount { get; private set; } + + /// + /// The current amount of virtual memory that this process has reserved for use + /// in the paging file or files. Those pages may or may not be in memory. + /// + /// + /// + ///

This metric is reported as follows by the other tools:

+ /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + ///
ToolMetric
+ /// Performance CountersPageFileBytes
+ /// Process Explorern/a
Task Manager (Windows 7)n/a
+ /// For more information, see the + /// PROCESS_MEMORY_COUNTERS_EX structure. + ///
+ public long PageFileBytes { get; private set; } + + /// + /// The maximum amount of virtual memory that this process has reserved for use in + /// the paging file or files. + /// + /// + /// + ///

This metric is reported as follows by the other tools:

+ /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + ///
ToolMetric
+ /// Performance CountersPageFileBytesPeak
+ /// Process Explorern/a
Task Manager (Windows 7)n/a
+ /// For more information, see the + /// PROCESS_MEMORY_COUNTERS_EX structure. + ///
+ public long PageFilePeakBytes { get; private set; } + + /// + /// The size of the nonpaged pool, an area of system memory (physical memory used by the operating + /// system) for objects that cannot be written to disk but must remain in physical memory as long as they are + /// allocated. For more information, see the GetProcessMemoryInfo + /// function and the PROCESS_MEMORY_COUNTERS_EX structure. + /// + /// + /// + ///

This metric is reported as follows by the other tools:

+ /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + ///
ToolMetric
+ /// Performance CountersPool Nonpaged Bytes
+ /// Process Explorern/a
Task Manager (Windows 7)NonPaged Pool
+ ///
+ [SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "Nonpaged")] + public long PoolNonpagedBytes { get; private set; } + + /// + /// The size of the paged pool, an area of system memory (physical memory used by the operating system) + /// for objects that can be written to disk when they are not being used. For more information, see + /// the GetProcessMemoryInfo function + /// and the PROCESS_MEMORY_COUNTERS_EX + /// structure. + /// + /// + /// + ///

This metric is reported as follows by the other tools:

+ /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + ///
ToolMetric
+ /// Performance CountersPool Paged Bytes
+ /// Process Explorern/a
Task Manager (Windows 7)Paged Pool
+ ///
+ public long PoolPagedBytes { get; private set; } + + /// + /// The number of threads currently active in the process. + /// + /// + /// + ///

This metric is reported as follows by the other tools:

+ /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + ///
ToolMetric
+ /// Performance Countersn/a
+ /// Process ExplorerThreads
Task Manager (Windows 7)Threads
+ ///
+ public long ThreadCount { get; private set; } + + /// + /// The time when the memory snapshot was taken. + /// + public DateTime Timestamp { get; private set; } + + /// + /// The number of handles to USER objects in use by the process. For more information see the + /// GetGuiResources function. + /// + /// + /// + ///

This metric is reported as follows by the other tools:

+ /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + ///
ToolMetric
+ /// Performance Countersn/a
+ /// Process ExplorerHandles : USER Objects
Task Manager (Windows 7)USER Handles
+ ///
+ public long UserObjectCount { get; private set; } + + /// + /// The current size of the virtual address space that the process is using. Use of virtual address space does + /// not necessarily imply corresponding use of either disk or main memory pages. Virtual space is finite, + /// and the process can limit its ability to load libraries. For more information see the + /// GlobalMemoryStatusEx function + /// and the MEMORYSTATUSEX structure. + /// This metric is calculated as MEMORYSTATUSEX.ullTotalVirtual ?MEMORYSTATUSEX.ullAvailVirtual. + /// + /// + /// + ///

This metric is reported as follows by the other tools:

+ /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + ///
ToolMetric
+ /// Performance CountersVirtualBytes
+ /// Process ExplorerVirtual Memory : Virtual Size
Task Manager (Windows 7)n/a
+ ///
+ public long VirtualMemoryBytes { get; private set; } + + /// + /// The current size of memory that this process has allocated that cannot be shared with other processes. For more + /// information see the GetProcessMemoryInfo + /// function and the PROCESS_MEMORY_COUNTERS_EX + /// structure (this metric corresponds to the PrivateUsage field in the structure). + /// + /// + /// + ///

This metric is reported as follows by the other tools:

+ /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + ///
ToolMetric
+ /// Performance CountersPrivateBytes
+ /// Process ExplorerVirtual Memory : Private Bytes
Task Manager (Windows 7)Commit Size
+ ///
+ public long VirtualMemoryPrivateBytes { get; private set; } + + /// + /// The current size of the working set of the process. The working set is the set of memory pages recently touched + /// by the threads in the process. If free memory in the computer is above a threshold, pages are left in the working set + /// of a process even if they are not in use. When free memory falls below a threshold, pages are trimmed from working sets. + /// If they are needed they will then be soft-faulted back into the working set before leaving main memory. + /// + /// + /// + ///

This metric is reported as follows by the other tools:

+ /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + ///
ToolMetric
+ /// Performance CountersWorkingSet
+ /// Process ExplorerPhysical Memory : Working Set
Task Manager (Windows 7)Working Set (Memory)
+ ///
+ public long WorkingSetBytes { get; private set; } + + /// + /// The maximum size, in bytes, of the working set of the process at any one time. + /// For more information see the GetProcessMemoryInfo + /// function and the PROCESS_MEMORY_COUNTERS_EX structure. + /// + /// + /// + ///

This metric is reported as follows by the other tools:

+ /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + ///
ToolMetric
+ /// Performance CountersWorkingSetPeak
+ /// Process ExplorerPhysical Memory : Peak Working Set
Task Manager (Windows 7)Peak Working Set (Memory)
+ ///
+ public long WorkingSetPeakBytes { get; private set; } + + /// + /// The size of the working set that is only used for the process and not shared nor shareable by other processes. + /// For more information see the Memory Performance Information article. + /// + /// + /// + ///

This metric is reported as follows by the other tools:

+ /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + ///
ToolMetric
+ /// Performance CountersWorkingSetPrivate
+ /// Process ExplorerPhysical Memory : Working Set : WS Private
Task Manager (Windows 7)Memory (Private Working Set)
+ /// + ///
+ public long WorkingSetPrivateBytes { get; private set; } + + /// + /// An instance of the class can only be created by using one of the static From* methods. + /// + private MemorySnapshot() + { + // Nothing + } + + #endregion + + #region Serialization / Deserialization Helpers + + + /// + /// Serializes a MemorySnapshot instance to xml format. + /// + /// The XmlDocument to which the MemorySnapshot is to be serialized + /// + internal XmlNode Serialize(XmlDocument xmlDoc) + { + XmlNode rootNode = xmlDoc.CreateElement("MemorySnapshot"); + + // Create memory stats attributes. + SerializeNodes(xmlDoc, rootNode, "GdiObjectCount", GdiObjectCount); + SerializeNodes(xmlDoc, rootNode, "HandleCount", HandleCount); + SerializeNodes(xmlDoc, rootNode, "PageFileBytes", PageFileBytes); + SerializeNodes(xmlDoc, rootNode, "PageFilePeakBytes", PageFilePeakBytes); + SerializeNodes(xmlDoc, rootNode, "PoolNonpagedBytes", PoolNonpagedBytes); + SerializeNodes(xmlDoc, rootNode, "PoolPagedBytes", PoolPagedBytes); + SerializeNodes(xmlDoc, rootNode, "ThreadCount", ThreadCount); + SerializeNodes(xmlDoc, rootNode, "UserObjectCount", UserObjectCount); + SerializeNodes(xmlDoc, rootNode, "VirtualMemoryBytes", VirtualMemoryBytes); + SerializeNodes(xmlDoc, rootNode, "VirtualMemoryPrivateBytes", VirtualMemoryPrivateBytes); + SerializeNodes(xmlDoc, rootNode, "WorkingSetBytes", WorkingSetBytes); + SerializeNodes(xmlDoc, rootNode, "WorkingSetPeakBytes", WorkingSetPeakBytes); + SerializeNodes(xmlDoc, rootNode, "WorkingSetPrivateBytes", WorkingSetPrivateBytes); + + // Save Timestamp. + XmlNode TimestampNode = xmlDoc.CreateElement("Timestamp"); + XmlAttribute attribute = xmlDoc.CreateAttribute("Value"); + attribute.InnerText = Timestamp.ToString(CultureInfo.InvariantCulture); + TimestampNode.Attributes.Append(attribute); + rootNode.AppendChild(TimestampNode); + + return rootNode; + } + + /// + /// De-Serializes a MemorySnapshot instance from xml format. + /// + /// The Xml Node from which the MemorySnapshot is to be de-serialized + /// + internal static MemorySnapshot Deserialize(XmlNode rootNode) + { + MemorySnapshot memorySnapshot = new MemorySnapshot(); + + memorySnapshot.GdiObjectCount = DeserializeNode(rootNode, "GdiObjectCount"); + memorySnapshot.HandleCount = DeserializeNode(rootNode, "HandleCount"); + memorySnapshot.PageFileBytes = DeserializeNode(rootNode, "PageFileBytes"); + memorySnapshot.PageFilePeakBytes = DeserializeNode(rootNode, "PageFilePeakBytes"); + memorySnapshot.PoolNonpagedBytes = DeserializeNode(rootNode, "PoolNonpagedBytes"); + memorySnapshot.PoolPagedBytes = DeserializeNode(rootNode, "PoolPagedBytes"); + memorySnapshot.ThreadCount = DeserializeNode(rootNode, "ThreadCount"); + memorySnapshot.UserObjectCount = DeserializeNode(rootNode, "UserObjectCount"); + memorySnapshot.VirtualMemoryBytes = DeserializeNode(rootNode, "VirtualMemoryBytes"); + memorySnapshot.VirtualMemoryPrivateBytes = DeserializeNode(rootNode, "VirtualMemoryPrivateBytes"); + memorySnapshot.WorkingSetBytes = DeserializeNode(rootNode, "WorkingSetBytes"); + memorySnapshot.WorkingSetPeakBytes = DeserializeNode(rootNode, "WorkingSetPeakBytes"); + memorySnapshot.WorkingSetPrivateBytes = DeserializeNode(rootNode, "WorkingSetPrivateBytes"); + + // Grab Timestamp. + XmlNode memoryStatNode = rootNode.SelectSingleNode("Timestamp"); + if (memoryStatNode == null) + { + throw new XmlException("MemorySnapshot file is missing value: Timestamp"); + } + + XmlAttribute attribute = memoryStatNode.Attributes["Value"]; + memorySnapshot.Timestamp = (DateTime)Convert.ToDateTime(attribute.InnerText, CultureInfo.InvariantCulture); + + return memorySnapshot; + } + + private void SerializeNodes(XmlDocument xmlDoc, XmlNode rootNode, string nodeName, long value) + { + XmlNode newNode = xmlDoc.CreateElement(nodeName); + XmlAttribute attribute = xmlDoc.CreateAttribute("Value"); + attribute.InnerText = value.ToString(CultureInfo.InvariantCulture); + newNode.Attributes.Append(attribute); + rootNode.AppendChild(newNode); + } + + private static long DeserializeNode(XmlNode rootNode, string nodeName) + { + XmlNode memoryStatNode = rootNode.SelectSingleNode(nodeName); + if (memoryStatNode == null) + { + throw new XmlException("MemorySnapshot file is missing value: " + nodeName); + } + + XmlAttribute attribute = memoryStatNode.Attributes["Value"]; + return (long)Convert.ToInt64(attribute.InnerText, NumberFormatInfo.InvariantInfo); + } + + #endregion + } +} diff --git a/src/dotnetCampus.UITest.WPFTestHelper/LeakDetection/MemorySnapshotCollection.cs b/src/dotnetCampus.UITest.WPFTestHelper/LeakDetection/MemorySnapshotCollection.cs new file mode 100644 index 0000000..2743edf --- /dev/null +++ b/src/dotnetCampus.UITest.WPFTestHelper/LeakDetection/MemorySnapshotCollection.cs @@ -0,0 +1,111 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + + +using System; +using System.Collections.ObjectModel; +using System.IO; +using System.Xml; + +namespace dotnetCampus.UITest.WPFTestHelper.LeakDetection +{ + /// + /// A collection of MemorySnapshots that can be serialized to an XML file. + /// + /// + /// + /// The following example demonstrates taking multiple memory snapshots of Notepad + /// and saving them on disk for later analysis. + /// + /// MemorySnapshotCollection c = new MemorySnapshotCollection(); + /// + /// Process p = Process.Start("notepad.exe"); + /// p.WaitForInputIdle(5000); + /// MemorySnapshot s1 = MemorySnapshot.FromProcess(p.Id); + /// c.Add(s1); + /// + /// // Perform operations that may cause a leak... + /// + /// MemorySnapshot s2 = MemorySnapshot.FromProcess(p.Id); + /// c.Add(s2); + /// + /// c.ToFile(@"MemorySnapshots.xml"); + /// + /// p.CloseMainWindow(); + /// p.Close(); + /// + /// + /// + /// + /// A MemorySnapshotCollection can also be loaded from a XML file. + /// + /// MemorySnapshotCollection c = MemorySnapshotCollection.FromFile(@"MemorySnapshots.xml"); + /// + /// + public class MemorySnapshotCollection : Collection + { + /// + /// Creates a MemorySnapshotCollection instance from data in the specified file. + /// + /// The path to the MemorySnapshotCollection file. + /// A MemorySnapshotCollection instance, containing memory information recorded in the specified file. + public static MemorySnapshotCollection FromFile(string filePath) + { + MemorySnapshotCollection msColllection = new MemorySnapshotCollection(); + + XmlDocument xmlDoc = new XmlDocument(); + using (Stream s = new FileInfo(filePath).OpenRead()) + { + try + { + xmlDoc.Load(s); + } + catch (XmlException) + { + throw new XmlException("MemorySnapshotCollection file \"" + filePath + "\" could not be loaded."); + } + } + + XmlNodeList msNodeList = xmlDoc.DocumentElement.SelectNodes("MemorySnapshot"); + foreach (XmlNode msNode in msNodeList) + { + // Desrialize node. + MemorySnapshot ms = MemorySnapshot.Deserialize(msNode); + + // Add to collection. + msColllection.Add(ms); + + } + + return msColllection; + } + + /// + /// Writes the current MemorySnapshotCollection to a file. + /// + /// The path to the output file. + public void ToFile(string filePath) + { + if (String.IsNullOrEmpty(filePath)) + { + throw new ArgumentNullException("MemorySnapshot.ToFile(): the specified file path \"" + filePath + "\" is null or empty."); + } + + XmlDocument xmlDoc = new XmlDocument(); + XmlNode rootNode = xmlDoc.CreateElement("MemorySnapshotCollection"); + + foreach (MemorySnapshot ms in this.Items) + { + // Call serializer on MemorySnapshot. + XmlNode msNode = ms.Serialize(xmlDoc); + + // Append to MemorySnapshotCollection. + rootNode.AppendChild(msNode); + } + + xmlDoc.AppendChild(rootNode); + xmlDoc.Save(filePath); + } + } +} diff --git a/src/dotnetCampus.UITest.WPFTestHelper/LeakDetection/NativeMethods.cs b/src/dotnetCampus.UITest.WPFTestHelper/LeakDetection/NativeMethods.cs new file mode 100644 index 0000000..921c319 --- /dev/null +++ b/src/dotnetCampus.UITest.WPFTestHelper/LeakDetection/NativeMethods.cs @@ -0,0 +1,108 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + + +using System; +using System.Runtime.InteropServices; + +namespace dotnetCampus.UITest.WPFTestHelper.LeakDetection +{ + internal static class NativeMethods + { + internal const int GR_GDIOBJECTS = 0; + internal const int GR_USEROBJECTS = 1; + + [DllImport("User32.dll")] + internal static extern int GetGuiResources(IntPtr hProcess, int flags); + + [DllImport("kernel32.dll")] + internal static extern void GetSystemInfo([MarshalAs(UnmanagedType.Struct)] ref SYSTEM_INFO lpSystemInfo); + + // Interop call the get workingset information. + [DllImport("psapi.dll", SetLastError = true)] + internal static extern int QueryWorkingSet(IntPtr hProcess, IntPtr info, int size); + + // Interop call the get performance memory counters + [DllImport("psapi.dll", SetLastError = true)] + internal static extern int GetProcessMemoryInfo(IntPtr hProcess, out PROCESS_MEMORY_COUNTERS_EX counters, int size); + } + + [StructLayout(LayoutKind.Sequential)] + internal struct SYSTEM_INFO + { + internal _PROCESSOR_INFO_UNION uProcessorInfo; + public uint dwPageSize; + public IntPtr lpMinimumApplicationAddress; + public IntPtr lpMaximumApplicationAddress; + public IntPtr dwActiveProcessorMask; + public uint dwNumberOfProcessors; + public uint dwProcessorType; + public uint dwAllocationGranularity; + public ushort dwProcessorLevel; + public ushort dwProcessorRevision; + } + + [StructLayout(LayoutKind.Explicit)] + internal struct _PROCESSOR_INFO_UNION + { + [FieldOffset(0)] + internal uint dwOemId; + [FieldOffset(0)] + internal ushort wProcessorArchitecture; + [FieldOffset(2)] + internal ushort wReserved; + } + + // Struct to hold performace memory counters. + [StructLayout(LayoutKind.Sequential)] + internal struct PROCESS_MEMORY_COUNTERS_EX + { + public int cb; + public int PageFaultCount; + public int PeakWorkingSetSize; + public int WorkingSetSize; + public int QuotaPeakPagedPoolUsage; + public int QuotaPagedPoolUsage; + public int QuotaPeakNonPagedPoolUsage; + public int QuotaNonPagedPoolUsage; + public int PagefileUsage; + public int PeakPagefileUsage; + public int PrivateUsage; + } + + [StructLayout(LayoutKind.Sequential)] + internal class PSAPI_WORKING_SET_INFORMATION + { + public int NumberOfEntries; + + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 1, ArraySubType = UnmanagedType.Struct)] + public PSAPI_WORKING_SET_BLOCK[] WorkingSetInfo; + } + + [StructLayout(LayoutKind.Sequential)] + internal struct BLOCK + { + public uint bitvector1; + + public uint Protection; + public uint ShareCount; + public uint Reserved; + public uint VirtualPage; + + public uint Shared + { + get { return ((uint)(((this.bitvector1 & 256u) >> 8))); } + } + } + + [StructLayout(LayoutKind.Explicit)] + internal struct PSAPI_WORKING_SET_BLOCK + { + [FieldOffset(0)] + public uint Flags; + + [FieldOffset(0)] + public BLOCK Block1; + } +} diff --git a/src/dotnetCampus.UITest.WPFTestHelper/ObjectComparison/GraphNode.cs b/src/dotnetCampus.UITest.WPFTestHelper/ObjectComparison/GraphNode.cs new file mode 100644 index 0000000..fde0e40 --- /dev/null +++ b/src/dotnetCampus.UITest.WPFTestHelper/ObjectComparison/GraphNode.cs @@ -0,0 +1,166 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + + +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Diagnostics; + +namespace dotnetCampus.UITest.WPFTestHelper.ObjectComparison +{ + /// + /// Represents one node in the object graph. + /// The root of the graph is a graph node. + /// + [DebuggerDisplay("{Name}")] + public class GraphNode + { + #region Public and Protected Members + + /// + /// A name to identify this node. When comparing, + /// the left and right nodes are matched by name. + /// + public string Name + { + get + { + return this.name; + } + + set + { + if (string.IsNullOrEmpty(value)) + { + throw new ArgumentException("Please provide a valid name", "value"); + } + this.name = value; + } + } + + /// + /// Gets the collection of child nodes to this + /// node. The child nodes represent properties or + /// fields on an object. + /// + public Collection Children + { + get + { + if (this.children == null) + { + this.children = new Collection(); + } + + return children; + } + } + + /// + /// Represents the immediate parent to this node. + /// The parent node of the root of the graph is null. + /// + public GraphNode Parent { get; set; } + + /// + /// Contains the value of the object represented by + /// this node. + /// + public object ObjectValue { get; set; } + + /// + /// Provides the System.Type of the object represented by + /// this node. Returns null if the object value is null. + /// + public Type ObjectType + { + get + { + Type objectType = null; + if (this.ObjectValue != null) + { + objectType = this.ObjectValue.GetType(); + } + + return objectType; + } + } + + /// + /// Gets the depth of this node from the root. If the depth of + /// the root node is 0, root.child is 1. + /// + public int Depth + { + get + { + GraphNode node = this; + int depth = 0; + while (node.Parent != null) + { + depth++; + node = node.Parent; + } + return depth; + } + } + + /// + /// Gets the fully qualified name of this node + /// If the root node has a child that is named child1 and the child + /// has another child that is named child12, qualified name of child12 + /// would be root.child1.child12. + /// + public string QualifiedName + { + get + { + GraphNode node = this; + string qualifiedName = this.Name; + while (node.Parent != null) + { + qualifiedName = node.Parent.Name + "." + qualifiedName; + node = node.Parent; + } + return qualifiedName; + } + } + + /// + /// Performs a depth-first traversal of the graph with + /// this node as root, and provides the nodes visited, in that + /// order. + /// + /// Nodes visited in depth-first order. + public IEnumerable GetNodesInDepthFirstOrder() + { + Stack pendingNodes = new Stack(); + pendingNodes.Push(this); + HashSet visitedNodes = new HashSet(); + while (pendingNodes.Count != 0) + { + GraphNode currentNode = pendingNodes.Pop(); + if (!visitedNodes.Contains(currentNode)) + { + foreach (GraphNode node in currentNode.Children) + { + pendingNodes.Push(node); + } + visitedNodes.Add(currentNode); + yield return currentNode; + } + } + } + + #endregion + + #region Private Data + + private Collection children; + private string name; + + #endregion + } +} diff --git a/src/dotnetCampus.UITest.WPFTestHelper/ObjectComparison/ObjectComparer.cs b/src/dotnetCampus.UITest.WPFTestHelper/ObjectComparison/ObjectComparer.cs new file mode 100644 index 0000000..561a723 --- /dev/null +++ b/src/dotnetCampus.UITest.WPFTestHelper/ObjectComparison/ObjectComparer.cs @@ -0,0 +1,251 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + + +using System; +using System.Collections.Generic; +using System.Linq; + +namespace dotnetCampus.UITest.WPFTestHelper.ObjectComparison +{ + /// + /// Represents a generic object comparer. This class uses an + /// instance to convert objects to graph + /// representations before comparing the representations. + /// + /// + /// Comparing two objects for equivalence is a relatively common task during test validation. + /// One example would be to test whether a type follows the rules required by a particular + /// serializer by saving and loading the object and comparing the two. A deep object + /// comparison is one where all the properties and its properties are compared repeatedly + /// until primitives are reached. The .NET Framework provides mechanisms to perform such comparisons but + /// requires the types in question to implement part of the comparison logic + /// (IComparable, .Equals). However, there are often types that do not follow + /// these mechanisms. This API provides a mechanism to deep compare two objects using + /// reflection. + /// + /// + /// + /// The following example demonstrates how to compare two objects using a general-purpose object + /// comparison strategy (represented by ). + /// + /// + /// Person p1 = new Person("Microsoft"); + /// p1.Children.Add(new Person("Peter")); + /// p1.Children.Add(new Person("Mary")); + /// + /// Person p2 = new Person("Microsoft"); + /// p2.Children.Add(new Person("Peter")); + /// + /// ObjectGraphFactory factory = new PublicPropertyObjectGraphFactory(); + /// ObjectComparer comparer = new ObjectComparer(factory); + /// Console.WriteLine( + /// "Objects p1 and p2 {0}", + /// comparer.Compare(p1, p2) ? "match!" : "do NOT match!"); + /// + /// + /// where Person is declared as follows: + /// + /// + /// class Person + /// { + /// public Person(string name) + /// { + /// Name = name; + /// Children = new Collection<Person>(); + /// } + /// public string Name { get; set; } + /// public Collection<Person> Children { get; private set; } + /// } + /// + /// + /// + /// + /// In addition, the object comparison API allows the user to get back a list of comparison mismatches. + /// For an example, see objects). + /// + public sealed class ObjectComparer + { + #region Constuctors + + /// + /// Creates an instance of the ObjectComparer class. + /// + /// An ObjectGraphFactory to use for + /// converting objects to graphs. + public ObjectComparer(ObjectGraphFactory factory) + { + if (factory == null) + { + throw new ArgumentNullException("factory"); + } + + this.objectGraphFactory = factory; + } + + #endregion + + #region Public and Protected Members + + /// + /// Gets the ObjectGraphFactory used to convert objects + /// to graphs. + /// + public ObjectGraphFactory ObjectGraphFactory + { + get + { + return this.objectGraphFactory; + } + } + + /// + /// Performs a deep comparison of two objects. + /// + /// The left object. + /// The right object. + /// true if the objects match. + public bool Compare(object leftValue, object rightValue) + { + IEnumerable mismatches; + return Compare(leftValue, rightValue, out mismatches); + } + + /// + /// Performs a deep comparison of two objects and provides + /// a list of mismatching nodes. + /// + /// The left object. + /// The right object. + /// The list of mismatches. + /// true if the objects match. + public bool Compare(object leftValue, object rightValue, out IEnumerable mismatches) + { + List mismatch; + bool isMatch = this.CompareObjects(leftValue, rightValue, out mismatch); + mismatches = mismatch; + + return isMatch; + } + + #endregion + + #region Private Members + + private bool CompareObjects(object leftObject, object rightObject, out List mismatches) + { + mismatches = new List(); + + // Get the graph from the objects + GraphNode leftRoot = this.ObjectGraphFactory.CreateObjectGraph(leftObject); + GraphNode rightRoot = this.ObjectGraphFactory.CreateObjectGraph(rightObject); + + // Get the nodes in breadth first order + List leftNodes = new List(leftRoot.GetNodesInDepthFirstOrder()); + List rightNodes = new List(rightRoot.GetNodesInDepthFirstOrder()); + + // For each node in the left tree, search for the + // node in the right tree and compare them + for (int i = 0; i < leftNodes.Count; i++) + { + GraphNode leftNode = leftNodes[i]; + + var nodelist = from node in rightNodes + where leftNode.QualifiedName.Equals(node.QualifiedName) + select node; + + List matchingNodes = nodelist.ToList(); + if (matchingNodes.Count != 1) + { + ObjectComparisonMismatch mismatch = new ObjectComparisonMismatch(leftNode, null, ObjectComparisonMismatchType.MissingRightNode); + mismatches.Add(mismatch); + continue; + } + + GraphNode rightNode = matchingNodes[0]; + + // Compare the nodes + ObjectComparisonMismatch nodesMismatch = CompareNodes(leftNode, rightNode); + if (nodesMismatch != null) + { + mismatches.Add(nodesMismatch); + } + } + + bool passed = mismatches.Count == 0 ? true : false; + + return passed; + } + + private static ObjectComparisonMismatch CompareNodes(GraphNode leftNode, GraphNode rightNode) + { + // Check if both are null + if (leftNode.ObjectValue == null && rightNode.ObjectValue == null) + { + return null; + } + + // check if one of them is null + if (leftNode.ObjectValue == null || rightNode.ObjectValue == null) + { + ObjectComparisonMismatch mismatch = new ObjectComparisonMismatch( + leftNode, + rightNode, + ObjectComparisonMismatchType.ObjectValuesDoNotMatch); + return mismatch; + } + + // compare type names // + if (!leftNode.ObjectType.Equals(rightNode.ObjectType)) + { + ObjectComparisonMismatch mismatch = new ObjectComparisonMismatch( + leftNode, + rightNode, + ObjectComparisonMismatchType.ObjectTypesDoNotMatch); + return mismatch; + } + + // compare primitives, strings + if (leftNode.ObjectType.IsPrimitive || leftNode.ObjectType == typeof(string)) + { + if (!leftNode.ObjectValue.Equals(rightNode.ObjectValue)) + { + ObjectComparisonMismatch mismatch = new ObjectComparisonMismatch( + leftNode, + rightNode, + ObjectComparisonMismatchType.ObjectValuesDoNotMatch); + return mismatch; + } + else + { + return null; + } + } + + // compare the child count + if (leftNode.Children.Count != rightNode.Children.Count) + { + var type = leftNode.Children.Count > rightNode.Children.Count ? + ObjectComparisonMismatchType.RightNodeHasFewerChildren : ObjectComparisonMismatchType.LeftNodeHasFewerChildren; + + ObjectComparisonMismatch mismatch = new ObjectComparisonMismatch( + leftNode, + rightNode, + type); + return mismatch; + } + + // No mismatch // + return null; + } + + #endregion + + #region Private Data + + private ObjectGraphFactory objectGraphFactory; + + #endregion + } +} diff --git a/src/dotnetCampus.UITest.WPFTestHelper/ObjectComparison/ObjectComparisonMismatch.cs b/src/dotnetCampus.UITest.WPFTestHelper/ObjectComparison/ObjectComparisonMismatch.cs new file mode 100644 index 0000000..05d8a13 --- /dev/null +++ b/src/dotnetCampus.UITest.WPFTestHelper/ObjectComparison/ObjectComparisonMismatch.cs @@ -0,0 +1,124 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + + +using System.Diagnostics; + +namespace dotnetCampus.UITest.WPFTestHelper.ObjectComparison +{ + /// + /// Represents one comparison mismatch. + /// + /// + /// + /// The following example shows how to derive the list of comparison mismatches. + /// + /// + /// Person p1 = new Person("Microsoft"); + /// p1.Children.Add(new Person("Peter")); + /// p1.Children.Add(new Person("Mary")); + /// + /// Person p2 = new Person("Microsoft"); + /// p2.Children.Add(new Person("Peter")); + /// + /// // Perform the compare operation + /// ObjectGraphFactory factory = new PublicPropertyObjectGraphFactory(); + /// ObjectComparer comparer = new ObjectComparer(factory); + /// IEnumerable<ObjectComparisonMismatch> m12 = new List<ObjectComparisonMismatch>(); + /// Console.WriteLine( + /// "Objects p1 and p2 {0}", + /// comparer.Compare(p1, p2, out m12) ? "match!" : "do NOT match!"); + /// + /// foreach (ObjectComparisonMismatch m in m12) + /// { + /// Console.WriteLine( + /// "Nodes '{0}' and '{1}' do not match. Mismatch message: '{2}'", + /// m.LeftObjectNode != null ? m.LeftObjectNode.Name : "null", + /// m.RightObjectNode != null ? m.LeftObjectNode.Name : "null", + /// m.MismatchType); + /// } + /// + /// + /// where Person is declared as follows: + /// + /// + /// class Person + /// { + /// public Person(string name) + /// { + /// Name = name; + /// Children = new Collection<Person>(); + /// } + /// public string Name { get; set; } + /// public Collection<Person> Children { get; private set; } + /// } + /// + /// + + [DebuggerDisplay("{MismatchType}: LeftNodeName={LeftObjectNode.QualifiedName}")] + public sealed class ObjectComparisonMismatch + { + #region Constructors + + /// + /// Creates an instance of the ObjectComparisonMismatch class. + /// + /// The node from the left object. + /// The node from the right object. + /// Represents the type of mismatch. + public ObjectComparisonMismatch(GraphNode leftObjectNode, GraphNode rightObjectNode, ObjectComparisonMismatchType mismatchType) + { + this.leftObjectNode = leftObjectNode; + this.rightObjectNode = rightObjectNode; + this.mismatchType = mismatchType; + } + + #endregion Public Members + + #region Public Members + + /// + /// Gets the node in the left object. + /// + public GraphNode LeftObjectNode + { + get + { + return this.leftObjectNode; + } + } + + /// + /// Gets the node in the right object. + /// + public GraphNode RightObjectNode + { + get + { + return this.rightObjectNode; + } + } + + /// + /// Represents the type of mismatch. + /// + public ObjectComparisonMismatchType MismatchType + { + get + { + return this.mismatchType; + } + } + + #endregion + + #region Private Data + + private GraphNode leftObjectNode; + private GraphNode rightObjectNode; + private ObjectComparisonMismatchType mismatchType; + + #endregion + } +} diff --git a/src/dotnetCampus.UITest.WPFTestHelper/ObjectComparison/ObjectComparisonMismatchType.cs b/src/dotnetCampus.UITest.WPFTestHelper/ObjectComparison/ObjectComparisonMismatchType.cs new file mode 100644 index 0000000..75d00ce --- /dev/null +++ b/src/dotnetCampus.UITest.WPFTestHelper/ObjectComparison/ObjectComparisonMismatchType.cs @@ -0,0 +1,43 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + + +namespace dotnetCampus.UITest.WPFTestHelper.ObjectComparison +{ + /// + /// Represents the type of mismatch. + /// + public enum ObjectComparisonMismatchType + { + /// + /// The node is missing in the right graph. + /// + MissingRightNode = 0, + + /// + /// The node is missing in the left graph. + /// + MissingLeftNode = 1, + + /// + /// The right node has fewer children than the left node. + /// + RightNodeHasFewerChildren = 2, + + /// + /// The left node has fewer children than the right node. + /// + LeftNodeHasFewerChildren = 3, + + /// + /// The node types do not match. + /// + ObjectTypesDoNotMatch = 4, + + /// + /// The node values do not match. + /// + ObjectValuesDoNotMatch = 5 + } +} diff --git a/src/dotnetCampus.UITest.WPFTestHelper/ObjectComparison/ObjectGraphFactory.cs b/src/dotnetCampus.UITest.WPFTestHelper/ObjectComparison/ObjectGraphFactory.cs new file mode 100644 index 0000000..7910f41 --- /dev/null +++ b/src/dotnetCampus.UITest.WPFTestHelper/ObjectComparison/ObjectGraphFactory.cs @@ -0,0 +1,68 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + + +using System; + +namespace dotnetCampus.UITest.WPFTestHelper.ObjectComparison +{ + /// + /// Creates a graph for the provided object. + /// + /// + /// The following example demonstrates the use of a simple factory to do shallow comparison of two objects. + /// + /// class Person + /// { + /// public Person(string name) + /// { + /// Name = name; + /// Children = new Collection<Person>(); + /// } + /// public string Name { get; set; } + /// public Collection<Person> Children { get; private set; } + /// } + /// + /// + /// class SimpleObjectGraphFactory : ObjectGraphFactory + /// { + /// public override GraphNode CreateObjectGraph(object o) + /// { + /// // Build the object graph with nodes that need to be compared. + /// // in this particular case, we only pick up the object itself + /// GraphNode node = new GraphNode(); + /// node.Name = "PersonObject"; + /// node.ObjectValue = (o as Person).Name; + /// return node; + /// } + /// } + /// + /// + /// Person p1 = new Person("Microsoft"); + /// p1.Children.Add(new Person("Peter")); + /// p1.Children.Add(new Person("Mary")); + /// + /// Person p2 = new Person("Microsoft"); + /// p2.Children.Add(new Person("Peter")); + /// + /// ObjectGraphFactory factory = new SimpleObjectGraphFactory(); + /// ObjectComparer comparer = new ObjectComparer(factory); + /// Console.WriteLine( + /// "Objects p1 and p2 {0}", + /// comparer.Compare(p1, p2) ? "match!" : "do NOT match!"); + /// + /// + public abstract class ObjectGraphFactory + { + /// + /// Creates a graph for the given object. + /// + /// The object to convert. + /// The root node of the created graph. + public virtual GraphNode CreateObjectGraph(object value) + { + throw new NotSupportedException("Please provide a behavior for this method in a derived class"); + } + } +} diff --git a/src/dotnetCampus.UITest.WPFTestHelper/ObjectComparison/PublicPropertyObjectGraphFactory.cs b/src/dotnetCampus.UITest.WPFTestHelper/ObjectComparison/PublicPropertyObjectGraphFactory.cs new file mode 100644 index 0000000..f8188b5 --- /dev/null +++ b/src/dotnetCampus.UITest.WPFTestHelper/ObjectComparison/PublicPropertyObjectGraphFactory.cs @@ -0,0 +1,214 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using System.Reflection; + +namespace dotnetCampus.UITest.WPFTestHelper.ObjectComparison +{ + /// + /// Creates a graph by extracting public instance properties in the object. If the + /// property is an IEnumerable, extract the items. If an exception is thrown + /// when accessing a property on the left object, it is considered a match if + /// the same exception type is thrown when accessing the property on the right + /// object. + /// + /// + /// + /// For examples, refer to . + /// + public sealed class PublicPropertyObjectGraphFactory : ObjectGraphFactory + { + #region Public Members + + /// + /// Creates a graph for the given object by extracting public properties. + /// + /// The object to convert. + /// The root node of the created graph. + public override GraphNode CreateObjectGraph(object value) + { + if (value == null) + { + throw new ArgumentNullException("value"); + } + + // Queue of pending nodes + Queue pendingQueue = new Queue(); + + // Dictionary of < object hashcode, node > - to lookup already visited objects + Dictionary visitedObjects = new Dictionary(); + + // Build the root node and enqueue it + GraphNode root = new GraphNode() + { + Name = "RootObject", + ObjectValue = value, + }; + + pendingQueue.Enqueue(root); + + while (pendingQueue.Count != 0) + { + GraphNode currentNode = pendingQueue.Dequeue(); + object nodeData = currentNode.ObjectValue; + Type nodeType = currentNode.ObjectType; + + // If we have reached a leaf node - + // no more processing is necessary + if (IsLeafNode(nodeData, nodeType)) + { + continue; + } + + // Handle loops by checking the visted objects + if (visitedObjects.Keys.Contains(nodeData.GetHashCode())) + { + // Caused by a cycle - we have alredy seen this node so + // use the existing node instead of creating a new one + GraphNode prebuiltNode = visitedObjects[nodeData.GetHashCode()]; + currentNode.Children.Add(prebuiltNode); + continue; + } + else + { + visitedObjects.Add(nodeData.GetHashCode(), currentNode); + } + + // Extract and add child nodes for current object // + Collection childNodes = GetChildNodes(nodeData); + foreach (GraphNode childNode in childNodes) + { + childNode.Parent = currentNode; + currentNode.Children.Add(childNode); + + pendingQueue.Enqueue(childNode); + } + } + + return root; + } + + #endregion + + #region Private Members + + /// + /// Given an object, get a list of the immediate child nodes + /// + /// The object whose child nodes need to be extracted + /// Collection of child graph nodes + private Collection GetChildNodes(object nodeData) + { + Collection childNodes = new Collection(); + + // Extract and add properties + foreach (GraphNode child in ExtractProperties(nodeData)) + { + childNodes.Add(child); + } + + // Extract and add IEnumerable content + if (IsIEnumerable(nodeData)) + { + foreach (GraphNode child in GetIEnumerableChildNodes(nodeData)) + { + childNodes.Add(child); + } + } + + return childNodes; + } + + private List ExtractProperties(object nodeData) + { + List childNodes = new List(); + + PropertyInfo[] properties = nodeData.GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance); + foreach (PropertyInfo property in properties) + { + object value = null; + + ParameterInfo[] parameters = property.GetIndexParameters(); + // Skip indexed properties and properties that cannot be read + if (property.CanRead && parameters.Length == 0) + { + try + { + value = property.GetValue(nodeData, null); + } + catch (Exception ex) + { + // If accessing the property threw an exception + // then make the type of exception as the child. + // Do we want to validate the entire exception object + // here ? - currently not doing to improve perf. + value = ex.GetType().ToString(); + } + + GraphNode childNode = new GraphNode() + { + Name = property.Name, + ObjectValue = value, + }; + + childNodes.Add(childNode); + } + }; + + return childNodes; + } + + private static List GetIEnumerableChildNodes(object nodeData) + { + List childNodes = new List(); + + IEnumerable enumerableData = nodeData as IEnumerable; + IEnumerator enumerator = enumerableData.GetEnumerator(); + + int count = 0; + while (enumerator.MoveNext()) + { + GraphNode childNode = new GraphNode() + { + Name = "IEnumerable" + count++, + ObjectValue = enumerator.Current, + }; + + childNodes.Add(childNode); + } + + return childNodes; + } + + private static bool IsIEnumerable(object nodeData) + { + IEnumerable enumerableData = nodeData as IEnumerable; + if (enumerableData != null && + enumerableData.GetType().IsPrimitive == false && + nodeData.GetType() != typeof(System.String)) + { + return true; + } + else + { + return false; + } + } + + private static bool IsLeafNode(object nodeData, Type nodeType) + { + return nodeData == null || + nodeType.IsPrimitive || + nodeType == typeof(string); + } + + #endregion + } +} diff --git a/src/dotnetCampus.UITest.WPFTestHelper/README.md b/src/dotnetCampus.UITest.WPFTestHelper/README.md new file mode 100644 index 0000000..fa9e436 --- /dev/null +++ b/src/dotnetCampus.UITest.WPFTestHelper/README.md @@ -0,0 +1,3 @@ +# dotnetCampus.UITest.WPFTestHelper + +Copy from https://github.com/dotnet/wpf-test/ \ No newline at end of file diff --git a/src/dotnetCampus.UITest.WPFTestHelper/Text/BidiProperty.cs b/src/dotnetCampus.UITest.WPFTestHelper/Text/BidiProperty.cs new file mode 100644 index 0000000..274538d --- /dev/null +++ b/src/dotnetCampus.UITest.WPFTestHelper/Text/BidiProperty.cs @@ -0,0 +1,197 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; + +namespace dotnetCampus.UITest.WPFTestHelper.Text +{ + /// + /// Bidi Unicode range property + /// + internal class BidiProperty : IStringProperty + { + /// + /// Dictionary to store Bidi control code points for RegExp use + /// + private Dictionary bidiDictionary = new Dictionary(); + + private List bidiPropertyRangeList = new List(); + + private List latinRangeList = new List(); + + private static readonly int[] exclusions = {0x0604, 0x0605, 0x061C, 0x061D, 0x0620, 0x065F, 0xFBB2, 0xFBB3, 0xFBB4, 0xFBB5, 0xFBB6, 0xFBB7, 0xFBB8, + 0xFBB9, 0xFBBA, 0xFBBB, 0xFBBC, 0xFBBD, 0xFBBE, 0xFBBF, 0xFBC0, 0xFBC1, 0xFBC2, 0xFBC3, 0xFBC4, 0xFBC5, 0xFBC6, 0xFBC7, 0xFBC8, 0xFBC9, 0xFBCA, + 0xFBCB, 0xFBCD, 0xFBCE, 0xFBCF, 0xFBD0, 0xFBD1, 0xFBD2, 0xFD40, 0xFD41, 0xFD42, 0xFD43, 0xFD44, 0xFD45, 0xFD46, 0xFD47, 0xFD48, 0xFD49, 0xFD4A, + 0xFD4B, 0xFD4C, 0xFD4D, 0xFD4E, 0xFD4F, 0xFD90, 0xFD91, 0xFDC8, 0xFDC9, 0xFDCA, 0xFDCB, 0xFDCC, 0xFDCD, 0xFDCE, 0xFDCF, 0xFDD0, 0xFDD1, 0xFDD2, + 0xFDD3, 0xFDD4, 0xFDD5, 0xFDD6, 0xFDD7, 0xFDD8, 0xFDD9, 0xFDDA, 0xFDDB, 0xFDDC, 0xFDDD, 0xFDDE, 0xFDDF, 0xFDE0, 0xFDE1, 0xFDE2, 0xFDE3, 0xFDE4, + 0xFDE5, 0xFDE6, 0xFDE7, 0xFDE8, 0xFDE9, 0xFDEA, 0xFDEB, 0xFDEC, 0xFDEB, 0xFDEC, 0xFDED, 0xFDEE, 0xFDEF, 0xFDFE, 0xFDFF, 0xFE75, 0xFEFD, 0xFEFE, + 0xFEFF, 0x0590, 0x05C8, 0x05C9, 0x05CA, 0x05CB, 0x05CC, 0x05CD, 0x05CE, 0x05CF, 0x05EB, 0x05EC, 0x05ED, 0x05EE, 0x05EF, 0x05F5, 0x05F6, 0x05F7, + 0x05F8, 0x05F9, 0x05FA, 0x05FB, 0x05FC, 0x05FD, 0x05FE, 0x05FF, 0xFB07, 0xFB08, 0xFB09, 0xFB0A, 0xFB0B, 0xFB0C, 0xFB0D, 0xFB0E, 0xFB0F, 0xFB10, + 0xFB11, 0xFB12, 0xFB18, 0xFB19, 0xFB1A, 0xFB1B, 0xFB1C, 0xFB37, 0xFB3D, 0xFB3F, 0xFB42, 0xFB45}; + + private int [] bidiMarks; + + /// + /// Define minimum code points need to be a bidi string + /// + public static readonly int MINNUMOFCODEPOINT = 2; + + /// + /// Define SurrogatePairDictionary class + /// Newline + /// + public BidiProperty(UnicodeRangeDatabase unicodeDb, Collection expectedRanges) + { + bool isValid = false; + + foreach (UnicodeRange range in expectedRanges) + { + if (RangePropertyCollector.BuildPropertyDataList( + unicodeDb, + range, + bidiPropertyRangeList, + "Arabic", + GroupAttributes.Name)) + { + isValid = true; + } + + if (RangePropertyCollector.BuildPropertyDataList( + unicodeDb, + range, + bidiPropertyRangeList, + "Hebrew", + GroupAttributes.Name)) + { + isValid = true; + } + } + + if (InitializeBidiDictionary(expectedRanges)) + { + isValid = true; + } + + if (!isValid) + { + throw new ArgumentOutOfRangeException("expectedRanges", "BidiProperty, Bidi ranges are beyond expected range. " + + "Refer to Arabic and Hebrew ranges."); + } + + // Reset isValid to validate Latin range + isValid = false; + foreach (UnicodeRange expectedRange in expectedRanges) + { + UnicodeRange range = RangePropertyCollector.GetRange(new UnicodeRange(0x0030, 0x0039), expectedRange); + if (null != range) + { + latinRangeList.Add(range); + isValid = true; + } + + range = RangePropertyCollector.GetRange(new UnicodeRange(0x0041, 0x005A), expectedRange); + if (null != range) + { + latinRangeList.Add(range); + isValid = true; + } + + range = RangePropertyCollector.GetRange(new UnicodeRange(0x0061, 0x007A), expectedRange); + if (null != range) + { + latinRangeList.Add(range); + isValid = true; + } + } + + if (!isValid) + { + throw new ArgumentOutOfRangeException("expectedRanges", "BidiProperty, Bidi ranges are beyond expected range. " + + "0x0030 - 0x0039, 0x0041 - 0x005A, and 0x0061 - 0x007A ranges are needed to construct Bidi string."); + } + } + + /// + /// Dictionary to store code points corresponding to culture. + /// + private bool InitializeBidiDictionary(Collection expectedRanges) + { + bool isValid = false; + + bidiDictionary.Add("LEFTTORIGHTMARK", '\u200E'); + bidiDictionary.Add("RIGHTTOLEFTMARK", '\u200F'); + bidiDictionary.Add("LEFTTORIGHTEMBEDDING", '\u202A'); // quoation needed + bidiDictionary.Add("RIGHTTOLEFTEMBEDDING", '\u202B'); // quoation needed + bidiDictionary.Add("POPDIRECTIONALFORMATTING", '\u202C'); + bidiDictionary.Add("LEFTTORIGHTOVERRIDE", '\u202D'); + bidiDictionary.Add("RIGHTTOLEFTOVERRIDE", '\u202E'); + + int i = 0; + bidiMarks = new int [bidiDictionary.Count]; + Dictionary.ValueCollection valueColl = bidiDictionary.Values; + foreach (char codePoint in valueColl) + { + foreach (UnicodeRange range in expectedRanges) + { + if (codePoint >= range.StartOfUnicodeRange && codePoint <= range.EndOfUnicodeRange) + { + bidiMarks[i++] = (int)codePoint; + isValid = true; + } + } + } + Array.Resize(ref bidiMarks, i); + return isValid; + } + + /// + /// Check if code point is in the property range + /// + public bool IsInPropertyRange(int codePoint) + { + bool isIn = false; + foreach (UnicodeRangeProperty prop in bidiPropertyRangeList) + { + if (codePoint >= prop.Range.StartOfUnicodeRange && codePoint <= prop.Range.EndOfUnicodeRange) + { + isIn = true; + break; + } + } + + return isIn; + } + + /// + /// Get random bidi string + /// + public string GetRandomCodePoints(int numOfProperty, int seed) + { + if (numOfProperty < 1) + { + throw new ArgumentOutOfRangeException("BidiProperty, numOfProperty, " + numOfProperty + " cannot be less than one."); + } + + // only support Arabic and Hebrew for current version + string bidiStr = string.Empty; + Random rand = new Random(seed); + int index = 0; + + for (int i=0; i < numOfProperty; i++) + { + index = rand.Next(0, bidiPropertyRangeList.Count); + bidiStr += TextUtil.GetRandomCodePoint(bidiPropertyRangeList[index].Range, 1, exclusions, seed); + index = rand.Next(0, latinRangeList.Count); + bidiStr += TextUtil.GetRandomCodePoint(latinRangeList[index], 1, null, seed); + } + + return bidiStr; + } + } +} + + + diff --git a/src/dotnetCampus.UITest.WPFTestHelper/Text/CombiningMarksProperty.cs b/src/dotnetCampus.UITest.WPFTestHelper/Text/CombiningMarksProperty.cs new file mode 100644 index 0000000..7857bb4 --- /dev/null +++ b/src/dotnetCampus.UITest.WPFTestHelper/Text/CombiningMarksProperty.cs @@ -0,0 +1,213 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; + +namespace dotnetCampus.UITest.WPFTestHelper.Text +{ + /// + /// Collect combining mark code points + /// + internal class CombiningMarksProperty : IStringProperty + { + /// + /// Dictionary to store code point corresponding to culture. + /// + private Dictionary combiningMarksDictionary = new Dictionary(); + + private List combiningMarksPropertyRangeList = new List(); + + private static readonly int[] exclusions = {0xFE27, 0xFE28, 0xFE29, 0xFE2A, 0xFE2B, 0xFE2C, 0xFE2D, 0xFE2F, 0x1DE7, 0x1DE8, 0x1DE9, 0x1DEA, + 0x1DEB, 0x1DEC, 0x1DED, 0x1DEE, 0x1DEF, 0x1DF0, 0x1DF1, 0x1DF2, 0x1DF3, 0x1DF4, 0x1DF5, 0x1DF6, 0x1DF7, 0x1DF8, 0x1DF9, 0x1DFA, 0x1DFB, 0x1DFC}; + + private int [] combiningMarks; + + /// + /// Define minimum code points needed to be a combining mark string + /// + public static readonly int MINNUMOFCODEPOINT = 2; + + /// + /// Define CombiningMarksProperty class + /// Newline + /// Newline + /// Newline + /// + public CombiningMarksProperty(UnicodeRangeDatabase unicodeDb, Collection expectedRanges) + { + bool isValid = false; + + foreach (UnicodeRange range in expectedRanges) + { + if (RangePropertyCollector.BuildPropertyDataList( + unicodeDb, + range, + combiningMarksPropertyRangeList, + "Combining Diacritics", + GroupAttributes.GroupName)) + { + isValid = true; + } + } + + if (InitializeCombiningMarksDictionary(expectedRanges)) + { + isValid = true; + } + + if (!isValid) + { + throw new ArgumentOutOfRangeException("expectedRanges", "CombiningMarksProperty, Combining mark ranges are beyond expected range. " + + "Refer to Combining Diacritics range."); + } + } + + private bool InitializeCombiningMarksDictionary(Collection expectedRanges) + { + // Grave and acute accent + char [] other = {'\u0302', '\u0307', '\u030A', '\u0315', '\u0316', '\u0317', '\u0318', '\u0319', '\u031A', '\u031C', '\u031D', + '\u031E', '\u031F', '\u0320', '\u0321', '\u0322', '\u0324', '\u032A', '\u032B', '\u032C', '\u032E', '\u0330', '\u0332', + '\u0333', '\u0334', '\u0335', '\u0336', '\u0337', '\u0338', '\u0339', '\u033A', '\u033B', '\u033C', '\u033D', '\u033F', + '\u0346', '\u0347', '\u0348', '\u0349', '\u034A', '\u034B', '\u034C', '\u034D', '\u034E', '\u034F', '\u0358', '\u0359', + '\u035A', '\u035B', '\u035C', '\u035D', '\u035E', '\u0360', '\u0361', '\u0362', '\u0323', '\u0328', '\u032D', '\u032F', + '\u1DC8', '\u1DC9', '\u1DCA', '\u1DCE', '\u1DCF', '\u1DD0', '\u1DD1', '\u1DD2', '\u1DD3', '\u1DD4', '\u1DD5', '\u1DD6', + '\u1DD7', '\u1DD8', '\u1DD9', '\u1DDA', '\u1DDB', '\u1DDC', '\u1DDD', '\u1DDE', '\u1DDF', '\u1DE0', '\u1DE1', '\u1DE2', + '\u1DE3', '\u1DE4', '\u1DE5', '\u1DE6', '\uFE20', '\uFE21', '\uFE22', '\uFE23'}; + combiningMarksDictionary.Add("other", other); + char [] vi = {'\u0303', '\u0308', '\u031B', '\u0323', '\u0340', '\u0341'}; + combiningMarksDictionary.Add("vi", vi); + char [] el = {'\u0300','\u0301', '\u0304', '\u0305', '\u0306', '\u0308', '\u0313', '\u0314', '\u0331', '\u0342', '\u0343', + '\u0344', '\u0345', '\u1DC0', '\u1DC1', '\u1DC4', '\u1DC5', '\u1DC6', '\u1DC7', '\uFE24', '\uFE25', '\uFE26'}; + combiningMarksDictionary.Add("el", el); + char [] hu = {'\u030B', '\u0350', '\u0351', '\u0352', '\u0353', '\u0354', '\u0355', '\u0356', '\u0357'}; + combiningMarksDictionary.Add("hu", hu); + char [] cs = {'\u030C'}; + combiningMarksDictionary.Add("cs", cs); + char [] id = {'\u030D', '\u030E', '\u0325'}; + combiningMarksDictionary.Add("id", id); + char [] ms = {'\u030D', '\u030E'}; + combiningMarksDictionary.Add("ms", ms); + char [] srsp = {'\u030F', '\u0311', '\u0313', '\u0314', '\u033E', '\u0351', '\u0352', '\u0353', '\u0354', '\u0355', '\u0356', + '\u0357', '\u1DC3'}; + combiningMarksDictionary.Add("sr-sp", srsp); + char [] hr = {'\u030F', '\u1DC3'}; + combiningMarksDictionary.Add("hr", hr); + char [] hi = {'\u0310', '\u0325'}; + combiningMarksDictionary.Add("hi", hi); + char [] azaz = {'\u0311', '\u0313', '\u0314', '\u033E', '\u0327'}; + combiningMarksDictionary.Add("az-az", azaz); + char [] uzuz = {'\u0311', '\u0313', '\u0314', '\u033E'}; + combiningMarksDictionary.Add("uz-uz", uzuz); + char [] lv = {'\u0312', '\u0326'}; + combiningMarksDictionary.Add("lv", lv); + char [] fi = {'\u0326', '\u0350', '\u0351', '\u0352', '\u0353', '\u0354', '\u0355', '\u0356', '\u0357'}; + combiningMarksDictionary.Add("fi", fi); + char [] hy = {'\u0313', '\u0314'}; + combiningMarksDictionary.Add("hy", hy); + char [] he = {'\u0323'}; + combiningMarksDictionary.Add("he", he); + char [] ar = {'\u0323'}; + combiningMarksDictionary.Add("ar", ar); + char [] ro = {'\u0326', '\u0350', '\u0351', '\u0352', '\u0353', '\u0354', '\u0355', '\u0356', '\u0357'}; + combiningMarksDictionary.Add("ro", ro); + char [] fr = {'\u0327'}; + combiningMarksDictionary.Add("fr", fr); + char [] tr = {'\u0327'}; + combiningMarksDictionary.Add("tr", tr); + char [] pl = {'\u0328'}; + combiningMarksDictionary.Add("pl", pl); + char [] lt = {'\u0328', '\u035B', '\u1DCB', '\u1DCC'}; + combiningMarksDictionary.Add("lt", lt); + char [] yoruba = {'\u0329'}; + combiningMarksDictionary.Add("yoruba", yoruba); + char [] de = {'\u0329', '\u0363', '\u0364', '\u0365', '\u0366', '\u0367', '\u0368', '\u0369', '\u036A', '\u036B', '\u036C', '\u036D', + '\u036E', '\u036F'}; + combiningMarksDictionary.Add("de", de); + char [] et = {'\u0350', '\u0351', '\u0352', '\u0353', '\u0354', '\u0355', '\u0356', '\u0357'}; + combiningMarksDictionary.Add("et", et); + char [] ru = {'\u030B', '\u0350', '\u0351', '\u0352', '\u0353', '\u0354', '\u0355', '\u0356', '\u0357', '\u1DC3'}; + combiningMarksDictionary.Add("ru", ru); + char [] sk = {'\u0351', '\u0352', '\u0353', '\u0354', '\u0355', '\u0356', '\u0357', '\u1DC3'}; + combiningMarksDictionary.Add("sk", sk); + char [] be = {'\u1DC3'}; + combiningMarksDictionary.Add("be", be); + char [] bg = {'\u1DC3'}; + combiningMarksDictionary.Add("bg", be); + char [] mk = {'\u1DC3'}; + combiningMarksDictionary.Add("mk", mk); + char [] sl = {'\u1DC3'}; + combiningMarksDictionary.Add("sl", sl); + char [] uk = {'\u1DC3'}; + combiningMarksDictionary.Add("uk", uk); + char [] symbol = {'\u20D0', '\u20D1', '\u20D2', '\u20D3', '\u20D4', '\u20D5', '\u20D6', '\u20D7', '\u20D8', '\u20D9', '\u20DA', + '\u20DF', '\u20E0', '\u20E1', '\u20E2', '\u20E3', '\u20E4', '\u20E5', '\u20E6', '\u20E7', '\u20E8', '\u20E9', '\u20EA', '\u20EB', + '\u20EC', '\u20ED', '\u20EF', '\u20F0'}; + combiningMarksDictionary.Add("symbol", symbol); + + bool isValid = false; + int i = 0; + combiningMarks = new int [other.Length + vi.Length + el.Length + hu.Length + cs.Length + id.Length + ms.Length + srsp.Length + hr.Length + + hi.Length + azaz.Length + uzuz.Length + lv.Length + fi.Length + hy.Length + he.Length + ar.Length + ro.Length + fr.Length + tr.Length + + pl.Length + lt.Length + yoruba.Length + de.Length + et.Length + ru.Length + sk.Length + be.Length + bg.Length + mk.Length + sl.Length + + uk.Length + symbol.Length]; + Dictionary.ValueCollection valueColl = combiningMarksDictionary.Values; + foreach (char [] values in valueColl) + { + foreach (char codePoint in values) + { + foreach (UnicodeRange range in expectedRanges) + { + if (codePoint >= range.StartOfUnicodeRange && codePoint <= range.EndOfUnicodeRange) + { + combiningMarks[i++] = (int)codePoint; + isValid = true; + } + } + } + } + Array.Resize(ref combiningMarks, i); + return isValid; + } + + /// + /// Check if code point is in the property range + /// + public bool IsInPropertyRange(int codePoint) + { + bool isIn = false; + foreach (UnicodeRangeProperty prop in combiningMarksPropertyRangeList) + { + if (codePoint >= prop.Range.StartOfUnicodeRange && codePoint <= prop.Range.EndOfUnicodeRange) + { + isIn = true; + break; + } + } + + return isIn; + } + + /// + /// Get random combining marks code points + /// + public string GetRandomCodePoints(int numOfProperty, int seed) + { + if (numOfProperty < 1) + { + throw new ArgumentOutOfRangeException( + "CombiningMarksProperty, numOfProperty, " + numOfProperty + " cannot be less than one."); + } + + Random rand = new Random(seed); + string combiningMarkStr = string.Empty; + int index = rand.Next(0, combiningMarksPropertyRangeList.Count); + combiningMarkStr += TextUtil.GetRandomCodePoint(combiningMarksPropertyRangeList[index].Range, numOfProperty, exclusions, seed); + + return combiningMarkStr; + } + } +} + diff --git a/src/dotnetCampus.UITest.WPFTestHelper/Text/EudcProperty.cs b/src/dotnetCampus.UITest.WPFTestHelper/Text/EudcProperty.cs new file mode 100644 index 0000000..3487559 --- /dev/null +++ b/src/dotnetCampus.UITest.WPFTestHelper/Text/EudcProperty.cs @@ -0,0 +1,103 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; + +namespace dotnetCampus.UITest.WPFTestHelper.Text +{ + /// + /// Collect EUDC code points + /// + internal class EudcProperty : IStringProperty + { + private List eudcRangeList = new List(); + + private int low; // 0xE000 + + private int high; // 0xF8FF + + /// + /// Define minimum code point needed to be an EUDC string + /// + public static readonly int MINNUMOFCODEPOINT = 1; + + + /// + /// Define SurrogatePairDictionary class + /// Newline + /// + public EudcProperty(UnicodeRangeDatabase unicodeDb, Collection expectedRanges) + { + low = 0xE000; high = 0xF8FF; + + bool isValid = false; + foreach (UnicodeRange range in expectedRanges) + { + if (RangePropertyCollector.BuildPropertyDataList( + unicodeDb, + range, + eudcRangeList, + "Private Use", + GroupAttributes.GroupName)) + { + isValid = true; + } + } + + if(!isValid) + { + throw new ArgumentOutOfRangeException("expectedRanges", "EudcProperty, EUDC ranges are beyond expected range. " + + "Refer to Private Use range."); + } + + foreach (UnicodeRangeProperty data in eudcRangeList) + { + if (data.Name.Equals("Private Use Area", StringComparison.OrdinalIgnoreCase)) + { + low = data.Range.StartOfUnicodeRange; + high = data.Range.EndOfUnicodeRange; + break; + } + } + } + + /// + /// Check if code point is in the property range + /// + public bool IsInPropertyRange(int codePoint) + { + bool isIn = false; + foreach (UnicodeRangeProperty prop in eudcRangeList) + { + if (codePoint >= prop.Range.StartOfUnicodeRange && codePoint <= prop.Range.EndOfUnicodeRange) + { + isIn = true; + break; + } + } + + return isIn; + } + + /// + /// Get random EUDC code points + /// + public string GetRandomCodePoints(int numOfProperty, int seed) + { + if (numOfProperty < 1) + { + throw new ArgumentOutOfRangeException("EudcProperty, numOfProperty, " + numOfProperty + " cannot be less than one."); + } + + string eudcStr = string.Empty; + eudcStr += TextUtil.GetRandomCodePoint(new UnicodeRange(low, high), numOfProperty, null, seed); + + return eudcStr; + } + } +} + + diff --git a/src/dotnetCampus.UITest.WPFTestHelper/Text/Group.cs b/src/dotnetCampus.UITest.WPFTestHelper/Text/Group.cs new file mode 100644 index 0000000..63831b2 --- /dev/null +++ b/src/dotnetCampus.UITest.WPFTestHelper/Text/Group.cs @@ -0,0 +1,32 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace dotnetCampus.UITest.WPFTestHelper.Text +{ + internal class Group + { + public Group(UnicodeRange range, string groupName, string name, string ids, UnicodeChart chart) + { + UnicodeRange = new UnicodeRange(range); + GroupName = groupName; + Name = name; + Ids = ids; + UnicodeChart = chart; + SubGroups = null; + } + + public UnicodeRange UnicodeRange { get; set; } + + public string GroupName { get; set; } + + public string Name { get; set; } + + public string Ids { get; set; } + + public UnicodeChart UnicodeChart { get; set; } + + public SubGroup [] SubGroups { get; set; } + } +} + diff --git a/src/dotnetCampus.UITest.WPFTestHelper/Text/GroupAttributes.cs b/src/dotnetCampus.UITest.WPFTestHelper/Text/GroupAttributes.cs new file mode 100644 index 0000000..98cc7eb --- /dev/null +++ b/src/dotnetCampus.UITest.WPFTestHelper/Text/GroupAttributes.cs @@ -0,0 +1,28 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace dotnetCampus.UITest.WPFTestHelper.Text +{ + /// + /// An enum type to have each enum type associate with attributes in Group + /// + internal enum GroupAttributes + { + /// + /// Name of the Group + /// + GroupName = 1, + + /// + /// Name + /// + Name, + + /// + /// Culture ids + /// + Ids + } +} + diff --git a/src/dotnetCampus.UITest.WPFTestHelper/Text/IStringProperty.cs b/src/dotnetCampus.UITest.WPFTestHelper/Text/IStringProperty.cs new file mode 100644 index 0000000..840ab05 --- /dev/null +++ b/src/dotnetCampus.UITest.WPFTestHelper/Text/IStringProperty.cs @@ -0,0 +1,25 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace dotnetCampus.UITest.WPFTestHelper.Text +{ + /// + /// Inteface for specific string property class + /// + internal interface IStringProperty + { + /// + /// Get next random code point or points that belongs to a specific + /// string property. number of code points does not necessarily translate + /// to number of chars since surrogate pair are two bytes + /// + string GetRandomCodePoints(int numOfProperty, int seed); + + /// + /// Check if code point is in the property range + /// + bool IsInPropertyRange(int codePoint); + } +} + diff --git a/src/dotnetCampus.UITest.WPFTestHelper/Text/LineBreakProperty.cs b/src/dotnetCampus.UITest.WPFTestHelper/Text/LineBreakProperty.cs new file mode 100644 index 0000000..e567155 --- /dev/null +++ b/src/dotnetCampus.UITest.WPFTestHelper/Text/LineBreakProperty.cs @@ -0,0 +1,120 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; + +namespace dotnetCampus.UITest.WPFTestHelper.Text +{ + /// + /// Collect line break code points + /// + internal class LineBreakProperty : IStringProperty + { + /// + /// Dictionary to store code points corresponding to culture. + /// + private Dictionary lineBreakCharDictionary = new Dictionary(); + + private int [] lineBreakCodePoints; + + /// + /// Define minimum code point needed to be a string has line break + /// + public static readonly int MINNUMOFCODEPOINT = 1; + + /// + /// Define LineBreakProperty class, + /// Newline + /// + public LineBreakProperty(Collection expectedRangse) + { + if (!InitializeLineBreakCharDictionary(expectedRangse)) + { + throw new ArgumentOutOfRangeException("expectedRangse", "LineBreakProperty, Linebreak ranges are beyond expected range. " + + "Refert to CR, LF, CRLF, NEL, VT, FF, LS, and PS."); + } + } + + private bool InitializeLineBreakCharDictionary(Collection expectedRanges) + { + char [] cr = {'\u000D'}; + lineBreakCharDictionary.Add("CR", cr); + char [] lf = {'\u000A'}; + lineBreakCharDictionary.Add("LF", lf); + char [] crlf = {'\u000D', '\u000A'}; + lineBreakCharDictionary.Add("CRLF", crlf); + char [] nel = {'\u0085'}; + lineBreakCharDictionary.Add("NEL", nel); + char [] vt = {'\u000B'}; + lineBreakCharDictionary.Add("VT", vt); + char [] ff = {'\u000C'}; + lineBreakCharDictionary.Add("FF", ff); + char [] ls = {'\u2028'}; + lineBreakCharDictionary.Add("LS", ls); + char [] ps = {'\u2029'}; + lineBreakCharDictionary.Add("PS", ps); + + int i = 0; + bool isValid = false; + lineBreakCodePoints = new int [cr.Length + lf.Length + crlf.Length + nel.Length + vt.Length + ff.Length + ls.Length + ps.Length]; + Dictionary.ValueCollection valueColl = lineBreakCharDictionary.Values; + foreach (char[] values in valueColl) + { + foreach (char codePoint in values) + { + foreach (UnicodeRange range in expectedRanges) + { + if (codePoint >= range.StartOfUnicodeRange && codePoint <= range.EndOfUnicodeRange) + { + lineBreakCodePoints[i++] = (int)codePoint; + isValid = true; + } + } + } + } + Array.Resize(ref lineBreakCodePoints, i); + return isValid; + } + + /// + /// Check if code point is in the property range + /// + public bool IsInPropertyRange(int codePoint) + { + bool isIn = false; + foreach (int i in lineBreakCodePoints) + { + if (i == codePoint) + { + isIn = true; + break; + } + } + + return isIn; + } + + /// + /// Get next line break points + /// + public string GetRandomCodePoints(int numOfProperty, int seed) + { + if (numOfProperty < 1) + { + throw new ArgumentOutOfRangeException("LineBreakProperty, numOfProperty, " + numOfProperty + " cannot be less than one."); + } + + string lineBreakStr = string.Empty; + Random rand = new Random(seed); + for (int i=0; i < numOfProperty; i++) + { + lineBreakStr += TextUtil.IntToString(lineBreakCodePoints[rand.Next(0, lineBreakCodePoints.Length)]); + } + + return lineBreakStr; + } + } +} diff --git a/src/dotnetCampus.UITest.WPFTestHelper/Text/NumberProperty.cs b/src/dotnetCampus.UITest.WPFTestHelper/Text/NumberProperty.cs new file mode 100644 index 0000000..fbc84e0 --- /dev/null +++ b/src/dotnetCampus.UITest.WPFTestHelper/Text/NumberProperty.cs @@ -0,0 +1,137 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; + +namespace dotnetCampus.UITest.WPFTestHelper.Text +{ + /// + /// Collect number code points + /// + internal class NumberProperty : IStringProperty + { + /// + /// Dictionary to store code point corresponding to culture. + /// + private Dictionary numberDictionary = new Dictionary(); + + private List numberDigitRangeList = new List(); + + private int [] numberCodePoints; + + /// + /// Define minimum code point needed to be a string has number + /// + public static readonly int MINNUMOFCODEPOINT = 1; + + /// + /// Define NumberProperty class, + /// Newline + /// + public NumberProperty(UnicodeRangeDatabase unicodeDb, Collection expectedRanges) + { + bool isValid = false; + + foreach (UnicodeRange range in expectedRanges) + { + if (RangePropertyCollector.BuildPropertyDataList( + unicodeDb, + range, + numberDigitRangeList, + "Numbers and Digits", + GroupAttributes.GroupName)) + { + isValid = true; + } + } + + if (InitializeNumberCharDictionary(expectedRanges)) + { + isValid = true; + } + + if (!isValid) + { + throw new ArgumentOutOfRangeException("expectedRanges", "NumberProperty, number ranges are beyond expected range. " + + "Refer to latin numberals and point, percent, plus, and minus signs, and comma."); + } + } + + private bool InitializeNumberCharDictionary(Collection expectedRanges) + { + bool isValid = false; + char [] latin = {'\u0030', '\u0031', '\u0032', '\u0033', '\u0034', '\u0035', '\u0036', '\u0037', '\u0038', '\u0039'}; + numberDictionary.Add("latin", latin); + char [] piont = {'\u002E'}; + numberDictionary.Add("piont", piont); + char [] percent = {'\u0025'}; + numberDictionary.Add("percent", percent); + char [] minus = {'\u002D'}; + numberDictionary.Add("minus", minus); + char [] plus = {'\u002B'}; + numberDictionary.Add("plus", plus); + char [] comma = {'\u002C'}; + numberDictionary.Add("comma", comma); + + int i = 0; + numberCodePoints = new int [latin.Length]; + foreach (char codePoint in numberDictionary["latin"]) + { + foreach (UnicodeRange range in expectedRanges) + { + if (codePoint >= range.StartOfUnicodeRange && codePoint <= range.EndOfUnicodeRange) + { + numberCodePoints[i++] = (int)codePoint; + isValid = true; + } + } + } + Array.Resize(ref numberCodePoints, i); + + return isValid; + } + + /// + /// Check if code point is in the property range + /// + public bool IsInPropertyRange(int codePoint) + { + bool isIn = false; + foreach (int i in numberCodePoints) + { + if (i == codePoint) + { + isIn = true; + break; + } + } + + return isIn; + } + + /// + /// Get number code points + /// + public string GetRandomCodePoints(int numOfProperty, int seed) + { + if (numOfProperty < 1) + { + throw new ArgumentOutOfRangeException("NumberProperty, numOfProperty, " + numOfProperty + " cannot be less than one."); + } + + string numStr = string.Empty; + Random rand = new Random(seed); + for (int i= 0; i < numOfProperty; i++) + { + int index = rand.Next(0, numberCodePoints.Length); + numStr += TextUtil.IntToString(numberCodePoints[index]); + } + + return numStr; + } + } +} + diff --git a/src/dotnetCampus.UITest.WPFTestHelper/Text/PropertyFactory.cs b/src/dotnetCampus.UITest.WPFTestHelper/Text/PropertyFactory.cs new file mode 100644 index 0000000..a598862 --- /dev/null +++ b/src/dotnetCampus.UITest.WPFTestHelper/Text/PropertyFactory.cs @@ -0,0 +1,228 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Generic; +using System.Collections.ObjectModel; + +namespace dotnetCampus.UITest.WPFTestHelper.Text +{ + /// + /// PropertyFactory class stores string property objects + /// + internal class PropertyFactory + { + private BidiProperty bidiProperty; + private CombiningMarksProperty combiningMarksProperty; + private EudcProperty eudcProperty; + private LineBreakProperty lineBreakProperty; + private NumberProperty numberProperty; + private SurrogatePairProperty surrogatePairProperty; + private TextNormalizationProperty textNormalizationProperty; + private TextSegmentationProperty textSegmentationProperty; + + /// + /// Enum all available properties + /// + public enum PropertyName + { + /// + /// Bidi property + /// + Bidi = 0, + + /// + /// Combining mark + /// + CombiningMarks, + + /// + /// EUDC + /// + Eudc, + + /// + /// Line break + /// + LineBreak, + + /// + /// Number + /// + Number, + + /// + /// Surrogate + /// + Surrogate, + + /// + /// Text normalization + /// + TextNormalization, + + /// + /// Text segmentation + /// + TextSegmentation + } + + private int minNumOfCodePoint; + + private Dictionary propertyDictionary; + + /// + /// Create property objects according to string properties + /// + public PropertyFactory(StringProperties properties, UnicodeRangeDatabase unicodeDb, Collection expectedRanges) + { + bidiProperty = null; + combiningMarksProperty = null; + eudcProperty = null; + lineBreakProperty = null; + numberProperty = null; + surrogatePairProperty = null; + textNormalizationProperty = null; + textSegmentationProperty = null; + minNumOfCodePoint = 0; + propertyDictionary = new Dictionary(); + CreateProperties(properties, unicodeDb, expectedRanges); + } + + /// + /// Check if property object exists + /// + public bool HasProperty(PropertyName propertyName) + { + if (PropertyName.Bidi == propertyName) + { + return null == bidiProperty ? false : true; + } + else if (PropertyName.CombiningMarks == propertyName) + { + return null == combiningMarksProperty ? false : true; + } + else if (PropertyName.Eudc == propertyName) + { + return null == eudcProperty ? false : true; + } + else if (PropertyName.LineBreak == propertyName) + { + return null == lineBreakProperty ? false : true; + } + else if (PropertyName.Number == propertyName) + { + return null == numberProperty ? false : true; + } + else if (PropertyName.Surrogate == propertyName) + { + return null == surrogatePairProperty ? false : true; + } + else if (PropertyName.TextNormalization == propertyName) + { + return null == textNormalizationProperty ? false : true; + } + else if (PropertyName.TextSegmentation == propertyName) + { + return null == textSegmentationProperty ? false : true; + } + else + { + return false; + } + } + + private void CreateProperties(StringProperties properties, UnicodeRangeDatabase unicodeDb, Collection expectedRanges) + { + if (null != properties.HasNumbers) + { + if ((bool)properties.HasNumbers) + { + numberProperty = new NumberProperty(unicodeDb, expectedRanges); + minNumOfCodePoint += NumberProperty.MINNUMOFCODEPOINT; + propertyDictionary.Add(PropertyName.Number, numberProperty); + } + } + + if (null != properties.IsBidirectional) + { + if ((bool)properties.IsBidirectional) + { + bidiProperty = new BidiProperty(unicodeDb, expectedRanges); + minNumOfCodePoint += BidiProperty.MINNUMOFCODEPOINT; + propertyDictionary.Add(PropertyName.Bidi, bidiProperty); + } + } + + if (null != properties.NormalizationForm) + { + textNormalizationProperty = new TextNormalizationProperty(unicodeDb, expectedRanges); + minNumOfCodePoint += TextNormalizationProperty.MINNUMOFCODEPOINT; + propertyDictionary.Add(PropertyName.TextNormalization, textNormalizationProperty); + } + + if (null != properties.MinNumberOfCombiningMarks) + { + if (0 != properties.MinNumberOfCombiningMarks) + { + combiningMarksProperty = new CombiningMarksProperty(unicodeDb, expectedRanges); + minNumOfCodePoint += CombiningMarksProperty.MINNUMOFCODEPOINT * (int)properties.MinNumberOfCombiningMarks; + propertyDictionary.Add(PropertyName.CombiningMarks, combiningMarksProperty); + } + } + + if (null != properties.MinNumberOfEndUserDefinedCodePoints) + { + if (0 != properties.MinNumberOfEndUserDefinedCodePoints) + { + eudcProperty = new EudcProperty(unicodeDb, expectedRanges); + minNumOfCodePoint += EudcProperty.MINNUMOFCODEPOINT * (int)properties.MinNumberOfEndUserDefinedCodePoints; + propertyDictionary.Add(PropertyName.Eudc, eudcProperty); + } + } + + if (null != properties.MinNumberOfLineBreaks) + { + if (0 != properties.MinNumberOfLineBreaks) + { + lineBreakProperty = new LineBreakProperty(expectedRanges); + minNumOfCodePoint += LineBreakProperty.MINNUMOFCODEPOINT * (int)properties.MinNumberOfLineBreaks; + propertyDictionary.Add(PropertyName.LineBreak, lineBreakProperty); + } + } + + if (null != properties.MinNumberOfSurrogatePairs) + { + if (0 != properties.MinNumberOfSurrogatePairs) + { + surrogatePairProperty = new SurrogatePairProperty(unicodeDb, expectedRanges); + minNumOfCodePoint += SurrogatePairProperty.MINNUMOFCODEPOINT * (int)properties.MinNumberOfSurrogatePairs; + propertyDictionary.Add(PropertyName.Surrogate, surrogatePairProperty); + } + } + + if (null != properties.MinNumberOfTextSegmentationCodePoints) + { + if (0 != properties.MinNumberOfTextSegmentationCodePoints) + { + textSegmentationProperty = new TextSegmentationProperty(unicodeDb, expectedRanges); + minNumOfCodePoint += TextSegmentationProperty.MINNUMOFCODEPOINT * (int)properties.MinNumberOfTextSegmentationCodePoints; + propertyDictionary.Add(PropertyName.TextSegmentation, textSegmentationProperty); + } + } + } + + /// + /// Minimum number of code points needed to have to cover non-null properties + /// + public int MinNumOfCodePoint { get {return minNumOfCodePoint; } } + + /// + /// Create property objects according to string properties + /// + public Dictionary PropertyDictionary { get { return propertyDictionary; } } + } +} + + + diff --git a/src/dotnetCampus.UITest.WPFTestHelper/Text/RangePropertyCollector.cs b/src/dotnetCampus.UITest.WPFTestHelper/Text/RangePropertyCollector.cs new file mode 100644 index 0000000..73c7513 --- /dev/null +++ b/src/dotnetCampus.UITest.WPFTestHelper/Text/RangePropertyCollector.cs @@ -0,0 +1,171 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Globalization; + +namespace dotnetCampus.UITest.WPFTestHelper.Text +{ + /// + /// Collect data according to name and save to PropertyData list + /// + internal static class RangePropertyCollector + { + /// + /// Get Unicode range according to Unicode chart provided + /// + public static UnicodeRange GetUnicodeChartRange(UnicodeRangeDatabase unicodeDb, UnicodeChart chart) + { + foreach (Group script in unicodeDb.Scripts) + { + if (script.UnicodeChart == chart) + { + return script.UnicodeRange; + } + + if (null != script.SubGroups) + { + foreach (SubGroup subScript in script.SubGroups) + { + if (subScript.UnicodeChart == chart) + { + return subScript.UnicodeRange; + } + } + } + } + + foreach (Group symbol in unicodeDb.SymbolsAndPunctuation) + { + if (symbol.UnicodeChart == chart) + { + return symbol.UnicodeRange; + } + + if (null != symbol.SubGroups) + { + foreach (SubGroup subSymbol in symbol.SubGroups) + { + if (subSymbol.UnicodeChart == chart) + { + return subSymbol.UnicodeRange; + } + } + } + } + + throw new ArgumentException(@"Invalid UnicodeChart, " + Enum.GetName(typeof(UnicodeChart), chart) + ". No match in the database."); + } + + /// + /// Get new range - if expectedRange is smaller, new range is expectedRange. Otherwise, return false + /// + public static UnicodeRange GetRange(UnicodeRange range, UnicodeRange expectedRange) + { + if (0 == expectedRange.StartOfUnicodeRange && TextUtil.MaxUnicodePoint == expectedRange.EndOfUnicodeRange) + { + // don't care if whole Unicode range is given + return new UnicodeRange(range.StartOfUnicodeRange,range.EndOfUnicodeRange); + } + + if (expectedRange.StartOfUnicodeRange > range.EndOfUnicodeRange || expectedRange.EndOfUnicodeRange < range.StartOfUnicodeRange) return null; + + int low = expectedRange.StartOfUnicodeRange > range.StartOfUnicodeRange ? expectedRange.StartOfUnicodeRange : range.StartOfUnicodeRange; + int high = expectedRange.EndOfUnicodeRange < range.EndOfUnicodeRange ? expectedRange.EndOfUnicodeRange : range.EndOfUnicodeRange; + return new UnicodeRange(low, high); + } + + /// + /// Walk through Unicode range database to build up property according to Group attribute + /// + public static bool BuildPropertyDataList( + UnicodeRangeDatabase unicodeDb, + UnicodeRange expectedRange, + List dataList, + string name, + GroupAttributes attribute) + { + bool isAdded = false; + + foreach (Group script in unicodeDb.Scripts) + { + string scriptAttrib = script.GroupName; + if (attribute == GroupAttributes.Name) scriptAttrib = script.Name; + else if (attribute == GroupAttributes.Ids) scriptAttrib = script.Ids; + + if (scriptAttrib.Equals(name, StringComparison.OrdinalIgnoreCase)) + { + UnicodeRange range = GetRange(script.UnicodeRange, expectedRange); + if (null != range) + { + dataList.Add(new UnicodeRangeProperty(TextUtil.UnicodeChartType.Script, script.Name, script.Ids, range)); + isAdded = true; + } + + if (null != script.SubGroups) + { + foreach (SubGroup subScript in script.SubGroups) + { + range = GetRange(subScript.UnicodeRange, expectedRange); + if (null != range) + { + dataList.Add(new UnicodeRangeProperty( + TextUtil.UnicodeChartType.Script, + subScript.SubGroupName, + subScript.SubIds, + range)); + isAdded = true; + } + } + } + } + } + + foreach (Group symbol in unicodeDb.SymbolsAndPunctuation) + { + string symbolAttrib = symbol.GroupName; + if (attribute == GroupAttributes.Name) symbolAttrib = symbol.Name; + else if (attribute == GroupAttributes.Ids) symbolAttrib = symbol.Ids; + + if (symbolAttrib.Equals(name, StringComparison.OrdinalIgnoreCase)) + { + TextUtil.UnicodeChartType type = TextUtil.UnicodeChartType.Other; + if ((symbol.GroupName.ToLower(CultureInfo.InvariantCulture)).Contains("symbols") || + (symbol.Name.ToLower(CultureInfo.InvariantCulture)).Contains("symbols")) + { + type = TextUtil.UnicodeChartType.Symbol; + } + else if((symbol.GroupName.ToLower(CultureInfo.InvariantCulture)).Contains("punctuation") || + (symbol.Name.ToLower(CultureInfo.InvariantCulture)).Contains("punctuation")) + { + type = TextUtil.UnicodeChartType.Punctuation; + } + + UnicodeRange range = GetRange(symbol.UnicodeRange, expectedRange); + if (null != range) + { + dataList.Add(new UnicodeRangeProperty(type, symbol.Name, symbol.Ids, range)); + isAdded = true; + } + + if (null != symbol.SubGroups) + { + foreach (SubGroup subSymbol in symbol.SubGroups) + { + range = GetRange(subSymbol.UnicodeRange, expectedRange); + if (null != range) + { + dataList.Add(new UnicodeRangeProperty(type, subSymbol.SubGroupName, subSymbol.SubIds, range)); + isAdded = true; + } + } + } + } + } + return isAdded; + } + } +} + diff --git a/src/dotnetCampus.UITest.WPFTestHelper/Text/StringFactory.cs b/src/dotnetCampus.UITest.WPFTestHelper/Text/StringFactory.cs new file mode 100644 index 0000000..c110a29 --- /dev/null +++ b/src/dotnetCampus.UITest.WPFTestHelper/Text/StringFactory.cs @@ -0,0 +1,835 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Text; + +namespace dotnetCampus.UITest.WPFTestHelper.Text +{ + /// + /// Provides factory methods for generation of text, interesting from the testing point of view. + /// + /// + /// + /// The following example demonstrates how to generate two equivalent strings + /// of a fixed length (20 Unicode points), with numbers. + /// A Unicode code point may correspond to 1 or 2 characters. For more information see : + /// + /// StringProperties properties = new StringProperties(); + /// + /// properties.MinNumberOfCodePoints = 20; + /// properties.MaxNumberOfCodePoints = 20; + /// properties.HasNumbers = true; + /// properties.UnicodeRanges.Add(new UnicodeRange(0, 0xFFFF)); + /// + /// string s1 = StringFactory.GenerateRandomString(properties, 5678); + /// string s2 = StringFactory.GenerateRandomString(properties, 5678); + /// + /// + public static class StringFactory + { + private static readonly UnicodeRangeDatabase database = new UnicodeRangeDatabase(); + private static PropertyFactory propertyFactory = null; + private static StringProperties properties = null; + private static StringProperties cachedProperties = null; + private static Collection ranges = new Collection(); + private static int minNumCodePoints = 0; + private static int maxNumCodePoints = 0; + private static int numOfCodePoints = 0; + + private static readonly List alphabetRangeList = new List(); + + /// + /// Returns a string, conforming to the provided. + /// + /// The properties of the strings to be generated by the factory. + /// The random number generator seed. + /// A string, conforming to the previously specified properties. + public static string GenerateRandomString(StringProperties stringProperties, int seed) + { + if (null == properties || IsPropertyChanged(stringProperties)) + { + properties = stringProperties; + // Make a deep copy of stringProperties to cache it. If user changes any property and calls this API again, + // InitializeProperties() is triggered. Otherwise, no need to re initialize properties for optimization. + CacheProperties(); + InitializeProperties(); + } + + string retStr = string.Empty; + Random rand = new Random(seed); + numOfCodePoints = rand.Next(minNumCodePoints, maxNumCodePoints); + + int numberOfProperties = propertyFactory.PropertyDictionary.Count; + if (0 == numberOfProperties) + { + for (int i=0; i < numOfCodePoints; i++) + { + retStr += TextUtil.IntToString(GetNextCodePoint(rand, seed)); + } + return retStr; + } + + int quote = numOfCodePoints / propertyFactory.MinNumOfCodePoint; + if (0 == quote) + { + throw new ArgumentOutOfRangeException( + "StringFactory, MinNumberOfCodePoints needs to be at least " + numberOfProperties * propertyFactory.MinNumOfCodePoint + "."); + } + + Dictionary.KeyCollection keyColl = propertyFactory.PropertyDictionary.Keys; + foreach (PropertyFactory.PropertyName name in keyColl) + { + if (PropertyFactory.PropertyName.Bidi == name) + { + retStr += GenerateBidiString(quote * BidiProperty.MINNUMOFCODEPOINT, seed); + } + else if (PropertyFactory.PropertyName.CombiningMarks == name) + { + retStr += GenerateCombiningMarkString(quote * CombiningMarksProperty.MINNUMOFCODEPOINT * (int)properties.MinNumberOfCombiningMarks, seed); + } + else if (PropertyFactory.PropertyName.Eudc == name) + { + retStr += GenerateStringWithEudc(quote * EudcProperty.MINNUMOFCODEPOINT * (int)properties.MinNumberOfEndUserDefinedCodePoints, seed); + } + else if (PropertyFactory.PropertyName.LineBreak == name) + { + retStr += GenerateStringWithLineBreak(quote * LineBreakProperty.MINNUMOFCODEPOINT * (int)properties.MinNumberOfLineBreaks, seed); + } + else if (PropertyFactory.PropertyName.Number == name) + { + retStr += GenerateStringWithNumber(quote * NumberProperty.MINNUMOFCODEPOINT, seed); + } + else if (PropertyFactory.PropertyName.Surrogate == name) + { + retStr += GenerateStringWithSurrogatePair(quote * SurrogatePairProperty.MINNUMOFCODEPOINT * (int)properties.MinNumberOfSurrogatePairs, seed); + } + else if (PropertyFactory.PropertyName.TextNormalization == name) + { + retStr += GenerateNormalizedString(quote * TextNormalizationProperty.MINNUMOFCODEPOINT, seed); + } + else if (PropertyFactory.PropertyName.TextSegmentation == name) + { + retStr += GenerateStringWithSegmentation( + quote * TextSegmentationProperty.MINNUMOFCODEPOINT * (int)properties.MinNumberOfTextSegmentationCodePoints, + seed); + } + } + + if (numOfCodePoints > 0) + { + for (int i=0; i < numOfCodePoints; i++) + { + retStr += TextUtil.IntToString(GetNextCodePoint(rand, seed)); + } + } + + if (null != properties.NormalizationForm) + { + retStr = retStr.Normalize((NormalizationForm)properties.NormalizationForm); + } + + return retStr; + } + + private static void CacheProperties() + { + if (null == cachedProperties) + { + cachedProperties = new StringProperties(); + } + + cachedProperties.UnicodeRanges.Clear(); + if (0 != properties.UnicodeRanges.Count) + { + foreach (UnicodeRange range in properties.UnicodeRanges) + { + cachedProperties.UnicodeRanges.Add(range); + } + } + + cachedProperties.HasNumbers = properties.HasNumbers; + cachedProperties.IsBidirectional = properties.IsBidirectional; + cachedProperties.NormalizationForm = properties.NormalizationForm; + cachedProperties.MinNumberOfCombiningMarks = properties.MinNumberOfCombiningMarks; + cachedProperties.MinNumberOfCodePoints = properties.MinNumberOfCodePoints; + cachedProperties.MaxNumberOfCodePoints = properties.MaxNumberOfCodePoints; + cachedProperties.MinNumberOfEndUserDefinedCodePoints = properties.MinNumberOfEndUserDefinedCodePoints; + cachedProperties.MinNumberOfLineBreaks = properties.MinNumberOfLineBreaks; + cachedProperties.MinNumberOfSurrogatePairs = properties.MinNumberOfSurrogatePairs; + cachedProperties.MinNumberOfTextSegmentationCodePoints = properties.MinNumberOfTextSegmentationCodePoints; + } + + private static bool IsPropertyChanged(StringProperties stringProperties) + { + if ((0 == cachedProperties.UnicodeRanges.Count && 0 != stringProperties.UnicodeRanges.Count) || + (0 != cachedProperties.UnicodeRanges.Count && 0 == stringProperties.UnicodeRanges.Count)) + { + return true; + } + else if (0 != cachedProperties.UnicodeRanges.Count && 0 != stringProperties.UnicodeRanges.Count) + { + if (cachedProperties.UnicodeRanges.Count != stringProperties.UnicodeRanges.Count) + { + return true; + } + + int i = 0; + foreach (UnicodeRange range in cachedProperties.UnicodeRanges) + { + if (range.StartOfUnicodeRange != stringProperties.UnicodeRanges[i].StartOfUnicodeRange) + { + return true; + } + + if (range.EndOfUnicodeRange != stringProperties.UnicodeRanges[i++].EndOfUnicodeRange) + { + return true; + } + } + } + + if (cachedProperties.HasNumbers != stringProperties.HasNumbers) + { + return true; + } + + if (cachedProperties.IsBidirectional != stringProperties.IsBidirectional) + { + return true; + } + + if (cachedProperties.NormalizationForm != stringProperties.NormalizationForm) + { + return true; + } + + if (cachedProperties.MinNumberOfCombiningMarks != stringProperties.MinNumberOfCombiningMarks) + { + return true; + } + + if (cachedProperties.MinNumberOfCodePoints != stringProperties.MinNumberOfCodePoints) + { + return true; + } + + if (cachedProperties.MaxNumberOfCodePoints != stringProperties.MaxNumberOfCodePoints) + { + return true; + } + + if (cachedProperties.MinNumberOfEndUserDefinedCodePoints != stringProperties.MinNumberOfEndUserDefinedCodePoints) + { + return true; + } + + if (cachedProperties.MinNumberOfLineBreaks != stringProperties.MinNumberOfLineBreaks) + { + return true; + } + + if (cachedProperties.MinNumberOfSurrogatePairs != stringProperties.MinNumberOfSurrogatePairs) + { + return true; + } + + if (cachedProperties.MinNumberOfTextSegmentationCodePoints != stringProperties.MinNumberOfTextSegmentationCodePoints) + { + return true; + } + + return false; + } + + private static void InitializeProperties() + { + if (0 != properties.UnicodeRanges.Count) + { + ranges.Clear(); + foreach (UnicodeRange range in properties.UnicodeRanges) + { + ranges.Add(range); + } + } + else + { + ranges.Add(new UnicodeRange(0, TextUtil.MaxUnicodePoint)); + } + + // Validation for Unicode ranges provided against each property is done when each property is created + propertyFactory = new PropertyFactory(properties, database, ranges); + + // Combining mark property needs Latin alphabet + if (propertyFactory.HasProperty(PropertyFactory.PropertyName.CombiningMarks)) + { + InitializeAlphabetRangeList(); + } + + // Get minimum number of points + minNumCodePoints = propertyFactory.MinNumOfCodePoint; + + if (null == properties.MinNumberOfCodePoints && null == properties.MaxNumberOfCodePoints) + { + maxNumCodePoints = TextUtil.MAXNUMOFCODEPOINT; + if (minNumCodePoints > maxNumCodePoints) + { + throw new ArgumentOutOfRangeException( + "StringFactory, maximum number of code points is greater than maximum allowed " + maxNumCodePoints + "."); + } + } + else if (null != properties.MinNumberOfCodePoints && null == properties.MaxNumberOfCodePoints) + { + minNumCodePoints = (int)properties.MinNumberOfCodePoints; + if (minNumCodePoints > TextUtil.MAXNUMOFCODEPOINT) + { + throw new ArgumentOutOfRangeException( + "StringFactory, maximum number of code points allowed is " + TextUtil.MAXNUMOFCODEPOINT + "."); + } + maxNumCodePoints = TextUtil.MAXNUMOFCODEPOINT; + } + else if (null == properties.MinNumberOfCodePoints && null != properties.MaxNumberOfCodePoints) + { + maxNumCodePoints = (int)properties.MaxNumberOfCodePoints; + if (maxNumCodePoints < propertyFactory.MinNumOfCodePoint) + { + throw new ArgumentOutOfRangeException( + "StringFactory, minimum number of code points needed is " + propertyFactory.MinNumOfCodePoint + "."); + } + } + else + { + minNumCodePoints = (int)properties.MinNumberOfCodePoints; + maxNumCodePoints = (int)properties.MaxNumberOfCodePoints; + if (minNumCodePoints > maxNumCodePoints) + { + throw new ArgumentOutOfRangeException("StringFactory, MinNumberOfCodePoints, " + minNumCodePoints + " cannot be bigger than " + + "MaxNumberOfCodePoints, " + maxNumCodePoints + "."); + } + } + } + + /// + /// Get next code point + /// + private static int GetNextCodePoint(Random rand, int seed) + { + int ctr = 0; + int index = rand.Next(0, ranges.Count); + int next = rand.Next(ranges[index].StartOfUnicodeRange, ranges[index].EndOfUnicodeRange); + while ((next >= 0xD800 && next <= 0xDBFF) || (next >= 0xDC00 && next <= 0xDFFF)) + { + if (propertyFactory.HasProperty(PropertyFactory.PropertyName.Surrogate)) + { + return SurrogatePairStringToInt( + (propertyFactory.PropertyDictionary[PropertyFactory.PropertyName.Surrogate]).GetRandomCodePoints(1, rand.Next())); + } + next = rand.Next(ranges[index].StartOfUnicodeRange, ranges[index].EndOfUnicodeRange); + ctr++; + if (TextUtil.MAXNUMITERATION == ctr) + { + throw new ArgumentOutOfRangeException( + "StringFactory, " + ctr + " loop reached." + "GetNextCodePoint aren't able to get code point beyond surrogate pair range. " + + "Check UnicodeChart range."); + } + } + + return next; + } + + /// + /// Convert a pair of Surrogate from string to UTF32 + /// + private static int SurrogatePairStringToInt(string pair) + { + int high = Convert.ToInt32(pair[0]); + int low = Convert.ToInt32(pair[1]); + return (high - 0xD800) * 0x400 + (low - 0xDC00) + 0x10000; + } + + /// + /// Construct bidi string + /// + private static string GenerateBidiString(int numOfPropertyCodePoints, int seed) + { + if (numOfPropertyCodePoints < 1) + { + throw new ArgumentOutOfRangeException("StringFactory, numOfPropertyCodePoints, " + numOfPropertyCodePoints + " cannot be less than one."); + } + numOfCodePoints -= numOfPropertyCodePoints; + + if (numOfCodePoints < 0) + { + throw new ArgumentOutOfRangeException("StringFactory, " + numOfCodePoints + + "left for the operation. Not enough code point to construct bidi string."); + } + + if (numOfPropertyCodePoints < BidiProperty.MINNUMOFCODEPOINT) + { + throw new ArgumentOutOfRangeException( + "StringFactory, minimum bidi string needs " + BidiProperty.MINNUMOFCODEPOINT + " code points."); + } + + Random rand = new Random(seed); + // Note, numOfBidi is number of property - 1 numOfBidi is 2 code points + int numOfBidi = rand.Next(1, numOfPropertyCodePoints / BidiProperty.MINNUMOFCODEPOINT); + int left = numOfPropertyCodePoints - numOfBidi * BidiProperty.MINNUMOFCODEPOINT; + + string bidiStr = string.Empty; + bidiStr += (propertyFactory.PropertyDictionary[PropertyFactory.PropertyName.Bidi]).GetRandomCodePoints(numOfBidi, seed); + if (left <= 0) + { + return bidiStr; + } + + string retStr = string.Empty; + int temp = 0; + + // Not using TextUtil.GetRandomCodePoint for more randomness + for (int i=1; i <= left; i++) + { + temp = GetNextCodePoint(rand, seed); + retStr += TextUtil.IntToString(temp); + } + retStr += bidiStr; + + return retStr; + } + + /// + /// Construct string with combining marks + /// + private static string GenerateCombiningMarkString(int numOfPropertyCodePoints, int seed) + { + if (numOfPropertyCodePoints < 1) + { + throw new ArgumentOutOfRangeException("StringFactory, numOfPropertyCodePoints, " + numOfPropertyCodePoints + " cannot be less than one."); + } + numOfCodePoints -= numOfPropertyCodePoints; + + if (numOfCodePoints < 0) + { + throw new ArgumentOutOfRangeException("StringFactory, " + numOfCodePoints + + "left for the operation. Not enough code point to construct string with combining mark."); + } + + // MinNumberOfCombiningMarks is null or not should have been checked + int numOfCombiningMarks = (int)properties.MinNumberOfCombiningMarks; + + if (numOfPropertyCodePoints < numOfCombiningMarks * CombiningMarksProperty.MINNUMOFCODEPOINT) + { + throw new ArgumentOutOfRangeException( + "StringFactory, minimum string cotains combining mark needs " + + numOfCombiningMarks * CombiningMarksProperty.MINNUMOFCODEPOINT + " code points."); + } + + // Construct combining marks string + Random rand = new Random(seed); + // Half Latin half combining symbols + int numOfLatin = numOfCombiningMarks; + int left = numOfPropertyCodePoints - numOfCombiningMarks * CombiningMarksProperty.MINNUMOFCODEPOINT; + + string latinStr = string.Empty; + int index = rand.Next(0, alphabetRangeList.Count); + // From a random range in alphabetRangeList + latinStr += TextUtil.GetRandomCodePoint(alphabetRangeList[index], numOfLatin, null, seed); + + string combiningMarkStr = string.Empty; + combiningMarkStr += + (propertyFactory.PropertyDictionary[PropertyFactory.PropertyName.CombiningMarks]).GetRandomCodePoints(numOfCombiningMarks, seed); + + string retStr = string.Empty; + int i = 0; + foreach (char next in latinStr) + { + retStr += next; + retStr += combiningMarkStr[i++]; + } + + // Leftover + for (i = 0; i < left; i++) + { + retStr += TextUtil.IntToString(GetNextCodePoint(rand, seed)); + } + + return retStr; + } + + /// + /// Construct string with EUDC + /// + private static string GenerateStringWithEudc(int numOfPropertyCodePoints, int seed) + { + if (numOfPropertyCodePoints < 1) + { + throw new ArgumentOutOfRangeException("StringFactory, numOfPropertyCodePoints, " + numOfPropertyCodePoints + " cannot be less than one."); + } + numOfCodePoints -= numOfPropertyCodePoints; + + if (numOfCodePoints < 0) + { + throw new ArgumentOutOfRangeException("StringFactory, " + numOfCodePoints + + "left for the operation. Not enough code point to construct string with EUDC."); + } + + // MinNumberOfEndUserDefinedCodePoints of EUDC is null or not should have been checked + int numOfEudc = (int)properties.MinNumberOfEndUserDefinedCodePoints; + + int left = numOfPropertyCodePoints - (numOfEudc * EudcProperty.MINNUMOFCODEPOINT); + + string eudcStr = string.Empty; + eudcStr += (propertyFactory.PropertyDictionary[PropertyFactory.PropertyName.Eudc]).GetRandomCodePoints(numOfEudc, seed); + if (left <= 0) + { + return eudcStr; + } + + string retStr = string.Empty; + Random rand = new Random(seed); + // numOfEudc is 0 or not is checked in PropertiesFactory + int quote = left / numOfEudc ; + + + int j = 0; + for (int i=1; i <= left; i++) + { + retStr += TextUtil.IntToString(GetNextCodePoint(rand, seed)); + + if (quote > 0 && j < eudcStr.Length) + { + if (i % quote == 0) + { + retStr += eudcStr[j++]; + } + } + } + + for (int i=j; i < eudcStr.Length; i++) + { + retStr += eudcStr[i]; + } + + return retStr; + } + + + /// + /// Construct string with Linebreaks + /// + private static string GenerateStringWithLineBreak(int numOfPropertyCodePoints, int seed) + { + if (numOfPropertyCodePoints < 1) + { + throw new ArgumentOutOfRangeException("StringFactory, numOfPropertyCodePoints, " + numOfPropertyCodePoints + " cannot be less than one."); + } + numOfCodePoints -= numOfPropertyCodePoints; + + if (numOfCodePoints < 0) + { + throw new ArgumentOutOfRangeException("StringFactory, " + numOfCodePoints + + "left for the operation. Not enough code point to construct string with line break."); + } + + // MinNumberOfLineBreaks is null or not should have been checked + int numOfLineBreaks = (int)properties.MinNumberOfLineBreaks; + + int left = numOfPropertyCodePoints - numOfLineBreaks * LineBreakProperty.MINNUMOFCODEPOINT; + + string lineBreakStr = string.Empty; + lineBreakStr += (propertyFactory.PropertyDictionary[PropertyFactory.PropertyName.LineBreak]).GetRandomCodePoints(numOfLineBreaks, seed); + if (left <= 0) + { + return lineBreakStr; + } + + string retStr = string.Empty; + Random rand = new Random(seed); + int quote = left / numOfLineBreaks; + + int j = 0; + for (int i=1; i <= left; i++) + { + retStr += TextUtil.IntToString(GetNextCodePoint(rand, seed)); + + if (quote > 0 && j < lineBreakStr.Length) + { + if (0 == i % quote) + { + retStr += lineBreakStr[j++]; + } + } + } + + for (int i=j; i < lineBreakStr.Length; i++) + { + retStr += lineBreakStr[i]; + } + + return retStr; + } + + /// + /// Construct string with numbers + /// + private static string GenerateStringWithNumber(int numOfPropertyCodePoints, int seed) + { + if (numOfPropertyCodePoints < 1) + { + throw new ArgumentOutOfRangeException("StringFactory, numOfPropertyCodePoints, " + numOfPropertyCodePoints + " cannot be less than one."); + } + numOfCodePoints -= numOfPropertyCodePoints; + + if (numOfCodePoints < 0) + { + throw new ArgumentOutOfRangeException("StringFactory, " + numOfCodePoints + + "left for the operation. Not enough code point to construct string with number."); + } + + int numOfNumbers = 0; + Random rand = new Random(seed); + numOfNumbers = rand.Next(1, numOfPropertyCodePoints / NumberProperty.MINNUMOFCODEPOINT); + + int left = numOfPropertyCodePoints - numOfNumbers * NumberProperty.MINNUMOFCODEPOINT; + + string numStr = string.Empty; + numStr += (propertyFactory.PropertyDictionary[PropertyFactory.PropertyName.Number]).GetRandomCodePoints(numOfNumbers, seed); + if (left <= 0) + { + return numStr; + } + + int quote = left / numOfNumbers; + string retStr = string.Empty; + + int j = 0; + for (int i=1; i <= left; i++) + { + retStr += TextUtil.IntToString(GetNextCodePoint(rand, seed)); + + if (quote > 0 && j < numStr.Length) + { + if (0 == i % quote) + { + retStr += numStr[j++]; + } + } + } + + for (int i=j; i < numStr.Length; i++) + { + retStr += numStr[i]; + } + + return retStr; + } + + /// + /// Construct string with Surrogate pairs + /// + private static string GenerateStringWithSurrogatePair(int numOfPropertyCodePoints, int seed) + { + if (numOfPropertyCodePoints < 1) + { + throw new ArgumentOutOfRangeException("StringFactory, numOfPropertyCodePoints, " + numOfPropertyCodePoints + " cannot be less than one."); + } + numOfCodePoints -= numOfPropertyCodePoints; + + if (numOfCodePoints < 0) + { + throw new ArgumentOutOfRangeException("StringFactory, " + numOfCodePoints + + "left for the operation. Not enough code points for surrogate pair string."); + } + + int numOfSurrogatePairs = (int)properties.MinNumberOfSurrogatePairs; + if (numOfPropertyCodePoints < numOfSurrogatePairs * SurrogatePairProperty.MINNUMOFCODEPOINT) + { + throw new ArgumentOutOfRangeException( + "StringFactory, minimum string cotains surrogate pair needs " + + numOfSurrogatePairs * SurrogatePairProperty.MINNUMOFCODEPOINT + " code points."); + } + + string surrogatePairStr = string.Empty; + surrogatePairStr += (propertyFactory.PropertyDictionary[PropertyFactory.PropertyName.Surrogate]).GetRandomCodePoints(numOfSurrogatePairs, seed); + int left = numOfPropertyCodePoints - numOfSurrogatePairs * SurrogatePairProperty.MINNUMOFCODEPOINT; + if (left <= 0) + { + return surrogatePairStr; + } + + string retStr = string.Empty; + int quote = left / numOfSurrogatePairs; + Random rand = new Random(seed); + + int j = 0; + for (int i=1; i <= left; i++) + { + retStr += TextUtil.IntToString(GetNextCodePoint(rand, seed)); + + if (quote > 0 && j < surrogatePairStr.Length) + { + if (0 == i % quote) + { + retStr += surrogatePairStr[j]; + retStr += surrogatePairStr[j+1]; + j += 2; + } + } + } + + for (int i=j; i < surrogatePairStr.Length; i++) + { + retStr += surrogatePairStr[i]; + } + + return retStr; + } + + /// + /// Construct normalized text string + /// + private static string GenerateNormalizedString(int numOfPropertyCodePoints, int seed) + { + if (numOfPropertyCodePoints < 1) + { + throw new ArgumentOutOfRangeException("StringFactory, numOfPropertyCodePoints, " + numOfPropertyCodePoints + " cannot be less than one."); + } + numOfCodePoints -= numOfPropertyCodePoints; + + if (numOfCodePoints < 0) + { + throw new ArgumentOutOfRangeException("StringFactory, " + numOfCodePoints + + "left for the operation. Not enough code points for normalized string."); + } + + Random rand = new Random(seed); + int numOfNormalizationCodePoint = rand.Next(1, numOfPropertyCodePoints / TextNormalizationProperty.MINNUMOFCODEPOINT); + int left = numOfPropertyCodePoints - numOfNormalizationCodePoint * TextNormalizationProperty.MINNUMOFCODEPOINT; + + string normalizedStr = string.Empty; + normalizedStr += (propertyFactory.PropertyDictionary[PropertyFactory.PropertyName.TextNormalization]).GetRandomCodePoints( + numOfNormalizationCodePoint, + seed); + if (left <= 0) + { + return normalizedStr; + } + + string retStr = string.Empty; + int quote = left / numOfNormalizationCodePoint; + + int j = 0; + for (int i=1; i <= left; i++) + { + retStr += TextUtil.IntToString(GetNextCodePoint(rand, seed)); + + if (quote > 0 && j < normalizedStr.Length) + { + if (0 == i % quote) + { + retStr += normalizedStr[j]; + if (normalizedStr[j] >= 0xD800 && normalizedStr[j] <= 0xDBFF) + { + retStr += normalizedStr[j+1]; + j++; + } + j++; + } + } + } + + for (int i=j; i < normalizedStr.Length; i++) + { + retStr += normalizedStr[i]; + } + + return retStr; + } + + /// + /// Construct string with segmentation code point + /// + private static string GenerateStringWithSegmentation(int numOfPropertyCodePoints, int seed) + { + if (numOfPropertyCodePoints < 1) + { + throw new ArgumentOutOfRangeException("StringFactory, numOfPropertyCodePoints, " + numOfPropertyCodePoints + " cannot be less than one."); + } + numOfCodePoints -= numOfPropertyCodePoints; + + if (numOfCodePoints < 0) + { + throw new ArgumentOutOfRangeException("StringFactory, " + numOfCodePoints + + "left for the operation. Not enough code point to segementation."); + } + + // MinNumberOfTextSegmentationCodePoints is null or not should have been checked + int numOfTextSegmentationChars = (int)properties.MinNumberOfTextSegmentationCodePoints; + int left = numOfPropertyCodePoints - numOfTextSegmentationChars * TextSegmentationProperty.MINNUMOFCODEPOINT; + + string segmentationStr = string.Empty; + segmentationStr += (propertyFactory.PropertyDictionary[PropertyFactory.PropertyName.TextSegmentation]).GetRandomCodePoints( + numOfTextSegmentationChars, + seed); + if (left <= 0) + { + return segmentationStr; + } + + string retStr = string.Empty; + Random rand = new Random(seed); + int quote = left / numOfTextSegmentationChars; + + int j = 0; + for (int i=1; i <= left; i++) + { + retStr += TextUtil.IntToString(GetNextCodePoint(rand, seed)); + + if (quote > 0 && j < segmentationStr.Length) + { + if (0 == i % quote) + { + retStr += segmentationStr[j++]; + } + } + } + + for (int i=j; i < segmentationStr.Length; i++) + { + retStr += segmentationStr[i]; + } + + return retStr; + } + + private static void InitializeAlphabetRangeList( ) + { + bool isValid = false; + + foreach (UnicodeRange range in ranges) + { + UnicodeRange newRange = RangePropertyCollector.GetRange(new UnicodeRange(0x0041, 0x005A), range); + if (null != newRange) + { + alphabetRangeList.Add(newRange); + isValid = true; + } + + newRange = RangePropertyCollector.GetRange(new UnicodeRange(0x0061, 0x007A), range); + if (null != newRange) + { + alphabetRangeList.Add(newRange); + isValid = true; + } + } + + if (!isValid) + { + throw new ArgumentOutOfRangeException("StringFactory, Latin alphabet ranges for Combining mark property are beyond expected. " + + "Refer to Latin Alphabet range 0x0041 - 0x005A and 0x0061 - 0x007A." + "All " + ranges.Count + " UniCodeRange is in Latin alphabet range"); + } + } + } +} diff --git a/src/dotnetCampus.UITest.WPFTestHelper/Text/StringProperties.cs b/src/dotnetCampus.UITest.WPFTestHelper/Text/StringProperties.cs new file mode 100644 index 0000000..122d9c3 --- /dev/null +++ b/src/dotnetCampus.UITest.WPFTestHelper/Text/StringProperties.cs @@ -0,0 +1,142 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.ObjectModel; +using System.Text; + +namespace dotnetCampus.UITest.WPFTestHelper.Text +{ + /// + /// Contains types for the generation, manipulation and validation of strings and text, for testing purposes. + /// + // Suppressed the warning that the class is never instantiated. + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1812")] + [System.Runtime.CompilerServices.CompilerGenerated()] + class NamespaceDoc + { + // Empty class used only for generation of namespace comments. + } + + /// + /// Defines the desired properties of a character string. + /// For more information on character strings, see this article. + /// + /// + /// Note that this class is used as "a filter" when generating character strings with + /// . Upon instantiation, all properties except CultureInfo of a + /// object (which are all Nullables) + /// have null values, which means that the object does not impose any filtering limitations on + /// the generated strings. + /// + /// Setting properties to non-null values means that the value of the property should be taken + /// into account by during string generation. For example, setting + /// to 10 means "generate strings with up to 10 code points". + /// + /// + /// + /// The following example demonstrates how to generate a Cyrillic string that + /// contains between 10 and 30 characters. + /// + /// StringProperties properties = new StringProperties(); + /// + /// properties.MinNumberOfCodePoints = 10; + /// properties.MaxNumberOfCodePoints = 30; + /// properties.UnicodeRanges.Add(new UnicodeRange(UnicodeChart.Cyrillic)); + /// + /// string s = StringFactory.GenerateRandomString(properties, 1234); + /// + /// + /// + /// + /// The following example demonstrates how to generate a string which + /// contains characters from more than one Unicode chart. + /// + /// StringProperties sp = new StringProperties(); + /// + /// sp.MinNumberOfCodePoints = 10; + /// sp.MaxNumberOfCodePoints = 20; + /// sp.UnicodeRanges.Add(new UnicodeRange(UnicodeChart.BraillePatterns)); + /// sp.UnicodeRanges.Add(new UnicodeRange(UnicodeChart.Cyrillic)); + /// + /// string s = StringFactory.GenerateRandomString(sp, 1234); + /// + /// + public class StringProperties + { + /// + /// Initializes a new instance of the class. + /// + public StringProperties() + { + UnicodeRanges = new Collection(); + MinNumberOfCombiningMarks = null; + HasNumbers = null; + IsBidirectional = null; + NormalizationForm = null; + MinNumberOfCodePoints = MaxNumberOfCodePoints = null; + MinNumberOfEndUserDefinedCodePoints = null; + MinNumberOfLineBreaks = null; + MinNumberOfSurrogatePairs = null; + MinNumberOfTextSegmentationCodePoints = null; + } + + /// + /// Determines whether the string belongs to one or more . + /// + public Collection UnicodeRanges { get; private set; } + + /// + /// Determines whether the string contains formatted numbers. + /// + public bool? HasNumbers { get; set; } + + /// + /// Determines whether the string is bi-directional. + /// + public bool? IsBidirectional { get; set; } + + /// + /// Determines the type of normalization to perform on the string. + /// For more information, see this article. + /// + public NormalizationForm? NormalizationForm { get; set; } + + /// + /// Determines the minimum number of combining marks in the string. + /// Combining marks (and combining + /// characters in general) are characters that are intended to modify other characters (e.g. accents, etc.) + /// + public int? MinNumberOfCombiningMarks { get; set; } + + /// + /// Determines the minimum number of code points (characters) in the string. + /// + public int? MinNumberOfCodePoints { get; set; } + + /// + /// Determines the maximum number of code points (characters) in the string. + /// + public int? MaxNumberOfCodePoints { get; set; } + + /// + /// Determines the minimum number of end-user-defined characters (EUDC) in the string. + /// + public int? MinNumberOfEndUserDefinedCodePoints { get; set; } + + /// + /// Determines the minimum number of line breaks in the string. + /// + public int? MinNumberOfLineBreaks { get; set; } + + /// + /// Determines the minimum number of surrogate pairs in the string. + /// + public int? MinNumberOfSurrogatePairs { get; set; } + + /// + /// Determines the minimum number of text segmentation code points in the string. + /// + public int? MinNumberOfTextSegmentationCodePoints { get; set; } + } +} diff --git a/src/dotnetCampus.UITest.WPFTestHelper/Text/SubGroup.cs b/src/dotnetCampus.UITest.WPFTestHelper/Text/SubGroup.cs new file mode 100644 index 0000000..c0f47f4 --- /dev/null +++ b/src/dotnetCampus.UITest.WPFTestHelper/Text/SubGroup.cs @@ -0,0 +1,46 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace dotnetCampus.UITest.WPFTestHelper.Text +{ + /// + /// sub Group DataStructure + /// + internal class SubGroup + { + /// + /// Define LineBreakDatabase class, + /// Newline + /// + public SubGroup(UnicodeRange range, string name, string ids, UnicodeChart chart) + { + UnicodeRange = new UnicodeRange(range); + SubGroupName = name; + SubIds = ids; + UnicodeChart = chart; + } + + /// + /// SubGroupRange property + /// + public UnicodeRange UnicodeRange { get; set; } + + /// + /// SubGroupName property + /// + public string SubGroupName { get; set; } + + /// + /// Enum Chart + /// + public UnicodeChart UnicodeChart { get; set; } + + /// + /// SubIds property + /// + public string SubIds { get; set; } + } +} + + diff --git a/src/dotnetCampus.UITest.WPFTestHelper/Text/SurrogatePairProperty.cs b/src/dotnetCampus.UITest.WPFTestHelper/Text/SurrogatePairProperty.cs new file mode 100644 index 0000000..08b302f --- /dev/null +++ b/src/dotnetCampus.UITest.WPFTestHelper/Text/SurrogatePairProperty.cs @@ -0,0 +1,147 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; + +namespace dotnetCampus.UITest.WPFTestHelper.Text +{ + /// + /// Collect surrogate pairs + /// + internal class SurrogatePairProperty : IStringProperty + { + private List surrogatePairRangeList = new List(); + private UnicodeRange surrogateRange; + + private int highMin = 0; // 0xD800 V5.2 + private int highMax = 0; // 0xDBFF V5.2 + private int lowMin = 0; // 0xDC00 V5.2 + private int lowMax = 0; // 0xDFFF V5.2 + + /// + /// Define minimum code point needed to have surrogate pair + /// + public static readonly int MINNUMOFCODEPOINT = 2; + + /// + /// Define SurrogatePairProperty class + /// Newline + /// Newline + /// + public SurrogatePairProperty(UnicodeRangeDatabase unicodeDb, Collection expectedRanges) + { + bool isValid = false; + + foreach (UnicodeRange range in expectedRanges) + { + if (RangePropertyCollector.BuildPropertyDataList( + unicodeDb, + range, + surrogatePairRangeList, + "Surrogates", + GroupAttributes.GroupName)) + { + foreach (UnicodeRangeProperty data in surrogatePairRangeList) + { + if (data.Name.Equals("High Surrogates", StringComparison.OrdinalIgnoreCase)) + { + highMin = data.Range.StartOfUnicodeRange; + highMax = data.Range.EndOfUnicodeRange; + } + else if (data.Name.Equals("Low Surrogates", StringComparison.OrdinalIgnoreCase)) + { + lowMin = data.Range.StartOfUnicodeRange; + lowMax = data.Range.EndOfUnicodeRange; + } + } + isValid = true; + } + + surrogateRange = RangePropertyCollector.GetRange(new UnicodeRange(0x10000, TextUtil.MaxUnicodePoint), range); + if (null != surrogateRange) + { + isValid = true; + } + } + + if (!isValid) + { + throw new ArgumentOutOfRangeException("expectedRanges", "SurrogatePairProperty, SurrogatePair ranges are beyond expected range. " + + "Refert to Surrogates range and UTF32."); + } + } + + /// + /// Check if code point is in the property range + /// + public bool IsInPropertyRange(int codePoint) + { + bool isIn = false; + if (codePoint > 0xFFFF) + { + if (null != surrogateRange) + { + if (codePoint >= surrogateRange.StartOfUnicodeRange && codePoint <= surrogateRange.EndOfUnicodeRange) + { + isIn = true; + } + } + } + + if (0 != highMin) + { + if ((codePoint >= highMin && codePoint <= highMax) || (codePoint >= lowMin && codePoint <= lowMax)) + { + isIn = true; + } + } + + return isIn; + } + + /// + /// Get random Surrogate pairs + /// + public string GetRandomCodePoints(int numOfProperty, int seed) + { + // NumOfProperty means number of pair + if (numOfProperty < 1) + { + throw new ArgumentOutOfRangeException("SurrogatePairProperty, numOfProperty, " + numOfProperty + " cannot be less than one."); + } + + string surrogateStr = string.Empty; + Random rnd = new Random(seed); + for (int i=1; i <= numOfProperty; i++) + { + if (null != surrogateRange && 0 != highMin) + { + if (0 == rnd.Next(0, 1)) + { + surrogateStr += Convert.ToChar(rnd.Next(highMin, highMax)); + surrogateStr += Convert.ToChar(rnd.Next(lowMin, lowMax)); + } + else + { + surrogateStr += TextUtil.IntToString(rnd.Next(surrogateRange.StartOfUnicodeRange, surrogateRange.EndOfUnicodeRange)); + } + } + else if (0 != highMin) + { + surrogateStr += Convert.ToChar(rnd.Next(highMin, highMax)); + surrogateStr += Convert.ToChar(rnd.Next(lowMin, lowMax)); + } + else + { + surrogateStr += TextUtil.IntToString(rnd.Next(surrogateRange.StartOfUnicodeRange, surrogateRange.EndOfUnicodeRange)); + } + } + + return surrogateStr; + } + } +} + diff --git a/src/dotnetCampus.UITest.WPFTestHelper/Text/TextNormalizationProperty.cs b/src/dotnetCampus.UITest.WPFTestHelper/Text/TextNormalizationProperty.cs new file mode 100644 index 0000000..f09b56a --- /dev/null +++ b/src/dotnetCampus.UITest.WPFTestHelper/Text/TextNormalizationProperty.cs @@ -0,0 +1,330 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; + +namespace dotnetCampus.UITest.WPFTestHelper.Text +{ + /// + /// TextNormalization property + /// + internal class TextNormalizationProperty : IStringProperty + { + /// + /// Dictionary to store code points corresponding to culture. + /// + private Dictionary textNormalizationPropertyDictionary = new Dictionary(); + + private List textNormalizationRangeList = new List(); + + private int [] codePointsWithDifferentNormalizationForms; + + /// + /// Define minimum code point needed to be a text normalization string + /// + public static readonly int MINNUMOFCODEPOINT = 1; + + /// + /// Define SurrogatePairDictionary class + /// Newline + /// Newline + /// + public TextNormalizationProperty(UnicodeRangeDatabase unicodeDb, Collection expectedRanges) + { + bool isValid = false; + + foreach (UnicodeRange range in expectedRanges) + { + if (RangePropertyCollector.BuildPropertyDataList( + unicodeDb, + range, + textNormalizationRangeList, + "Latin", + GroupAttributes.Name)) + { + isValid = true; + } + if (RangePropertyCollector.BuildPropertyDataList( + unicodeDb, + range, + textNormalizationRangeList, + "CJK Unified Ideographs (Han)", + GroupAttributes.Name)) + { + isValid = true; + } + if (RangePropertyCollector.BuildPropertyDataList( + unicodeDb, + range, + textNormalizationRangeList, + "CJK Compatibility Ideographs", + GroupAttributes.Name)) + { + isValid = true; + } + if (RangePropertyCollector.BuildPropertyDataList( + unicodeDb, + range, + textNormalizationRangeList, + "Katakana", + GroupAttributes.Name)) + { + isValid = true; + } + if (RangePropertyCollector.BuildPropertyDataList( + unicodeDb, + range, + textNormalizationRangeList, + "Hangul Jamo", + GroupAttributes.Name)) + { + isValid = true; + } + if (RangePropertyCollector.BuildPropertyDataList( + unicodeDb, + range, + textNormalizationRangeList, + "Hangul Syllables", + GroupAttributes.Name)) + { + isValid = true; + } + if (RangePropertyCollector.BuildPropertyDataList( + unicodeDb, + range, + textNormalizationRangeList, + "Arabic", + GroupAttributes.Name)) + { + isValid = true; + } + if (RangePropertyCollector.BuildPropertyDataList( + unicodeDb, + range, + textNormalizationRangeList, + "Greek", + GroupAttributes.Name)) + { + isValid = true; + } + } + + + if (InitializeTextNormalizationPropertyDictionary(expectedRanges)) + { + isValid = true; + } + + if (!isValid) + { + throw new ArgumentOutOfRangeException("expectedRanges", "TextNormalizationProperty, " + + "code points for text normalization ranges are beyond expected range. " + "Refert to Latin, CJK Unified Ideographs (Han) " + + "CJK Compatibility Ideographs, Katakana, Hangul Jamo, Hangul Syllables, Arabic, and Greek ranges."); + } + } + + /// + /// Dictionary to store code points corresponding to culture. + /// + private bool InitializeTextNormalizationPropertyDictionary(Collection expectedRanges) + { + int [] othersymbols = {0xFFE4, 0x21CD, 0xFFE8, 0xFFED, 0xFFEE, 0x3036, 0x1D15E, 0x1D15F, 0x1D160, 0x1D161, 0x1D162, + 0x1D163, 0x1D164, 0x1D1BB, 0x1D1BD, 0x1D1BF, 0x1D1BC, 0x1D1BE, 0x1D1C0}; + textNormalizationPropertyDictionary.Add("othersymbols", othersymbols); + + int [] modifiersymbols = {0x00B4, 0x0384, 0x1FFD, 0x02DC, 0x00AF, 0xFFE3, 0x02D8, 0x02D9, 0x00A8, 0x1FED, 0x0385, + 0x1FEE, 0x1FC1, 0x02DA, 0x02DD, 0x1FBD, 0x1FBF, 0x1FCD, 0x1FCE, 0x1FCF, 0x1FFE, 0x1FDD, 0x1FDE, 0x1FDF, 0x00B8, + 0x02DB, 0x1FC0, 0x309B, 0x309C, 0xFF3E, 0x1FEF, 0xFF40}; + textNormalizationPropertyDictionary.Add("modifiersymbols", modifiersymbols); + + int [] currencysymbols = {0xFE69, 0xFF04, 0xFFE0, 0xFFE1, 0xFFE5, 0xFFE6}; + textNormalizationPropertyDictionary.Add("currencysymbols", currencysymbols); + + int [] mathsymbols = {0x207A, 0x208A, 0xFB29, 0xFE62, 0xFF0B, 0x2A74, 0xFE64, 0xFF1C, 0x226E, 0x207C, 0x208C, 0xFE66, + 0xFF1D, 0x2A75, 0x2A76, 0x2260, 0xFE65, 0xFF1E, 0x226F, 0xFF5C, 0xFF5E, 0xFFE2, 0xFFE9, 0x219A, 0xFFEA, 0xFFEB, + 0x219B, 0xFFEC, 0x21AE, 0x21CF, 0x21CE, 0x1D6DB, 0x1D715, 0x1D74F, 0x1D789, 0x1D7C3, 0x2204, 0x1D6C1, 0x1D6FB, + 0x1D735, 0x1D76F, 0x1D7A9, 0x2209, 0x220C, 0x2140, 0x207B, 0x208B, 0x2224, 0x2226, 0x222C, 0x222D, 0x2A0C, 0x222F, + 0x2230, 0x2241, 0x2244, 0x2247, 0x2249, 0x226D, 0x2262, 0x2270, 0x2271, 0x2274, 0x2275, 0x2278, 0x2279, 0x2280, + 0x2281, 0x22E0, 0x22E1, 0x2284, 0x2285, 0x2288, 0x2289, 0x22E2, 0x22E3, 0x22AC, 0x22AD, 0x22AE, 0x22AF, 0x22EA, + 0x22EB, 0x22EC, 0x22ED, 0x2ADC}; + textNormalizationPropertyDictionary.Add("mathsymbols", mathsymbols); + + int [] modifierletter = {0x037A, 0x0374, 0xFF9E, 0xFF9F, 0xFF70}; + textNormalizationPropertyDictionary.Add("modifierletter", modifierletter); + + int [] otherletter = {0xFE70, 0xFE72, 0xFC5E, 0xFE74, 0xFC5F, 0xFE76, 0xFC60, 0xFE78, 0xFC61, 0xFE7A, 0xFC62, 0xFE7C, + 0xFC63, 0xFE7E, 0xFE71, 0xFE77, 0xFCF2, 0xFE79, 0xFCF3, 0xFE7B, 0xFCF4, 0xFE7D, 0xFE7F}; + textNormalizationPropertyDictionary.Add("otherletter", otherletter); + + int [] nonspacingmark = {0x0340, 0x0341, 0x0344, 0x0343}; + textNormalizationPropertyDictionary.Add("nonspacingmark", nonspacingmark); + + int [] spaceseparator = {0x00A0, 0x2000, 0x2001, 0x2002, 0x2003, 0x2004, 0x2005, 0x2006, 0x2007, 0x2008, 0x2009, 0x200A, + 0x202F, 0x205F, 0x3000}; + textNormalizationPropertyDictionary.Add("spaceseparator", spaceseparator); + + int [] decimalnumber = {0xFF10, 0x1D7CE, 0x1D7D8, 0x1D7E2, 0x1D7EC, 0x1D7F6, 0xFF11, 0x1D7CF, 0x1D7D9, 0x1D7E3, 0x1D7ED, + 0x200A, 0x1D7F7, 0xFF12, 0x1D7D0, 0x1D7DA, 0x1D7E4, 0x1D7EE, 0x1D7F8, 0xFF13, 0x1D7D1, 0x1D7DB, 0x1D7E5, 0x1D7EF, + 0x1D7F9,0xFF14, 0x1D7D2, 0x1D7DC, 0x1D7E6, 0x1D7F0, 0x1D7FA, 0xFF15, 0x1D7D3, 0x1D7DD, 0x1D7E7, 0x1D7F1, 0x1D7FB, + 0xFF16, 0x1D7D4, 0x1D7DE, 0x1D7E8, 0x1D7F2, 0x1D7FC, 0xFF17, 0x1D7D5, 0x1D7DF, 0x1D7E9, 0x1D7F3, 0x1D7FD, 0xFF18, + 0x1D7D6, 0x1D7E0, 0x1D7EA, 0x1D7F4, 0x1D7FE, 0xFF19, 0x1D7D7, 0x1D7E1, 0x1D7EB, 0x1D7F5, 0x1D7FF}; + textNormalizationPropertyDictionary.Add("decimalnumber", decimalnumber); + + int [] othernumber = {0x2474, 0x247D, 0x247E, 0x247F, 0x2480, 0x2481, 0x2482, 0x2483, 0x2484, 0x2485, 0x2486, 0x2475, 0x2487, 0x2476, + 0x2477, 0x2478, 0x2479, 0x247A, 0x247B, 0x247C, 0x2070, 0x2080, 0x24EA, 0x1F101, 0x1F100, 0x2189,0x00B9, 0x2081, 0x2460, 0x1F102, + 0x2488, 0x2469, 0x2491, 0x246A, 0x2492, 0x246B, 0x2493, 0x246C, 0x2494, 0x246D, 0x2495, 0x246E, 0x2496, 0x246F, 0x2497, 0x2470, + 0x2498, 0x2499, 0x2472, 0x249A, 0x215F, 0x2152, 0x00BD, 0x2153, 0x00BC, 0x2155, 0x2159, 0x2150, 0x215B, 0x2151, 0x00B2, 0x2082, + 0x2461, 0x1F103, 0x2489, 0x2473, 0x249B, 0x3251, 0x3252, 0x3253, 0x3254, 0x3255, 0x3256, 0x3257, 0x3258, 0x3259, 0x2154, 0x2156, + 0x00B3, 0x2083, 0x2462, 0x1F104, 0x248A, 0x325A, 0x325B, 0x325C, 0x325D, 0x325E, 0x325F, 0x32B1, 0x32B2, 0x32B3, 0x32B4, 0x00BE, + 0x2157, 0x215C, 0x2074, 0x2084, 0x2463, 0x1F105, 0x248B, 0x32B5, 0x32B6, 0x32B7, 0x32B8, 0x32B9, 0x32BA, 0x32BB, 0x32BC, 0x32BD, + 0x32BE, 0x2158, 0x2075, 0x2085, 0x2464, 0x1F106, 0x248C, 0x32BF, 0x215A, 0x215D, 0x2076, 0x2086, 0x2465, 0x1F107, 0x248D, 0x2077, + 0x2087, 0x2466, 0x1F108, 0x248E, 0x215E, 0x2078, 0x2088, 0x2467, 0x1F109, 0x248F, 0x2079, 0x2089, 0x2468, 0x1F10A, 0x2490}; + textNormalizationPropertyDictionary.Add("othernumber", othernumber); + + int [] kaithi = {0x1109A, 0x1109C, 0x110AB}; + textNormalizationPropertyDictionary.Add("kaithi", kaithi); + + int [] balinese = {0x1B06, 0x1B08, 0x1B0A, 0x1B0C, 0x1B0E, 0x1B12, 0x1B3B, 0x1B3D, 0x1B40, 0x1B41, 0x1B43}; + textNormalizationPropertyDictionary.Add("balinese", balinese); + + int [] tifinagh = {0x2D6F}; + textNormalizationPropertyDictionary.Add("tifinagh", tifinagh); + + int [] hiragana = {0x3094, 0x304C, 0x304E, 0x3050, 0x3052, 0x3054, 0x3056, 0x3058, 0x305A, 0x305C, 0x305E, 0x3060, 0x3062, 0x3065, + 0x3067, 0x3069, 0x3070, 0x3071, 0x3073, 0x3074, 0x3076, 0x3077, 0x3079, 0x307A, 0x1F200, 0x307C, 0x307D, 0x309F, 0x309E}; + textNormalizationPropertyDictionary.Add("hiragana", hiragana); + + int [] georgian = {0x10FC}; + textNormalizationPropertyDictionary.Add("georgian", georgian); + + int [] myanmar = {0x1026}; + textNormalizationPropertyDictionary.Add("myanmar", myanmar); + + int [] tibetan = {0x0F0C, 0x0F69, 0x0F43, 0x0F4D, 0x0F52, 0x0F57, 0x0F5C, 0x0F73, 0x0F75, 0x0F81, 0x0FB9, 0x0F93, 0x0F9D, 0x0FA2, 0x0FA7, + 0x0FAC, 0x0F77, 0x0F76, 0x0F79, 0x0F78}; + textNormalizationPropertyDictionary.Add("tibetan", tibetan); + + int [] lao = {0x0EDC, 0x0EDD, 0x0EB3}; + textNormalizationPropertyDictionary.Add("lao", lao); + + int [] th = {0x0E33}; + textNormalizationPropertyDictionary.Add("th", th); + + int [] sinhala = {0x0DDA, 0x0DDC, 0x0DDD, 0x0DDE}; + textNormalizationPropertyDictionary.Add("sinhala", sinhala); + + int [] malayalam = {0x0D4A, 0x0D4C, 0x0D4B}; + textNormalizationPropertyDictionary.Add("malayalam", malayalam); + + int [] kannada = {0x0CC0, 0x0CCA, 0x0CCB, 0x0CC7, 0x0CC8}; + textNormalizationPropertyDictionary.Add("kannada", kannada); + + int [] telugu = {0x0C48}; + textNormalizationPropertyDictionary.Add("telugu", telugu); + + int [] ta = {0x0B94, 0x0BCA, 0x0BCC, 0x0BCB}; + textNormalizationPropertyDictionary.Add("ta", ta); + + int [] oriya = {0x0B5C, 0x0B5D, 0x0B4B, 0x0B48, 0x0B4C}; + textNormalizationPropertyDictionary.Add("oriya", oriya); + + int [] gurmukhi = {0x0A59, 0x0A5A, 0x0A5B, 0x0A5E, 0x0A33, 0x0A36}; + textNormalizationPropertyDictionary.Add("gurmukhi", gurmukhi); + + int [] bengali = {0x09DC, 0x09DD, 0x09DF, 0x09CB, 0x09CC}; + textNormalizationPropertyDictionary.Add("bengali", bengali); + + int [] devanagari = {0x0958, 0x0959, 0x095A, 0x095B, 0x095C, 0x095D, 0x0929, 0x095E, 0x095E, 0x0931, 0x0934}; + textNormalizationPropertyDictionary.Add("devanagari", devanagari); + + int [] he = {0x2135, 0xFB21, 0xFB2E, 0xFB2F, 0xFB30, 0xFB4F, 0x2136, 0xFB31, 0xFB4C, 0x2137, 0xFB32, 0x2138, 0xFB22, 0xFB33, 0xFB23, 0xFB34, + 0xFB4B, 0xFB35, 0xFB36, 0xFB38, 0xFB1D, 0xFB39, 0xFB3A, 0xFB24, 0xFB3B, 0xFB4D, 0xFB25, 0xFB3C, 0xFB26, 0xFB3E, 0xFB40, 0xFB41, 0xFB20, + 0xFB43, 0xFB44, 0xFB4E, 0xFB46, 0xFB47, 0xFB27, 0xFB48, 0xFB49, 0xFB2C, 0xFB2D, 0xFB2D, 0xFB2B, 0xFB28, 0xFB4A, 0xFB1F}; + textNormalizationPropertyDictionary.Add("he", he); + + int [] hy = {0x0587, 0xFB14, 0xFB15, 0xFB17, 0xFB13, 0xFB16}; + textNormalizationPropertyDictionary.Add("hy", hy); + + int [] cyrillic = {0x04D0, 0x04D1, 0x04D2, 0x04D3, 0x0403, 0x0453, 0x0400, 0x0450, 0x04D6, 0x04D7, 0x0401, 0x0451, 0x04C1, 0x04C2, 0x04DC, + 0x04DD, 0x04DE, 0x04DF, 0x040D, 0x045D, 0x04E2, 0x04E3, 0x0419, 0x0439, 0x04E4, 0x04E5, 0x040C, 0x045C, 0x1D78, 0x04E6, 0x04E7, 0x04EE, + 0xFB20, 0x04EF, 0x040E, 0x045E, 0x04F0, 0x04F1, 0x04F2, 0x04F3, 0x04F4, 0x04F5, 0x04F6, 0x04F7, 0x04F8, 0x04F9, 0x04EC, 0x04ED, 0x0407, + 0x0457, 0x0476, 0x0477, 0x04DA, 0x04DB, 0x04EA, 0x04EB}; + textNormalizationPropertyDictionary.Add("cyrillic", cyrillic); + + int i = 0; + bool isValid = false; + codePointsWithDifferentNormalizationForms = new int [othersymbols.Length + modifiersymbols.Length + currencysymbols.Length + mathsymbols.Length + + modifierletter.Length + otherletter.Length + nonspacingmark.Length + spaceseparator.Length + decimalnumber.Length + othernumber.Length + + kaithi.Length + balinese.Length + tifinagh.Length + hiragana.Length + georgian.Length + myanmar.Length + tibetan.Length + lao.Length + + th.Length + sinhala.Length + malayalam.Length + kannada.Length + telugu.Length + ta.Length + oriya.Length + gurmukhi.Length + bengali.Length + + devanagari.Length + he.Length + hy.Length + cyrillic.Length]; + + Dictionary.ValueCollection valueColl = textNormalizationPropertyDictionary.Values; + foreach (int [] values in valueColl) + { + foreach (int codePoint in values) + { + foreach (UnicodeRange range in expectedRanges) + { + if (codePoint >= range.StartOfUnicodeRange && codePoint <= range.EndOfUnicodeRange) + { + codePointsWithDifferentNormalizationForms[i++] = codePoint; + isValid = true; + } + } + } + } + Array.Resize(ref codePointsWithDifferentNormalizationForms, i); + Array.Sort(codePointsWithDifferentNormalizationForms); + return isValid; + } + + /// + /// Check if code point is in the property range + /// + public bool IsInPropertyRange(int codePoint) + { + bool isIn = false; + foreach (UnicodeRangeProperty prop in textNormalizationRangeList) + { + if (codePoint >= prop.Range.StartOfUnicodeRange && codePoint <= prop.Range.EndOfUnicodeRange) + { + isIn = true; + break; + } + } + + return isIn; + } + + /// + /// Get random normalizeable code points + /// + public string GetRandomCodePoints(int numOfProperty, int seed) + { + if (numOfProperty < 1) + { + throw new ArgumentOutOfRangeException( + "TextNormalizationProperty, numOfProperty, " + numOfProperty + " cannot be less than one."); + } + + string textNormalizationStr = string.Empty; + string numStr = string.Empty; + Random rand = new Random(seed); + for (int i= 0; i < numOfProperty; i++) + { + int index = rand.Next(0, codePointsWithDifferentNormalizationForms.Length); + textNormalizationStr += TextUtil.IntToString(codePointsWithDifferentNormalizationForms[index]); + } + + return textNormalizationStr; + } + } +} + + + + diff --git a/src/dotnetCampus.UITest.WPFTestHelper/Text/TextSegmentationProperty.cs b/src/dotnetCampus.UITest.WPFTestHelper/Text/TextSegmentationProperty.cs new file mode 100644 index 0000000..5878098 --- /dev/null +++ b/src/dotnetCampus.UITest.WPFTestHelper/Text/TextSegmentationProperty.cs @@ -0,0 +1,247 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; + +namespace dotnetCampus.UITest.WPFTestHelper.Text +{ + /// + /// Collect of text segmentation code points + /// + internal class TextSegmentationProperty : IStringProperty + { + /// + /// Dictionary to store code points corresponding to culture. + /// + private Dictionary sampleGraphemeClusterDictionary = new Dictionary(); + private Dictionary graphemeClusterBreakPropertyValuesDictionary = new Dictionary(); + private Dictionary wordBreakPropertyValuesDictionary = new Dictionary(); + private Dictionary sentenceBreakPropertyValuesDictionary = new Dictionary(); + + private List textSegmentationRangeList = new List(); + + private int [] textSegmentationCodePoints; + + /// + /// Define minimum code point needed to be a text segmentation string + /// + public static readonly int MINNUMOFCODEPOINT = 1; + + /// + /// Define LineBreakDictionary class, + /// Newline + /// + public TextSegmentationProperty(UnicodeRangeDatabase unicodeDb, Collection expectedRanges) + { + bool isValid = false; + + foreach (UnicodeRange range in expectedRanges) + { + if (RangePropertyCollector.BuildPropertyDataList( + unicodeDb, + range, + textSegmentationRangeList, + "Controls", + GroupAttributes.Name)) + { + isValid = true; + } + } + + if (InitializeDictionaries(expectedRanges)) + { + isValid = true; + } + + if (!isValid) + { + throw new ArgumentOutOfRangeException("expectedRanges", "TextSegmentationProperty, " + + "code points for text segmentation ranges are beyond expected range. " + "Refert to Controls ranges"); + } + } + + private bool InitializeDictionaries(Collection expectedRanges) + { + char [] ko = {'\u1100', '\u1161', '\u11A8'}; + sampleGraphemeClusterDictionary.Add("Ko", ko); + char [] ta = {'\u0BA8', '\u0BBF'}; + sampleGraphemeClusterDictionary.Add("ta", ta); + char [] th = {'\u0E40', '\u0E01'}; + sampleGraphemeClusterDictionary.Add("th", th); + char [] devanagari = {'\u0937', '\u093F', '\u0915', '\u094D', '\u0937', '\u093F'}; + sampleGraphemeClusterDictionary.Add("devanagari", devanagari); + char [] sk = {'\u0063', '\u0068'}; + sampleGraphemeClusterDictionary.Add("sk", sk); + char [] other = {'\u0067', '\u0308', '\u006B', '\u02B7'}; + sampleGraphemeClusterDictionary.Add("other", other); + + char [] all = {'\u000D', '\u000A', '\u0000', '\u0001', '\u0002', '\u0003', '\u0004', '\u0005', '\u0006', '\u0007', '\u0008', '\u0009', '\u000B', + '\u000C', '\u000E', '\u000F', '\u0010', '\u0011', '\u0012', '\u0013', '\u0014', '\u0015', '\u0016', '\u0017', '\u0018', '\u0019', '\u001A', + '\u001B', '\u001C', '\u001D', '\u001E', '\u001F', '\u0020', '\u007F', '\u0080', '\u0081', '\u0082', '\u0083', '\u0084', '\u0085', '\u0086', + '\u0087', '\u0088', '\u0089', '\u008A', '\u008B', '\u008C', '\u008D', '\u008E', '\u008F', '\u0090', '\u0091', '\u0092', '\u0093', '\u0094', + '\u0095', '\u0096', '\u0097', '\u0098', '\u0099', '\u009A', '\u009B', '\u009C', '\u009D', '\u009E', '\u009F', '\u00A0', '\u00AD', '\u2000', + '\u2001', '\u2002', '\u2003', '\u2004', '\u2005', '\u2006', '\u2007', '\u2008', '\u2009', '\u200A'}; + graphemeClusterBreakPropertyValuesDictionary.Add("all", all); + char [] th1 = {'\u0E30', '\u0E32', '\u0E33', '\u0E40', '\u0E41', '\u0E42', '\u0E43', '\u0E44', '\u0E45'}; + graphemeClusterBreakPropertyValuesDictionary.Add("th", th1); + char [] lao = {'\u0EB0', '\u0EB2', '\u0EB3', '\u0EC0', '\u0EC1', '\u0EC2', '\u0EC3', '\u0EC4'}; + graphemeClusterBreakPropertyValuesDictionary.Add("lao", lao); + char [] ko1 = {'\u1100', '\u1101', '\u1102', '\u1103', '\u1104', '\u1105', '\u1106', '\u1107', '\u1108', '\u1109', '\u110A', '\u110B', '\u110C', + '\u110D', '\u110E', '\u110F', '\u1110', '\u1111', '\u1112', '\u1113', '\u1114', '\u1115', '\u1116', '\u1117', '\u1118', '\u1119', '\u111A', + '\u111B', '\u111C', '\u111D', '\u111E', '\u111F', '\u1120', '\u1121', '\u1122', '\u1123', '\u1124', '\u1125', '\u1126', '\u1127', '\u1128', + '\u1129', '\u112A', '\u112B', '\u112C', '\u112D', '\u112E', '\u112F', '\u1130', '\u1131', '\u1132', '\u1133', '\u1134', '\u1135', '\u1136', + '\u1137', '\u1138', '\u1139', '\u1140', '\u1141', '\u1142', '\u1143', '\u1144', '\u1145', '\u1146', '\u1147', '\u1148', '\u1149', '\u114A', + '\u114B', '\u114C', '\u114D', '\u114E', '\u114F', '\u1150', '\u1151', '\u1152', '\u1153', '\u1154', '\u1155', '\u1156', '\u1157', '\u1158', + '\u1159', '\u111F', '\u1160', '\u1161', '\u1162', '\u1163', '\u1164', '\u1165', '\u1166', '\u1167', '\u1168', '\u1169', '\u116A', '\u116B', + '\u116C', '\u116D', '\u116E', '\u116F', '\u1170', '\u1171', '\u1172', '\u1173', '\u1174', '\u1175', '\u1176', '\u1177', '\u1178', '\u1179', + '\u117A', '\u117B', '\u117C', '\u117D', '\u117E', '\u117F', '\u1180', '\u1181', '\u1182', '\u1183', '\u1184', '\u1185', '\u1186', '\u1187', + '\u1188', '\u1189', '\u118A', '\u118B', '\u118C', '\u118D', '\u118E', '\u118F', '\u1190', '\u1191', '\u1192', '\u1193', '\u1194', '\u1195', + '\u1196', '\u1197', '\u1198', '\u1199', '\u119A', '\u119B', '\u119C', '\u119E', '\u119F', '\u11A0', '\u11A1', '\u11A2', '\u11A8', '\u11A9', + '\u11AA', '\u11AB', '\u11AC', '\u11AD', '\u11AE', '\u11AF', '\u11B0', '\u11B1', '\u11B2', '\u11B3', '\u11B4', '\u11B5', '\u11B6', '\u11B7', + '\u11B8', '\u11B9', '\u11BA', '\u11BB', '\u11BC', '\u11BD', '\u11BE', '\u11BF', '\u11C0', '\u11C1', '\u11C2', '\u11C3', '\u11C4', '\u11C5', + '\u11C6', '\u11C7', '\u11C8', '\u11C9', '\u11CA', '\u11CB', '\u11CC', '\u11CE', '\u11CF', '\u11D0', '\u11D1', '\u11D2', '\u11D3', '\u11D4', + '\u11D5', '\u11D6', '\u11D7', '\u11D8', '\u11D9', '\u11DA', '\u11DB', '\u11DC', '\u11DE', '\u11DF', '\u11F0', '\u11F1', '\u11F2', '\u11F3', + '\u11F4', '\u11F5', '\u11F7', '\u11F8', '\u11F9', '\uAC00', '\uAC1C', '\uAC38', '\uAC01', '\uAC02', '\uAC03', '\uAc04'}; + graphemeClusterBreakPropertyValuesDictionary.Add("ko", ko1); + + char [] all1 = {'\u000A', '\u000D', '\u000B', '\u000C', '\u0020', '\u0027', '\u0085', '\u002D', '\u002E', '\u202F','\u00A0', '\u2028', '\u2029', + '\u2000', '\u2001', '\u2002', '\u2003', '\u2004', '\u2005', '\u2006', '\u2007', '\u2008', '\u2009', '\u200A', '\u2010', '\u2011', '\u2018', + '\u2019', '\u201B', '\u2024', '\uFE52', '\uFF07', '\uFF0E', '\u00B7', '\u05F4', '\u2027', '\u003A', '\u0387', '\uFE13', '\uFE55', '\uFF1A', + '\u066C', '\uFE50', '\uFE54', '\uFE63', '\uFF0D', '\uFF0C', '\uFF1B'}; + wordBreakPropertyValuesDictionary.Add("all", all1); + char [] katakana = {'\u3031', '\u3032', '\u3033', '\u3034', '\u3035', '\u309B', '\u309C', '\u30A0', '\u30FC', '\uFF70'}; + wordBreakPropertyValuesDictionary.Add("ja", katakana); + char [] he = {'\u05F3'}; + wordBreakPropertyValuesDictionary.Add("he", he); + char [] hy = {'\u055A', '\u058A'}; + wordBreakPropertyValuesDictionary.Add("hy", hy); + char [] tibet = {'\u0F0B'}; + wordBreakPropertyValuesDictionary.Add("tibet", tibet); + char [] mongolia = {'\u1806'}; + wordBreakPropertyValuesDictionary.Add("mongolia", mongolia); + + char [] all2 = {'\u000A', '\u000D', '\u0085', '\u00A0', '\u05F3', '\u2000', '\u2001', '\u2002', '\u2003', '\u2004', '\u2005', '\u2006', '\u2007', + '\u2008', '\u2009', '\u200A', '\u2028', '\u2029', '\u002E', '\u2024', '\uFE52', '\uFF0E', '\u002D', '\u003A', '\u055D', '\u060C', '\u060D', + '\u07F8', '\u1802', '\u1808', '\u2013', '\u2014', '\u3001', '\uFE10', '\uFE11', '\uFE13', '\uFE31', '\uFE32', '\uFE50', '\uFE51', '\uFE55', + '\uFE58', '\uFE63', '\uFF0C', '\uFF0D', '\uFF1A', '\uFF64'}; + sentenceBreakPropertyValuesDictionary.Add("all", all2); + + bool isValid = false; + int i = 0; + textSegmentationCodePoints = new int [ko.Length + ta.Length + th.Length + devanagari.Length + sk.Length + other.Length + all.Length + th1.Length + + lao.Length + ko1.Length + all1.Length + katakana.Length + he.Length + hy.Length + tibet.Length + mongolia.Length + all2.Length]; + + Dictionary.ValueCollection valueColl1 = sampleGraphemeClusterDictionary.Values; + foreach (char [] values in valueColl1) + { + foreach (char codePoint in values) + { + foreach (UnicodeRange range in expectedRanges) + { + if (codePoint >= range.StartOfUnicodeRange && codePoint <= range.EndOfUnicodeRange) + { + textSegmentationCodePoints[i++] = (int)codePoint; + isValid = true; + } + } + } + } + + Dictionary.ValueCollection valueColl2 = graphemeClusterBreakPropertyValuesDictionary.Values; + foreach (char [] values in valueColl2) + { + foreach (char codePoint in values) + { + foreach (UnicodeRange range in expectedRanges) + { + if (codePoint >= range.StartOfUnicodeRange && codePoint <= range.EndOfUnicodeRange) + { + textSegmentationCodePoints[i++] = (int)codePoint; + isValid = true; + } + } + } + } + + Dictionary.ValueCollection valueColl3 = wordBreakPropertyValuesDictionary.Values; + foreach(char [] values in valueColl3) + { + foreach (char codePoint in values) + { + foreach (UnicodeRange range in expectedRanges) + { + if (codePoint >= range.StartOfUnicodeRange && codePoint <= range.EndOfUnicodeRange) + { + textSegmentationCodePoints[i++] = (int)codePoint; + isValid = true; + } + } + } + } + + Dictionary.ValueCollection valueColl4 = sentenceBreakPropertyValuesDictionary.Values; + foreach(char [] values in valueColl4) + { + foreach (char codePoint in values) + { + foreach (UnicodeRange range in expectedRanges) + { + if (codePoint >= range.StartOfUnicodeRange && codePoint <= range.EndOfUnicodeRange) + { + textSegmentationCodePoints[i++] = (int)codePoint; + isValid = true; + } + } + } + } + Array.Resize(ref textSegmentationCodePoints, i); + Array.Sort(textSegmentationCodePoints); + + return isValid; + } + + /// + /// Check if code point is in the property range + /// + public bool IsInPropertyRange(int codePoint) + { + bool isIn = false; + foreach (UnicodeRangeProperty prop in textSegmentationRangeList) + { + if (codePoint >= prop.Range.StartOfUnicodeRange && codePoint <= prop.Range.EndOfUnicodeRange) + { + isIn = true; + break; + } + } + + return isIn; + } + + /// + /// Get number code points + /// + public string GetRandomCodePoints(int numOfProperty, int seed) + { + if (numOfProperty < 1) + { + throw new ArgumentOutOfRangeException( + "TextSegmentationProperty, numOfProperty, " + numOfProperty + " cannot be less than one."); + } + + string textSegmentationStr = string.Empty; + Random rand = new Random(seed); + for (int i= 0; i < numOfProperty; i++) + { + int index = rand.Next(0, textSegmentationCodePoints.Length); + textSegmentationStr += TextUtil.IntToString(textSegmentationCodePoints[index]); + } + + return textSegmentationStr; + } + } +} + diff --git a/src/dotnetCampus.UITest.WPFTestHelper/Text/TextUtil.cs b/src/dotnetCampus.UITest.WPFTestHelper/Text/TextUtil.cs new file mode 100644 index 0000000..a2243ba --- /dev/null +++ b/src/dotnetCampus.UITest.WPFTestHelper/Text/TextUtil.cs @@ -0,0 +1,424 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Globalization; + +namespace dotnetCampus.UITest.WPFTestHelper.Text +{ + /// + /// A collection of const, enum, and struc declaration and static utility function + /// + internal static class TextUtil + { + /// + /// Define number of scripts in Unicode + /// Newline + /// + public static readonly int NUMOFSCRIPTS = 103; + + /// + /// Define number of symbols and punctuation in Unicode + /// Newline + /// + public static readonly int NUMOFSYMBOLSANDPUNCTUATION = 44; + + /// + /// Maximum Unicode Point value + /// Newline + /// + public static readonly int MaxUnicodePoint = 0x10FFFF; + + /// + /// Maximum number of code points defined for a string to be generated + /// + public static readonly int MAXNUMOFCODEPOINT = 300; + + /// + /// Maximum number iteration used in while loop to guard from infinite loops. + /// + public static readonly int MAXNUMITERATION = 128; + + /// + /// Defined ids to help identifying which ----/region where the script or symbol is used. If id is defined in LCID, + /// LCID is used. Otherwise, spell the full name in lower case. '-' is used to omitted. + /// Newline + /// + public enum CultureIds + { + /// + /// Null - don't care + /// + Null = 0, + + /// + /// Can be used in any content + /// + any, + + /// + /// Arabic ---- + /// + ar, + + /// + /// Azerbaijani - Cyrillic + /// + azaz, + + /// + /// Bangladesh + /// + bangladesh, + + /// + /// Canada + /// + ca, + + /// + /// Cambodia + /// + cambodia, + + /// + /// Cameroon + /// + cameroon, + + /// + /// Carians + /// + carians, + + /// + /// Cuneiform + /// + cuneiform, + + /// + /// Cyprus + /// + cyprus, + + /// + /// German + /// + de, + + /// + /// Egypt + /// + eg, + + /// + /// Greece + /// + el, + + /// + /// English speaking ---- + /// + en, + + /// + /// Ethiopia + /// + ethiopia, + + /// + /// Georgia + /// + georgia, + + /// + /// Glagolitsa + /// + glagolitsa, + + /// + /// Israel + /// + he, + + /// + /// India + /// + hi, + + /// + /// Armenia + /// + hy, + + /// + /// Indonesia + /// + id, + + /// + /// Ireland + /// + ie, + + /// + /// Iran /Percian + /// + iran, + + /// + /// Japan + /// + ja, + + /// + /// Kharoshthi + /// + kharoshthi, + + /// + /// Korea + /// + ko, + + /// + /// Lao + /// + lao, + + /// + /// Latin + /// + latin, + + /// + /// Lycian + /// + lycia, + + /// + /// Maldives + /// + maldives, + + /// + /// Monglian + /// + mongolia, + + /// + /// Myanmar + /// + myanmar, + + /// + /// Nepal + /// + nepal, + + /// + /// N'KO + /// + nko, + + /// + /// Turkic ancient form + /// + oldturkic, + + /// + /// Not classified + /// + other, + + /// + /// Phillippines + /// + ph, + + /// + /// Phaistos + /// + phaistosdisc, + + /// + /// Phoenician + /// + phoenicia, + + /// + /// Samaritan + /// + samaria, + + /// + /// Singapore + /// + singapore, + + /// + /// Somalia + /// + somalia, + + /// + /// Sri Lanka + /// + srilanka, + + /// + /// Serbian - Cyrillic + /// + srsp, + + /// + /// Syloti + /// + sylotinagri, + + /// + /// Syriac scripts + /// + syriac, + + /// + /// Thailand + /// + th, + + /// + /// Tifinagh + /// + tifinagh, + + /// + /// US native languages + /// + us, + + /// + /// Uzbek - Cyrillic + /// + uzuz, + + /// + /// Vai + /// + vai, + + /// + /// Vietnam + /// + vi, + + /// + /// Chinese + /// + zh, + + /// + /// Chinese ---- + /// + zhtw + } + + /// + /// Unicode character code chart types + /// + public enum UnicodeChartType + { + /// + /// Unicode character code chart types is Script + /// + Script=1, + + /// + /// Unicode character code chart types is Symbol + /// + Symbol, + + /// + /// Unicode character code chart types is Punctuation + /// + Punctuation, + + /// + /// Unicode character code chart types is Other than three types above + /// + Other + } + + /// + /// Get a random Unicode point (points if it is Surrogate) from the given range + /// + public static string GetRandomCodePoint(UnicodeRange range, int iterations, int [] exclusions, int seed) + { + Random rand = new Random(seed); + int codePoint = 0; + string retStr = string.Empty; + + if (null != exclusions) + { + Array.Sort(exclusions); + } + + for (int i=0; i < iterations; i++) + { + codePoint = rand.Next(range.StartOfUnicodeRange, range.EndOfUnicodeRange); + if (null != exclusions) + { + int index = Array.BinarySearch(exclusions, codePoint); + int ctr = 0; + while (index >= 0) + { + codePoint = rand.Next(range.StartOfUnicodeRange, range.EndOfUnicodeRange); + index = Array.BinarySearch(exclusions, codePoint); + ctr ++; + if (MAXNUMITERATION == ctr) + { + throw new ArgumentOutOfRangeException("TextUtil, " + ctr + " loop has been reached. GetRandomCodePoint may have infinite loop." + + " Range " + String.Format(CultureInfo.InvariantCulture, "0x{0:X}", range.StartOfUnicodeRange) + " - " + + String.Format(CultureInfo.InvariantCulture,"0x{0:X}", range.EndOfUnicodeRange) + " are likely excluded "); + } + } + } + + if (codePoint > 0xFFFF) + { + // In case it is surrogate + retStr += Convert.ToChar((codePoint - 0x10000)/0x400 + 0xD800); + retStr += Convert.ToChar((codePoint - 0x10000)%0x400 + 0xDC00); + } + else + { + retStr += Convert.ToChar(codePoint); + } + } + + return retStr; + } + + /// + /// Convert int to string + /// + public static string IntToString(int codePoint) + { + string retStr = string.Empty; + + if (codePoint > 0xFFFF) + { + // In case it is surrogate + retStr += Convert.ToChar((codePoint - 0x10000)/0x400 + 0xD800); + retStr += Convert.ToChar((codePoint - 0x10000)%0x400 + 0xDC00); + } + else + { + retStr += Convert.ToChar(codePoint); + } + + return retStr; + } + } +} + diff --git a/src/dotnetCampus.UITest.WPFTestHelper/Text/UnicodeChart.cs b/src/dotnetCampus.UITest.WPFTestHelper/Text/UnicodeChart.cs new file mode 100644 index 0000000..99bea3b --- /dev/null +++ b/src/dotnetCampus.UITest.WPFTestHelper/Text/UnicodeChart.cs @@ -0,0 +1,1136 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace dotnetCampus.UITest.WPFTestHelper.Text +{ + /// + /// Specifies a Unicode character code chart. + /// + /// + /// + /// The Unicode standard defines a number of different character subsets, which are called + /// Unicode character code charts (or Unicode charts for short). These charts are available + /// on http://unicode.org/charts. The charts divide and categorize + /// all symbols available in the Unicode range (0x0000 - 0x10FFFF) according to their common characteristics. + /// + public enum UnicodeChart + { + /// + /// Additional Arrows Chart + /// + AdditionalArrows, + /// + /// Additional Shapes Chart + /// + AdditionalShapes, + /// + /// Additional Squared Symbols Chart + /// + AdditionalSquaredSymbols, + /// + /// Aegean Numbers Chart + /// + AegeanNumbers, + /// + /// Alphabetic Presentation Forms Chart + /// + AlphabeticPresentationForms, + /// + /// Ancient Greek Musical Notation Chart + /// + AncientGreekMusicalNotation, + /// + /// Ancient Greek Numbers Chart + /// + AncientGreekNumbers, + /// + /// Ancient Symbols Chart + /// + AncientSymbols, + /// + /// APL symbols Chart + /// + AplSymbols, + /// + /// Arabic Chart + /// + Arabic, + /// + /// Arabic Presentation Forms-A Chart + /// + ArabicPresentationFormsA, + /// + /// Arabic Presentation Forms-B Chart + /// + ArabicPresentationFormsB, + /// + /// Arabic Supplement Chart + /// + ArabicSupplement, + /// + /// Aramaic Imperial Chart + /// + AramaicImperial, + /// + /// Armenian Chart + /// + Armenian, + /// + /// Armenian Ligatures Chart + /// + ArmenianLigatures, + /// + /// Arrows Chart + /// + Arrows, + /// + /// ASCII Characters Chart + /// + AsciiCharacters, + /// + /// ASCII Digits Chart + /// + AsciiDigits, + /// + /// ASCII Punctuation Chart + /// + AsciiPunctuation, + /// + /// Same as BMP Chart + /// + AtEndOf, + /// + /// Avestan Chart + /// + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1704")] + Avestan, + /// + /// Balinese Chart + /// + Balinese, + /// + /// Bamum Chart + /// + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1704")] + Bamum, + /// + /// Basic operators Division Multiplication Chart + /// + BasicOperatorsDivisionMultiplication, + /// + /// Basic operators Plus Factorial Chart + /// + BasicOperatorsPlusFactorial, + /// + /// Bengali Chart + /// + Bengali, + /// + /// Block Elements Chart + /// + BlockElements, + /// + /// BMP Chart + /// + Bmp, + /// + /// Bopomofo Chart + /// + Bopomofo, + /// + /// Bopomofo Extended Chart + /// + BopomofoExtended, + /// + /// Box Drawing Chart + /// + BoxDrawing, + /// + /// Braille Patterns Chart + /// + BraillePatterns, + /// + /// Buginese Chart + /// + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1704")] + Buginese, + /// + /// Buhid Chart + /// + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1704")] + Buhid, + /// + /// Byzantine Musical Symbols Chart + /// + ByzantineMusicalSymbols, + /// + /// C0 Chart + /// + C0, + /// + /// C1 Chart + /// + C1, + /// + /// Card suits Chart + /// + CardSuits, + /// + /// Carian Chart + /// + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1704")] + Carian, + /// + /// Cham Chart + /// + Cham, + /// + /// Cherokee Chart + /// + Cherokee, + /// + /// Chess/Checkers Chart + /// + ChessCheckers, + /// + /// CJK Compatibility Chart + /// + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1704")] + CjkCompatibility, + /// + /// CJK Compatibility Forms Chart + /// + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1704")] + CjkCompatibilityForms, + /// + /// CJK Compatibility Ideographs Chart + /// + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1704")] + CjkCompatibilityIdeographs, + /// + /// CJK Compatibility Ideographs Supplement Chart + /// + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1704")] + CjkCompatibilityIdeographsSupplement, + /// + /// CJK ExtensionA Chart + /// + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1704")] + CjkExtensionA, + /// + /// CJK Extension-B Chart + /// + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1704")] + CjkExtensionB, + /// + /// CJK Extension-C Chart + /// + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1704")] + CjkExtensionC, + /// + /// CJK Radicals / KangXi Chart + /// + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1704")] + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1709")] + CjkKangXiRadicals, + /// + /// CJK Radicals Supplement + /// + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1704")] + CjkRadicalsSupplement, + /// + /// CJK Strokes Chart + /// + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1704")] + CjkStrokes, + /// + /// CJK Symbols and Punctuation Chart + /// + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1704")] + CjkSymbolsAndPunctuation, + /// + /// CJK Unified Ideographs Chart + /// + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1704")] + CjkUnifiedIdeographs, + /// + /// Combining Diacritical Marks Chart + /// + CombiningDiacriticalMarks, + /// + /// Combining Diacritical Marks for Symbols Chart + /// + CombiningDiacriticalMarksForSymbols, + /// + /// Combining Diacritical Marks Supplement Chart + /// + CombiningDiacriticalMarksSupplement, + /// + /// Combining HalfMarks Chart + /// + CombiningHalfMarks, + /// + /// Common Indic Number Forms Chart + /// + CommonIndicNumberForms, + /// + /// Control Pictures Chart + /// + ControlPictures, + /// + /// C0 and C1 Chart + /// + Controls, + /// + /// Coptic Chart + /// + Coptic, + /// + /// Coptic in Greek Block Chart + /// + CopticInGreekBlock, + /// + /// Counting Rod Numerals Chart + /// + CountingRodNumerals, + /// + /// Cuneiform Chart + /// + Cuneiform, + /// + /// Cuneiform Numbers and Punctuation Chart + /// + CuneiformNumbersAndPunctuation, + /// + /// Currency Symbols Chart + /// + CurrencySymbols, + /// + /// Cypriot Syllabary Chart + /// + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1704")] + CypriotSyllabary, + /// + /// Cyrillic Chart + /// + Cyrillic, + /// + /// Cyrillic Extended-A Chart + /// + CyrillicExtendedA, + /// + /// Cyrillic Extended-B Chart + /// + CyrillicExtendedB, + /// + /// Cyrillic Supplement Chart + /// + CyrillicSupplement, + /// + /// Deseret Chart + /// + Deseret, + /// + /// Devanagari Chart + /// + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1704")] + Devanagari, + /// + /// Devanagari Extended Chart + /// + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1704")] + DevanagariExtended, + /// + /// Dingbats Chart + /// + Dingbats, + /// + /// Dollar Sign Chart + /// + DollarSign, + /// + /// Domino Tiles Chart + /// + DominoTiles, + /// + /// Egyptian Hieroglyphs Chart + /// + EgyptianHieroglyphs, + /// + /// Enclosed Alphanumerics Chart + /// + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1704")] + EnclosedAlphanumerics, + /// + /// Enclosed Alphanumeric Supplement Chart + /// + EnclosedAlphanumericSupplement, + /// + /// Enclosed CJK Letters and Months Chart + /// + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1704")] + EnclosedCjkLettersAndMonths, + /// + /// Enclosed Ideographic Supplement Chart + /// + EnclosedIdeographicSupplement, + /// + /// Ethiopic Chart + /// + Ethiopic, + /// + /// Ethiopic Extended Chart + /// + EthiopicExtended, + /// + /// Ethiopic Supplement Chart + /// + EthiopicSupplement, + /// + /// Euro Sign Chart + /// + EuroSign, + /// + /// Floors and ceilings + /// + FloorsAndCeilings, + /// + /// Fullwidth ASCII Digits Chart + /// + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1704")] + FullwidthAsciiDigits, + /// + /// Fullwidth ASCII Punctuation Chart + /// + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1704")] + FullwidthAsciiPunctuation, + /// + /// Fullwidth Currency Symbols Chart + /// + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1704")] + FullwidthCurrencySymbols, + /// + /// Fullwidth Latin Letters Chart + /// + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1704")] + FullwidthLatinLetters, + /// + /// Same as Chess/Checkers Chart + /// + GameSymbols, + /// + /// General Punctuation Chart + /// + GeneralPunctuation, + /// + /// Geometric Shapes Chart + /// + GeometricShapes, + /// + /// Georgian Chart + /// + Georgian, + /// + /// Georgian Supplement Chart + /// + GeorgianSupplement, + /// + /// Glagolitic Chart + /// + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1704")] + Glagolitic, + /// + /// Gothic Chart + /// + Gothic, + /// + /// Greek Chart + /// + Greek, + /// + /// Greek Extended Chart + /// + GreekExtended, + /// + /// Gujarati Chart + /// + Gujarati, + /// + /// Gurmukhi Chart + /// + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1704")] + Gurmukhi, + /// + /// Halfwidth and Fullwidth Forms Chart + /// + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1704")] + HalfwidthAndFullwidthForms, + /// + /// Half width Jamo Chart + /// + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1704")] + HalfwidthJamo, + /// + /// Half width Katakana Chart + /// + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1704")] + HalfwidthKatakana, + /// + /// Hangul Compatibility Jamo Chart + /// + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1704")] + HangulCompatibilityJamo, + /// + /// Hangul Jamo Chart + /// + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1704")] + HangulJamo, + /// + /// Hangul Jamo ExtendedA Chart + /// + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1704")] + HangulJamoExtendedA, + /// + /// Hangul Jamo Extended-B Chart + /// + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1704")] + HangulJamoExtendedB, + /// + /// Hangul Syllables Chart + /// + HangulSyllables, + /// + /// Hanunoo Chart + /// + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1704")] + Hanunoo, + /// + /// Hebrew Chart + /// + Hebrew, + /// + /// Hebrew Presentation Forms Chart + /// + HebrewPresentationForms, + /// + /// High Surrogates Chart + /// + HighSurrogates, + /// + /// Hiragana Chart + /// + Hiragana, + /// + /// Ideographic Description Characters Chart + /// + IdeographicDescriptionCharacters, + /// + /// Invisible Operators Chart + /// + InvisibleOperators, + /// + /// IPA Extensions Chart + /// + IpaExtensions, + /// + /// Japanese Chess Chart + /// + JapaneseChess, + /// + /// Javanese Chart + /// + Javanese, + /// + /// Kaithi Chart + /// + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1704")] + Kaithi, + /// + /// Kanbun Chart + /// + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1704")] + Kanbun, + /// + /// Kannada Chart + /// + Kannada, + /// + /// Katakana Chart + /// + Katakana, + /// + /// Katakana Phonetic Extensions Chart + /// + KatakanaPhoneticExtensions, + /// + /// Kayah Li Chart + /// + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1704")] + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1709")] + KayahLi, + /// + /// Kharoshthi Chart + /// + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1704")] + Kharoshthi, + /// + /// Khmer Chart + /// + Khmer, + /// + /// Khmer Symbols Chart + /// + KhmerSymbols, + /// + /// Lao Chart + /// + Lao, + /// + /// Latin Chart + /// + Latin, + /// + /// Latin-1 Punctuation Chart + /// + Latin1Punctuation, + /// + /// Latin-1 Supplement Chart + /// + Latin1Supplement, + /// + /// Latin Extended-A Chart + /// + LatinExtendedA, + /// + /// Latin Extended Additional Chart + /// + LatinExtendedAdditional, + /// + /// Latin Extended-B Chart + /// + LatinExtendedB, + /// + /// Latin Extended-C Chart + /// + LatinExtendedC, + /// + /// Latin Extended-D Chart + /// + LatinExtendedD, + /// + /// Latin Ligatures Chart + /// + LatinLigatures, + /// + /// Layout Controls Chart + /// + LayoutControls, + /// + /// Lepcha Chart + /// + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1704")] + Lepcha, + /// + /// Letterlike Symbols Chart + /// + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1704")] + LetterlikeSymbols, + /// + /// Limbu Chart + /// + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1704")] + Limbu, + /// + /// Linear B Syllabary and Linear B Ideograms Chart + /// + LinearB, + /// + /// Linear B Ideograms Chart + /// + LinearBIdeograms, + /// + /// Linear B Syllabary Chart + /// + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1704")] + LinearBSyllabary, + /// + /// Lisu Chart + /// + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1704")] + Lisu, + /// + /// Low Surrogates Chart + /// + LowSurrogates, + /// + /// Lycian Chart + /// + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1704")] + Lycian, + /// + /// Lydian Chart + /// + Lydian, + /// + /// Mahjong Tiles Chart + /// + MahjongTiles, + /// + /// Malayalam Chart + /// + Malayalam, + /// + /// Mark Chart + /// + Mark, + /// + /// Mathematical Alphanumeric Symbols Chart + /// + MathematicalAlphanumericSymbols, + /// + /// Mathematical Operators Chart + /// + MathematicalOperators, + /// + /// Meetei Mayek Chart + /// + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1704")] + MeeteiMayek, + /// + /// Miscellaneous Mathematical SymbolsA Chart + /// + MiscellaneousMathematicalSymbolsA, + /// + /// Miscellaneous Mathematical SymbolsB Chart + /// + MiscellaneousMathematicalSymbolsB, + /// + /// Miscellaneous Symbols Chart + /// + MiscellaneousSymbols, + /// + /// Miscellaneous Symbols and Arrows Chart + /// + MiscellaneousSymbolsAndArrows, + /// + /// Miscellaneous Technical Chart + /// + MiscellaneousTechnical, + /// + /// Modifier Tone Letters Chart + /// + ModifierToneLetters, + /// + /// Mongolian Chart + /// + Mongolian, + /// + /// Musical Symbols Chart + /// + MusicalSymbols, + /// + /// Myanmar Chart + /// + Myanmar, + /// + /// Myanmar Extended-A Chart + /// + MyanmarExtendedA, + /// + /// New Tai Lue Chart + /// + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1704")] + NewTaiLue, + /// + /// N'Ko Chart + /// + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1704")] + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1709")] + NKo, + /// + /// Number Forms Chart + /// + NumberForms, + /// + /// Ogham Chart + /// + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1704")] + Ogham, + /// + /// Ol Chiki Chart + /// + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1704")] + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1709")] + OlChiki, + /// + /// Old Italic Chart + /// + OldItalic, + /// + /// Old Persian Chart + /// + OldPersian, + /// + /// Old South Arabian Chart + /// + OldSouthArabian, + /// + /// Old Turkic Chart + /// + OldTurkic, + /// + /// Optical Character Recognition Chart + /// + OpticalCharacterRecognition, + /// + /// Oriya Chart + /// + Oriya, + /// + /// Osmanya Chart + /// + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1704")] + Osmanya, + /// + /// Pahlavi Inscriptional Chart + /// + PahlaviInscriptional, + /// + /// Parthian Inscriptional Chart + /// + ParthianInscriptional, + /// + /// Pfennig Chart + /// + Pfennig, + /// + /// Phags-Pa Chart + /// + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1704")] + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1709")] + PhagsPa, + /// + /// Phaistos Disc Chart + /// + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1704")] + PhaistosDisc, + /// + /// Phoenician Chart + /// + Phoenician, + /// + /// Phonetic Extensions Chart + /// + PhoneticExtensions, + /// + /// Phonetic Extensions Supplement Chart + /// + PhoneticExtensionsSupplement, + /// + /// Plane 1 Chart + /// + Plane1, + /// + /// Plane 10 Chart + /// + Plane10, + /// + /// Plane 11 Chart + /// + Plane11, + /// + /// Plane 12 Chart + /// + Plane12, + /// + /// Plane 13 Chart + /// + Plane13, + /// + /// Plane 14 Chart + /// + Plane14, + /// + /// Plane 15 Chart + /// + Plane15, + /// + /// Plane 16 Chart + /// + Plane16, + /// + /// Plane 2 Chart + /// + Plane2, + /// + /// Plane 3 Chart + /// + Plane3, + /// + /// Plane 4 Chart + /// + Plane4, + /// + /// Plane 5 Chart + /// + Plane5, + /// + /// Plane 6 Chart + /// + Plane6, + /// + /// Plane 7 Chart + /// + Plane7, + /// + /// Plane 8 Chart + /// + Plane8, + /// + /// Plane 9 Chart + /// + Plane9, + /// + /// Private Use Area Chart + /// + PrivateUseArea, + /// + /// Rejang Chart + /// + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1704")] + Rejang, + /// + /// Reserved Range Chart + /// + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1700")] + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1704")] + ReservedRange, + /// + /// Rial Sign Chart + /// + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1704")] + RialSign, + /// + /// Roman Symbols Chart + /// + RomanSymbols, + /// + /// Rumi Numeral Symbols Chart + /// + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1704")] + RumiNumeralSymbols, + /// + /// Runic Chart + /// + Runic, + /// + /// Samaritan Chart + /// + Samaritan, + /// + /// Saurashtra Chart + /// + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1704")] + Saurashtra, + /// + /// Shavian Chart + /// + Shavian, + /// + /// Sinhala Chart + /// + Sinhala, + /// + /// Small Form Variants Chart + /// + SmallFormVariants, + /// + /// Spacing Modifier Letters Chart + /// + SpacingModifierLetters, + /// + /// Specials Chart + /// + Specials, + /// + /// Sundanese Chart + /// + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1704")] + Sundanese, + /// + /// Super and Subscripts Chart + /// + SuperAndSubscripts, + /// + /// Superscripts and Subscripts Chart + /// + SuperscriptsAndSubscripts, + /// + /// Supplemental Arrows-A Chart + /// + SupplementalArrowsA, + /// + /// Supplemental Arrows-B Chart + /// + SupplementalArrowsB, + /// + /// Supplemental Mathematical Operators Chart + /// + SupplementalMathematicalOperators, + /// + /// Supplemental Punctuation Chart + /// + SupplementalPunctuation, + /// + /// Supplementary Private Use Area-A Chart + /// + SupplementaryPrivateUseAreaA, + /// + /// Supplementary Private Use Area-B Chart + /// + SupplementaryPrivateUseAreaB, + /// + /// Syloti Nagri Chart + /// + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1704")] + SylotiNagri, + /// + /// Syriac Chart + /// + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1704")] + Syriac, + /// + /// Tagalog Chart + /// + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1704")] + Tagalog, + /// + /// Tagbanwa Chart + /// + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1704")] + Tagbanwa, + /// + /// Tags Chart + /// + Tags, + /// + /// Tai Le Chart + /// + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1709")] + TaiLe, + /// + /// Tai Tham Chart + /// + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1704")] + TaiTham, + /// + /// Tai Viet Chart + /// + TaiViet, + /// + /// Tai Xuan Jing Symbols Chart + /// + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1704")] + TaiXuanJingSymbols, + /// + /// Tamil Chart + /// + Tamil, + /// + /// Telugu Chart + /// + Telugu, + /// + /// Thaana Chart + /// + Thaana, + /// + /// Thai Chart + /// + Thai, + /// + /// Tibetan Chart + /// + Tibetan, + /// + /// Tifinagh Chart + /// + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1704")] + Tifinagh, + /// + /// Ugaritic Chart + /// + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1704")] + Ugaritic, + /// + /// Unified Canadian Aboriginal Syllabics Chart + /// + UnifiedCanadianAboriginalSyllabics, + /// + /// Unified Canadian Aboriginal Syllabics ExtendedChart + /// + UnifiedCanadianAboriginalSyllabicsExtended, + /// + /// Vai Chart + /// + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1704")] + Vai, + /// + /// Variation Selectors Chart + /// + VariationSelectors, + /// + /// Variation Selectors Supplement Chart + /// + VariationSelectorsSupplement, + /// + /// Vedic Extensions Chart + /// + VedicExtensions, + /// + /// Vertical Forms Chart + /// + VerticalForms, + /// + /// Yen Pound and Cent Chart + /// + YenPoundAndCent, + /// + /// Yi Syllables and Yi Radicals Chart + /// + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1704")] + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1709")] + Yi, + /// + /// Yijing Hexagram Symbols Chart + /// + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1704")] + YijingHexagramSymbols, + /// + /// Yijing Mono- Di- and Trigrams Chart + /// + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1704")] + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1709")] + YijingMonoDiAndTrigrams, + /// + /// Same as Yijing Mono- Di- and Trigrams Chart + /// + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1704")] + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1709")] + YijingSymbols, + /// + /// Yi Radicals Chart + /// + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1704")] + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1709")] + YiRadicals, + /// + /// Yi Syllables Chart + /// + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1704")] + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1709")] + YiSyllables, + } +} + diff --git a/src/dotnetCampus.UITest.WPFTestHelper/Text/UnicodeRange.cs b/src/dotnetCampus.UITest.WPFTestHelper/Text/UnicodeRange.cs new file mode 100644 index 0000000..5ca36cc --- /dev/null +++ b/src/dotnetCampus.UITest.WPFTestHelper/Text/UnicodeRange.cs @@ -0,0 +1,81 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Globalization; + +namespace dotnetCampus.UITest.WPFTestHelper.Text +{ + /// + /// Represents a Unicode range. + /// A UnicodeRange instance can be created by either providing start and end of the + /// desired Unicode range or by providing a . + /// + public class UnicodeRange + { + /// + /// Create a UnicodeRange instance, using the provided UnicodeChart Enum type. + /// + /// Group name of scripts, symbols or punctuations (e.g. "European Scripts", "Punctuation", etc.) + public UnicodeRange(UnicodeChart chart) + { + UnicodeRange range = RangePropertyCollector.GetUnicodeChartRange(new UnicodeRangeDatabase(), chart); + startOfUnicodeRange = range.StartOfUnicodeRange; + endOfUnicodeRange = range.EndOfUnicodeRange; + } + + /// + /// Create a UnicodeRange instance, using the provided start and end of the Unicode range + /// + /// Start of the Unicode range (e.g. 0x0000, etc.) + /// End of the Unicode range (e.g. 0xFFFF, etc.) + public UnicodeRange(int start, int end) + { + Init(start, end); + } + + /// + /// Copy constructor + /// + /// A UnicodeRange object to be copied + public UnicodeRange(UnicodeRange range) + { + startOfUnicodeRange = range.StartOfUnicodeRange; + endOfUnicodeRange = range.EndOfUnicodeRange; + } + + private void Init(int low, int high) + { + if (low > high) + { + throw new ArgumentOutOfRangeException ("UnicodeRange, low " + low + " shouldn't be greater than high " + high + " value."); + } + else if (low < 0) + { + throw new ArgumentOutOfRangeException ("UnicodeRange, low is" + low + ", cannot be less than 0x0."); + } + else if (high > TextUtil.MaxUnicodePoint) + { + throw new ArgumentOutOfRangeException ("UnicodeRange, high cannot be greater than " + + String.Format(CultureInfo.InvariantCulture, "0x{0:X}", TextUtil.MaxUnicodePoint) + "."); + } + startOfUnicodeRange = low; + endOfUnicodeRange = high; + } + + /// + /// Get the start of the Unicode range + /// + public int StartOfUnicodeRange { get { return startOfUnicodeRange; } } + + /// + /// Get the end of the Unicode range + /// + public int EndOfUnicodeRange { get { return endOfUnicodeRange; } } + + private int startOfUnicodeRange; + private int endOfUnicodeRange; + } +} + diff --git a/src/dotnetCampus.UITest.WPFTestHelper/Text/UnicodeRangeDatabase.cs b/src/dotnetCampus.UITest.WPFTestHelper/Text/UnicodeRangeDatabase.cs new file mode 100644 index 0000000..a6380f3 --- /dev/null +++ b/src/dotnetCampus.UITest.WPFTestHelper/Text/UnicodeRangeDatabase.cs @@ -0,0 +1,437 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace dotnetCampus.UITest.WPFTestHelper.Text +{ + /// + /// Collection of Unicode ranges including Scripts, Symbols, and Punctuation + /// + internal class UnicodeRangeDatabase + { + private static readonly Group [] scripts = new Group[TextUtil.NUMOFSCRIPTS]; + private static readonly Group [] symbolsAndPunctuation = new Group[TextUtil.NUMOFSYMBOLSANDPUNCTUATION]; + + /// + /// Define UnicodeRangeDatabase class, + /// Newline + /// + public UnicodeRangeDatabase( ) + { + if (null == scripts[0]) + { + InitializeScripts(); + } + + if (null == symbolsAndPunctuation[0]) + { + InitializeSymbolsAndPunctuation(); + } + } + + /// + /// Getter for scripts + /// + public Group [] Scripts { get { return scripts; } } + + /// + /// Getter for symbols and punctuation + /// + public Group [] SymbolsAndPunctuation { get { return symbolsAndPunctuation; } } + + private static void InitializeScripts() + { + // For the 3rd parameter ids - all lower cases + // If LCID exists, Short String description is used; otherwise, spell out the whole name in lower case. + // If it applies to the root culture e.g. zh-cn, zh-hk, zh-tw, the root culture id is used e.g. "zh". '-' is omitted. + // If it can be used for various culuture, "any" is used. + // If LCID does not exist, full spell of the culture is the id and space is omitted. + scripts[0] = new Group(new UnicodeRange(0x0530, 0x058F), "European Scripts", "Armenian", "hy", UnicodeChart.Armenian); + scripts[0].SubGroups = new SubGroup[1]; + scripts[0].SubGroups[0] = new SubGroup(new UnicodeRange(0xFB00, 0xFB4F), "Armenian Ligatures", "hy", UnicodeChart.ArmenianLigatures); + + scripts[1] = new Group(new UnicodeRange(0x2C80, 0x2CFF), "European Scripts", "Coptic", "eg", UnicodeChart.Coptic); + scripts[1].SubGroups = new SubGroup[1]; + scripts[1].SubGroups[0] = new SubGroup(new UnicodeRange(0x0370, 0x03FF), "Coptic in Greek block", "eg,el", UnicodeChart.CopticInGreekBlock); + + scripts[2] = new Group(new UnicodeRange(0x10800, 0x1083F), "European Scripts", "Cypriot Syllabary", "cyprus", UnicodeChart.CypriotSyllabary); + + scripts[3] = new Group(new UnicodeRange(0x0400, 0x04FF), "European Scripts", "Cyrillic", "azaz,srsp,uzuz", UnicodeChart.Cyrillic); + scripts[3].SubGroups = new SubGroup[3]; + scripts[3].SubGroups[0] = new SubGroup(new UnicodeRange(0x0500, 0x052F), "Cyrillic Supplement", "azaz,srsp,uzuz", UnicodeChart.CyrillicSupplement); + scripts[3].SubGroups[1] = new SubGroup(new UnicodeRange(0x2DE0, 0x2DFF), "Cyrillic Extended-A", "azaz,srsp,uzuz", UnicodeChart.CyrillicExtendedA); + scripts[3].SubGroups[2] = new SubGroup(new UnicodeRange(0xA640, 0xA69F), "Cyrillic Extended-B", "azaz,srsp,uzuz", UnicodeChart.CyrillicExtendedB); + + scripts[4] = new Group(new UnicodeRange(0x10A0, 0x10FF), "European Scripts", "Georgian", "georgia", UnicodeChart.Georgian); + scripts[4].SubGroups = new SubGroup[1]; + scripts[4].SubGroups[0] = new SubGroup(new UnicodeRange(0x2D00, 0x2D2F), "Georgian Supplement", "georgia", UnicodeChart.GeorgianSupplement); + + scripts[5] = new Group(new UnicodeRange(0x2C00, 0x2C5F), "European Scripts", "Glagolitic", "glagolitsa", UnicodeChart.Glagolitic); + scripts[6] = new Group(new UnicodeRange(0x10330, 0x1034F), "European Scripts", "Gothic", "de", UnicodeChart.Gothic); + + scripts[7] = new Group(new UnicodeRange(0x0370, 0x03FF), "European Scripts", "Greek", "el", UnicodeChart.Greek); + scripts[7].SubGroups = new SubGroup[1]; + scripts[7].SubGroups[0] = new SubGroup(new UnicodeRange(0x1F00, 0x1FFF), "Greek Extended", "el", UnicodeChart.GreekExtended); + + scripts[8] = new Group(new UnicodeRange(0x0000, 0x007F), "European Scripts", "Latin", "latin", UnicodeChart.Latin); + scripts[8].SubGroups = new SubGroup[8]; + scripts[8].SubGroups[0] = new SubGroup(new UnicodeRange(0x0080, 0x00FF), "Latin-1 Supplement", "latin", UnicodeChart.Latin1Supplement); + scripts[8].SubGroups[1] = new SubGroup(new UnicodeRange(0x0100, 0x017F), "Latin Extended-A", "latin", UnicodeChart.LatinExtendedA); + scripts[8].SubGroups[2] = new SubGroup(new UnicodeRange(0x0180, 0x024F), "Latin Extended-B", "latin", UnicodeChart.LatinExtendedB); + scripts[8].SubGroups[3] = new SubGroup(new UnicodeRange(0x2C60, 0x2C7F), "Latin Extended-C", "latin", UnicodeChart.LatinExtendedC); + scripts[8].SubGroups[4] = new SubGroup(new UnicodeRange(0xA720, 0xA7FF), "Latin Extended-D", "latin", UnicodeChart.LatinExtendedD); + scripts[8].SubGroups[5] = new SubGroup(new UnicodeRange(0x1E00, 0x1EFF), "Latin Extended Additional", "latin", UnicodeChart.LatinExtendedAdditional); + scripts[8].SubGroups[6] = new SubGroup(new UnicodeRange(0xFB00, 0xFB4F), "Latin Ligatures", "latin", UnicodeChart.LatinLigatures); + scripts[8].SubGroups[7] = new SubGroup(new UnicodeRange(0xFF00, 0xFFEF), "FullWidth Latin Letters", "latin", UnicodeChart.FullwidthLatinLetters); + + // Linear B doesn't have a range specified. Use the range of both sub groups + scripts[9] = new Group(new UnicodeRange(0x10000, 0x100FF), "European Scripts", "Linear B", "other", UnicodeChart.LinearB); + scripts[9].SubGroups = new SubGroup[2]; + scripts[9].SubGroups[0] = new SubGroup(new UnicodeRange(0x10000, 0x1007F), "Linear B Syllabary", "other", UnicodeChart.LinearBSyllabary); + scripts[9].SubGroups[1] = new SubGroup(new UnicodeRange(0x10080, 0x100FF), "Linear B Ideograms", "other", UnicodeChart.LinearBIdeograms); + + scripts[10] = new Group(new UnicodeRange(0x1680, 0x169F), "European Scripts", "Ogham", "ie", UnicodeChart.Ogham); + scripts[11] = new Group(new UnicodeRange(0x10300, 0x1032F), "European Scripts", "Old Italic", "other", UnicodeChart.OldItalic); + scripts[12] = new Group(new UnicodeRange(0x101D0, 0x101FF), "European Scripts", "Phaistos Disc", "phaistosdisc", UnicodeChart.PhaistosDisc); + scripts[13] = new Group(new UnicodeRange(0x16A0, 0x16FF), "European Scripts", "Runic", "de", UnicodeChart.Runic); + scripts[14] = new Group(new UnicodeRange(0x10450, 0x1047F), "European Scripts", "Shavian", "en", UnicodeChart.Shavian); + + scripts[15] = new Group(new UnicodeRange(0x0250, 0x02AF), "Phonetic Symbols", "IPA Extensions", "latin", UnicodeChart.IpaExtensions); + + scripts[16] = new Group(new UnicodeRange(0x1D00, 0x1D7F), "Phonetic Symbols", "Phonetic Extensions", "latin", UnicodeChart.PhoneticExtensions); + scripts[16].SubGroups = new SubGroup[1]; + scripts[16].SubGroups[0] = new SubGroup(new UnicodeRange(0x1D80, 0x1D8F), "Phonetic Extensions Supplement", "latin", UnicodeChart.PhoneticExtensionsSupplement); + + scripts[17] = new Group(new UnicodeRange(0xA700, 0xA71F), "Phonetic Symbols", "Modifier Tone Letters", "other", UnicodeChart.ModifierToneLetters); + scripts[18] = new Group(new UnicodeRange(0x02B0, 0x02FF), "Phonetic Symbols", "Spacing Modifier Letters", "other", UnicodeChart.SpacingModifierLetters); + scripts[19] = new Group(new UnicodeRange(0x2070, 0x209F), "Phonetic Symbols", "Superscripts and Subscripts", "any", UnicodeChart.SuperscriptsAndSubscripts); + + scripts[20] = new Group(new UnicodeRange(0x0300, 0x036F), "Combining Diacritics", "Combining Diacritical Marks", "other", UnicodeChart.CombiningDiacriticalMarks); + scripts[20].SubGroups = new SubGroup[1]; + scripts[20].SubGroups[0] = new SubGroup(new UnicodeRange(0x1DC0, 0x1DFF), "Combining Diacritical Marks Supplement", "other", UnicodeChart.CombiningDiacriticalMarksSupplement); + + scripts[21] = new Group(new UnicodeRange(0xFE20, 0xFE2F), "Combining Diacritics", "Combining Half Marks", "other", UnicodeChart.CombiningHalfMarks); + + scripts[22] = new Group(new UnicodeRange(0xA6A0, 0xA6FF), "African Scripts", "Bamum", "cameroon", UnicodeChart.Bamum); + scripts[23] = new Group(new UnicodeRange(0x13000, 0x1342F), "African Scripts", "Egyptian Hieroglyphs", "eg", UnicodeChart.EgyptianHieroglyphs); + + scripts[24] = new Group(new UnicodeRange(0x1200, 0x137F), "African Scripts", "Ethiopic", "ethiopia", UnicodeChart.Ethiopic); + scripts[24].SubGroups = new SubGroup[2]; + scripts[24].SubGroups[0] = new SubGroup(new UnicodeRange(0x1380, 0x139F), "Ethiopic Supplement", "ethiopia", UnicodeChart.EthiopicSupplement); + scripts[24].SubGroups[1] = new SubGroup(new UnicodeRange(0x2D80, 0x2DDF), "Ethiopic Extended", "ethiopia", UnicodeChart.EthiopicExtended); + + scripts[25] = new Group(new UnicodeRange(0xA700, 0xA71F), "African Scripts", "N'ko", "nko", UnicodeChart.NKo); + scripts[26] = new Group(new UnicodeRange(0x10480, 0x104AF), "African Scripts", "Osmanya", "somalia", UnicodeChart.Osmanya); + scripts[27] = new Group(new UnicodeRange(0x2D30, 0x2D7F), "African Scripts", "Tifinagh", "tifinagh", UnicodeChart.Tifinagh); + scripts[28] = new Group(new UnicodeRange(0xA500, 0xA63F), "African Scripts", "Vai", "vai", UnicodeChart.Vai); + + scripts[29] = new Group(new UnicodeRange(0x0600, 0x06FF), "Middle Eastern Scripts", "Arabic", "ar", UnicodeChart.Arabic); + scripts[29].SubGroups = new SubGroup[3]; + scripts[29].SubGroups[0] = new SubGroup(new UnicodeRange(0x0750, 0x077F), "Arabic Supplement", "ar", UnicodeChart.ArabicSupplement); + scripts[29].SubGroups[1] = new SubGroup(new UnicodeRange(0xFB50, 0xFDFF), "Arabic Presentation Forms-A", "ar", UnicodeChart.ArabicPresentationFormsA); + scripts[29].SubGroups[2] = new SubGroup(new UnicodeRange(0xFE70, 0xFEFF), "Arabic Presentation Forms-B", "ar", UnicodeChart.ArabicPresentationFormsB); + + scripts[30] = new Group(new UnicodeRange(0x10840, 0x1085F), "Middle Eastern Scripts", "Aramaic, Imperial", "he", UnicodeChart.AramaicImperial); + scripts[31] = new Group(new UnicodeRange(0x10B00, 0x10B3F), "Middle Eastern Scripts", "Avestan", "iran", UnicodeChart.Avestan); + scripts[32] = new Group(new UnicodeRange(0x102A0, 0x102DF), "Middle Eastern Scripts", "Carian", "carians", UnicodeChart.Carian); + + scripts[33] = new Group(new UnicodeRange(0x12000, 0x123FF), "Middle Eastern Scripts", "Cuneiform", "cuneiform", UnicodeChart.Cuneiform); + scripts[33].SubGroups = new SubGroup[3]; + scripts[33].SubGroups[0] = new SubGroup(new UnicodeRange(0x12400, 0x1247F), "Cuneiform Numbers and Punctuation", "cuneiform", UnicodeChart.CuneiformNumbersAndPunctuation); + scripts[33].SubGroups[1] = new SubGroup(new UnicodeRange(0x103A0, 0x103DF), "Old Persian", "cuneiform", UnicodeChart.OldPersian); + scripts[33].SubGroups[2] = new SubGroup(new UnicodeRange(0x10380, 0x1039F), "Ugaritic", "cuneiform", UnicodeChart.Ugaritic); + + scripts[34] = new Group(new UnicodeRange(0x0590, 0x05FF), "Middle Eastern Scripts", "Hebrew", "he", UnicodeChart.Hebrew); + scripts[34].SubGroups = new SubGroup[1]; + scripts[34].SubGroups[0] = new SubGroup(new UnicodeRange(0xFB00, 0xFB4F), "Hebrew Presentation Forms", "he", UnicodeChart.HebrewPresentationForms); + + scripts[35] = new Group(new UnicodeRange(0x10280, 0x1029F), "Middle Eastern Scripts", "Lycian", "lycia", UnicodeChart.Lycian); + scripts[36] = new Group(new UnicodeRange(0x10920, 0x1093F), "Middle Eastern Scripts", "Lydian", "lycia", UnicodeChart.Lydian); + scripts[37] = new Group(new UnicodeRange(0x10A60, 0x10A7F), "Middle Eastern Scripts", "Old South Arabian", "ar", UnicodeChart.OldSouthArabian); + scripts[38] = new Group(new UnicodeRange(0x10B60, 0x10B7F), "Middle Eastern Scripts", "Pahlavi, Inscriptional", "iran", UnicodeChart.PahlaviInscriptional); + scripts[39] = new Group(new UnicodeRange(0x10B40, 0x10B5FF), "Middle Eastern Scripts", "Parthian, Inscriptional", "iran", UnicodeChart.ParthianInscriptional); + scripts[40] = new Group(new UnicodeRange(0x10900, 0x1091F), "Middle Eastern Scripts", "Phoenician", "phoenicia", UnicodeChart.Phoenician); + scripts[41] = new Group(new UnicodeRange(0x0800, 0x083F), "Middle Eastern Scripts", "Samaritan", "samaria", UnicodeChart.Samaritan); + scripts[42] = new Group(new UnicodeRange(0x0700, 0x074F), "Middle Eastern Scripts", "Syriac", "syriac", UnicodeChart.Syriac); + + scripts[43] = new Group(new UnicodeRange(0x1800, 0x18AF), "Central Asian Scripts", "Mongolian", "mongolia", UnicodeChart.Mongolian); + scripts[44] = new Group(new UnicodeRange(0x10C00, 0x10C4F), "Central Asian Scripts", "Old Turkic", "oldturkic", UnicodeChart.OldTurkic); + scripts[45] = new Group(new UnicodeRange(0xA840, 0xA87F), "Central Asian Scripts", "Phags-Pa", "zh", UnicodeChart.PhagsPa); + scripts[46] = new Group(new UnicodeRange(0x0F00, 0x0FFF), "Central Asian Scripts", "Tibetan", "zh", UnicodeChart.Tibetan); + + scripts[47] = new Group(new UnicodeRange(0x0980, 0x09FF), "South Asian Scripts", "Bengali", "bangladesh,hi", UnicodeChart.Bengali); + + scripts[48] = new Group(new UnicodeRange(0x0900, 0x097F), "South Asian Scripts", "Devanagari", "hi,nepal", UnicodeChart.Devanagari); + scripts[48].SubGroups = new SubGroup[1]; + scripts[48].SubGroups[0] = new SubGroup(new UnicodeRange(0xA8E0, 0xA8FF), "Devanagari Extended", "hi", UnicodeChart.DevanagariExtended); + + scripts[49] = new Group(new UnicodeRange(0x0A80, 0x0AFF), "South Asian Scripts", "Gujarati", "hi", UnicodeChart.Gujarati); + scripts[50] = new Group(new UnicodeRange(0x0A00, 0x0A7F), "South Asian Scripts", "Gurmukhi", "hi", UnicodeChart.Gurmukhi); + scripts[51] = new Group(new UnicodeRange(0x11080, 0x110CF), "South Asian Scripts", "Kaithi", "hi", UnicodeChart.Kaithi); + scripts[52] = new Group(new UnicodeRange(0x0C80, 0x0CFF), "South Asian Scripts", "Kannada", "hi", UnicodeChart.Kannada); + scripts[53] = new Group(new UnicodeRange(0x10A00, 0x10A5F), "South Asian Scripts", "Kharoshthi", "kharoshthi", UnicodeChart.Kharoshthi); + scripts[54] = new Group(new UnicodeRange(0x1C00, 0x1C4F), "South Asian Scripts", "Lepcha", "zh,nepal,hi", UnicodeChart.Lepcha); + scripts[55] = new Group(new UnicodeRange(0x1900, 0x194F), "South Asian Scripts", "Limbu", "nepal", UnicodeChart.Limbu); + scripts[56] = new Group(new UnicodeRange(0x0D00, 0x0D7F), "South Asian Scripts", "Malayalam", "hi", UnicodeChart.Malayalam); + scripts[57] = new Group(new UnicodeRange(0xABC0, 0xABFF), "South Asian Scripts", "Meetei Mayek", "hi", UnicodeChart.MeeteiMayek); + scripts[58] = new Group(new UnicodeRange(0x1C50, 0x1C7F), "South Asian Scripts", "Ol Chiki", "hi", UnicodeChart.OlChiki); + scripts[59] = new Group(new UnicodeRange(0x0B00, 0x0B7F), "South Asian Scripts", "Oriya", "hi", UnicodeChart.Oriya); + scripts[60] = new Group(new UnicodeRange(0xA880, 0xA8DF), "South Asian Scripts", "Saurashtra", "hi", UnicodeChart.Saurashtra); + scripts[61] = new Group(new UnicodeRange(0x0D80, 0x0DFF), "South Asian Scripts", "Sinhala", "srilanka", UnicodeChart.Sinhala); + scripts[62] = new Group(new UnicodeRange(0xA800, 0xA82F), "South Asian Scripts", "Syloti Nagri", "sylotinagri", UnicodeChart.SylotiNagri); + scripts[63] = new Group(new UnicodeRange(0x0B80, 0x0BFF), "South Asian Scripts", "Tamil", "hi,srilanka,singapore", UnicodeChart.Tamil); + scripts[64] = new Group(new UnicodeRange(0x0C00, 0x0C7F), "South Asian Scripts", "Telugu", "hi", UnicodeChart.Telugu); + scripts[65] = new Group(new UnicodeRange(0x0780, 0x07BF), "South Asian Scripts", "Thaana", "maldives", UnicodeChart.Thaana); + scripts[66] = new Group(new UnicodeRange(0x1CD0, 0x1CFF), "South Asian Scripts", "Vedic Extensions", "hi", UnicodeChart.VedicExtensions); + + scripts[67] = new Group(new UnicodeRange(0x1B00, 0x1B7F), "Southeast Asian Scripts", "Balinese", "id", UnicodeChart.Balinese); + scripts[68] = new Group(new UnicodeRange(0x1A00, 0x1A1F), "Southeast Asian Scripts", "Buginese", "id", UnicodeChart.Buginese); + scripts[69] = new Group(new UnicodeRange(0xAA00, 0xAA5F), "Southeast Asian Scripts", "Cham", "vi,th,cambodia", UnicodeChart.Cham); + scripts[70] = new Group(new UnicodeRange(0xA980, 0xA9DF), "Southeast Asian Scripts", "Javanese", "id", UnicodeChart.Javanese); + scripts[71] = new Group(new UnicodeRange(0xA900, 0xA92F), "Southeast Asian Scripts", "Kayah Li", "myanmar", UnicodeChart.KayahLi); + + scripts[72] = new Group(new UnicodeRange(0x1780, 0x17FF), "Southeast Asian Scripts", "Khmer", "cambodia", UnicodeChart.Khmer); + scripts[72].SubGroups = new SubGroup[1]; + scripts[72].SubGroups[0] = new SubGroup(new UnicodeRange(0x17E0, 0x17FF), "Khmer Symbols", "cambodia", UnicodeChart.KhmerSymbols); + + scripts[73] = new Group(new UnicodeRange(0x0E80, 0x0EFF), "Southeast Asian Scripts", "Lao", "lao", UnicodeChart.Lao); + + scripts[74] = new Group(new UnicodeRange(0x1000, 0x109F), "Southeast Asian Scripts", "Myanmar", "myanmar", UnicodeChart.Myanmar); + scripts[74].SubGroups = new SubGroup[1]; + scripts[74].SubGroups[0] = new SubGroup(new UnicodeRange(0xAA60, 0xAA7F), "Myanmar Extended-A", "myanmar", UnicodeChart.MyanmarExtendedA); + + scripts[75] = new Group(new UnicodeRange(0x1980, 0x19DF), "Southeast Asian Scripts", "New Tai Lue", "zh", UnicodeChart.NewTaiLue); + scripts[76] = new Group(new UnicodeRange(0xA930, 0xA95F), "Southeast Asian Scripts", "Rejang", "id", UnicodeChart.Rejang); + scripts[77] = new Group(new UnicodeRange(0x1B80, 0x1BBF), "Southeast Asian Scripts", "Sundanese", "id", UnicodeChart.Sundanese); + scripts[78] = new Group(new UnicodeRange(0x1950, 0x197F), "Southeast Asian Scripts", "Tai Le", "zh", UnicodeChart.TaiLe); + scripts[79] = new Group(new UnicodeRange(0x1A20, 0x1AAF), "Southeast Asian Scripts", "Tai Tham", "th", UnicodeChart.TaiTham); + scripts[80] = new Group(new UnicodeRange(0xAA80, 0xAADF), "Southeast Asian Scripts", "Tai Viet", "vi", UnicodeChart.TaiViet); + scripts[81] = new Group(new UnicodeRange(0x0E00, 0x0E7F), "Southeast Asian Scripts", "Thai", "th", UnicodeChart.Thai); + + scripts[82] = new Group(new UnicodeRange(0x1740, 0x175F), "Philippine Scripts", "Buhid", "ph", UnicodeChart.Buhid); + scripts[83] = new Group(new UnicodeRange(0x1720, 0x173F), "Philippine Scripts", "Hanunoo", "ph", UnicodeChart.Hanunoo); + scripts[84] = new Group(new UnicodeRange(0x1700, 0x171F), "Philippine Scripts", "Tagalog", "ph", UnicodeChart.Tagalog); + scripts[85] = new Group(new UnicodeRange(0x1760, 0x177F), "Philippine Scripts", "Tagbanwa", "ph", UnicodeChart.Tagbanwa); + + scripts[86] = new Group(new UnicodeRange(0x3100, 0x312F), "East Asian Scripts", "Bopomofo", "zhtw", UnicodeChart.Bopomofo); + scripts[86].SubGroups = new SubGroup[1]; + scripts[86].SubGroups[0] = new SubGroup(new UnicodeRange(0x31A0, 0x31BF), "Bopomofo Extended", "zhtw", UnicodeChart.BopomofoExtended); + + scripts[87] = new Group(new UnicodeRange(0x4E00, 0x9FCF), "East Asian Scripts", "CJK Unified Ideographs (Han)", "zh,ja,ko", UnicodeChart.CjkUnifiedIdeographs); + scripts[87].SubGroups = new SubGroup[3]; + scripts[87].SubGroups[0] = new SubGroup(new UnicodeRange(0x3400, 0x4DBF), "CJK Extension-A", "zh,ja,ko", UnicodeChart.CjkExtensionA); + scripts[87].SubGroups[1] = new SubGroup(new UnicodeRange(0x20000, 0x2A6DF), "CJK Extension B", "zh,ja,ko", UnicodeChart.CjkExtensionB); + scripts[87].SubGroups[2] = new SubGroup(new UnicodeRange(0x2A700, 0x2B73F), "CJK Extension C", "zh,ja,ko", UnicodeChart.CjkExtensionC); + + scripts[88] = new Group(new UnicodeRange(0xF900, 0xFAFF), "East Asian Scripts", "CJK Compatibility Ideographs", "zh,ja,ko", UnicodeChart.CjkCompatibilityIdeographs); + scripts[88].SubGroups = new SubGroup[1]; + scripts[88].SubGroups[0] = new SubGroup(new UnicodeRange(0x2F800, 0x2FA1F), "CJK Compatibility Ideographs Supplement", "zh,ja,ko", UnicodeChart.CjkCompatibilityIdeographsSupplement); + + scripts[89] = new Group(new UnicodeRange(0x2F00, 0x2FDF), "East Asian Scripts", "CJK Radicals // KangXi Radicals", "zh,ja,ko", UnicodeChart.CjkKangXiRadicals); + scripts[89].SubGroups = new SubGroup[3]; + scripts[89].SubGroups[0] = new SubGroup(new UnicodeRange(0x2E80, 0x2EFF), "CJK Radicals Supplement", "zh,ja,ko", UnicodeChart.CjkRadicalsSupplement); + scripts[89].SubGroups[1] = new SubGroup(new UnicodeRange(0x2E80, 0x2EFF), "CJK Strokes", "zh,ja,ko", UnicodeChart.CjkStrokes); + scripts[89].SubGroups[2] = new SubGroup(new UnicodeRange(0x31C0, 0x31EF), "Ideographic Description Characters", "zh,ja,ko", UnicodeChart.IdeographicDescriptionCharacters); + + scripts[90] = new Group(new UnicodeRange(0x1100, 0x11FF), "East Asian Scripts", "Hangul Jamo", "ko", UnicodeChart.HangulJamo); + scripts[90].SubGroups = new SubGroup[4]; + scripts[90].SubGroups[0] = new SubGroup(new UnicodeRange(0xA960, 0xA97F), "Hangul Jamo Extended-A", "ko", UnicodeChart.HangulJamoExtendedA); + scripts[90].SubGroups[1] = new SubGroup(new UnicodeRange(0xD7B0, 0xD7FF), "Hangul Jamo Extended-B", "ko", UnicodeChart.HangulJamoExtendedB); + scripts[90].SubGroups[2] = new SubGroup(new UnicodeRange(0x3130, 0x318F), "Hangul Compatibility Jamo", "ko", UnicodeChart.HangulCompatibilityJamo); + scripts[90].SubGroups[3] = new SubGroup(new UnicodeRange(0xFF00, 0xFFEF), "Halfwidth Jamo", "ko", UnicodeChart.HalfwidthJamo); + + scripts[91] = new Group(new UnicodeRange(0xAC00, 0xD7AF), "East Asian Scripts", "Hangul Syllables", "ko", UnicodeChart.HangulSyllables); + scripts[92] = new Group(new UnicodeRange(0x3040, 0x309F), "East Asian Scripts", "Hiragana", "ja", UnicodeChart.Hiragana); + + scripts[93] = new Group(new UnicodeRange(0x30A0, 0x30FF), "East Asian Scripts", "Katakana", "ja", UnicodeChart.Katakana); + scripts[93].SubGroups = new SubGroup[2]; + scripts[93].SubGroups[0] = new SubGroup(new UnicodeRange(0x31F0, 0x31FF), "Katakana Phonetic Extensions", "ja", UnicodeChart.KatakanaPhoneticExtensions); + scripts[93].SubGroups[1] = new SubGroup(new UnicodeRange(0xFF00, 0xFFEF), "Halfwidth Katakana", "ja", UnicodeChart.HalfwidthKatakana); + + scripts[94] = new Group(new UnicodeRange(0x3190, 0x319F), "East Asian Scripts", "Kanbun", "zh,ja", UnicodeChart.Kanbun); + scripts[95] = new Group(new UnicodeRange(0xA4D0, 0xA4FF), "East Asian Scripts", "Lisu", "zh", UnicodeChart.Lisu); + + // Yi does not have code range defined. Use the range of both sub groups. + scripts[96] = new Group(new UnicodeRange(0xA000, 0xA4CF), "East Asian Scripts", "Yi", "zh", UnicodeChart.Yi); + scripts[96].SubGroups = new SubGroup[2]; + scripts[96].SubGroups[0] = new SubGroup(new UnicodeRange(0xA000, 0xA48F), "Yi Syllables", "zh", UnicodeChart.YiSyllables); + scripts[96].SubGroups[1] = new SubGroup(new UnicodeRange(0xA490, 0xA4CF), "Yi Radicals", "zh", UnicodeChart.YiRadicals); + + scripts[97] = new Group(new UnicodeRange(0x13A0, 0x13FF), "----n Scripts", "Cherokee", "us", UnicodeChart.Cherokee); + scripts[98] = new Group(new UnicodeRange(0x10440, 0x1044F), "----n Scripts", "Deseret", "us", UnicodeChart.Deseret); + + scripts[99] = new Group(new UnicodeRange(0x1400, 0x167F), "----n Scripts", "Unified Canadian Aboriginal Syllabics", "ca", UnicodeChart.UnifiedCanadianAboriginalSyllabics); + scripts[99].SubGroups = new SubGroup[1]; + scripts[99].SubGroups[0] = new SubGroup(new UnicodeRange(0x18B0, 0x18FF), "UCAS Extended", "ca", UnicodeChart.UnifiedCanadianAboriginalSyllabicsExtended); + + scripts[100] = new Group(new UnicodeRange(0xFB00, 0xFB4F), "Other", "Alphabetic Presentation Forms", "latin,he,hy", UnicodeChart.AlphabeticPresentationForms); + scripts[101] = new Group(new UnicodeRange(0xFF00, 0xFFEF), "Other", "Halfwidth and Fullwidth Forms", "latin,ja", UnicodeChart.HalfwidthAndFullwidthForms); + scripts[102] = new Group(new UnicodeRange(0x0000, 0x007F), "Other", "ASCII Characters", "latin", UnicodeChart.AsciiCharacters); + } + + private static void InitializeSymbolsAndPunctuation() + { + symbolsAndPunctuation[0] = new Group(new UnicodeRange(0x2000, 0x206F), "Punctuation", "General Punctuation", "any", UnicodeChart.GeneralPunctuation); + symbolsAndPunctuation[0].SubGroups = new SubGroup[4]; + symbolsAndPunctuation[0].SubGroups[0] = new SubGroup(new UnicodeRange(0x0000, 0x007F), "ASCII Punctuation", "latin", UnicodeChart.AsciiPunctuation); + symbolsAndPunctuation[0].SubGroups[1] = new SubGroup(new UnicodeRange(0x0080, 0x00FF), "Latin-1 Punctuation", "latin", UnicodeChart.Latin1Punctuation); + symbolsAndPunctuation[0].SubGroups[2] = new SubGroup(new UnicodeRange(0xFE50, 0xFE6F), "Small Form Variants", "any", UnicodeChart.SmallFormVariants); + symbolsAndPunctuation[0].SubGroups[3] = new SubGroup(new UnicodeRange(0x2E00, 0x2E7F), "Supplemental Punctuation", "other", UnicodeChart.SupplementalPunctuation); + + symbolsAndPunctuation[1] = new Group(new UnicodeRange(0x3000, 0x303F), "Punctuation", "CJK Symbols and Punctuation", "zh,ja,ko", UnicodeChart.CjkSymbolsAndPunctuation); + symbolsAndPunctuation[1].SubGroups = new SubGroup[3]; + symbolsAndPunctuation[1].SubGroups[0] = new SubGroup(new UnicodeRange(0xFE30, 0xFE4F), "CJK Compatibility Forms", "zh,ja,ko", UnicodeChart.CjkCompatibilityForms); + symbolsAndPunctuation[1].SubGroups[1] = new SubGroup(new UnicodeRange(0xFF00, 0xFFEF), "Fullwidth ASCII Punctuation", "zh,ja,ko", UnicodeChart.FullwidthAsciiPunctuation); + symbolsAndPunctuation[1].SubGroups[2] = new SubGroup(new UnicodeRange(0xFE10, 0xFE1F), "Vertical Forms", "zh,ja,ko", UnicodeChart.VerticalForms); + + symbolsAndPunctuation[2] = new Group(new UnicodeRange(0x2100, 0x214F), "Alphanumeric Symbols", "Letterlike Symbols", "any", UnicodeChart.LetterlikeSymbols); + symbolsAndPunctuation[2].SubGroups = new SubGroup[1]; + symbolsAndPunctuation[2].SubGroups[0] = new SubGroup(new UnicodeRange(0x10190, 0x101CF), "Roman Symbols", "latin", UnicodeChart.RomanSymbols); + + symbolsAndPunctuation[3] = new Group(new UnicodeRange(0x1D400, 0x1D7FF), "Alphanumeric Symbols", "Mathematical Alphanumeric Symbols", "any", UnicodeChart.MathematicalAlphanumericSymbols); + + symbolsAndPunctuation[4] = new Group(new UnicodeRange(0x2460, 0x124FF), "Alphanumeric Symbols", "Enclosed Alphanumerics", "any", UnicodeChart.EnclosedAlphanumerics); + symbolsAndPunctuation[4].SubGroups = new SubGroup[1]; + symbolsAndPunctuation[4].SubGroups[0] = new SubGroup(new UnicodeRange(0x1F100, 0x1F1FF), "Enclosed Alphanumerics Supplement", "any", UnicodeChart.EnclosedAlphanumericSupplement); + + symbolsAndPunctuation[5] = new Group(new UnicodeRange(0x3200, 0x32FF), "Alphanumeric Symbols", "Enclosed CJK Letters and Months", "zh,ja,ko", UnicodeChart.EnclosedCjkLettersAndMonths); + symbolsAndPunctuation[5].SubGroups = new SubGroup[1]; + symbolsAndPunctuation[5].SubGroups[0] = new SubGroup(new UnicodeRange(0x1F200, 0x1F2FF), "Enclosed Ideographic Supplement", "zh,ja,ko", UnicodeChart.EnclosedIdeographicSupplement); + + symbolsAndPunctuation[6] = new Group(new UnicodeRange(0x3300, 0x33FF), "Alphanumeric Symbols", "CJK Compatibility", "zh,ja,ko", UnicodeChart.CjkCompatibility); + symbolsAndPunctuation[6].SubGroups = new SubGroup[1]; + symbolsAndPunctuation[6].SubGroups[0] = new SubGroup(new UnicodeRange(0x2100, 0x214F), "Additional Squared Symbols", "zh,ja,ko", UnicodeChart.AdditionalSquaredSymbols); + + symbolsAndPunctuation[7] = new Group(new UnicodeRange(0x2300, 0x23FF), "Technical Symbols", "APL symbols", "any", UnicodeChart.AplSymbols); + symbolsAndPunctuation[8] = new Group(new UnicodeRange(0x2400, 0x243F), "Technical Symbols", "Control Pictures", "any", UnicodeChart.ControlPictures); + symbolsAndPunctuation[9] = new Group(new UnicodeRange(0x2300, 0x23FF), "Technical Symbols", "Miscellaneous Technical", "any", UnicodeChart.MiscellaneousTechnical); + symbolsAndPunctuation[10] = new Group(new UnicodeRange(0x2440, 0x245F), "Technical Symbols", "Optical Character Recognition (OCR)", "any", UnicodeChart.OpticalCharacterRecognition); + symbolsAndPunctuation[11] = new Group(new UnicodeRange(0x20D0, 0x20FF), "Combining Diacritics", "Combining Diacritical Marks for Symbols", "other", UnicodeChart.CombiningDiacriticalMarksForSymbols); + + symbolsAndPunctuation[12] = new Group(new UnicodeRange(0x10100, 0x1013F), "Numbers and Digits", "Aegean", "el", UnicodeChart.AegeanNumbers); + symbolsAndPunctuation[13] = new Group(new UnicodeRange(0x10140, 0x1018F), "Numbers and Digits", "Ancient Greek Numbers", "el", UnicodeChart.AncientGreekNumbers); + + symbolsAndPunctuation[14] = new Group(new UnicodeRange(0x0000, 0x007F), "Numbers and Digits", "ASCII Digits", "latin", UnicodeChart.AsciiDigits); + symbolsAndPunctuation[14].SubGroups = new SubGroup[1]; + symbolsAndPunctuation[14].SubGroups[0] = new SubGroup(new UnicodeRange(0xFF00, 0xFFEF), "Fullwidth ASCII Digits", "latin,ja", UnicodeChart.FullwidthAsciiDigits); + + symbolsAndPunctuation[15] = new Group(new UnicodeRange(0xA830, 0xA83F), "Numbers and Digits", "Common Indic Number Forms", "hi", UnicodeChart.CommonIndicNumberForms); + symbolsAndPunctuation[16] = new Group(new UnicodeRange(0x1D360, 0x1D37F), "Numbers and Digits", "Counting Rod Numerals", "other", UnicodeChart.CountingRodNumerals); + symbolsAndPunctuation[17] = new Group(new UnicodeRange(0x12400, 0x1247F), "Numbers and Digits", "Cuneiform Numbers and Punctuation", "cuneiform", UnicodeChart.CuneiformNumbersAndPunctuation); + symbolsAndPunctuation[18] = new Group(new UnicodeRange(0x2150, 0x218F), "Numbers and Digits", "Number Forms", "latin", UnicodeChart.NumberForms); + symbolsAndPunctuation[19] = new Group(new UnicodeRange(0x10E60, 0x10E7F), "Numbers and Digits", "Rumi Numeral Symbols", "rumi", UnicodeChart.RumiNumeralSymbols); + symbolsAndPunctuation[20] = new Group(new UnicodeRange(0x2070, 0x209F), "Numbers and Digits", "Super and Subscripts", "any", UnicodeChart. SuperAndSubscripts); + + symbolsAndPunctuation[21] = new Group(new UnicodeRange(0x2190, 0x21FF), "Mathematical Symbols", "Arrows", "latin", UnicodeChart.Arrows); + symbolsAndPunctuation[21].SubGroups = new SubGroup[3]; + symbolsAndPunctuation[21].SubGroups[0] = new SubGroup(new UnicodeRange(0x27F0, 0x27FF), "Supplemental Arrows-A", "latin", UnicodeChart.SupplementalArrowsA); + symbolsAndPunctuation[21].SubGroups[1] = new SubGroup(new UnicodeRange(0x2900, 0x297F), "Supplemental Arrows-B", "latin", UnicodeChart.SupplementalArrowsB); + symbolsAndPunctuation[21].SubGroups[2] = new SubGroup(new UnicodeRange(0x2B00, 0x2BFF), "Additional Arrows", "latin", UnicodeChart.AdditionalArrows); + + symbolsAndPunctuation[22] = new Group(new UnicodeRange(0x1D400, 0x1D7FF), "Mathematical Symbols", "Mathematical Alphanumeric Symbols", "latin", UnicodeChart.MathematicalAlphanumericSymbols); + symbolsAndPunctuation[22].SubGroups = new SubGroup[1]; + symbolsAndPunctuation[22].SubGroups[0] = new SubGroup(new UnicodeRange(0x2100, 0x214F), "Letterlike Symbols", "latin", UnicodeChart.LetterlikeSymbols); + + symbolsAndPunctuation[23] = new Group(new UnicodeRange(0x2200, 0x22FF), "Mathematical Symbols", "Mathematical Operators", "any", UnicodeChart.MathematicalOperators); + symbolsAndPunctuation[23].SubGroups = new SubGroup[6]; + symbolsAndPunctuation[23].SubGroups[0] = new SubGroup(new UnicodeRange(0x0000, 0x007F), "Basic operators: Plus, Factorial,", "any", UnicodeChart.BasicOperatorsPlusFactorial); + symbolsAndPunctuation[23].SubGroups[1] = new SubGroup(new UnicodeRange(0x0080, 0x00FF), "Division, Multiplication", "any", UnicodeChart.BasicOperatorsDivisionMultiplication); + symbolsAndPunctuation[23].SubGroups[2] = new SubGroup(new UnicodeRange(0x2A00, 0x2AFF), "Supplemental Mathematical Operators", "any", UnicodeChart.SupplementalMathematicalOperators); + symbolsAndPunctuation[23].SubGroups[3] = new SubGroup(new UnicodeRange(0x27C0, 0x27EF), "Miscellaneous Mathematical Symbols-A", "any", UnicodeChart.MiscellaneousMathematicalSymbolsA); + symbolsAndPunctuation[23].SubGroups[4] = new SubGroup(new UnicodeRange(0x2980, 0x29FF), "Miscellaneous Mathematical Symbols-B", "any", UnicodeChart.MiscellaneousMathematicalSymbolsB); + symbolsAndPunctuation[23].SubGroups[5] = new SubGroup(new UnicodeRange(0x2300, 0x23FF), "Floors and Ceilings", "any", UnicodeChart.FloorsAndCeilings); + + symbolsAndPunctuation[24] = new Group(new UnicodeRange(0x25A0, 0x25FF), "Mathematical Symbols", "Geometric Shapes", "any", UnicodeChart.GeometricShapes); + symbolsAndPunctuation[24].SubGroups = new SubGroup[3]; + symbolsAndPunctuation[24].SubGroups[0] = new SubGroup(new UnicodeRange(0x2B00, 0x2BFF), "Additional Shapes", "any", UnicodeChart.AdditionalShapes); + symbolsAndPunctuation[24].SubGroups[1] = new SubGroup(new UnicodeRange(0x2500, 0x257F), "Box Drawing", "any", UnicodeChart.BoxDrawing); + symbolsAndPunctuation[24].SubGroups[2] = new SubGroup(new UnicodeRange(0x2580, 0x259F), "Block Elements", "any", UnicodeChart.BlockElements); + + symbolsAndPunctuation[25] = new Group(new UnicodeRange(0x10190, 0x101CF), "Other Symbols", "Ancient Symbols", "other", UnicodeChart.AncientSymbols); + symbolsAndPunctuation[26] = new Group(new UnicodeRange(0x2800, 0x28FF), "Other Symbols", "Braille Patterns", "other", UnicodeChart.BraillePatterns); + + symbolsAndPunctuation[27] = new Group(new UnicodeRange(0x20A0, 0x20CF), "Other Symbols", "Currency Symbols", "any", UnicodeChart.CurrencySymbols); + symbolsAndPunctuation[27].SubGroups = new SubGroup[7]; + symbolsAndPunctuation[27].SubGroups[0] = new SubGroup(new UnicodeRange(0x0000, 0x007F), "Dollar Sign", "any", UnicodeChart.DollarSign); + symbolsAndPunctuation[27].SubGroups[1] = new SubGroup(new UnicodeRange(0x20A0, 0x20CF), "Euro Sign", "any", UnicodeChart.EuroSign); + symbolsAndPunctuation[27].SubGroups[2] = new SubGroup(new UnicodeRange(0x0080, 0x00FF), "Yen, Pound and Cent", "any", UnicodeChart.YenPoundAndCent); + symbolsAndPunctuation[27].SubGroups[3] = new SubGroup(new UnicodeRange(0xFF00, 0xFFEF), "Fullwidth Currency Symbols", "any", UnicodeChart.FullwidthCurrencySymbols); + symbolsAndPunctuation[27].SubGroups[4] = new SubGroup(new UnicodeRange(0x2100, 0x214F), "Mark", "de", UnicodeChart.Mark); + symbolsAndPunctuation[27].SubGroups[5] = new SubGroup(new UnicodeRange(0x20A0, 0x20CF), "Pfennig", "de", UnicodeChart.Pfennig); + symbolsAndPunctuation[27].SubGroups[6] = new SubGroup(new UnicodeRange(0xFB50, 0xFDFF), "Rial Sign", "iran", UnicodeChart.RialSign); + + symbolsAndPunctuation[28] = new Group(new UnicodeRange(0x2700, 0x27BF), "Other Symbols", "Dingbats", "any", UnicodeChart.Dingbats); + + // Game Symbols range is not defined. Use checker/chess range. + symbolsAndPunctuation[29] = new Group(new UnicodeRange(0x2600, 0x26FF), "Other Symbols", "Game Symbols", "other", UnicodeChart.GameSymbols); + symbolsAndPunctuation[29].SubGroups = new SubGroup[5]; + symbolsAndPunctuation[29].SubGroups[0] = new SubGroup(new UnicodeRange(0x2600, 0x26FF), "Chess//Checkers", "other", UnicodeChart.ChessCheckers); + symbolsAndPunctuation[29].SubGroups[1] = new SubGroup(new UnicodeRange(0x1F030, 0x1F09F), "Domino Tiles", "other", UnicodeChart.DominoTiles); + symbolsAndPunctuation[29].SubGroups[2] = new SubGroup(new UnicodeRange(0x2600, 0x26FF), "Japanese Chess", "ja", UnicodeChart.JapaneseChess); + symbolsAndPunctuation[29].SubGroups[3] = new SubGroup(new UnicodeRange(0x1F000, 0x1F02F), "Mahjong Tiles", "ja,zh", UnicodeChart.MahjongTiles); + symbolsAndPunctuation[29].SubGroups[4] = new SubGroup(new UnicodeRange(0x2600, 0x26FF), "Card suits", "other", UnicodeChart.CardSuits); + + symbolsAndPunctuation[30] = new Group(new UnicodeRange(0x2600, 0x26FF), "Other Symbols", "Miscellaneous Symbols", "any", UnicodeChart.MiscellaneousSymbols); + symbolsAndPunctuation[30].SubGroups = new SubGroup[1]; + symbolsAndPunctuation[30].SubGroups[0] = new SubGroup(new UnicodeRange(0x2B00, 0x2BFF), "Miscellaneous Symbols and Arrows", "any", UnicodeChart.MiscellaneousSymbolsAndArrows); + + symbolsAndPunctuation[31] = new Group(new UnicodeRange(0x1D100, 0x1D1FF), "Other Symbols", "Musical Symbols", "any", UnicodeChart.MusicalSymbols); + symbolsAndPunctuation[31].SubGroups = new SubGroup[2]; + symbolsAndPunctuation[31].SubGroups[0] = new SubGroup(new UnicodeRange(0x1D200, 0x1D24F), "Ancient Greek Musical Notation", "el", UnicodeChart.AncientGreekMusicalNotation); + symbolsAndPunctuation[31].SubGroups[1] = new SubGroup(new UnicodeRange(0x1D000, 0x1D0FF), "Byzantine Musical Symbols", "other", UnicodeChart.ByzantineMusicalSymbols); + + // Yijing Symbols is not defined. Use Yijing Mono-, Di- and Trigrams range. + symbolsAndPunctuation[32] = new Group(new UnicodeRange(0x2600, 0x26FF), "Other Symbols", "Yijing Symbols", "zh", UnicodeChart.YijingSymbols); + symbolsAndPunctuation[32].SubGroups = new SubGroup[3]; + symbolsAndPunctuation[32].SubGroups[0] = new SubGroup(new UnicodeRange(0x2600, 0x26FF), "Yijing Mono-, Di- and Trigrams", "zh", UnicodeChart.YijingMonoDiAndTrigrams); + symbolsAndPunctuation[32].SubGroups[1] = new SubGroup(new UnicodeRange(0x4DC0, 0x4DFF), "Yijing Hexagram Symbols", "zh", UnicodeChart.YijingHexagramSymbols); + symbolsAndPunctuation[32].SubGroups[2] = new SubGroup(new UnicodeRange(0x1D300, 0x1D35F), "Tai Xuan Jing Symbols", "zh", UnicodeChart.TaiXuanJingSymbols); + + // Controls is not defined. Use C0 and C1 range. + symbolsAndPunctuation[33] = new Group(new UnicodeRange(0x0000, 0x00FF), "Specials", "Controls", "any", UnicodeChart.Controls); + symbolsAndPunctuation[33].SubGroups = new SubGroup[4]; + symbolsAndPunctuation[33].SubGroups[0] = new SubGroup(new UnicodeRange(0x0000, 0x007F), "C0", "any", UnicodeChart.C0); + symbolsAndPunctuation[33].SubGroups[1] = new SubGroup(new UnicodeRange(0x0080, 0x00FF), "C1", "any", UnicodeChart.C1); + symbolsAndPunctuation[33].SubGroups[2] = new SubGroup(new UnicodeRange(0x2000, 0x206F), "Layout Controls", "any", UnicodeChart.LayoutControls); + symbolsAndPunctuation[33].SubGroups[3] = new SubGroup(new UnicodeRange(0x2000, 0x206F), "Invisible Operators", "any", UnicodeChart.InvisibleOperators); + + symbolsAndPunctuation[34] = new Group(new UnicodeRange(0xFFF0, 0xFFFF), "Specials", "Specials", "any", UnicodeChart.Specials); + symbolsAndPunctuation[35] = new Group(new UnicodeRange(0xE0000, 0xE007F), "Specials", "Tags", "any", UnicodeChart.Tags); + + symbolsAndPunctuation[36] = new Group(new UnicodeRange(0xFE00, 0xFE0F), "Specials", "Variation Selectors", "any", UnicodeChart.VariationSelectors); + symbolsAndPunctuation[36].SubGroups = new SubGroup[1]; + symbolsAndPunctuation[36].SubGroups[0] = new SubGroup(new UnicodeRange(0xE0100, 0xE01EF), "Variation Selectors Supplement", "any", UnicodeChart.VariationSelectorsSupplement); + + symbolsAndPunctuation[37] = new Group(new UnicodeRange(0xE000, 0xF8FF), "Private Use", "Private Use Area", "any", UnicodeChart.PrivateUseArea); + symbolsAndPunctuation[38] = new Group(new UnicodeRange(0xF0000, 0xFFFFD), "Private Use", "Supplementary Private Use Area-A", "any", UnicodeChart.SupplementaryPrivateUseAreaA); + symbolsAndPunctuation[39] = new Group(new UnicodeRange(0x100000, 0x10FFFD), "Private Use", "Supplementary Private Use Area-B", "any", UnicodeChart.SupplementaryPrivateUseAreaB); + + symbolsAndPunctuation[40] = new Group(new UnicodeRange(0xD800, 0xDBFF), "Surrogates", "High Surrogates", "zh", UnicodeChart.HighSurrogates); + symbolsAndPunctuation[41] = new Group(new UnicodeRange(0xDC00, 0xDFFF), "Surrogates", "Low Surrogates", "zh", UnicodeChart.LowSurrogates); + + symbolsAndPunctuation[42] = new Group(new UnicodeRange(0xFB50, 0xFDFF), "Noncharacters in UnicodeCharts", "Reserved range", "any", UnicodeChart.ReservedRange); + + // at end of... is not defined. Use BMP range. + symbolsAndPunctuation[43] = new Group(new UnicodeRange(0xFFF0, 0xFFFF), "Noncharacters in UnicodeCharts", "at end of...", "any", UnicodeChart.AtEndOf); + symbolsAndPunctuation[43].SubGroups = new SubGroup[17]; + symbolsAndPunctuation[43].SubGroups[0] = new SubGroup(new UnicodeRange(0xFFF0, 0xFFFF), "BMP", "any", UnicodeChart.Bmp); + symbolsAndPunctuation[43].SubGroups[1] = new SubGroup(new UnicodeRange(0x1FF80, 0x1FFFF), "Plane 1", "any", UnicodeChart.Plane1); + symbolsAndPunctuation[43].SubGroups[2] = new SubGroup(new UnicodeRange(0x2FF80, 0x2FFFF), "Plane 2", "any", UnicodeChart.Plane2); + symbolsAndPunctuation[43].SubGroups[3] = new SubGroup(new UnicodeRange(0x3FF80, 0x3FFFF), "Plane 3", "any", UnicodeChart.Plane3); + symbolsAndPunctuation[43].SubGroups[4] = new SubGroup(new UnicodeRange(0x4FF80, 0x4FFFF), "Plane 4", "any", UnicodeChart.Plane4); + symbolsAndPunctuation[43].SubGroups[5] = new SubGroup(new UnicodeRange(0x5FF80, 0x5FFFF), "Plane 5", "any", UnicodeChart.Plane5); + symbolsAndPunctuation[43].SubGroups[6] = new SubGroup(new UnicodeRange(0x6FF80, 0x6FFFF), "Plane 6", "any", UnicodeChart.Plane6); + symbolsAndPunctuation[43].SubGroups[7] = new SubGroup(new UnicodeRange(0x7FF80, 0x7FFFF), "Plane 7", "any", UnicodeChart.Plane7); + symbolsAndPunctuation[43].SubGroups[8] = new SubGroup(new UnicodeRange(0x8FF80, 0x8FFFF), "Plane 8", "any", UnicodeChart.Plane8); + symbolsAndPunctuation[43].SubGroups[9] = new SubGroup(new UnicodeRange(0x9FF80, 0x9FFFF), "Plane 9", "any", UnicodeChart.Plane9); + symbolsAndPunctuation[43].SubGroups[10] = new SubGroup(new UnicodeRange(0xAFF80, 0xAFFFF), "Plane 10", "any", UnicodeChart.Plane10); + symbolsAndPunctuation[43].SubGroups[11] = new SubGroup(new UnicodeRange(0xBFF80, 0xBFFFF), "Plane 11", "any", UnicodeChart.Plane11); + symbolsAndPunctuation[43].SubGroups[12] = new SubGroup(new UnicodeRange(0xCFF80, 0xCFFFF), "Plane 12", "any", UnicodeChart.Plane12); + symbolsAndPunctuation[43].SubGroups[13] = new SubGroup(new UnicodeRange(0xdFF80, 0xDFFFF), "Plane 13", "any", UnicodeChart.Plane13); + symbolsAndPunctuation[43].SubGroups[14] = new SubGroup(new UnicodeRange(0xEFF80, 0xEFFFF), "Plane 14", "any", UnicodeChart.Plane14); + symbolsAndPunctuation[43].SubGroups[15] = new SubGroup(new UnicodeRange(0xFFF80, 0xFFFFF), "Plane 15", "any", UnicodeChart.Plane15); + symbolsAndPunctuation[43].SubGroups[16] = new SubGroup(new UnicodeRange(0x10FF80, 0x10FFFF), "Plane 16", "any", UnicodeChart.Plane16); + } + } +} + diff --git a/src/dotnetCampus.UITest.WPFTestHelper/Text/UnicodeRangeProperty.cs b/src/dotnetCampus.UITest.WPFTestHelper/Text/UnicodeRangeProperty.cs new file mode 100644 index 0000000..45c727f --- /dev/null +++ b/src/dotnetCampus.UITest.WPFTestHelper/Text/UnicodeRangeProperty.cs @@ -0,0 +1,58 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace dotnetCampus.UITest.WPFTestHelper.Text +{ + /// + /// UnicodeRangeProperty class to store data for a property + /// + internal class UnicodeRangeProperty + { + private TextUtil.UnicodeChartType type; + public string cultureIds; + + /// + /// constructor of PropertyData stuct + /// + public UnicodeRangeProperty(TextUtil.UnicodeChartType type, string name, string ids, UnicodeRange range) + { + Type = type; + Name = name; + CultureIDs = ids; + Range = new UnicodeRange(range.StartOfUnicodeRange, range.EndOfUnicodeRange); + } + + /// + /// Default constructor to null or zero all attributes + /// + public UnicodeRangeProperty() + { + Type = TextUtil.UnicodeChartType.Other; + Name = null; + CultureIDs = null; + Range = new UnicodeRange(0, 0); + } + + /// + /// type of the property + /// + public TextUtil.UnicodeChartType Type { set { type = value; } } + + /// + /// name of the property + /// + public string Name { get; set; } + + /// + /// culuture IDs for the property + /// + public string CultureIDs { set { cultureIds = value; } } + + /// + /// UnicodeRange for the property + /// + public UnicodeRange Range { get; set; } + } +} + diff --git a/src/dotnetCampus.UITest.WPFTestHelper/Theming/HwndInfo.cs b/src/dotnetCampus.UITest.WPFTestHelper/Theming/HwndInfo.cs new file mode 100644 index 0000000..9d58923 --- /dev/null +++ b/src/dotnetCampus.UITest.WPFTestHelper/Theming/HwndInfo.cs @@ -0,0 +1,29 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; + +namespace dotnetCampus.UITest.WPFTestHelper.Theming +{ + internal struct HwndInfo + { + public HwndInfo(IntPtr hWnd) + { + this.hWnd = hWnd; + NativeMethods.GetWindowThreadProcessId(hWnd, out ProcessId); + } + + /// + /// Hwnd + /// + public IntPtr hWnd; + + /// + /// Process ID of Hwnd + /// + public int ProcessId; + + + } +} diff --git a/src/dotnetCampus.UITest.WPFTestHelper/Theming/NativeMethods.cs b/src/dotnetCampus.UITest.WPFTestHelper/Theming/NativeMethods.cs new file mode 100644 index 0000000..c822edf --- /dev/null +++ b/src/dotnetCampus.UITest.WPFTestHelper/Theming/NativeMethods.cs @@ -0,0 +1,78 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Runtime.InteropServices; +using System.Text; + +namespace dotnetCampus.UITest.WPFTestHelper.Theming +{ + internal static class NativeMethods + { + #region Const data + + private const string Gdi32Dll = "GDI32.dll"; + private const string User32Dll = "User32.dll"; + + public const int SW_HIDE = 0; + public const int SW_SHOWNORMAL = 1; + public const int SW_NORMAL = 1; + public const int SW_SHOWMINIMIZED = 2; + public const int SW_SHOWMAXIMIZED = 3; + public const int SW_MAXIMIZE = 3; + public const int SW_SHOWNOACTIVATE = 4; + public const int SW_SHOW = 5; + public const int SW_MINIMIZE = 6; + public const int SW_SHOWMINNOACTIVE = 7; + public const int SW_SHOWNA = 8; + public const int SW_RESTORE = 9; + public const int SW_SHOWDEFAULT = 10; + public const int SW_FORCEMINIMIZE = 11; + public const int SW_MAX = 11; + + #endregion Const data + + #region Methods + + [DllImport(User32Dll)] + public static extern IntPtr GetForegroundWindow(); + + [DllImport(User32Dll)] + public static extern bool BringWindowToTop(IntPtr hWnd); + + [DllImport(User32Dll, EntryPoint = "IsWindowVisible", PreserveSig = true)] + public static extern bool IsWindowVisible(IntPtr hWnd); + + [DllImport(User32Dll)] + public static extern bool ShowWindow(IntPtr hWnd, int nCmdShow); + + [DllImport(User32Dll)] + public static extern void SetForegroundWindow(IntPtr hWnd); + + [DllImport(User32Dll)] + public static extern bool CloseWindow(IntPtr hWnd); + + [DllImport(User32Dll)] + public static extern bool DestroyWindow(IntPtr hWnd); + + [DllImport(User32Dll, EntryPoint = "GetClassName")] + public static extern int GetClassName(IntPtr hwnd, [MarshalAs(UnmanagedType.LPStr)] StringBuilder buf, int nMaxCount); + + [DllImport(User32Dll, EntryPoint = "GetWindowText")] + public static extern int GetWindowText(IntPtr hwnd, [MarshalAs(UnmanagedType.LPStr)] StringBuilder buf, int nMaxCount); + + [DllImport(User32Dll, EntryPoint = "EnumThreadWindows", PreserveSig = true, SetLastError = true)] + public static extern bool EnumThreadWindows(int threadId, EnumProcCallback callback, IntPtr lParam); + + [DllImport(User32Dll, EntryPoint = "EnumChildWindows", PreserveSig = true, SetLastError = true)] + public static extern bool EnumChildWindows(IntPtr hWndParent, EnumProcCallback callback, IntPtr lParam); + + public delegate bool EnumProcCallback(IntPtr hwnd, IntPtr lParam); + + [DllImport(User32Dll)] + public static extern int GetWindowThreadProcessId(IntPtr hWnd, out int processId); + + #endregion Methods + } +} diff --git a/src/dotnetCampus.UITest.WPFTestHelper/Theming/Theme.cs b/src/dotnetCampus.UITest.WPFTestHelper/Theming/Theme.cs new file mode 100644 index 0000000..2847a30 --- /dev/null +++ b/src/dotnetCampus.UITest.WPFTestHelper/Theming/Theme.cs @@ -0,0 +1,839 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Diagnostics; +using System.IO; +using System.Threading; +using dotnetCampus.UITest.WPFTestHelper.Input; +using Microsoft.Win32; + +namespace dotnetCampus.UITest.WPFTestHelper.Theming +{ + /// + /// Enables changing of the Theme configuration of the system. + /// + /// + /// + /// The following example demonstrates changing the OS theme to each available + /// system theme to verify a control's appearance. + /// + /// + /// Theme originalTheme = Theme.GetCurrent(); + /// + /// try + /// { + /// Theme[] availableThemes = Theme.GetAvailableSystemThemes(); + /// foreach (Theme theme in availableThemes) + /// { + /// Theme.SetCurrent(theme); + /// VerifyMyControlAppearance(theme); + /// } + /// } + /// finally + /// { + /// Theme.SetCurrent(originalTheme); + /// } + /// + /// + public class Theme + { + #region Private Data + + private readonly static string ThemeProcessName; + private readonly static string ThemeDir = Environment.ExpandEnvironmentVariables(@"%WINDIR%\Resources\Themes"); + private readonly static string AccessibleThemesDir = Environment.ExpandEnvironmentVariables(@"%WINDIR%\Resources\Ease of Access Themes"); + + #endregion Private Data + + #region Enums + + /// + /// Enum for High Contrast theme names. These + /// do not work on Vista - ok to use on Win7+. + /// + public enum HighContrastTheme + { + /// + /// High Contrast #1 + /// Background: Black + /// Foreground: Yellow + /// + hc1, + /// + /// High Contrast #2 + /// Background: Black + /// Foreground: Green + /// + hc2, + /// + /// High Contrast Black + /// Background: Black + /// Foreground: White + /// + hcblack, + /// + /// High Contrast White + /// Background: White + /// Foreground: Black + /// + hcwhite + } + + #endregion + + #region Constructors + + static Theme() + { + // 6.1 (Vista) and below have a different process for Theme automation + if (Environment.OSVersion.Version < new Version("6.1")) + { + ThemeProcessName = "rundll32"; + } + else + { + ThemeProcessName = "explorer"; + } + } + + /// + /// No default constructor for Theme class + /// + private Theme() + { + } + + /// + /// Constructor that takes a file and retrieves the rest + /// of the info directly from the theme file. + /// + private Theme(FileInfo path) + { + Path = path; + IsEnabled = true; + SetThemeProperties(this); + } + + private Theme(FileInfo path, string name, string style, bool isEnabled) + { + Path = path; + Name = name; + Style = style; + IsEnabled = isEnabled; + } + + #endregion Constructors + + #region Public Static Members + + /// + /// Returns the current OS theme. + /// + /// Returns the current OS theme. + public static Theme GetCurrent() + { + string themeFilename = GetCurrentThemePath(); + string style = GetCurrentThemeStyle(); + bool isEnabled = GetCurrentThemeIsEnabled(); + string themeName = string.Empty; + if (!string.IsNullOrEmpty(themeFilename)) + { + themeName = System.IO.Path.GetFileNameWithoutExtension(themeFilename); + } + + return new Theme(new FileInfo(themeFilename), themeName, style, isEnabled); + } + + /// + /// Sets the current OS theme with the given theme parameter. + /// + /// The theme to set the OS theme to. + public static void SetCurrent(Theme theme) + { + lock (typeof(Theme)) + { + EnsureTheme(theme); + + // only change the theme if we are in a different theme + if (GetCurrent().Path.FullName != theme.Path.FullName) + { + // Copy the file to another location before setting the theme. + // The reason for this is that if the custom theme is left after the test + // executes and a test harness possibly deletes the current theme, the system + // issues an error that prevents the system from restoring the default theme + string destFilename = System.IO.Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.InternetCache), + System.IO.Path.GetFileName(theme.Path.FullName)); + File.Copy(theme.Path.FullName, destFilename, true); + + // programmatically setting theme behavior is different for 6.1 and up + if (Environment.OSVersion.Version < new Version("6.1")) + { + MonitorProcess(ThemeProcessName, theme.Path.FullName); + } + else + { + SetThemeThroughExplorer(theme.Path.FullName); + } + } + + WaitForThemeSet(theme.Path.FullName); + } + } + + /// + /// Sets the current OS theme with the given fileInfo parameter. + /// + /// The fileInfo to set the theme to. + public static void SetCurrent(FileInfo fileInfo) + { + SetCurrent(new Theme(fileInfo)); + } + + /// + /// Sets the current OS theme using the given HighContrastTheme parameter + /// + /// + public static void SetCurrent(HighContrastTheme theme) + { + var themes = new List(Theme.GetAccessibleThemes()); + + var hcTheme = themes.Find((Theme t) => + { + return (string.Equals(t.Name, theme.ToString(), StringComparison.InvariantCultureIgnoreCase)); + + }); + + if (hcTheme == null) + { + throw new NotSupportedException($"Theme {theme.ToString()} is not supported"); + } + + Theme.SetCurrent(hcTheme); + } + + /// + /// Sets the current OS theme with the given theme parameter + /// + /// + /// + public static ThemeSwitcher SetTheme(Theme theme) + { + return new ThemeSwitcher(theme); + } + + /// + /// Sets the current OS theme using the given HighContrastTheme parameter. + /// + /// + /// + public static ThemeSwitcher SetTheme(HighContrastTheme theme) + { + return new ThemeSwitcher(theme); + } + + /// + /// Returns all the available system themes on the OS. + /// + /// Returns all the available system themes + public static Theme[] GetAvailableSystemThemes() + { + return GetThemesFromDirectory(ThemeDir); + } + + /// + /// Returns all the available accessible themes on the OS + /// + /// + public static Theme[] GetAccessibleThemes() + { + try + { + return GetThemesFromDirectory(AccessibleThemesDir); + } + catch (Exception e) + { + throw new PlatformNotSupportedException("This method is not supported on Vista", e); + } + } + + + #endregion Public Static Members + + #region Public Properties + + /// + /// Gets the theme name. + /// + public string Name + { + get; + private set; + } + + /// + /// Gets the path of the theme. + /// + /// + /// Should be of type *.theme. + /// + public FileInfo Path + { + get; + private set; + } + + /// + /// Gets the color style of the theme. + /// + public string Style + { + get; + private set; + } + + /// + /// Gets the IsEnabled state of the theme. + /// + public bool IsEnabled + { + get; + private set; + } + + #endregion Public Properties + + #region Private Members + + private static string GetCurrentThemePath() + { + // 6.1 (Vista) and below have a different process for Theme automation + if (Environment.OSVersion.Version < new Version("6.1")) + { + using (RegistryKey currentTheme = Registry.CurrentUser.OpenSubKey(@"Software\Microsoft\Plus!\Themes\Current")) + { + if (currentTheme == null) + { + return null; + } + + return currentTheme.GetValue(null) as string; + } + } + else + { + string themeFilename = Registry.GetValue(@"HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Themes", "CurrentTheme", "") as string; + if (!String.IsNullOrEmpty(themeFilename)) + { + return themeFilename.ToLowerInvariant(); + } + + return null; + } + } + + private static string GetCurrentThemeStyle() + { + string themeStyle = Registry.GetValue(@"HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\ThemeManager", "ColorName", "") as string; + if (!string.IsNullOrEmpty(themeStyle)) + { + return themeStyle.ToLowerInvariant(); + } + + return null; + } + + private static bool GetCurrentThemeIsEnabled() + { + string themeActive = (string)Registry.GetValue(@"HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\ThemeManager", "ThemeActive", string.Empty); + return string.Compare(themeActive, "1") == 0; + } + + private static void SetThemeProperties(Theme theme) + { + EnsureTheme(theme); + + // find the [visualstyles]colorstyle property in the the theme file + string themeStyle = string.Empty; + using (var fs = new FileStream(theme.Path.FullName, FileMode.Open, FileAccess.Read)) + { + using (var sr = new StreamReader(fs)) + { + var line = sr.ReadLine(); + while (!sr.EndOfStream && line != null) + { + if (line.ToLowerInvariant() == "[visualstyles]") + { + line = sr.ReadLine(); + while (!sr.EndOfStream && !string.IsNullOrEmpty(line)) + { + if (line.ToLowerInvariant().Contains("colorstyle")) + { + var keyValuePair = line.Split(new string[] { "=" }, StringSplitOptions.RemoveEmptyEntries); + if (keyValuePair != null && keyValuePair.Length == 2) + { + themeStyle = keyValuePair[1]; + break; + } + } + + line = sr.ReadLine(); + } + + break; + } + + line = sr.ReadLine(); + } + } + } + + if (!string.IsNullOrEmpty(themeStyle)) + { + theme.Style = themeStyle; + } + + theme.Name = System.IO.Path.GetFileNameWithoutExtension(theme.Path.FullName).ToLowerInvariant(); + } + + private static void EnsureTheme(Theme theme) + { + if (theme == null) + { + throw new ArgumentException("theme cannot be null."); + } + + if (theme.Path != null && string.IsNullOrEmpty(theme.Path.FullName)) + { + throw new ArgumentException("theme path cannot be null or empty."); + } + + if (!File.Exists(theme.Path.FullName)) + { + throw new ArgumentException("The theme '" + theme.Path.FullName + "' does not exist.", theme.Path.FullName); + } + + if (System.IO.Path.GetExtension(theme.Path.FullName).ToLowerInvariant() != ".theme") + { + throw new ArgumentException("The theme file must have a .theme extention.", "filename"); + } + } + + private static void WaitForThemeSet(string themeToBeSet) + { + int counter = 0; + int max = 20; + + do + { + Thread.Sleep(1500); + counter++; + } while (counter < max && Theme.GetCurrent().Path.FullName.ToLower() != themeToBeSet.ToLower()); + } + + /// + /// For the case of OS versons below 6.1 the process for setting + /// the theme is as follows: + /// + /// 1. launch the .theme file + /// 2. wait for the new rundll32 to start. + /// 3. press enter to select the theme and close the dialog + /// + private static void MonitorProcess(string processName, string themeFilename) + { + // Kill all processes with name = processName. + // This will help ensure that FindProcessName will always return the + // one unique process that we should wait on. + KillProcesses(processName); + + // Get the active window since the window activation will be lost + IntPtr activehWnd = IntPtr.Zero; + ManualResetEvent waitEvent = new ManualResetEvent(false); + System.Timers.Timer timer = null; + bool enterFlag = true; + + try + { + // initialize the wait event + waitEvent.Reset(); + activehWnd = NativeMethods.GetForegroundWindow(); + + // set the actual theme which launches the rundll32.exe window + Process themeChangeProcess = new Process(); + themeChangeProcess.StartInfo.FileName = themeFilename; + themeChangeProcess.Start(); + + bool themeSet = false; + + // wait for the window to activate + timer = new System.Timers.Timer(1500); + timer.Elapsed += (s, e) => + { + if (enterFlag) + { + enterFlag = false; + + // find the process to monitor + Process process = FindProcessFromName(processName); + if (process != null) + { + process.Refresh(); + + HwndInfo[] topLevelWindows = WindowEnumerator.GetTopLevelVisibleWindows(process); + if (topLevelWindows.Length > 0) + { + // In case the dialog was opened previously + NativeMethods.SetForegroundWindow(topLevelWindows[0].hWnd); + Thread.Sleep(1000); + + // The process monitor calls back more than once, only do this action once. + if (!themeSet) + { + themeSet = true; + + // press enter to confirm and set the theme + Keyboard.Type(Key.Return); + } + } + } + else + { + // For good measure + Thread.Sleep(1000); + waitEvent.Set(); + } + + enterFlag = true; + } + }; + timer.Start(); + + waitEvent.WaitOne(60000, false); + } + finally + { + enterFlag = false; + + if (timer != null) + { + timer.Stop(); + timer.Dispose(); + timer = null; + } + + // Restore the active window + if (activehWnd != IntPtr.Zero) + { + NativeMethods.SetForegroundWindow(activehWnd); + } + } + } + + private static List FindProcessesFromName(string processName) + { + Process[] currentProcesses = new Process[0]; + List result = new List(); + + try + { // Workaround whidbey + currentProcesses = Process.GetProcesses(); + } + catch (Win32Exception) + { + //Unable to enumerate Process Moduals (this is probobly a whidbey + } + + foreach (Process process in currentProcesses) + { + // Get the name and ensure that the process is still running + string procName; + try + { + procName = process.ProcessName; + // Query to process to ensure that we have access to it and it has not exited + if (process.HasExited) + { + continue; + } + } + catch (InvalidOperationException) + { + // the process has exited + continue; + } + catch (Win32Exception) + { + // strange whidbey + + continue; + } + + if (string.Equals(procName, processName, StringComparison.InvariantCultureIgnoreCase)) + { + result.Add(process); + } + } + + return result; + } + + private static Process FindProcessFromName(string processName) + { + List processes = FindProcessesFromName(processName); + + if ((processes.Count != 1) || (processes[0].HasExited)) + { + throw new Exception(string.Format("Unique process with name: {0} not found", processName)); + } + + return processes[0]; + } + + private static void KillProcesses(string processName) + { + List processes = FindProcessesFromName(processName); + + foreach (Process process in processes) + { + if (process.HasExited) + { + continue; + } + + process.Kill(); + } + } + + + + /// + /// For the case of OS versons 6.1 and above the process for setting + /// the theme is as follows: + /// + /// 1. launch the .theme file (theme is automatically selected) + /// 2. wait for explorer to launch the 'personalization' child window + /// 3. close the 'personalization' window + /// + /// Unlike MonitorProcess, explorer.exe is already running and a subwindow is launched for + /// this process. + /// + private static void SetThemeThroughExplorer(string themeFilename) + { + // Get the active window since the window activation will be lost + // by lauching the ControlPanel.Personalization window. + IntPtr activehWnd = IntPtr.Zero; + ManualResetEvent waitEvent = new ManualResetEvent(false); + System.Timers.Timer timer = null; + bool enterFlag = true; + + try + { + // initialize the wait event + waitEvent.Reset(); + activehWnd = NativeMethods.GetForegroundWindow(); + + // set the actual theme which launches the personalization window + Process themeChangeProcess = new Process(); + themeChangeProcess.StartInfo.FileName = themeFilename; + themeChangeProcess.StartInfo.UseShellExecute = true; + themeChangeProcess.Start(); + + // wait for the window to activate + timer = new System.Timers.Timer(1500); + timer.Elapsed += (s, e) => + { + if (enterFlag) + { + enterFlag = false; + + foreach (var process in Process.GetProcesses()) + { + if (process.ProcessName.ToLower() == ThemeProcessName) + { + IntPtr hWnd = WindowEnumerator.FindFirstWindowWithCaption(process, "personalization"); + if (hWnd != IntPtr.Zero) + { + // first make sure it's active + int maxCounter = 20; + int counter = 0; + IntPtr foregroundHWnd = NativeMethods.GetForegroundWindow(); + Console.WriteLine("Thread: " + Thread.CurrentThread.ManagedThreadId + ", foregourndHWnd: " + foregroundHWnd + ", hWnd: " + hWnd); + while ((foregroundHWnd != hWnd || !NativeMethods.IsWindowVisible(hWnd)) && + counter < maxCounter && + timer != null) + { + Console.WriteLine("Thread: " + Thread.CurrentThread.ManagedThreadId + ", restore the window. hWnd: " + hWnd + ", foregroundHWnd: " + foregroundHWnd); + + NativeMethods.ShowWindow(hWnd, NativeMethods.SW_RESTORE); + Thread.Sleep(500); + + NativeMethods.BringWindowToTop(hWnd); + Thread.Sleep(500); + + foregroundHWnd = NativeMethods.GetForegroundWindow(); + counter++; + } + + if (foregroundHWnd == hWnd) + { + timer.Stop(); + + CloseAndWaitForWindow(); + waitEvent.Set(); + } + + break; + } + } + } + + enterFlag = true; + } + }; + timer.Start(); + + waitEvent.WaitOne(60000, false); + } + finally + { + enterFlag = false; + + if (timer != null) + { + timer.Stop(); + timer.Dispose(); + timer = null; + } + + // Restore the active window + if (activehWnd != IntPtr.Zero) + { + NativeMethods.SetForegroundWindow(activehWnd); + } + } + } + + private static void CloseAndWaitForWindow() + { + // close the window + Keyboard.Press(Key.Alt); + Keyboard.Press(Key.F4); + Keyboard.Release(Key.F4); + Keyboard.Release(Key.Alt); + + int counter = 0; + int max = 20; + bool found = true; + while (counter < max && found) + { + Thread.Sleep(1000); + counter++; + + found = false; + foreach (var process in Process.GetProcesses()) + { + if (process.ProcessName.ToLower() == ThemeProcessName) + { + IntPtr hWnd = WindowEnumerator.FindFirstWindowWithCaption(process, "personalization"); + if (hWnd != IntPtr.Zero) + { + found = true; + break; + } + } + } + } + } + + private static Theme[] GetThemesFromDirectory(string directory) + { + string[] files = Directory.GetFiles(directory, "*.theme"); + Theme[] themes = new Theme[files.Length]; + for (int i = 0; i < files.Length; i++) + { + themes[i] = new Theme(new FileInfo(files[i])); + } + + return themes; + } + + #endregion + + #region Public Types + + /// + /// Helper class capable of reverting to original theme + /// using the IDispose pattern + /// + public class ThemeSwitcher: IDisposable + { + + #region IDisposable Support + + private bool _disposed = false; // To detect redundant calls + + /// + /// + /// + /// + protected virtual void Dispose(bool disposing) + { + if (!_disposed) + { + _disposed = true; + Theme.SetCurrent(_originalTheme); + } + } + + /// + /// + /// + ~ThemeSwitcher() + { + Dispose(false); + } + + /// + /// + /// + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + #endregion + + /// + /// + /// + /// + public ThemeSwitcher(Theme theme):this() + { + Theme.SetCurrent(theme); + } + + /// + /// + /// + /// + public ThemeSwitcher(HighContrastTheme theme): this() + { + Theme.SetCurrent(theme); + } + + private ThemeSwitcher() + { + _originalTheme = Theme.GetCurrent(); + } + + Theme _originalTheme; + } + + #endregion + } +} diff --git a/src/dotnetCampus.UITest.WPFTestHelper/Theming/WindowEnumerator.cs b/src/dotnetCampus.UITest.WPFTestHelper/Theming/WindowEnumerator.cs new file mode 100644 index 0000000..877bd3f --- /dev/null +++ b/src/dotnetCampus.UITest.WPFTestHelper/Theming/WindowEnumerator.cs @@ -0,0 +1,182 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Diagnostics; +using System.Text; + +namespace dotnetCampus.UITest.WPFTestHelper.Theming +{ + /// + /// Class for Enumerating Window Handles + /// + internal static class WindowEnumerator + { + internal static HwndInfo[] GetTopLevelVisibleWindows(Process process) + { + ProcessThreadCollection threads; + + try + { + threads = process.Threads; + } + catch (InvalidOperationException) + { + //the process has exited + return new HwndInfo[0]; + } + catch (Win32Exception) + { + //The process has already exited or access is denied (this is probobly a whidbey + return new HwndInfo[0]; + } + + var list = new List(); + foreach (ProcessThread thread in process.Threads) + { + NativeMethods.EnumThreadWindows( + thread.Id, + (IntPtr hWnd, IntPtr lParam) => + { + if (NativeMethods.IsWindowVisible(hWnd)) + { + list.Add(new HwndInfo(hWnd)); + } + + return true; + }, + IntPtr.Zero); + } + + return list.ToArray(); + } + + internal static HwndInfo[] GetVisibleWindows(Process process) + { + var list = new List(); + var windows = GetTopLevelVisibleWindows(process); + foreach (HwndInfo window in windows) + { + if (NativeMethods.IsWindowVisible(window.hWnd)) + { + list.Add(new HwndInfo(window.hWnd)); + } + + // search child windows + NativeMethods.EnumChildWindows( + window.hWnd, + (IntPtr hWnd, IntPtr lParam) => + { + if (NativeMethods.IsWindowVisible(hWnd)) + { + list.Add(new HwndInfo(hWnd)); + } + + return true; + }, + IntPtr.Zero); + + } + + return list.ToArray(); + } + + internal static IntPtr FindFirstWindowWithClassName(Process process, string classname) + { + var foundhWnd = IntPtr.Zero; + var windows = GetTopLevelVisibleWindows(process); + foreach (HwndInfo window in windows) + { + if (IsVisibleWithClassName(window.hWnd, classname)) + { + return window.hWnd; + } + + //search child windows + NativeMethods.EnumChildWindows( + window.hWnd, + (IntPtr hWnd, IntPtr lParam) => + { + if (IsVisibleWithClassName(hWnd, classname)) + { + foundhWnd = hWnd; + return false; + } + return true; + }, + IntPtr.Zero); + + } + + return foundhWnd; + } + + internal static bool IsVisibleWithClassName(IntPtr hWnd, string classname) + { + var sb = new StringBuilder(256); + NativeMethods.GetClassName(hWnd, sb, 256); + return (NativeMethods.IsWindowVisible(hWnd) && sb.ToString().Contains(classname)); + } + + internal static IntPtr FindFirstWindowWithCaption(Process process, string caption) + { + IntPtr foundhWnd = IntPtr.Zero; + HwndInfo[] windows = GetTopLevelVisibleWindows(process); + foreach (HwndInfo window in windows) + { + if (IsVisibleWithCaption(window.hWnd, caption)) + { + return window.hWnd; + } + + //search child windows + NativeMethods.EnumChildWindows( + window.hWnd, + (IntPtr hWnd, IntPtr lParam) => + { + if (IsVisibleWithCaption(hWnd, caption)) + { + foundhWnd = hWnd; + return false; + } + + return true; + }, + IntPtr.Zero); + } + + return foundhWnd; + } + + internal static bool IsVisibleWithCaption(IntPtr hWnd, string caption) + { + var sb = new StringBuilder(256); + NativeMethods.GetWindowText(hWnd, sb, 256); + return (NativeMethods.IsWindowVisible(hWnd) && sb.ToString().ToLower().Contains(caption.ToLower())); + } + + internal static HwndInfo[] GetOutOfProcessVisibleChildWindows(IntPtr parenthWnd) + { + var parentWindow = new HwndInfo(parenthWnd); + var list = new List(); + NativeMethods.EnumChildWindows( + parenthWnd, + (IntPtr hWnd, IntPtr lParam) => + { + var childWindow = new HwndInfo(hWnd); + if (NativeMethods.IsWindowVisible(hWnd) && parentWindow.ProcessId != childWindow.ProcessId) + { + list.Add(childWindow); + } + + return true; + }, + IntPtr.Zero); + + return list.ToArray(); + } + } +} diff --git a/src/dotnetCampus.UITest.WPFTestHelper/VariationGeneration/CandidateCoverage.cs b/src/dotnetCampus.UITest.WPFTestHelper/VariationGeneration/CandidateCoverage.cs new file mode 100644 index 0000000..cd49f55 --- /dev/null +++ b/src/dotnetCampus.UITest.WPFTestHelper/VariationGeneration/CandidateCoverage.cs @@ -0,0 +1,16 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + + +namespace dotnetCampus.UITest.WPFTestHelper.VariationGeneration +{ + /// + /// Pairs a combination in consideration for addition to a variation with how many combinations it will cover + /// + internal class CandidateCoverage + { + public ValueCombination Value { get; set; } + public int CoverageCount { get; set; } + } +} diff --git a/src/dotnetCampus.UITest.WPFTestHelper/VariationGeneration/Constraint.cs b/src/dotnetCampus.UITest.WPFTestHelper/VariationGeneration/Constraint.cs new file mode 100644 index 0000000..f165c7b --- /dev/null +++ b/src/dotnetCampus.UITest.WPFTestHelper/VariationGeneration/Constraint.cs @@ -0,0 +1,78 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + + +using System; +using System.Linq.Expressions; +using dotnetCampus.UITest.WPFTestHelper.VariationGeneration.Constraints; + +namespace dotnetCampus.UITest.WPFTestHelper.VariationGeneration +{ + /// + /// Represents a relationship between parameters and their values, or other constraints. + /// + /// + /// Exhaustively testing all possible inputs to any nontrivial software component is generally not possible + /// because of the enormous number of variations. Combinatorial testing is one approach that achieves high coverage + /// with a much smaller set of variations. Pairwise, the most common combinatorial strategy, tests every possible + /// pair of values. Higher orders of combinations (three-wise, four-wise, and so on) can also be used for higher coverage + /// at the expense of more variations. See Pairwise Testing and + /// + /// Pairwise Testing in Real World for more resources. + /// + /// Ideally, all parameters in a model are independent; however, this is generally not the case. Constraints define + /// combinations of values that are impossible in the variations produced by the using + /// combinatorial testing techniques. + /// + + public abstract class Constraint where T : new() + { + /// + /// Calculates the exclusions for this constraint. + /// + /// The model + /// + /// A table containing the interaction between parameters for this constraint. All values are marked Excluded or Covered. + /// + internal abstract ParameterInteraction GetExcludedCombinations(Model model); + + /// + /// Calcultes whether the specified value satisfies the constraint or has insufficient data to do so. + /// + /// The model. + /// The value. + /// The calculated result. + internal abstract ConstraintSatisfaction SatisfiesContraint(Model model, ValueCombination combination); + + /// + /// Holds a precalculated to avoid recalculation. + /// + internal ParameterInteraction CachedInteraction { get; set; } + + /// + /// Clears CachedInteraction for the constraint and any children. + /// + internal abstract void ClearCache(); + + /// + /// Creates a new predicate for an if-then-else constraint. + /// + /// The test as an expression. + /// The predicate. + public static IfPredicate If(Expression> predicate) + { + return new IfPredicate(predicate); + } + + /// + /// Creates a new ConditionalConstraint. + /// + /// The test as an expression. + /// The constraint. + public static ConditionalConstraint Conditional(Expression> predicate) + { + return new ConditionalConstraint(predicate); + } + } +} diff --git a/src/dotnetCampus.UITest.WPFTestHelper/VariationGeneration/ConstraintExtensions.cs b/src/dotnetCampus.UITest.WPFTestHelper/VariationGeneration/ConstraintExtensions.cs new file mode 100644 index 0000000..fe2e064 --- /dev/null +++ b/src/dotnetCampus.UITest.WPFTestHelper/VariationGeneration/ConstraintExtensions.cs @@ -0,0 +1,41 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + + +using System; +using System.Linq.Expressions; +using dotnetCampus.UITest.WPFTestHelper.VariationGeneration.Constraints; + +namespace dotnetCampus.UITest.WPFTestHelper.VariationGeneration +{ + /// + /// Holds extension methods that construct if-then-else constraints. + /// + public static class ConstraintExtensions + { + /// + /// Constructs an if-then constraint. + /// + /// Type of the variation being acted on. + /// The "if" portion of the if-then. + /// The test of the "then". + /// The if-then constraint. + public static IfThenConstraint Then(this IfPredicate ifPredicate, Expression> predicate) where T : new() + { + return new IfThenConstraint(ifPredicate, predicate); + } + + /// + /// Constructs an if-then-else constraint. + /// + /// Type of the variation being acted on. + /// The "if-then" portion of the if-then-else. + /// The test of the "else". + /// The if-then-else constraint. + public static IfThenElseConstraint Else(this IfThenConstraint ifThenConstraint, Expression> predicate) where T : new() + { + return new IfThenElseConstraint(ifThenConstraint, predicate); + } + } +} diff --git a/src/dotnetCampus.UITest.WPFTestHelper/VariationGeneration/ConstraintSatisfaction.cs b/src/dotnetCampus.UITest.WPFTestHelper/VariationGeneration/ConstraintSatisfaction.cs new file mode 100644 index 0000000..6cf9aae --- /dev/null +++ b/src/dotnetCampus.UITest.WPFTestHelper/VariationGeneration/ConstraintSatisfaction.cs @@ -0,0 +1,14 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + + +namespace dotnetCampus.UITest.WPFTestHelper.VariationGeneration +{ + internal enum ConstraintSatisfaction + { + Satisfied, + Unsatisfied, + InsufficientData + } +} diff --git a/src/dotnetCampus.UITest.WPFTestHelper/VariationGeneration/Constraints/CachedExpressionConstraintData.cs b/src/dotnetCampus.UITest.WPFTestHelper/VariationGeneration/Constraints/CachedExpressionConstraintData.cs new file mode 100644 index 0000000..e4ab928 --- /dev/null +++ b/src/dotnetCampus.UITest.WPFTestHelper/VariationGeneration/Constraints/CachedExpressionConstraintData.cs @@ -0,0 +1,15 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + + +using System.Collections.Generic; + +namespace dotnetCampus.UITest.WPFTestHelper.VariationGeneration.Constraints +{ + class CachedExpressionConstraintData + { + public IList Parameters { get; set; } + public ParameterInteraction Interaction { get; set; } + } +} diff --git a/src/dotnetCampus.UITest.WPFTestHelper/VariationGeneration/Constraints/ConditionalConstraint.cs b/src/dotnetCampus.UITest.WPFTestHelper/VariationGeneration/Constraints/ConditionalConstraint.cs new file mode 100644 index 0000000..831f3b2 --- /dev/null +++ b/src/dotnetCampus.UITest.WPFTestHelper/VariationGeneration/Constraints/ConditionalConstraint.cs @@ -0,0 +1,142 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; + +namespace dotnetCampus.UITest.WPFTestHelper.VariationGeneration.Constraints +{ + /// + /// Provides a constraint that uses expressions to determine the validity of a variation in a model. + /// + public class ConditionalConstraint : Constraint where T : new() + { + /// + /// Initializes a new ConditionalConstraint(T) instance. + /// + public ConditionalConstraint() + { + } + + /// + /// Initializes a new ConditionalConstraint(T) instance. + /// + /// The condition. + public ConditionalConstraint(Expression> condition) + { + Condition = condition; + } + + /// + /// An expression that takes a variation as input and returns true if it is valid. + /// + public Expression> Condition { get; set; } + + internal override ParameterInteraction GetExcludedCombinations(Model model) + { + if (CachedInteraction != null) + { + return new ParameterInteraction(CachedInteraction); + } + + parameters = new ParameterListBuilder(mapExpressionsToRequiredParams, model.Parameters, typeof(T)).GetParameters(Condition); + + CachedInteraction = CreateInteraction(model, Condition, parameters, Condition.Parameters[0]); + + foreach (var item in mapExpressionsToRequiredParams) + { + if (item.Key == Condition) + { + mapExpressionsToRequiredParams[item.Key].Interaction = CachedInteraction; + continue; + } + + mapExpressionsToRequiredParams[item.Key].Interaction = CreateInteraction(model, item.Key, item.Value.Parameters, Condition.Parameters[0]); + } + + return CachedInteraction; + } + + static ParameterInteraction CreateInteraction(Model model, Expression expression, IList parameters, ParameterExpression parameterExpr) + { + + Func filter = expression is Expression> ? + ((Expression>)expression).Compile() : + Expression.Lambda>(expression, parameterExpr).Compile(); + + + var parameterIndices = (from parameter in parameters + select model.Parameters.IndexOf(parameter)).ToList(); + + parameterIndices.Sort(); + + ParameterInteraction interaction = new ParameterInteraction(parameterIndices); + List valueTable = ParameterInteractionTable.GenerateValueTable(model.Parameters, interaction); + + foreach (var valueIndices in valueTable) + { + ValueCombinationState comboState = filter(BuildVariation(model, parameterIndices, valueIndices)) ? ValueCombinationState.Covered : ValueCombinationState.Excluded; + interaction.Combinations.Add(new ValueCombination(valueIndices, interaction) { State = comboState }); + } + + return interaction; + } + + internal override ConstraintSatisfaction SatisfiesContraint(Model model, ValueCombination combination) + { + if (CachedInteraction == null) + { + GetExcludedCombinations(model); + } + + if(CachedInteraction.Parameters.Any((index) => combination.ParameterToValueMap.ContainsKey(index))) + { + // supplied combination is a superset + if (CachedInteraction.Parameters.All((index) => combination.ParameterToValueMap.ContainsKey(index))) + { + var combo = CachedInteraction.Combinations.First((c) => ParameterInteractionTable.MatchCombination(c, combination)); + return combo.State == ValueCombinationState.Excluded ? ConstraintSatisfaction.Unsatisfied : ConstraintSatisfaction.Satisfied; + } + else + { + return new ConstraintSatisfactionExpressionVisitor(mapExpressionsToRequiredParams).SatisfiesConstraint(Condition, model, combination); + } + } + else + { + // supplied combination is disjoint + return ConstraintSatisfaction.InsufficientData; + } + } + + internal override void ClearCache() + { + CachedInteraction = null; + } + + static T BuildVariation(Model model, IList parameterIndices, int[] valueIndices) + { + Variation v = new Variation(); + for (int i = 0; i < parameterIndices.Count; i++) + { + var parameter = model.Parameters[parameterIndices[i]]; + v[parameter.Name] = ((ParameterValueBase)parameter.GetAt(valueIndices[i])).GetValue(); + } + + if (typeof(T) == typeof(Variation)) + { + return (T)((object)v); + } + + return new VariationsWrapper(model.propertiesMap, null).AssignParameterValues(v); + + } + + private Dictionary mapExpressionsToRequiredParams = new Dictionary(); + private List parameters; + } +} diff --git a/src/dotnetCampus.UITest.WPFTestHelper/VariationGeneration/Constraints/ConstraintSatisfactionExpressionVisitor.cs b/src/dotnetCampus.UITest.WPFTestHelper/VariationGeneration/Constraints/ConstraintSatisfactionExpressionVisitor.cs new file mode 100644 index 0000000..cbaebed --- /dev/null +++ b/src/dotnetCampus.UITest.WPFTestHelper/VariationGeneration/Constraints/ConstraintSatisfactionExpressionVisitor.cs @@ -0,0 +1,101 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + + +using System.Collections.Generic; +using System.Linq.Expressions; + +namespace dotnetCampus.UITest.WPFTestHelper.VariationGeneration.Constraints +{ + class ConstraintSatisfactionExpressionVisitor + { + Dictionary mapExpressionsToRequiredParams = new Dictionary(); + + public ConstraintSatisfactionExpressionVisitor(Dictionary expressionData) + { + mapExpressionsToRequiredParams = expressionData; + } + + public ConstraintSatisfaction SatisfiesConstraint(Expression expression, Model model, ValueCombination combination) where T : new() + { + switch(expression.NodeType) + { + case ExpressionType.AndAlso: + { + var andAlso = (BinaryExpression)expression; + ConstraintSatisfaction left = SatisfiesConstraint(andAlso.Left, model, combination); + if (left == ConstraintSatisfaction.Unsatisfied) + { + return ConstraintSatisfaction.Unsatisfied; + } + + ConstraintSatisfaction right = SatisfiesConstraint(andAlso.Right, model, combination); + if (right == ConstraintSatisfaction.Unsatisfied) + { + return ConstraintSatisfaction.Unsatisfied; + } + + if (left == right) + { + return left; + } + else + { + return ConstraintSatisfaction.InsufficientData; + } + } + + case ExpressionType.Conditional: + var conditional = (ConditionalExpression)expression; + ConstraintSatisfaction test = SatisfiesConstraint(conditional.Test, model, combination); + if (test == ConstraintSatisfaction.InsufficientData) + { + return ConstraintSatisfaction.InsufficientData; + } + + if (test == ConstraintSatisfaction.Satisfied) + { + return SatisfiesConstraint(conditional.IfTrue, model, combination); + } + else + { + return SatisfiesConstraint(conditional.IfFalse, model, combination); + } + case ExpressionType.Lambda: + return SatisfiesConstraint(((LambdaExpression)expression).Body, model, combination); + case ExpressionType.OrElse: + { + var orElse = (BinaryExpression)expression; + ConstraintSatisfaction left = SatisfiesConstraint(orElse.Left, model, combination); + if (left == ConstraintSatisfaction.Satisfied) + { + return ConstraintSatisfaction.Satisfied; + } + + ConstraintSatisfaction right = SatisfiesConstraint(orElse.Right, model, combination); + if (right == ConstraintSatisfaction.Satisfied) + { + return ConstraintSatisfaction.Satisfied; + } + + if (left == right) + { + return left; + } + else + { + return ConstraintSatisfaction.InsufficientData; + } + } + + default: + if (!mapExpressionsToRequiredParams.ContainsKey(expression)) + { + throw new InternalVariationGenerationException("Expected data for expression not found."); + } + return InternalConstraintHelpers.SatisfiesContraint(model, combination, mapExpressionsToRequiredParams[expression].Interaction); + } + } + } +} diff --git a/src/dotnetCampus.UITest.WPFTestHelper/VariationGeneration/Constraints/ExpressionVisitor.cs b/src/dotnetCampus.UITest.WPFTestHelper/VariationGeneration/Constraints/ExpressionVisitor.cs new file mode 100644 index 0000000..7d2e013 --- /dev/null +++ b/src/dotnetCampus.UITest.WPFTestHelper/VariationGeneration/Constraints/ExpressionVisitor.cs @@ -0,0 +1,372 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + + +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq.Expressions; + +namespace dotnetCampus.UITest.WPFTestHelper.VariationGeneration.Constraints +{ + internal class ExpressionVisitor + { + protected ExpressionVisitor() + { + } + + protected virtual Expression Visit(Expression exp) + { + if (exp == null) + return exp; + switch (exp.NodeType) + { + case ExpressionType.Negate: + case ExpressionType.NegateChecked: + case ExpressionType.Not: + case ExpressionType.Convert: + case ExpressionType.ConvertChecked: + case ExpressionType.ArrayLength: + case ExpressionType.Quote: + case ExpressionType.TypeAs: + return this.VisitUnary((UnaryExpression)exp); + case ExpressionType.Add: + case ExpressionType.AddChecked: + case ExpressionType.Subtract: + case ExpressionType.SubtractChecked: + case ExpressionType.Multiply: + case ExpressionType.MultiplyChecked: + case ExpressionType.Divide: + case ExpressionType.Modulo: + case ExpressionType.And: + case ExpressionType.AndAlso: + case ExpressionType.Or: + case ExpressionType.OrElse: + case ExpressionType.LessThan: + case ExpressionType.LessThanOrEqual: + case ExpressionType.GreaterThan: + case ExpressionType.GreaterThanOrEqual: + case ExpressionType.Equal: + case ExpressionType.NotEqual: + case ExpressionType.Coalesce: + case ExpressionType.ArrayIndex: + case ExpressionType.RightShift: + case ExpressionType.LeftShift: + case ExpressionType.ExclusiveOr: + return this.VisitBinary((BinaryExpression)exp); + case ExpressionType.TypeIs: + return this.VisitTypeIs((TypeBinaryExpression)exp); + case ExpressionType.Conditional: + return this.VisitConditional((ConditionalExpression)exp); + case ExpressionType.Constant: + return this.VisitConstant((ConstantExpression)exp); + case ExpressionType.Parameter: + return this.VisitParameter((ParameterExpression)exp); + case ExpressionType.MemberAccess: + return this.VisitMemberAccess((MemberExpression)exp); + case ExpressionType.Call: + return this.VisitMethodCall((MethodCallExpression)exp); + case ExpressionType.Lambda: + return this.VisitLambda((LambdaExpression)exp); + case ExpressionType.New: + return this.VisitNew((NewExpression)exp); + case ExpressionType.NewArrayInit: + case ExpressionType.NewArrayBounds: + return this.VisitNewArray((NewArrayExpression)exp); + case ExpressionType.Invoke: + return this.VisitInvocation((InvocationExpression)exp); + case ExpressionType.MemberInit: + return this.VisitMemberInit((MemberInitExpression)exp); + case ExpressionType.ListInit: + return this.VisitListInit((ListInitExpression)exp); + default: + throw new Exception(string.Format("Unhandled expression type: '{0}'", exp.NodeType)); + } + } + + protected virtual MemberBinding VisitBinding(MemberBinding binding) + { + switch (binding.BindingType) + { + case MemberBindingType.Assignment: + return this.VisitMemberAssignment((MemberAssignment)binding); + case MemberBindingType.MemberBinding: + return this.VisitMemberMemberBinding((MemberMemberBinding)binding); + case MemberBindingType.ListBinding: + return this.VisitMemberListBinding((MemberListBinding)binding); + default: + throw new Exception(string.Format("Unhandled binding type '{0}'", binding.BindingType)); + } + } + + protected virtual ElementInit VisitElementInitializer(ElementInit initializer) + { + ReadOnlyCollection arguments = this.VisitExpressionList(initializer.Arguments); + if (arguments != initializer.Arguments) + { + return Expression.ElementInit(initializer.AddMethod, arguments); + } + return initializer; + } + + protected virtual Expression VisitUnary(UnaryExpression u) + { + Expression operand = this.Visit(u.Operand); + if (operand != u.Operand) + { + return Expression.MakeUnary(u.NodeType, operand, u.Type, u.Method); + } + return u; + } + + protected virtual Expression VisitBinary(BinaryExpression b) + { + Expression left = this.Visit(b.Left); + Expression right = this.Visit(b.Right); + Expression conversion = this.Visit(b.Conversion); + if (left != b.Left || right != b.Right || conversion != b.Conversion) + { + if (b.NodeType == ExpressionType.Coalesce && b.Conversion != null) + return Expression.Coalesce(left, right, conversion as LambdaExpression); + else + return Expression.MakeBinary(b.NodeType, left, right, b.IsLiftedToNull, b.Method); + } + return b; + } + + protected virtual Expression VisitTypeIs(TypeBinaryExpression b) + { + Expression expr = this.Visit(b.Expression); + if (expr != b.Expression) + { + return Expression.TypeIs(expr, b.TypeOperand); + } + return b; + } + + protected virtual Expression VisitConstant(ConstantExpression c) + { + return c; + } + + protected virtual Expression VisitConditional(ConditionalExpression c) + { + Expression test = this.Visit(c.Test); + Expression ifTrue = this.Visit(c.IfTrue); + Expression ifFalse = this.Visit(c.IfFalse); + if (test != c.Test || ifTrue != c.IfTrue || ifFalse != c.IfFalse) + { + return Expression.Condition(test, ifTrue, ifFalse); + } + return c; + } + + protected virtual Expression VisitParameter(ParameterExpression p) + { + return p; + } + + protected virtual Expression VisitMemberAccess(MemberExpression m) + { + Expression exp = this.Visit(m.Expression); + if (exp != m.Expression) + { + return Expression.MakeMemberAccess(exp, m.Member); + } + return m; + } + + protected virtual Expression VisitMethodCall(MethodCallExpression m) + { + Expression obj = this.Visit(m.Object); + IEnumerable args = this.VisitExpressionList(m.Arguments); + if (obj != m.Object || args != m.Arguments) + { + return Expression.Call(obj, m.Method, args); + } + return m; + } + + protected virtual ReadOnlyCollection VisitExpressionList(ReadOnlyCollection original) + { + List list = null; + for (int i = 0, n = original.Count; i < n; i++) + { + Expression p = this.Visit(original[i]); + if (list != null) + { + list.Add(p); + } + else if (p != original[i]) + { + list = new List(n); + for (int j = 0; j < i; j++) + { + list.Add(original[j]); + } + list.Add(p); + } + } + if (list != null) + { + return list.AsReadOnly(); + } + return original; + } + + protected virtual MemberAssignment VisitMemberAssignment(MemberAssignment assignment) + { + Expression e = this.Visit(assignment.Expression); + if (e != assignment.Expression) + { + return Expression.Bind(assignment.Member, e); + } + return assignment; + } + + protected virtual MemberMemberBinding VisitMemberMemberBinding(MemberMemberBinding binding) + { + IEnumerable bindings = this.VisitBindingList(binding.Bindings); + if (bindings != binding.Bindings) + { + return Expression.MemberBind(binding.Member, bindings); + } + return binding; + } + + protected virtual MemberListBinding VisitMemberListBinding(MemberListBinding binding) + { + IEnumerable initializers = this.VisitElementInitializerList(binding.Initializers); + if (initializers != binding.Initializers) + { + return Expression.ListBind(binding.Member, initializers); + } + return binding; + } + + protected virtual IEnumerable VisitBindingList(ReadOnlyCollection original) + { + List list = null; + for (int i = 0, n = original.Count; i < n; i++) + { + MemberBinding b = this.VisitBinding(original[i]); + if (list != null) + { + list.Add(b); + } + else if (b != original[i]) + { + list = new List(n); + for (int j = 0; j < i; j++) + { + list.Add(original[j]); + } + list.Add(b); + } + } + if (list != null) + return list; + return original; + } + + protected virtual IEnumerable VisitElementInitializerList(ReadOnlyCollection original) + { + List list = null; + for (int i = 0, n = original.Count; i < n; i++) + { + ElementInit init = this.VisitElementInitializer(original[i]); + if (list != null) + { + list.Add(init); + } + else if (init != original[i]) + { + list = new List(n); + for (int j = 0; j < i; j++) + { + list.Add(original[j]); + } + list.Add(init); + } + } + if (list != null) + return list; + return original; + } + + protected virtual Expression VisitLambda(LambdaExpression lambda) + { + Expression body = this.Visit(lambda.Body); + if (body != lambda.Body) + { + return Expression.Lambda(lambda.Type, body, lambda.Parameters); + } + return lambda; + } + + protected virtual NewExpression VisitNew(NewExpression nex) + { + IEnumerable args = this.VisitExpressionList(nex.Arguments); + if (args != nex.Arguments) + { + if (nex.Members != null) + return Expression.New(nex.Constructor, args, nex.Members); + else + return Expression.New(nex.Constructor, args); + } + return nex; + } + + protected virtual Expression VisitMemberInit(MemberInitExpression init) + { + NewExpression n = this.VisitNew(init.NewExpression); + IEnumerable bindings = this.VisitBindingList(init.Bindings); + if (n != init.NewExpression || bindings != init.Bindings) + { + return Expression.MemberInit(n, bindings); + } + return init; + } + + protected virtual Expression VisitListInit(ListInitExpression init) + { + NewExpression n = this.VisitNew(init.NewExpression); + IEnumerable initializers = this.VisitElementInitializerList(init.Initializers); + if (n != init.NewExpression || initializers != init.Initializers) + { + return Expression.ListInit(n, initializers); + } + return init; + } + + protected virtual Expression VisitNewArray(NewArrayExpression na) + { + IEnumerable exprs = this.VisitExpressionList(na.Expressions); + if (exprs != na.Expressions) + { + if (na.NodeType == ExpressionType.NewArrayInit) + { + return Expression.NewArrayInit(na.Type.GetElementType(), exprs); + } + else + { + return Expression.NewArrayBounds(na.Type.GetElementType(), exprs); + } + } + return na; + } + + protected virtual Expression VisitInvocation(InvocationExpression iv) + { + IEnumerable args = this.VisitExpressionList(iv.Arguments); + Expression expr = this.Visit(iv.Expression); + if (args != iv.Arguments || expr != iv.Expression) + { + return Expression.Invoke(expr, args); + } + return iv; + } + } + +} diff --git a/src/dotnetCampus.UITest.WPFTestHelper/VariationGeneration/Constraints/IfPredicate.cs b/src/dotnetCampus.UITest.WPFTestHelper/VariationGeneration/Constraints/IfPredicate.cs new file mode 100644 index 0000000..ab09bd7 --- /dev/null +++ b/src/dotnetCampus.UITest.WPFTestHelper/VariationGeneration/Constraints/IfPredicate.cs @@ -0,0 +1,21 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + + +using System; +using System.Linq.Expressions; + +namespace dotnetCampus.UITest.WPFTestHelper.VariationGeneration.Constraints +{ + /// + /// Represents the predicate of a constraint with a logical implication. + /// + /// The type of variation being operated on. + public class IfPredicate where T : new() + { + internal IfPredicate(Expression> predicate) { Predicate = predicate; } + + internal Expression> Predicate { get; set; } + } +} diff --git a/src/dotnetCampus.UITest.WPFTestHelper/VariationGeneration/Constraints/IfThenConstraint.cs b/src/dotnetCampus.UITest.WPFTestHelper/VariationGeneration/Constraints/IfThenConstraint.cs new file mode 100644 index 0000000..e042b31 --- /dev/null +++ b/src/dotnetCampus.UITest.WPFTestHelper/VariationGeneration/Constraints/IfThenConstraint.cs @@ -0,0 +1,49 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + + +using System; +using System.Linq.Expressions; + +namespace dotnetCampus.UITest.WPFTestHelper.VariationGeneration.Constraints +{ + /// + /// Represents a condition if the expression in the predicate is true. + /// + /// The type of variation being operated on. + public class IfThenConstraint : ConditionalConstraint where T : new() + { + internal IfThenConstraint(IfPredicate ifTest, Expression> predicate) + { + Condition = predicate; + IfPredicate = ifTest; + + InnerConstraint = new ConditionalConstraint(BuildInternalConstraint(IfPredicate.Predicate.Body, Condition.Body, Expression.Constant(true))); + } + + internal ConditionalConstraint InnerConstraint { get; private set; } + + internal IfPredicate IfPredicate { get; private set; } + + internal override ParameterInteraction GetExcludedCombinations(Model model) + { + return InnerConstraint.GetExcludedCombinations(model); + } + + internal override ConstraintSatisfaction SatisfiesContraint(Model model, ValueCombination combination) + { + return InnerConstraint.SatisfiesContraint(model, combination); + } + + internal static Expression> BuildInternalConstraint(Expression ifExpr, Expression thenExpr, Expression elseExpr) + { + ParameterExpression parameter = Expression.Parameter(typeof(T), "parameter"); + LambdaParameterExpressionVisitor visitor = new LambdaParameterExpressionVisitor(parameter); + + Expression conditional = Expression.Condition(visitor.ReplaceParameter(ifExpr), visitor.ReplaceParameter(thenExpr), visitor.ReplaceParameter(elseExpr)); + + return (Expression>)Expression.Lambda(conditional, parameter); + } + } +} diff --git a/src/dotnetCampus.UITest.WPFTestHelper/VariationGeneration/Constraints/IfThenElseConstraint.cs b/src/dotnetCampus.UITest.WPFTestHelper/VariationGeneration/Constraints/IfThenElseConstraint.cs new file mode 100644 index 0000000..f120b92 --- /dev/null +++ b/src/dotnetCampus.UITest.WPFTestHelper/VariationGeneration/Constraints/IfThenElseConstraint.cs @@ -0,0 +1,39 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + + +using System; +using System.Linq.Expressions; + +namespace dotnetCampus.UITest.WPFTestHelper.VariationGeneration.Constraints +{ + /// + /// Optionally represents a condition if the expression in is false. + /// + /// The type of variation being operated on. + public class IfThenElseConstraint : ConditionalConstraint where T : new() + { + internal IfThenElseConstraint(IfThenConstraint ifThen, Expression> predicate) + { + Condition = predicate; + IfThen = ifThen; + + InnerConstraint = new ConditionalConstraint(IfThenConstraint.BuildInternalConstraint(IfThen.IfPredicate.Predicate.Body, IfThen.Condition.Body, Condition.Body)); + } + + internal IfThenConstraint IfThen { get; private set; } + + internal ConditionalConstraint InnerConstraint { get; private set; } + + internal override ParameterInteraction GetExcludedCombinations(Model model) + { + return InnerConstraint.GetExcludedCombinations(model); + } + + internal override ConstraintSatisfaction SatisfiesContraint(Model model, ValueCombination combination) + { + return InnerConstraint.SatisfiesContraint(model, combination); + } + } +} diff --git a/src/dotnetCampus.UITest.WPFTestHelper/VariationGeneration/Constraints/LambdaParameterExpressionVisitor.cs b/src/dotnetCampus.UITest.WPFTestHelper/VariationGeneration/Constraints/LambdaParameterExpressionVisitor.cs new file mode 100644 index 0000000..e7c24ab --- /dev/null +++ b/src/dotnetCampus.UITest.WPFTestHelper/VariationGeneration/Constraints/LambdaParameterExpressionVisitor.cs @@ -0,0 +1,32 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + + +using System.Linq.Expressions; + +namespace dotnetCampus.UITest.WPFTestHelper.VariationGeneration.Constraints +{ + class LambdaParameterExpressionVisitor : ExpressionVisitor + { + + public LambdaParameterExpressionVisitor(ParameterExpression parameter) + { + this.parameter = parameter; + } + + public Expression ReplaceParameter(Expression exp) + { + return Visit(exp); + } + + ParameterExpression parameter; + + protected override Expression VisitParameter(ParameterExpression p) + { + return this.parameter; + } + + + } +} diff --git a/src/dotnetCampus.UITest.WPFTestHelper/VariationGeneration/Constraints/ParameterListBuilder.cs b/src/dotnetCampus.UITest.WPFTestHelper/VariationGeneration/Constraints/ParameterListBuilder.cs new file mode 100644 index 0000000..34367b2 --- /dev/null +++ b/src/dotnetCampus.UITest.WPFTestHelper/VariationGeneration/Constraints/ParameterListBuilder.cs @@ -0,0 +1,141 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using System.Reflection; + +namespace dotnetCampus.UITest.WPFTestHelper.VariationGeneration.Constraints +{ + internal class ParameterListBuilder : ExpressionVisitor + { + public ParameterListBuilder(Dictionary parameterMap, IEnumerable modelParameters, Type variationType) + { + this.parameterMap = parameterMap; + this.modelParameters = modelParameters; + this.variationType = variationType; + } + + Dictionary parameterMap; + IEnumerable modelParameters; + Type variationType; + + List Parameters { get; set; } + public List GetParameters(Expression expression) + { + Parameters = new List(); + Visit(expression); + parameterMap[expression] = new CachedExpressionConstraintData + { + Parameters = this.Parameters + }; + + return Parameters; + } + + protected override Expression VisitBinary(BinaryExpression b) + { + if (b.NodeType == ExpressionType.AndAlso || b.NodeType == ExpressionType.OrElse) + { + var parametersLeft = new ParameterListBuilder(parameterMap, modelParameters, variationType).GetParameters(b.Left); + var parametersRight = new ParameterListBuilder(parameterMap, modelParameters, variationType).GetParameters(b.Right); + + MergeParameterLists(parametersLeft); + MergeParameterLists(parametersRight); + return b; + } + else + { + return base.VisitBinary(b); + } + } + + protected override Expression VisitConditional(ConditionalExpression c) + { + var parametersTest = new ParameterListBuilder(parameterMap, modelParameters, variationType).GetParameters(c.Test); + var parametersIfTrue = new ParameterListBuilder(parameterMap, modelParameters, variationType).GetParameters(c.IfTrue); + var parametersIfFalse = new ParameterListBuilder(parameterMap, modelParameters, variationType).GetParameters(c.IfFalse); + + MergeParameterLists(parametersTest); + MergeParameterLists(parametersIfTrue); + MergeParameterLists(parametersIfFalse); + return c; + } + + protected override Expression VisitMemberAccess(MemberExpression m) + { + if (m.Expression.Type == variationType) + { + ParameterBase parameter = modelParameters.FirstOrDefault(p => p.Name == m.Member.Name); + if (parameter != null && !Parameters.Contains(parameter)) + { + Parameters.Add(parameter); + } + } + return m; + } + + protected override Expression VisitMethodCall(MethodCallExpression m) + { + if (CheckForGetValueCall(m.Method)) + { + var parameter = Expression.Lambda>(m.Object).Compile()(); + + if (!Parameters.Contains(parameter)) + { + Parameters.Add(parameter); + } + + return m; + } + else + { + return base.VisitMethodCall(m); + } + } + + protected override Expression VisitParameter(ParameterExpression p) + { + if (p.Type == variationType) + { + throw new InvalidOperationException("The Variation parameter can only be passed to calls of Parameter<>.GetValue."); + } + + return base.VisitParameter(p); + } + + bool CheckForGetValueCall(MethodInfo method) + { + if (!method.DeclaringType.IsGenericType) + { + return false; + } + + var genericArgs = method.DeclaringType.GetGenericArguments(); + + if (genericArgs.Length != 1) + { + return false; + } + + var expectedType = typeof(Parameter<>).MakeGenericType(genericArgs[0]); + + return method == expectedType.GetMethod("GetValue"); + } + + void MergeParameterLists(IList candidates) + { + foreach (var candidate in candidates) + { + if (!Parameters.Contains(candidate)) + { + Parameters.Add(candidate); + } + } + } + } +} diff --git a/src/dotnetCampus.UITest.WPFTestHelper/VariationGeneration/Constraints/TaggedValueConstraint.cs b/src/dotnetCampus.UITest.WPFTestHelper/VariationGeneration/Constraints/TaggedValueConstraint.cs new file mode 100644 index 0000000..786f613 --- /dev/null +++ b/src/dotnetCampus.UITest.WPFTestHelper/VariationGeneration/Constraints/TaggedValueConstraint.cs @@ -0,0 +1,116 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + + +using System.Collections.Generic; +using System.Linq; + +namespace dotnetCampus.UITest.WPFTestHelper.VariationGeneration.Constraints +{ + /// + /// Tagging values in a model creates implicit constraints (only one tagged value per variation). This class + /// explicitly defines those constraints. + /// + internal class TaggedValueConstraint : Constraint where T : new() + { + internal override ParameterInteraction GetExcludedCombinations(Model model) + { + if (CachedInteraction == null) + { + var taggedValues = GetParametersWithTaggedValues(model); + CachedInteraction = new ParameterInteraction(taggedValues.Select((p) => p.ParameterIndex)); + var combinationIndices = ParameterInteractionTable.GenerateValueTable(model.Parameters, CachedInteraction); + foreach (var combinationIndex in combinationIndices) + { + int tagCount = 0; + ValueCombination combination = new ValueCombination(combinationIndex, CachedInteraction); + combination.State = ValueCombinationState.Covered; + + for (int i = 0; i < taggedValues.Count; i++) + { + if (taggedValues[i].ValueIndices.BinarySearch(combinationIndex[i]) >= 0) + { + tagCount++; + } + + if (tagCount > 1) + { + break; + } + } + + if (tagCount > 1) + { + combination.State = ValueCombinationState.Excluded; + } + + CachedInteraction.Combinations.Add(combination); + } + } + + return new ParameterInteraction(CachedInteraction); + } + + + internal override ConstraintSatisfaction SatisfiesContraint(Model model, ValueCombination combination) + { + if (CachedInteraction == null) + { + GetExcludedCombinations(model); + } + + return InternalConstraintHelpers.SatisfiesContraint(model, combination, CachedInteraction); + } + + internal override void ClearCache() + { + CachedInteraction = null; + } + + private static IList GetParametersWithTaggedValues(Model model) + { + var indices = new List(); + for (int i = 0; i < model.Parameters.Count; i++) + { + ParamaterAndValueIndices index = null; + for (int j = 0; j < model.Parameters[i].Count; j++) + { + if(IsTagged(model.Parameters[i].GetAt(j), model.DefaultVariationTag)) + { + if(index == null) + { + index = new ParamaterAndValueIndices(); + index.ParameterIndex = i; + } + + index.ValueIndices.Add(j); + } + } + + if (index != null) + { + indices.Add(index); + } + } + + return indices; + } + + private static bool IsTagged(object value, object defaultTag) + { + return value is ParameterValueBase && + ((ParameterValueBase)value).Tag != null && + ((ParameterValueBase)value).Tag != defaultTag; + } + + private class ParamaterAndValueIndices + { + public int ParameterIndex { get; set; } + + private List valueIndices = new List(); + public List ValueIndices { get { return valueIndices; } } + } + + } +} diff --git a/src/dotnetCampus.UITest.WPFTestHelper/VariationGeneration/InternalConstraintHelpers.cs b/src/dotnetCampus.UITest.WPFTestHelper/VariationGeneration/InternalConstraintHelpers.cs new file mode 100644 index 0000000..7f50825 --- /dev/null +++ b/src/dotnetCampus.UITest.WPFTestHelper/VariationGeneration/InternalConstraintHelpers.cs @@ -0,0 +1,43 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + + +using System.Diagnostics; + +namespace dotnetCampus.UITest.WPFTestHelper.VariationGeneration +{ + /// + /// Helper function for determining relations between internal constraint tables. + /// + internal static class InternalConstraintHelpers + { + // helper to implement Constraint.SatisfiesConstraint + internal static ConstraintSatisfaction SatisfiesContraint(Model model, ValueCombination combination, ParameterInteraction interaction) where T : new() + { + Debug.Assert(model != null && combination != null && interaction != null); + + var parameterMap = combination.ParameterToValueMap; + for(int i = 0; i < interaction.Parameters.Count; i++) + { + if (!parameterMap.ContainsKey(interaction.Parameters[i])) + { + return ConstraintSatisfaction.InsufficientData; + } + } + + for(int i = 0; i < interaction.Combinations.Count; i++) + { + if (ParameterInteractionTable.MatchCombination(interaction.Combinations[i], combination)) + { + if (interaction.Combinations[i].State == ValueCombinationState.Excluded) + { + return ConstraintSatisfaction.Unsatisfied; + } + } + } + + return ConstraintSatisfaction.Satisfied; + } + } +} diff --git a/src/dotnetCampus.UITest.WPFTestHelper/VariationGeneration/InternalVariationGenerationException.cs b/src/dotnetCampus.UITest.WPFTestHelper/VariationGeneration/InternalVariationGenerationException.cs new file mode 100644 index 0000000..fd3f213 --- /dev/null +++ b/src/dotnetCampus.UITest.WPFTestHelper/VariationGeneration/InternalVariationGenerationException.cs @@ -0,0 +1,33 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + + +using System; +using System.Runtime.Serialization; + +namespace dotnetCampus.UITest.WPFTestHelper.VariationGeneration +{ + /// + /// Indicates an error in the generated internal variation table prevents completion of generation. + /// This indicates a + + [Serializable] + internal class InternalVariationGenerationException : Exception + { + public InternalVariationGenerationException() + : base() + { + } + + public InternalVariationGenerationException(string message) + : base(message) + { + } + + protected InternalVariationGenerationException(SerializationInfo info, StreamingContext context) + : base(info, context) + { + } + } +} diff --git a/src/dotnetCampus.UITest.WPFTestHelper/VariationGeneration/Model.cs b/src/dotnetCampus.UITest.WPFTestHelper/VariationGeneration/Model.cs new file mode 100644 index 0000000..519e04f --- /dev/null +++ b/src/dotnetCampus.UITest.WPFTestHelper/VariationGeneration/Model.cs @@ -0,0 +1,457 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; + +namespace dotnetCampus.UITest.WPFTestHelper.VariationGeneration +{ + /// + /// Contains all the parameters and constraints for the system under test, and produces a + /// set of variations by using combinatorial testing techniques. + /// + /// The type of variations that should be generated. + /// + /// + /// For examples, refer to and . + /// + public class Model where T : new() + { + /// + /// Initializes a new model with parameters to be inferred by reflection. + /// + public Model() + { + } + + /// + /// Initializes a new model with the specified parameters. + /// + /// The parameters. + public Model(IEnumerable parameters) : this(parameters, null) + { + } + + /// + /// Initializes a new model with the specified parameters and constraints. + /// + /// The parameters. + /// The constraints. + public Model(IEnumerable parameters, IEnumerable> constraints) + { + if (parameters != null) + { + this.parameters.AddRange(parameters); + } + + if (constraints != null) + { + this.constraints.AddRange(constraints); + } + } + + List parameters = new List(); + + /// + /// The parameters in the model. + /// + public IList Parameters { get { return parameters; } } + + List> constraints = new List>(); + + /// + /// The constraints in the model. + /// + public ICollection> Constraints { get { return constraints; } } + + /// + /// The default tag for generated variations. Set this property when no value in the variation has been tagged. The default is null. + /// + public object DefaultVariationTag { get; set; } + + /// + /// Generates an order-wise set of variations using a constant seed. + /// + /// The order of the selected combinations (2 is every pair, 3 is every triple, and so on). Must be between 1 and the number of parameters. + /// The variations. + public virtual IEnumerable GenerateVariations(int order) + { + return GenerateVariations(order, defaultSeedValue); + } + + /// + /// Generates an order-wise set of variations using the specified seed for random generation. + /// + /// The order of the selected combinations (2 is every pair, 3 is every triple, and so on). Must be between 1 and the number of parameters. + /// The seed that is used for random generation. + /// The variations. + public virtual IEnumerable GenerateVariations(int order, int seed) + { + // create parameters and set values if model is supplied in a template + propertiesMap = null; + if (typeof(T) != typeof(Variation)) + { + if (parameters.Count == 0) + { + propertiesMap = CreateParameters(seed); + } + else + { + propertiesMap = CreatePropertyMapFromParameters(); + } + } + + + // validate parameters + if (order < 1 || order > Parameters.Count) + { + throw new ArgumentOutOfRangeException("order", order, "order must be between 1 and the number of parameters."); + } + + if (typeof(T) == typeof(Variation)) + { + return VariationGenerator.GenerateVariations(this, order, seed).Cast(); + } + + return new VariationsWrapper(propertiesMap, + VariationGenerator.GenerateVariations(this, order, seed)); + } + + private Dictionary CreatePropertyMapFromParameters() + { + var propertyMap = new Dictionary(); + foreach (ParameterBase parameter in Parameters) + { + PropertyInfo prop = typeof(T).GetProperty(parameter.Name, BindingFlags.Instance | BindingFlags.Public | BindingFlags.IgnoreCase); + if (prop == null || !prop.CanWrite) + { + throw new InvalidOperationException("Unable to find valid property for '" + parameter.Name + "'. Parameter name must match with a writable property."); + } + propertyMap[parameter.Name] = prop; + } + + return propertyMap; + } + + private const int defaultSeedValue = 12345; + internal Dictionary propertiesMap; + + #region Declarative scenario methods + /// + /// Retrieves array of the attributes of specified type + /// given member is decorated with. + /// + /// Attribute type. + /// Class member. + /// Array of attributes defined for the member; if none + /// defined the array is zero length. + static TAttribute[] GetAttributes(MemberInfo memberInfo) where TAttribute : Attribute + { + object[] attributes = Attribute.GetCustomAttributes(memberInfo, typeof(TAttribute), true); + if (attributes.Length < 1) + { + return new TAttribute[0]; + } + return attributes as TAttribute[]; + } + + /// + /// Iterates over model properties, validates them and + /// retrieves properly attributed ones. + /// + /// Dictionary of properties keyed off by the name. + static Dictionary ReadPropertiesMetadata() + { + Dictionary propertiesMap = new Dictionary(); + PropertyInfo[] properties = typeof(T).GetProperties(BindingFlags.Instance | BindingFlags.Public); + foreach (PropertyInfo property in properties) + { + ParameterAttribute[] attributes = GetAttributes(property); + if (attributes.Length == 0) + { + continue; + } + + if (!property.CanWrite) + { + throw new InvalidOperationException(string.Format("'{0}' is not writable. Only writable properties can define parameters.", property.Name)); + } + + // validate all attributes defined + // each attribute must have at least one value + foreach (ParameterAttribute attribute in attributes) + { + if (attribute.Values.Length == 0) + { + throw new Exception(string.Format("ParameterAttribute for {0} property has no values", property.Name)); + } + } + propertiesMap[property.Name] = property; + } + + return propertiesMap; + } + + /// + /// Creates model parameters based on attributes set on + /// model properties. + /// + /// Seed used to select values for equivalnce + /// classes generation. + Dictionary CreateParameters(int seed) + { + // get set of relevantly attributed properties + Dictionary propertiesMap = ReadPropertiesMetadata(); + List values = new List(); + parameters.Clear(); + + // for each property create model parameter and fill it with values + foreach (string propertyName in propertiesMap.Keys) + { + values.Clear(); + PropertyInfo property = propertiesMap[propertyName]; + Type propertyType = property.PropertyType; + ParameterAttribute[] attributes = GetAttributes(property); + + // adding values from attributes defined into the list + if (attributes.Length > 1) + { + // if more than one attribute defined, equivalence classes are + // defined for the property; random value needs to be picked from + // each one + values.AddRange(CreateEquivalenceClassValues(seed, propertyType, attributes)); + } + else + { + // since the properties here are always attributed + // there exist at least one attribute; + // just adding all values from attribute + values.AddRange(attributes[0].Values); + } + + // create a parameter with specified name, type and list of values + // and add it to the model + this.parameters.Add(CreateParameter(propertyName, propertyType, values)); + } + + return propertiesMap; + } + + /// + /// Returns list of values selected\created based on attributes + /// provided. + /// Equivalence class functionality is achieved by using + /// seed provided to create random generator and then use it to + /// select one random value from each attribute. + /// + /// Seed used to select value from equivalence class. + /// Type used to create ParameterValue instance to incorporate + /// Weight\Tag (if defined). + /// List of attributes to select values from. + /// List of values selected\created from attrubutes. + static object[] CreateEquivalenceClassValues(int seed, Type valueType, ParameterAttribute[] attributes) + { + List values = new List(); + foreach (ParameterAttribute attribute in attributes) + { + Type parameterValueType = null; + Random random = new Random(seed); + + object value = attribute.Values[random.Next(attribute.Values.Length - 1)]; + + // if weight or tag is defined, need to create parameter value + // o\w just using value from attribute + if (attribute.Weight > 0 || attribute.Tag != null) + { + if (parameterValueType == null) + { + parameterValueType = typeof(ParameterValue<>).MakeGenericType(valueType); + } + ParameterValueBase parameterValue = Activator.CreateInstance(parameterValueType, value, attribute.Tag, attribute.Weight) + as ParameterValueBase; + values.Add(parameterValue); + } + else + { + values.Add(value); + } + } + return values.ToArray(); + } + + /// + /// Creates parameter with name and type as specified and + /// add values to it. + /// + /// Name of the parameter. + /// Type of the parameter. + /// List of values to add to created parameter. + static ParameterBase CreateParameter(string parameterName, Type parameterType, List parameterValues) + { + // create generic parameter of the right type (based off type supplied) + Type genericParameterType = typeof(Parameter<>).MakeGenericType(parameterType); + ParameterBase parameter = Activator.CreateInstance(genericParameterType, parameterName) + as ParameterBase; + + // as parameter is generic, to add value need to call appropriate + // generic method - either one using T or ParameterValue + MethodInfo methodValue = parameter.GetType().GetMethod("Add", + new Type[] { parameterType }); + MethodInfo methodParameterValue = parameter.GetType().GetMethod("Add", + new Type[] { typeof(ParameterValue<>).MakeGenericType(parameterType) }); + + // call appropriate generic add method based on type of every value + foreach (object value in parameterValues) + { + if (value is ParameterValueBase) + { + methodParameterValue.Invoke(parameter, new object[] { value }); + } + else + { + methodValue.Invoke(parameter, new object[] { value }); + } + } + return parameter; + } + #endregion + } + + + + + /// + /// Provides a general-purpose model that generates a . + /// See also . + /// + /// + /// + /// Exhaustively testing all possible inputs to any nontrivial software component is generally not possible + /// because of the enormous number of variations. Combinatorial testing is one approach to achieve high coverage + /// with a much smaller set of variations. Pairwise, the most common combinatorial strategy, tests every possible + /// pair of values. Higher orders of combinations (three-wise, four-wise, and so on) can also be used for higher coverage + /// at the expense of more variations. See Pairwise Testing and + /// + /// Pairwise Testing in Real World for more resources. + /// + /// + /// + /// The following example shows how to create a set of test-run configurations by using + /// a model that only uses variables. + /// + /// // Specify the parameters and parameter values + /// var os = new Parameter<string>("OS") { "WinXP", "Win2k3", "Vista", "Win7" }; + /// var memory = new Parameter<int>("Memory") { 512, 1024, 2048, 4096 }; + /// var graphics = new Parameter<string>("Graphics") { "Nvidia", "ATI", "Intel" }; + /// var lang = new Parameter<string>("Lang") { "enu", "jpn", "deu", "chs", "ptb" }; + /// + /// var parameters = new List<ParameterBase> { os, memory, graphics, lang }; + /// + /// var model = new Model(parameters); + /// + /// // The model is complete; now generate the configurations and print out + /// foreach (var config in model.GenerateVariations(2)) + /// { + /// Console.WriteLine("{0} {1} {2} {3}", + /// config["OS"], + /// config["Memory"], + /// config["Graphics"], + /// config["Lang"]); + /// } + /// + /// + /// + /// + /// The following example shows how to create variations for a vacation planner that has a signature like this: + /// CallVacationPlanner(string destination, int hotelQuality, string activity). This example demonstrates that certain + /// activities are only available for certain destinations. + /// + /// var de = new Parameter<string>("Destination") { "Whistler", "Hawaii", "Las Vegas" }; + /// var ho = new Parameter<int>("Hotel Quality") { 5, 4, 3, 2, 1 }; + /// var ac = new Parameter<string>("Activity") { "gambling", "swimming", "shopping", "skiing" }; + /// + /// var parameters = new List<ParameterBase> { de, ho, ac }; + /// var constraints = new List<Constraint<Variation>> + /// { + /// // If going to Whistler or Hawaii, then no gambling + /// Constraint<Variation> + /// .If(v => de.GetValue(v) == "Whistler" || de.GetValue(v) == "Hawaii") + /// .Then(v => ac.GetValue(v) != "gambling"), + /// + /// // If going to Las Vegas or Hawaii, then no skiing + /// Constraint<Variation> + /// .If(v => de.GetValue(v) == "Las Vegas" || de.GetValue(v) == "Hawaii") + /// .Then(v => ac.GetValue(v) != "skiing"), + /// + /// // If going to Whistler, then no swimming + /// Constraint<Variation> + /// .If(v => de.GetValue(v) == "Whistler") + /// .Then(v => ac.GetValue(v) != "swimming"), + /// }; + /// var model = new Model(parameters, constraints); + /// + /// + /// foreach (var vacationOption in model.GenerateVariations(2)) + /// { + /// Console.WriteLine("{0}, {1} stars -- {2}", + /// vacationOption["Destination"], + /// vacationOption["Hotel Quality"], + /// vacationOption["Activity"]); + /// } + /// + /// + /// + /// + /// The following example shows how to create variations for a vacation planner that adds weights and tags + /// to certain values. Adding weights changes the frequency in which a value will occur. Adding tags allows expected values + /// to be added to variations. + /// + /// var de = new Parameter<string>("Destination") + /// { + /// "Whistler", + /// "Hawaii", + /// new ParameterValue<string>("Las Vegas") { Weight = 10.0 }, + /// new ParameterValue<string>("Cleveland") { Tag = false } + /// }; + /// var ho = new Parameter<int>("Hotel Quality") { 5, 4, 3, 2, 1 }; + /// var ac = new Parameter<string>("Activity") { "gambling", "swimming", "shopping", "skiing" }; + /// var parameters = new List<ParameterBase> { de, ho, ac }; + /// + /// var model = new Model(parameters) { DefaultVariationTag = true }; + /// + /// foreach (var v in model.GenerateVariations(1)) + /// { + /// Console.WriteLine("{0} {1} {2} {3}", + /// v["Destination"], + /// v["Hotel Quality"], + /// v["Activity"], + /// ((bool)v.Tag == false) ? "--> don't go!" : ""); + /// } + /// + /// + public class Model : Model + { + /// + /// Initializes a new model with the specified parameters. + /// + /// The parameters. + public Model(IEnumerable parameters) + : base(parameters, null) + { + } + + /// + /// Initializes a new model with the specified parameters and constraints. + /// + /// The parameters. + /// The constraints. + public Model(IEnumerable parameters, IEnumerable> constraints) + : base(parameters, constraints) + { + } + } +} diff --git a/src/dotnetCampus.UITest.WPFTestHelper/VariationGeneration/Parameter.cs b/src/dotnetCampus.UITest.WPFTestHelper/VariationGeneration/Parameter.cs new file mode 100644 index 0000000..8fd10bf --- /dev/null +++ b/src/dotnetCampus.UITest.WPFTestHelper/VariationGeneration/Parameter.cs @@ -0,0 +1,194 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + + +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; + +namespace dotnetCampus.UITest.WPFTestHelper.VariationGeneration +{ + /// + /// Represents a single variable and its values in the model whose values are of the specified type. + /// Combinations of these values are used in the combinatorial generation of variations by the . + /// + /// + /// Exhaustively testing all possible inputs to any nontrivial software component is generally not possible + /// because of the enormous number of variations. Combinatorial testing is one approach to achieve high coverage + /// with a much smaller set of variations. Pairwise, the most common combinatorial strategy, tests every possible + /// pair of values. Higher orders of combinations (three-wise, four-wise, and so on) can also be used for higher coverage + /// at the expense of more variations. See Pairwise Testing and + /// + /// Pairwise Testing in Real World for more resources. + /// + /// The type of values that represent this parameter. + [SuppressMessage("Microsoft.Naming", "CA1710:IdentifiersShouldHaveCorrectSuffix", Scope = "type", Target = "Microsoft.Test.VariationGeneration.Parameter", Justification = "The suggested name ParameterCollection is confusing.")] + public class Parameter : ParameterBase, IList> + { + /// + /// Initializes a new instance of the parameter class using the specified name. + /// + /// The name of the parameter. + public Parameter(string name) : base(name) + { + } + + /// + /// Returns the value of this parameter in this variation. + /// + /// The variation. + /// The value. + public T GetValue(Variation v) + { + return (T)v[Name]; + } + + /// + /// Determines the index of a specific item in the . + /// + /// The object to locate in the . + /// The index of the item (if the item is found in the list); otherwise, -1. + public int IndexOf(ParameterValue item) + { + return values.IndexOf(item); + } + + /// + /// Inserts an item into the at the specified index. + /// + /// The zero-based index where the item should be inserted. + /// The object to insert into the . + public void Insert(int index, ParameterValue item) + { + values.Insert(index, item); + } + + /// + /// Removes the value at the specified index. + /// + /// The zero-based index of the item that should be removed. + public void RemoveAt(int index) + { + values.RemoveAt(index); + } + + /// + /// Gets or sets the value at the specified index. + /// + /// The zero-based index of the element to get or set. + /// The element at the specified index. + public ParameterValue this[int index] + { + get + { + return values[index]; + } + set + { + values[index] = value; + } + } + + /// + /// Adds a value to the . + /// + /// The object to add to the . + public void Add(ParameterValue item) + { + values.Add(item); + } + + /// + /// Adds a value to the . This value is wrapped in a . + /// + /// The value to wrap and add. + public void Add(T item) + { + values.Add(new ParameterValue(item)); + } + + /// + /// Removes all values from the . + /// + public void Clear() + { + values.Clear(); + } + + /// + /// Determines whether the contains a specific value. + /// + /// The object to locate in the . + /// true if the value is found in the ; otherwise, false. + public bool Contains(ParameterValue item) + { + return values.Contains(item); + } + + /// + /// Copies the elements of the to an array, starting at a particular array index. + /// + /// The one-dimensional array that is the destination of the elements copied from . The array must have zero-based indexing. + /// The zero-based index in the array where copying begins. + public void CopyTo(ParameterValue[] array, int arrayIndex) + { + values.CopyTo(array, arrayIndex); + } + + /// + /// Returns false. + /// + public bool IsReadOnly + { + get { return false; } + } + + /// + /// Removes the first occurrence of a specific value from the . + /// + /// The value to remove from the . + /// true if the value was successfully removed from the ; otherwise, false. This method also returns false if the value is not found in the original . + public bool Remove(ParameterValue item) + { + return values.Remove(item); + } + + /// + /// Returns an enumerator that iterates through the . + /// + /// An IEnumerator(T) that can be used to iterate through the collection. + public IEnumerator> GetEnumerator() + { + return values.GetEnumerator(); + } + + /// + /// Returns an enumerator that iterates through a collection. + /// + /// An IEnumerator object that can be used to iterate through the collection. + System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() + { + return values.GetEnumerator(); + } + + /// + /// The number of values in this parameter. + /// + public override int Count + { + get { return values.Count; } + } + + /// + /// Returns the item at the given index. + /// + /// The index. + /// The item. + public override object GetAt(int index) + { + return values[index]; + } + + private List> values = new List>(); + } +} diff --git a/src/dotnetCampus.UITest.WPFTestHelper/VariationGeneration/ParameterAttribute.cs b/src/dotnetCampus.UITest.WPFTestHelper/VariationGeneration/ParameterAttribute.cs new file mode 100644 index 0000000..50f8512 --- /dev/null +++ b/src/dotnetCampus.UITest.WPFTestHelper/VariationGeneration/ParameterAttribute.cs @@ -0,0 +1,126 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + + +using System; + +namespace dotnetCampus.UITest.WPFTestHelper.VariationGeneration +{ + +// Suppressing CS3015 'ParameterAttribute' has no accessible constructors +// which use only CLS-compliant types (params keyword is not CLS compliant) +// Providing empty constructor is not an option since attribute must always +// be created with list of values and validation can be performed only at +// runtime +#pragma warning disable 3015 + + /// + /// Specifies an attribute to decorate the properties of the class + /// that is used for generating model parameters. + /// + /// + /// + /// When a property is decorated with a single attribute, + /// the values from the attribute are added to the parameters that are + /// created on the model and used for variation generation. + /// When multiple attributes decorate the same property, a single + /// value is selected from the values of each attribute (the number of + /// values in the Model parameter is equal to the number of attributes + /// that are set on the property). + /// + /// + /// + /// The example below demonstrates how to use the Parameter attribute to declare a variation model. + /// + /// // First, provide a model class definition + /// class OsConfigurationEx + /// { + /// [Parameter(512, 1024, 2048)] + /// public int Memory { get; set; } + /// + /// [Parameter("WinXP", "Vista", "Win2K8", "Win7")] + /// public string OS { get; set; } + /// + /// [Parameter("enu", "jpn", "deu", "chs", "ptb")] + /// public string Language { get; set; } + /// + /// [Parameter("NVidia", "ATI", "Intel")] + /// public string Graphics { get; set; } + /// } + /// + /// // Then instantiate a model + /// var model = new Model<OsConfigurationEx>(); + /// foreach (OsConfigurationEx c in model.GenerateVariations(2)) + /// { + /// Console.WriteLine("{0} {1} {2} {3}", + /// c.Memory, + /// c.OS, + /// c.Language, + /// c.Graphics); + /// } + /// + /// + /// + /// + /// This example shows how to use attributes of the model definition + /// class to support equivalence classes with different weights. + /// + /// class OsConfigurationEx + /// { + /// [Parameter("WinXP", Weight = .3F)] + /// [Parameter("Vista", "Win7", Weight = .5F)] + /// public string OS { get; set; } + /// + /// [Parameter("EN-US")] + /// [Parameter("JP-JP", "CN-CH")] + /// [Parameter("HE-IL", "AR-SA")] + /// public string Language { get; set; } + /// } + /// + /// var model = new Model<OsConfigurationEx>(); + /// foreach (OsConfigurationEx c in model.GenerateVariations(1)) + /// { + /// Console.WriteLine("{0} {1}", + /// c.OS, + /// c.Language); + /// } + /// + /// + [AttributeUsage(AttributeTargets.Property, + AllowMultiple = true, + Inherited = true)] + public class ParameterAttribute : Attribute + { + /// + /// The values that are used when generating a model parameter. + /// + public object[] Values + { + get; + private set; + } + + /// + /// Instantiates an attribute with a list of values. + /// + /// A list of values. + public ParameterAttribute(params object[] values) + { + Values = values.Clone() as object[]; + } + + /// + /// Optional. The weight to assign to a model parameter + /// that is created based on an attribute. + /// + public float Weight { get; set; } + + /// + /// Optional. The tag to assign to a model parameter + /// that is created based on an attribute. + /// + public object Tag { get; set; } + } +#pragma warning restore 3015 +} diff --git a/src/dotnetCampus.UITest.WPFTestHelper/VariationGeneration/ParameterBase.cs b/src/dotnetCampus.UITest.WPFTestHelper/VariationGeneration/ParameterBase.cs new file mode 100644 index 0000000..8c9a589 --- /dev/null +++ b/src/dotnetCampus.UITest.WPFTestHelper/VariationGeneration/ParameterBase.cs @@ -0,0 +1,39 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + + +namespace dotnetCampus.UITest.WPFTestHelper.VariationGeneration +{ + /// + /// Provides a base class for the functionality that all parameters must implement. + /// + public abstract class ParameterBase + { + /// + /// Initializes a new instance of the base class using the specified name. + /// + /// The name of the parameter. + public ParameterBase(string name) + { + Name = name; + } + + /// + /// The name of the parameter. + /// + public string Name { get; set; } + + /// + /// The number of values in this parameter. + /// + public abstract int Count { get; } + + /// + /// Returns the value at the given index. + /// + /// The index. + /// The item. + public abstract object GetAt(int index); + } +} diff --git a/src/dotnetCampus.UITest.WPFTestHelper/VariationGeneration/ParameterInteraction.cs b/src/dotnetCampus.UITest.WPFTestHelper/VariationGeneration/ParameterInteraction.cs new file mode 100644 index 0000000..5796bc1 --- /dev/null +++ b/src/dotnetCampus.UITest.WPFTestHelper/VariationGeneration/ParameterInteraction.cs @@ -0,0 +1,115 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + + +using System; +using System.Collections.Generic; +using System.Linq; + +namespace dotnetCampus.UITest.WPFTestHelper.VariationGeneration +{ + /// + /// A table containing a subset of parameters and all the possible value combinations of that subset. + /// + internal class ParameterInteraction + { + public ParameterInteraction(ParameterInteraction interaction) + { + this.Parameters = new List(interaction.Parameters); + this.Combinations = new List(interaction.Combinations.Count); + foreach (var combination in interaction.Combinations) + { + var newCombination = new ValueCombination(combination); + Combinations.Add(newCombination); + } + } + + public ParameterInteraction(IEnumerable parameters) + { + this.Combinations = new List(); + this.Parameters = new List(parameters); + } + + public IList Parameters { get; private set; } + + public IList Combinations { get; private set; } + + public int GetUncoveredCombinationsCount() + { + return Combinations.Count((c) => c.State == ValueCombinationState.Uncovered); + } + + /// + /// Returns true when obj refers to the same subset of parameters. + /// + /// Object to compare + /// Whether subsets are equal + public override bool Equals(object obj) + { + if (obj == null) + { + return false; + } + + if (!(obj is ParameterInteraction)) + { + return false; + } + + var interaction = (ParameterInteraction)obj; + + if (this.Parameters.Count != interaction.Parameters.Count) + { + return false; + } + + for (int i = 0; i < this.Parameters.Count; i++) + { + if (this.Parameters[i] != interaction.Parameters[i]) + { + return false; + } + } + + return true; + } + + public override int GetHashCode() + { + return Parameters.Aggregate(0, (seed, next) => seed ^ next); + } + + /// + /// Returns a new table that is the union of the input tables. + /// + /// The model's parameters + /// First table + /// Second table + /// Function to calculate state of merged value combinations + /// The new table + public static ParameterInteraction Merge(IList parameters, ParameterInteraction first, ParameterInteraction second, Func newState) + where T : new() + { + List parameterIndices = first.Parameters.Union(second.Parameters).ToList(); + parameterIndices.Sort(); + + var mergedInteraction = new ParameterInteraction(parameterIndices); + + var valueTable = ParameterInteractionTable.GenerateValueTable(parameters, mergedInteraction); + foreach (var value in valueTable) + { + mergedInteraction.Combinations.Add(new ValueCombination(value, mergedInteraction)); + } + + foreach (var combination in mergedInteraction.Combinations) + { + var firstMatch = first.Combinations.First((c) => ParameterInteractionTable.MatchCombination(c, combination)); + var secondMatch = second.Combinations.First((c) => ParameterInteractionTable.MatchCombination(c, combination)); + combination.State = newState(firstMatch.State, secondMatch.State); + } + + return mergedInteraction; + } + } +} diff --git a/src/dotnetCampus.UITest.WPFTestHelper/VariationGeneration/ParameterInteractionTable.cs b/src/dotnetCampus.UITest.WPFTestHelper/VariationGeneration/ParameterInteractionTable.cs new file mode 100644 index 0000000..bc621ba --- /dev/null +++ b/src/dotnetCampus.UITest.WPFTestHelper/VariationGeneration/ParameterInteractionTable.cs @@ -0,0 +1,509 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + + +using System; +using System.Collections.Generic; +using System.Linq; +using dotnetCampus.UITest.WPFTestHelper.VariationGeneration.Constraints; + +namespace dotnetCampus.UITest.WPFTestHelper.VariationGeneration +{ + /// + /// Table containing all the interactions between parameters in the model, and all possible value combinations with their current state. + /// + internal class ParameterInteractionTable where T : new() + { + public ParameterInteractionTable(Model model, int order) + { + GenerateInteractionsForParameters(model, order); + GenerateInteractionsForConstraints(model, order); + + excludedCombinations = (from interaction in Interactions + from combination in interaction.Combinations + where combination.State == ValueCombinationState.Excluded + select combination).ToList(); + } + + List interactions = new List(); + public IList Interactions { get { return interactions; } } + + /// + /// Test whether all value combinations are Covered or Excluded + /// + /// The result + public bool IsCovered() + { + return !Interactions.Any((i) => i.Combinations.Any((c) => c.State == ValueCombinationState.Uncovered)); + } + + List excludedCombinations; + + /// + /// Returns all excluded combinations in the table + /// + public IEnumerable ExcludedCombinations { get { return excludedCombinations; } } + + // implicit constraints are represented as explicit constraints internally + // this is the constraints in the model + the internal constraints + List> completeConstraints = new List>(); + + // walks the constraints and adds any needed interactions and marks existing combinations as Excluded + private void GenerateInteractionsForConstraints(Model model, int order) + { + completeConstraints.Clear(); + completeConstraints.AddRange(model.Constraints); + completeConstraints.Add(new TaggedValueConstraint()); + + // clear any pregenerated data + foreach (var constraint in completeConstraints) + { + constraint.ClearCache(); + } + + var constraintInteractions = new List(); + foreach (var constraint in completeConstraints) + { + var constraintInteraction = constraint.GetExcludedCombinations(model); + constraintInteractions.Add(constraintInteraction); + + MergeConstraintInteraction(order, constraintInteraction); + } + + var uncoveredCombinations = + from interaction in Interactions + from combination in interaction.Combinations + where combination.State == ValueCombinationState.Uncovered + select combination; + + // make sure uncovered combinations don't violate any constraints + foreach (var combination in uncoveredCombinations) + { + foreach (var constraint in completeConstraints) + { + if (constraint.SatisfiesContraint(model, combination) == ConstraintSatisfaction.Unsatisfied) + { + combination.State = ValueCombinationState.Excluded; + } + } + } + + GenerateDependentConstraintInteractions(model, order, constraintInteractions); + + // if any interaction has only excluded combinations then everything is excluded + if (Interactions.Any((i) => i.Combinations.All((c) => c.State == ValueCombinationState.Excluded))) + { + uncoveredCombinations = + from interaction in Interactions + from combination in interaction.Combinations + where combination.State == ValueCombinationState.Uncovered + select combination; + + foreach (var combination in uncoveredCombinations) + { + combination.State = ValueCombinationState.Excluded; + } + } + } + + // adds an interaction generated from a constraint to the table + private void MergeConstraintInteraction(int order, ParameterInteraction constraintInteraction) + { + // is the constraint already in the table + if (!Interactions.Contains(constraintInteraction)) + { + // if the interaction has more parameters than the order we can't mark any of the existing combinations excluded + // if the interaction is shorter we need to mark the existing combinations excluded + if (constraintInteraction.Parameters.Count > order) + { + Interactions.Add(constraintInteraction); + } + else + { + var excludedValues = constraintInteraction.Combinations.Where((c) => c.State == ValueCombinationState.Excluded); + + var candidateInteractions = + from interaction in Interactions + where constraintInteraction.Parameters.All((i) => interaction.Parameters.Contains(i)) + select interaction; + + var excludedCombinations = + from interaction in candidateInteractions + from combination in interaction.Combinations + where excludedValues.Any((c) => MatchCombination(c, combination)) + select combination; + + foreach (var combination in excludedCombinations) + { + combination.State = ValueCombinationState.Excluded; + } + } + } + else + { + // mark combinations excluded by the constraint excluded in the table + var interaction = Interactions.First((i) => i.Equals(constraintInteraction)); + + var excludedValues = constraintInteraction.Combinations.Where((c) => c.State == ValueCombinationState.Excluded); + + var combinations = interaction.Combinations.Where((c) => excludedValues.Any((excluded) => MatchCombination(excluded, c))); + foreach (var combination in combinations) + { + combination.State = ValueCombinationState.Excluded; + } + } + } + + // for the following system: + // if A == 0 then B == 0 + // if B == 0 then C == 0 + // if C == 0 then A == 1 + // all combinations with A == 0 need to be excluded, but this is impossible to determine when looking at individual constraints + // to do this: + // - create groups of constraints where all members have a parameter that overlaps with another constraint + // - create ParameterInteraction for each group of constraints + // - for the existing uncovered combinations, if they excluded in every matching combination in the group interaction, mark excluded + // - higher order combinations that match the order of a constraint's interaction can also need to be exculded + private void GenerateDependentConstraintInteractions(Model model, int order, List constraintInteractions) + { + // group all the constraint parameter interactions that share parameters + var dependentConstraintSets = new List>(); + while (constraintInteractions.Count > 0) + { + var dependentConstraintSet = new List(); + var interactionsToExplore = new List(); + + interactionsToExplore.Add(constraintInteractions[0]); + constraintInteractions.RemoveAt(0); + + while (interactionsToExplore.Count > 0) + { + ParameterInteraction current = interactionsToExplore[0]; + interactionsToExplore.RemoveAt(0); + dependentConstraintSet.Add(current); + + var dependentInteractions = + (from constraint in constraintInteractions + where constraint.Parameters.Any((i) => current.Parameters.Contains(i)) + select constraint).ToList(); + + foreach (var dependentInteraction in dependentInteractions) + { + constraintInteractions.Remove(dependentInteraction); + } + + interactionsToExplore.AddRange(dependentInteractions); + } + + dependentConstraintSets.Add(dependentConstraintSet); + } + + // walk over the groups of constraints + foreach (var dependentConstraintSet in dependentConstraintSets) + { + // if there's only one constraint no more processing is necessary + if (dependentConstraintSet.Count <= 1) + { + continue; + } + + // merge the interactions of all the constraints + var uniqueParameters = new Dictionary(); + var parameterInteractionCounts = new List(); + for (int i = 0; i < dependentConstraintSet.Count; i++) + { + foreach (var parameter in dependentConstraintSet[i].Parameters) + { + uniqueParameters[parameter] = true; + } + + if (dependentConstraintSet[i].Parameters.Count > order) + { + parameterInteractionCounts.Add(dependentConstraintSet[i].Parameters.Count); + } + } + + var sortedParameters = uniqueParameters.Keys.ToList(); + sortedParameters.Sort(); + + ParameterInteraction completeInteraction = new ParameterInteraction(sortedParameters); + var valueTable = ParameterInteractionTable.GenerateValueTable(model.Parameters, completeInteraction); + foreach (var value in valueTable) + { + completeInteraction.Combinations + .Add(new ValueCombination(value, completeInteraction){ State = ValueCombinationState.Covered }); + } + + // calculate the excluded combinations in the new uber interaction + var completeInteractionExcludedCombinations = new List(); + + // find the combinations from the uber interaction that aren't excluded + // if a combination is a subset of any of these it is not excluded + var allowedCombinations = new List(); + + foreach (var combination in completeInteraction.Combinations) + { + bool exclude = false; + foreach (var constraint in completeConstraints) + { + if (constraint.SatisfiesContraint(model, combination) == ConstraintSatisfaction.Unsatisfied) + { + exclude = true; + break; + } + } + + if (exclude) + { + combination.State = ValueCombinationState.Excluded; + completeInteractionExcludedCombinations.Add(combination); + } + else + { + allowedCombinations.Add(combination); + } + } + + // find the existing combinations that are never allowed in the uber interaction + var individualInteractionExcludedCombinations = + from interaction in Interactions + from combination in interaction.Combinations + where !combination.ParameterToValueMap.Any((p) => !completeInteraction.Parameters.Contains(p.Key)) + && !allowedCombinations.Any((c) => MatchCombination(combination, c)) + select combination; + + // mark the combinations in the table as excluded + foreach (var combination in individualInteractionExcludedCombinations) + { + combination.State = ValueCombinationState.Excluded; + } + + GenerateHigherOrderDependentExclusions(model, order, parameterInteractionCounts, completeInteraction, allowedCombinations); + } + } + + private void GenerateHigherOrderDependentExclusions(Model model, int order, List parameterInteractionCounts, ParameterInteraction completeInteraction, IEnumerable allowedCombinations) + { + // generate the combinations for orders between order and completerInteraction.Parameters.Count + foreach (var count in parameterInteractionCounts) + { + IList parameterCombinations = GenerateCombinations(completeInteraction.Parameters.Count, count); + foreach (var combinations in parameterCombinations) + { + var interaction = new ParameterInteraction(combinations.Select((i) => completeInteraction.Parameters[i])); + var possibleValues = ParameterInteractionTable.GenerateValueTable(model.Parameters, interaction); + foreach (var value in possibleValues) + { + interaction.Combinations + .Add(new ValueCombination(value, interaction) { State = ValueCombinationState.Covered }); + } + + // find combinations that should be excluded + var excludedCombinations = new List(); + foreach (var combination in interaction.Combinations) + { + if (!combination.ParameterToValueMap.Any((p) => !completeInteraction.Parameters.Contains(p.Key)) + && !allowedCombinations.Any((c) => MatchCombination(combination, c))) + { + excludedCombinations.Add(combination); + } + } + + if (excludedCombinations.Count() > 0) + { + foreach (var combination in excludedCombinations) + { + combination.State = ValueCombinationState.Excluded; + } + + MergeConstraintInteraction(order, interaction); + } + } + } + } + + // do all the values in subset match with the whole + public static bool MatchCombination(ValueCombination subSet, ValueCombination whole) + { + var wholeParameterMap = whole.ParameterToValueMap; + var subSetParameterMap = subSet.ParameterToValueMap; + foreach (var key in subSet.Keys) + { + if (!wholeParameterMap.ContainsKey(key) || + subSetParameterMap[key] != wholeParameterMap[key]) + { + return false; + } + } + + return true; + } + + // generate the n-wise interactions for the parameters + private void GenerateInteractionsForParameters(Model model, int order) + { + IList parameterCombinations = GenerateCombinations(model.Parameters.Count, order); + + foreach (var parameterCombination in parameterCombinations) + { + Interactions.Add(new ParameterInteraction(parameterCombination)); + } + + GenerateValueCombinations(model); + } + + // create all the values for each combination + private void GenerateValueCombinations(Model model) + { + for (int i = 0; i < Interactions.Count; i++) + { + var interaction = Interactions[i]; + var valueTable = GenerateValueTable(model.Parameters, interaction); + + for (int j = 0; j < valueTable.Count; j++) + { + var values = valueTable[j]; + var combination = new ValueCombination(values, interaction); + var tag = model.DefaultVariationTag; + double weight = 1.0; + + interaction.Combinations.Add(combination); + for (int k = 0; k < values.Length; k++) + { + var value = model.Parameters[interaction.Parameters[k]].GetAt(values[k]); + + if (value is ParameterValueBase) + { + var parameterValue = (ParameterValueBase)value; + if (parameterValue.Weight != 1.0) + { + weight = weight == 1.0 ? parameterValue.Weight : Math.Max(weight, parameterValue.Weight); + } + + if (parameterValue.Tag != null && parameterValue.Tag != model.DefaultVariationTag) + { + if (tag != model.DefaultVariationTag) + { + combination.State = ValueCombinationState.Excluded; + } + else + { + tag = parameterValue.Tag; + } + } + } + } + + combination.Weight = weight; + combination.Tag = tag; + } + } + } + + // returns a table with all possible value combinations for the given interaction + internal static List GenerateValueTable(IList parameters, ParameterInteraction interaction) + { + + List possibleValueTable = new List(); + for (int i = 0; i < interaction.Parameters.Count; i++) + { + possibleValueTable.Add(GenerateOrderedArray(parameters[interaction.Parameters[i]].Count)); + } + + int valueTableCount = possibleValueTable.Aggregate(1, (seed, array) => seed * array.Length); + + var table = new List(valueTableCount); + for (int i = 0; i < valueTableCount; i++) + { + table.Add(new int[possibleValueTable.Count]); + } + + int[] repeats = new int[possibleValueTable.Count]; + for (int i = possibleValueTable.Count - 1; i >= 0; i--) + { + int repeat = 1; + if (i < possibleValueTable.Count - 1) + { + repeat = possibleValueTable[i + 1].Length * repeats[i + 1]; + } + + repeats[i] = repeat; + } + + for (int i = 0; i < table.Count; i++) + { + for (int j = 0; j < table[i].Length; j++) + { + table[i][j] = possibleValueTable[j][(i / repeats[j]) % possibleValueTable[j].Length]; + } + } + + return table; + } + + private static int[] GenerateOrderedArray(int count) + { + int[] value = new int[count]; + for (int i = 0; i < count; i++) + { + value[i] = i; + } + return value; + } + + private static IList GenerateCombinations(int totalElements, int combinationSize) + { + // calculate the number of combinations totalElements choose combinationSize + // totalElements!/(combinationSize! * (totalElements - combinationSize)!) + long totalCombinations = Factorial(totalElements) / (Factorial(combinationSize) * Factorial(totalElements - combinationSize)); + + var combinations = new List(totalCombinations > int.MaxValue ? int.MaxValue : (int)totalCombinations); + + for (int i = 0; i < totalCombinations; i++) + { + combinations.Add(new int[combinationSize]); + } + + int[] currentCombination = GenerateOrderedArray(combinationSize); + + for (int i = 0; i < combinations.Count; i++) + { + currentCombination.CopyTo(combinations[i], 0); + + int startIndex = combinationSize - 1; + for (; startIndex >= 0; startIndex--) + { + if (currentCombination[startIndex] < totalElements - (combinationSize - startIndex)) + { + break; + } + } + + if (startIndex < 0) + { + break; + } + + int currentItem = currentCombination[startIndex]; + for (; startIndex < combinationSize; startIndex++) + { + currentItem++; + currentCombination[startIndex] = currentItem; + } + } + + return combinations; + } + + private static long Factorial(int n) + { + long total = 1; + for (int i = 2; i <= n; i++) + { + total *= (long)i; + } + return total; + } + } +} diff --git a/src/dotnetCampus.UITest.WPFTestHelper/VariationGeneration/ParameterValue.cs b/src/dotnetCampus.UITest.WPFTestHelper/VariationGeneration/ParameterValue.cs new file mode 100644 index 0000000..3d40e14 --- /dev/null +++ b/src/dotnetCampus.UITest.WPFTestHelper/VariationGeneration/ParameterValue.cs @@ -0,0 +1,67 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + + +namespace dotnetCampus.UITest.WPFTestHelper.VariationGeneration +{ + /// + /// Represents a single value in a parameter. + /// + /// The type of the value. + public class ParameterValue : ParameterValueBase + { + /// + /// Initializes a new value with the specified value. + /// + /// The value. + public ParameterValue(T value) : this(value, null, 1.0) + { + } + + /// + /// Initializes a new value with the specified value and an expected result. + /// + /// The value. + /// A user defined tag. + public ParameterValue(T value, object tag) : this(value, tag, 1.0) + { + } + + /// + /// Initializes a new value with the specified value and weight. + /// + /// The value. + /// The weight of the value. + public ParameterValue(T value, double weight) : this(value, null, weight) + { + } + + /// + /// Initializes a new value with the specified value, tag, and weight. + /// + /// The value. + /// A user-defined tag. + /// The weight of the value. + public ParameterValue(T value, object tag, double weight) + { + Value = value; + Tag = tag; + Weight = weight; + } + + /// + /// The actual value. + /// + public T Value { get; set; } + + /// + /// Returns the value that this ParameterValue represents. + /// + /// The value. + public override object GetValue() + { + return Value; + } + } +} diff --git a/src/dotnetCampus.UITest.WPFTestHelper/VariationGeneration/ParameterValueBase.cs b/src/dotnetCampus.UITest.WPFTestHelper/VariationGeneration/ParameterValueBase.cs new file mode 100644 index 0000000..8917e4e --- /dev/null +++ b/src/dotnetCampus.UITest.WPFTestHelper/VariationGeneration/ParameterValueBase.cs @@ -0,0 +1,39 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + + +namespace dotnetCampus.UITest.WPFTestHelper.VariationGeneration +{ + /// + /// Represents a single value in a parameter. + /// + public abstract class ParameterValueBase + { + /// + /// Tags the value with a user-defined expected result. + /// At most, one tagged value appears in a variation. The default is null. + /// + public object Tag { get; set; } + + /// + /// A value that indicates whether this value should be chosen more or less frequently. + /// Larger values are chosen more often. The default is 1.0. + /// + /// + /// Weighting creates preferences for certain values. Because of the nature of the algorithm used, + /// the actual weight has no intrinsic meaning (weighting one value at 10.0 and the others at 1.0 + /// does not mean the first value appears 10 times more often). The primary goal of the algorithm + /// is to cover all the combinations while using the fewest possible test cases, which often + /// contradicts the preference for honoring the weight. Weight acts as a tie breaker when candidate + /// values cover the same number of combinations. + /// + public double Weight { get; set; } + + /// + /// Returns the value that this ParameterValue represents. + /// + /// The value. + public abstract object GetValue(); + } +} diff --git a/src/dotnetCampus.UITest.WPFTestHelper/VariationGeneration/ValueCombination.cs b/src/dotnetCampus.UITest.WPFTestHelper/VariationGeneration/ValueCombination.cs new file mode 100644 index 0000000..453d56c --- /dev/null +++ b/src/dotnetCampus.UITest.WPFTestHelper/VariationGeneration/ValueCombination.cs @@ -0,0 +1,80 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + + +using System; +using System.Collections; +using System.Collections.Generic; + +namespace dotnetCampus.UITest.WPFTestHelper.VariationGeneration +{ + + + /// + /// A single value in the model + /// + internal class ValueCombination + { + public ValueCombination(ValueCombination combination) + { + this.parameterToValueMap = new Dictionary(combination.ParameterToValueMap); + this.State = combination.State; + keys = new KeyCollection(parameterToValueMap.Keys); + } + + public ValueCombination(IList values, ParameterInteraction interaction) + { + if (values.Count != interaction.Parameters.Count) + { + throw new ArgumentOutOfRangeException("values", "values and interaction must be the same length."); + } + + this.parameterToValueMap = new Dictionary(interaction.Parameters.Count); + for (int i = 0; i < values.Count; i++) + { + parameterToValueMap[interaction.Parameters[i]] = values[i]; + } + State = ValueCombinationState.Uncovered; + keys = new KeyCollection(parameterToValueMap.Keys); + } + + public ValueCombinationState State { get; set; } + + private Dictionary parameterToValueMap; + /// + /// Dictionary that maps a parameter to its value + /// + public IDictionary ParameterToValueMap { get { return parameterToValueMap; } } + + + public IEnumerable Keys { get { return keys; } } + + public double Weight { get; set; } + public object Tag { get; set; } + + private KeyCollection keys; + + private class KeyCollection : IEnumerable + { + IEnumerator enumerator; + + public KeyCollection(IEnumerable keys) + { + enumerator = keys.GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + enumerator.Reset(); + return enumerator; + } + + public IEnumerator GetEnumerator() + { + enumerator.Reset(); + return enumerator; + } + } + } +} diff --git a/src/dotnetCampus.UITest.WPFTestHelper/VariationGeneration/ValueCombinationState.cs b/src/dotnetCampus.UITest.WPFTestHelper/VariationGeneration/ValueCombinationState.cs new file mode 100644 index 0000000..6d59a57 --- /dev/null +++ b/src/dotnetCampus.UITest.WPFTestHelper/VariationGeneration/ValueCombinationState.cs @@ -0,0 +1,14 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + + +namespace dotnetCampus.UITest.WPFTestHelper.VariationGeneration +{ + internal enum ValueCombinationState + { + Uncovered, + Covered, + Excluded + } +} diff --git a/src/dotnetCampus.UITest.WPFTestHelper/VariationGeneration/Variation.cs b/src/dotnetCampus.UITest.WPFTestHelper/VariationGeneration/Variation.cs new file mode 100644 index 0000000..a8df8ee --- /dev/null +++ b/src/dotnetCampus.UITest.WPFTestHelper/VariationGeneration/Variation.cs @@ -0,0 +1,49 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + + +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; + +namespace dotnetCampus.UITest.WPFTestHelper.VariationGeneration +{ + /// + /// Represents a tuple that has a single value for every + /// in the . The Model produces these by using combinatorial testing techniques. + /// + /// + /// Exhaustively testing all possible inputs to any nontrivial software component is generally impossible + /// due to the enormous number of variations. Combinatorial testing is one approach to achieve high coverage + /// with a much smaller set of variations. Pairwise, the most common combinatorial strategy, test every possible + /// pair of values. Higher orders of combinations (3-wise, 4-wise, etc.) can also be used for higher coverage + /// at the expense of more variations. See Pairwise Testing and + /// + /// Pairwise Testing in Real World for more resources. + /// + [SuppressMessage("Microsoft.Naming", "CA1710:IdentifiersShouldHaveCorrectSuffix", Scope = "type", Target = "Microsoft.Test.VariationGeneration.Variation", Justification = "The suggested name VariationDictionary is confusing.")] + [SuppressMessage("Microsoft.Usage", "CA2237:MarkISerializableTypesWithSerializable", Scope = "type", Target = "Microsoft.Test.VariationGeneration.Variation", Justification = "Not currently used across app domains and this pollutes the om.")] + public class Variation : Dictionary + { + /// + /// Initializes a new instance of the Variation class. + /// + public Variation() : this(null) + { + } + + /// + /// Initializes a new variation that has the specified expected result. + /// + /// Specifies whether this variation contains a user-defined tag. + public Variation(object tag) + { + Tag = tag; + } + + /// + /// Indicates whether a value has been tagged with an expected result. The default is null. + /// + public object Tag { get; private set; } + } +} diff --git a/src/dotnetCampus.UITest.WPFTestHelper/VariationGeneration/VariationGenerator.cs b/src/dotnetCampus.UITest.WPFTestHelper/VariationGeneration/VariationGenerator.cs new file mode 100644 index 0000000..f72f4bd --- /dev/null +++ b/src/dotnetCampus.UITest.WPFTestHelper/VariationGeneration/VariationGenerator.cs @@ -0,0 +1,282 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + + +using System; +using System.Collections.Generic; +using System.Linq; + +namespace dotnetCampus.UITest.WPFTestHelper.VariationGeneration +{ + /// + /// The core generation engine. Takes in a model and transforms it into a table of all possible values that + /// need to be covered or excluded. Uses that table to create variations and transforms that into the public + /// + /// + internal static class VariationGenerator + { + /// + /// Entry point to VariationGenerator + /// + /// The model + /// The order of the parameters (pairwise, 3-wise, etc) + /// Random seed to use + /// Generated Variations + public static IEnumerable GenerateVariations(Model model, int order, int seed) where T : new() + { + // calculate the number variations to exhaustively test the model, + // useful to determine if something is wrong during generation + long maxVariations = model.Parameters.Aggregate((long)1, (total, next) => total * (long)next.Count); + var variationIndices = GenerateVariationIndices(Prepare(model, order),model.Parameters.Count, seed, maxVariations, model.DefaultVariationTag); + + return from v in variationIndices + select IndicesToVariation(model, v); + } + + // generate all the values to cover or exclude + private static ParameterInteractionTable Prepare(Model model, int order) where T : new() + { + return new ParameterInteractionTable(model, order); + } + + // this is the actual generation function + // returns a list of indices that allow lookup of the actual value in the model + private static IList GenerateVariationIndices(ParameterInteractionTable interactions, int variationSize, int seed, long maxVariations, object defaultTag) where T : new() + { + Random random = new Random(seed); + List variations = new List(); + + // while there a uncovered values + while (!interactions.IsCovered()) + { + int[] candidate = new int[variationSize]; + object variationTag = defaultTag; + + // this is a scatch variable so new arrays won't be allocated for every candidate + int[] proposedCandidate = new int[variationSize]; + for (int i = 0; i < candidate.Length; i++) + { + // -1 indicates an empty slot + candidate[i] = -1; + } + + IEnumerable candidateInteractions = interactions.Interactions; + // while there are empty slots + while (candidate.Any((i) => i == -1)) + { + // if all the slots are empty + if (candidate.All((i) => i == -1)) + { + // then pick the first uncovered combination from the most uncovered parameter interaction + int mostUncovered = + interactions.Interactions.Max((i) => i.GetUncoveredCombinationsCount()); + + var interaction = interactions.Interactions.First((i) => i.GetUncoveredCombinationsCount() == mostUncovered); + var combination = interaction.Combinations.First((c) => c.State == ValueCombinationState.Uncovered); + + foreach (var valuePair in combination.ParameterToValueMap) + { + candidate[valuePair.Key] = valuePair.Value; + } + + variationTag = combination.Tag == null || combination.Tag == defaultTag ? variationTag : combination.Tag; + combination.State = ValueCombinationState.Covered; + } + else + { + // find interactions that aren't covered by the current candidate variation + var incompletelyCoveredInteractions = + from interaction in candidateInteractions + where interaction.Parameters.Any((i) => candidate[i] == -1) + select interaction; + + candidateInteractions = incompletelyCoveredInteractions; + // find values that can be added to the current candidate + var compatibleValues = new List(); + foreach (var interaction in incompletelyCoveredInteractions) + { + foreach (var combination in interaction.Combinations) + { + if (IsCompatibleValue(combination, candidate)) + { + compatibleValues.Add(combination); + } + } + } + + // get the uncovered values + var uncoveredValues = compatibleValues.Where((v) => v.State == ValueCombinationState.Uncovered).ToList(); + + // calculate what the candidate will look like if add an uncovered value + var proposedCandidates = new List(); + foreach (var uncoveredValue in uncoveredValues) + { + CreateProposedCandidate(uncoveredValue, candidate, proposedCandidate); + + if (!IsExcluded(interactions.ExcludedCombinations, proposedCandidate)) + { + var coverage = new CandidateCoverage + { + Value = uncoveredValue, + CoverageCount = uncoveredValues.Count((v) => IsCovered(v, proposedCandidate)), + }; + + proposedCandidates.Add(coverage); + } + + } + + // if any of the proposed candidates isn't exclude + if (proposedCandidates.Count > 0) + { + // find the value that will cover the most combinations + int maxCovered = proposedCandidates.Max((c) => c.CoverageCount); + double maxWeight = proposedCandidates.Where((c) => c.CoverageCount == maxCovered).Max((c) => c.Value.Weight); + ValueCombination proposedValue = proposedCandidates.First((c) => c.CoverageCount == maxCovered && c.Value.Weight == maxWeight).Value; + + // add this value to candidate and mark all values as such + foreach (var valuePair in proposedValue.ParameterToValueMap) + { + candidate[valuePair.Key] = valuePair.Value; + } + + variationTag = proposedValue.Tag == null || proposedValue.Tag == defaultTag ? variationTag : proposedValue.Tag; + + // get the newly covered values so they can be marked + var newlyCoveredValue = uncoveredValues.Where((v) => IsCovered(v, candidate)).ToList(); + + foreach (var value in newlyCoveredValue) + { + value.State = ValueCombinationState.Covered; + } + } + else + { + // no uncovered values can be added with violating a constraint, add a random covered value + var compatibleWeightBuckets = compatibleValues.GroupBy((v) => v.Weight).OrderByDescending((v) => v.Key); + ValueCombination value = null; + bool combinationFound = false; + foreach (var bucket in compatibleWeightBuckets) + { + int count = bucket.Count(); + int attempts = 0; + + do + { + value = bucket.ElementAt(random.Next(count - 1)); + CreateProposedCandidate(value, candidate, proposedCandidate); + + if (!interactions.ExcludedCombinations.Any((c) => IsCovered(c, proposedCandidate))) + { + combinationFound = true; + } + + attempts++; + + // this is a heuristic, since we're pulling random values just going to count probably + // means we've attempted duplicates, going to 2 * count means we've probably tried + // everything at least once + if (attempts > count * 2) + { + break; + } + } + while (!combinationFound); + + if (combinationFound) + { + break; + } + } + + if (!combinationFound) + { + throw new InternalVariationGenerationException("Unable to find candidate with no exclusions."); + } + + // add this value to candidate and mark all values as such + + foreach (var valuePair in value.ParameterToValueMap) + { + candidate[valuePair.Key] = valuePair.Value; + } + + variationTag = value.Tag == null || value.Tag == defaultTag ? variationTag : value.Tag; + } + } + } + + variations.Add(new VariationIndexTagPair { Indices = candidate, Tag = variationTag }); + + // more variations than are need to exhaustively test the model have been adde + if (variations.Count > maxVariations) + { + throw new InternalVariationGenerationException("More variations than an exhaustive suite produced."); + } + } + + return variations; + } + + // is this value covered by the candidate + private static bool IsCovered(ValueCombination value, int[] candidate) + { + foreach (int i in value.Keys) + { + if (candidate[i] != value.ParameterToValueMap[i]) + { + return false; + } + } + + return true; + } + + // does this candidate violate any constraints + private static bool IsExcluded(IEnumerable excludedValues, int[] candidate) + { + return excludedValues.Any((c) => IsCovered(c, candidate)); + } + + // add this value to the candidate + private static void CreateProposedCandidate(ValueCombination value, int[] baseCandidate, int[] proposed) + { + baseCandidate.CopyTo(proposed, 0); + + foreach (var valuePair in value.ParameterToValueMap) + { + proposed[valuePair.Key] = valuePair.Value; + } + } + + // can this value be added to this candidate + private static bool IsCompatibleValue(ValueCombination value, int[] candidate) + { + foreach (int i in value.Keys) + { + if (candidate[i] != value.ParameterToValueMap[i] && candidate[i] != -1) + { + return false; + } + } + + return true; + } + + // map value indices to actual values and create a Variation + private static Variation IndicesToVariation(Model model, VariationIndexTagPair pair) where T : new () + { + Variation v = new Variation(pair.Tag); + + for (int i = 0; i < pair.Indices.Length; i++) + { + var value = model.Parameters[i].GetAt(pair.Indices[i]) is ParameterValueBase ? + ((ParameterValueBase)model.Parameters[i].GetAt(pair.Indices[i])).GetValue() : model.Parameters[i].GetAt(pair.Indices[i]); + v.Add(model.Parameters[i].Name, value); + } + + return v; + } + } +} diff --git a/src/dotnetCampus.UITest.WPFTestHelper/VariationGeneration/VariationIndexTagPair.cs b/src/dotnetCampus.UITest.WPFTestHelper/VariationGeneration/VariationIndexTagPair.cs new file mode 100644 index 0000000..4a0ef62 --- /dev/null +++ b/src/dotnetCampus.UITest.WPFTestHelper/VariationGeneration/VariationIndexTagPair.cs @@ -0,0 +1,16 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + + +namespace dotnetCampus.UITest.WPFTestHelper.VariationGeneration +{ + /// + /// Contains the indices of variation and the corresponding tag. Used to build the actual Variation. + /// + internal class VariationIndexTagPair + { + public int[] Indices { get; set; } + public object Tag { get; set; } + } +} diff --git a/src/dotnetCampus.UITest.WPFTestHelper/VariationGeneration/VariationsWrapper.cs b/src/dotnetCampus.UITest.WPFTestHelper/VariationGeneration/VariationsWrapper.cs new file mode 100644 index 0000000..7c82584 --- /dev/null +++ b/src/dotnetCampus.UITest.WPFTestHelper/VariationGeneration/VariationsWrapper.cs @@ -0,0 +1,75 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + + +using System.Collections.Generic; +using System.Reflection; + +namespace dotnetCampus.UITest.WPFTestHelper.VariationGeneration +{ + /// + /// Wrapper around IEnumerable that creates list of T instances + /// and assigns those instances properties based on list of generic + /// variations and reflection mapping metadata. + /// + /// The wrapper is used to translate list of generic Variations generated + /// by the model and containing key\value pairs of parameter name\value + /// to list of strongly type variation definition instances. + /// + /// + internal class VariationsWrapper : IEnumerable where T : new() + { + IEnumerable variations; + Dictionary propertiesMap; + + /// + /// Initializes a single object from a variation. + /// + /// The source variation. + /// The initialized object. + public T AssignParameterValues(Variation variation) + { + T value = new T(); + foreach (string parameterName in variation.Keys) + { + if (propertiesMap.ContainsKey(parameterName)) + { + PropertyInfo propertyInfo = propertiesMap[parameterName]; + propertyInfo.SetValue(value, variation[parameterName], null); + } + } + return value; + } + + /// + /// Initializes new wrapper. + /// + /// Map of properties to use when mapping + /// generic variation object to strongly typed T instance. + /// List of generic variations to wrap. + public VariationsWrapper(Dictionary propertiesMap, IEnumerable variations) + { + this.variations = variations; + this.propertiesMap = propertiesMap; + } + + #region IEnumerable implementation + public IEnumerator GetEnumerator() + { + foreach (Variation variation in variations) + { + yield return AssignParameterValues(variation); + } + } + + System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() + { + foreach (Variation variation in variations) + { + yield return AssignParameterValues(variation); + } + } + #endregion + } +} diff --git a/src/dotnetCampus.UITest.WPFTestHelper/VisualVerification/ColorDifference.cs b/src/dotnetCampus.UITest.WPFTestHelper/VisualVerification/ColorDifference.cs new file mode 100644 index 0000000..e42c3f8 --- /dev/null +++ b/src/dotnetCampus.UITest.WPFTestHelper/VisualVerification/ColorDifference.cs @@ -0,0 +1,88 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + + +namespace dotnetCampus.UITest.WPFTestHelper.VisualVerification +{ + /// + /// Represents the per-channel difference between two colors. + /// + public class ColorDifference + { + #region Constructors + + /// + /// Initializes a new instance of the ColorDifference class using values of zero, indicating no difference. + /// + public ColorDifference() + { + A = 0; + R = 0; + G = 0; + B = 0; + } + + /// + /// Initializes a new instance of the ColorDifference class, using the specified alpha, red, green and blue values. + /// + /// The alpha (transparency) color channel difference. + /// The red color channel difference. + /// The green color channel difference. + /// The blue color channel difference. + public ColorDifference(byte alpha, byte red, byte green, byte blue) + { + A = alpha; + R = red; + G = green; + B = blue; + } + + #endregion + + #region Public Properties + + /// + /// Alpha (transparency) color channel difference. + /// + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1704")] + public byte A { get; set; } + + /// + /// Red color channel difference. + /// + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1704")] + public byte R { get; set; } + + /// + /// Green color channel difference. + /// + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1704")] + public byte G { get; set; } + + /// + /// Blue color channel difference. + /// + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1704")] + public byte B { get; set; } + + #endregion + + #region Internal Members + + /// + /// Returns true if this color is less than or equal to reference color on all channels. + /// + /// The reference color to evaluate against. + /// True if this color is less than or equal to reference on all channels. + internal bool MeetsTolerance(ColorDifference reference) + { + return (this.A <= reference.A) && + (this.R <= reference.R) && + (this.G <= reference.G) && + (this.B <= reference.B); + } + + #endregion + } +} diff --git a/src/dotnetCampus.UITest.WPFTestHelper/VisualVerification/ColorExtensions.cs b/src/dotnetCampus.UITest.WPFTestHelper/VisualVerification/ColorExtensions.cs new file mode 100644 index 0000000..9c01142 --- /dev/null +++ b/src/dotnetCampus.UITest.WPFTestHelper/VisualVerification/ColorExtensions.cs @@ -0,0 +1,84 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + + +using System; +using System.Drawing; + +namespace dotnetCampus.UITest.WPFTestHelper.VisualVerification +{ + /// + /// Container for internal Extension methods on the Color type. + /// + internal static class ColorExtensions + { + #region Internal Static Members + /// + /// Compares colors by producing an absolute valued Color Difference object + /// + /// The first color + /// The second colar + /// The Color Difference of the two colors + internal static ColorDifference Compare(this Color color1, Color color2) + { + ColorDifference diff = new ColorDifference(); + diff.A = (byte)Math.Abs(color1.A - color2.A); + diff.R = (byte)Math.Abs(color1.R - color2.R); + diff.G = (byte)Math.Abs(color1.G - color2.G); + diff.B = (byte)Math.Abs(color1.B - color2.B); + return diff; + } + + /// + /// Color differencing helper for snapshot comparisons. + /// + /// The first color + /// The second color + /// If set to false, the Alpha channel is overridden to full opacity, rather than the difference. + /// This is important for visualization, especially if both colors are fully opaque, as the difference produces a fully transparent difference. + /// + internal static Color Subtract(this Color color1, Color color2, bool subtractAlpha) + { + ColorDifference diff = Compare(color1, color2); + if (!subtractAlpha) + { + diff.A = 255; + } + return Color.FromArgb(diff.A, diff.R, diff.G, diff.B); + } + + /// + /// Performs Bitwise OR of Color bits. + /// + /// The first color. + /// The second color. + /// + internal static Color Or(this Color color1, Color color2) + { + ColorDifference orValue = new ColorDifference(); + orValue.A = (byte)(color1.A | color2.A); + orValue.R = (byte)(color1.R | color2.R); + orValue.G = (byte)(color1.G | color2.G); + orValue.B = (byte)(color1.B | color2.B); + return Color.FromArgb(orValue.A, orValue.R, orValue.G, orValue.B); + } + + /// + /// Performs Bitwise AND of Color bits. + /// + /// The first color. + /// The second color. + /// + internal static Color And(this Color color1, Color color2) + { + ColorDifference andValue = new ColorDifference(); + andValue.A = (byte)(color1.A & color2.A); + andValue.R = (byte)(color1.R & color2.R); + andValue.G = (byte)(color1.G & color2.G); + andValue.B = (byte)(color1.B & color2.B); + return Color.FromArgb(andValue.A, andValue.R, andValue.G, andValue.B); + } + #endregion + } +} diff --git a/src/dotnetCampus.UITest.WPFTestHelper/VisualVerification/HighFidelityColor.cs b/src/dotnetCampus.UITest.WPFTestHelper/VisualVerification/HighFidelityColor.cs new file mode 100644 index 0000000..982eff2 --- /dev/null +++ b/src/dotnetCampus.UITest.WPFTestHelper/VisualVerification/HighFidelityColor.cs @@ -0,0 +1,53 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + + +using System.Drawing; + +namespace dotnetCampus.UITest.WPFTestHelper.VisualVerification +{ + /// + /// Represents colors channels as a set of 0-1 floats for higher fidelity internal color processing. + /// + internal struct HighFidelityColor + { + internal HighFidelityColor(Color color) + { + A = (float)color.A / 255; + R = (float)color.R / 255; + G = (float)color.G / 255; + B = (float)color.B / 255; + } + + internal Color ToColor() + { + return Color.FromArgb((int)(A * 255), (int)(R * 255), (int)(G * 255), (int)(B * 255)); + } + + public static HighFidelityColor operator +(HighFidelityColor color1, HighFidelityColor color2) + { + HighFidelityColor result; + result.A = color1.A + color2.A; + result.R = color1.R + color2.R; + result.G = color1.G + color2.G; + result.B = color1.B + color2.B; + return result; + } + + + internal HighFidelityColor Modulate(float scale) + { + A *= scale; + R *= scale; + G *= scale; + B *= scale; + return this; + } + + internal float A; + internal float R; + internal float G; + internal float B; + } +} diff --git a/src/dotnetCampus.UITest.WPFTestHelper/VisualVerification/Histogram.cs b/src/dotnetCampus.UITest.WPFTestHelper/VisualVerification/Histogram.cs new file mode 100644 index 0000000..ae6e2ad --- /dev/null +++ b/src/dotnetCampus.UITest.WPFTestHelper/VisualVerification/Histogram.cs @@ -0,0 +1,313 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + + +using System; +using System.Collections.Generic; +using System.Drawing; +using System.Drawing.Imaging; +using System.Globalization; +using System.IO; +using System.Xml; + +namespace dotnetCampus.UITest.WPFTestHelper.VisualVerification +{ + /// + /// The Histogram class represents a histogram curve, expressed in terms of frequency (proportion of total pixels) + /// over brightness (from 0 to 255). In other words, the Histogram class represents the percentage (proportion) of + /// pixels that have brightness of 0, 1, etc. This page provides + /// a good introduction to image histograms. + ///

+ /// For testing purposes "brightness" is often equated to "difference". Thus, one is able to construct a "difference + /// histogram" from a "difference shapshot" and compare that histogram to a histogram of "expected maximum differences" + /// (also knows as a "tolerance histogram") in order to determine whether a visual verification test passes or fails. + ///

+ /// A Histogram object can be loaded from a XML file or generated from a Snapshot object. + ///

+ public class Histogram + { + #region Constructor + /// + /// Creates a zero tolerance histogram. + /// + internal Histogram() + { + graph = new double[histogramSize]; + for (int i = 0; i < histogramSize; i++) + { + graph[i] = 0; + } + } + #endregion + + #region Static Initializers + /// + /// Creates a Histogram object from an existing Snapshot object. + /// + /// The Snapshot object to derive the Histogram from. + /// A new instance of Histogram, based on the provided snapshot. + public static Histogram FromSnapshot(Snapshot snapshot) + { + Histogram h = new Histogram(); + //Here we count how much each pixel "weighs" to build our histogram + double contributionPerPixel = 1 / (double)(snapshot.Width * snapshot.Height); + + for (int row = 0; row < snapshot.Height; row++) + { + for (int column = 0; column < snapshot.Width; column++) + { + //Scale up the brightness from 0-1 to 0-256 to index the value to a slot on the graph + h.graph[(Byte)(snapshot[row, column].GetBrightness() * 256)] += contributionPerPixel; + } + } + return h; + } + + /// + /// Creates a Histogram object from a histogram curve file. + /// + /// Name of the file containing the histogram curve. + /// A new instance of Histogram, based on the specified file. + public static Histogram FromFile(string filePath) + { + XmlDocument xmlDoc = new XmlDocument(); + using (Stream s = new FileInfo(filePath).OpenRead()) + { + xmlDoc.Load(s); + } + + //Tolerance Node List is a means of containing multiple Tolerance curve profiles within a histogram + //One and only one tolerance element is supported + XmlNodeList toleranceNodeList = xmlDoc.DocumentElement.SelectNodes("descendant::Tolerance"); + if (toleranceNodeList.Count != 1) + { + throw new XmlException("An unsupported number of Tolerance sets were found."); + } + XmlNode toleranceNode = toleranceNodeList[0]; + + //Point nodes can be incompletely defined - these will be interpolated in + XmlNodeList nodeList = toleranceNode.SelectNodes("Point"); + if (nodeList.Count == 0) + { + throw new XmlException("An insufficient number of Points were found"); + } + + SortedDictionary loadedTable = new SortedDictionary(); + for (int t = 0; t < nodeList.Count; t++) + { + byte x = byte.Parse(nodeList[t].Attributes["x"].InnerText, NumberFormatInfo.InvariantInfo); + double y = double.Parse(nodeList[t].Attributes["y"].InnerText, NumberFormatInfo.InvariantInfo); + VerifyPoint(x, y); + loadedTable[x] = y; + } + + // Populate the histogram with the loaded points, and interpolate for any omitted elements. + Histogram result = new Histogram(); + result.InterpolatePoints(loadedTable); + return result; + } + #endregion + + #region Public Members + + //provide some access to the histogram data. + /// + /// The data of the histogram. + /// + /// Which column of the histogram you want. + /// A double value between 0 and 1. + public double this[int column] + { + get { return graph[column]; } + set { graph[column] = value; } + } + + /// + /// Create a snapshot to visualize this histogram, and save it to a file. + /// The graph will be 100 pixels high and 256 columns wide - one for each 'bin' in the histogram. + /// The snapshot generated will be framed and slightly larger. + /// + public void ToGraph(string filePath, ImageFormat imageFormat) + { + Snapshot s = new Snapshot(2 + VisualizationHeight, 4 + histogramSize);//put a border around it + + //clear the rect. + for (int row = 0; row < s.Height; row++) + { + for (int col = 0; col < s.Width; col++) + { + s[row, col] = VisualizationBGColor; + } + } + + //frame the data. We could use some more explanatory text perhaps + s.DrawLine(0, 0, histogramSize - 1, VisualizationMGColor); // left side + + //ensure that the drawn values are normalized (defensive coding) + double maxHeight = 0; + for (int col = 0; col < histogramSize; col++) + { + maxHeight = Math.Max(graph[col], maxHeight); + } + + int heightPrev = 0; + //Visualize the histogram with a 2d line graph + for (int col = 0; col < histogramSize - 2; col++) + { + //draw a line into the snapshot to represent the height of this histogram bin + int height = (int)(graph[col] / maxHeight * VisualizationHeight); + + //if (col > 0) { heightPrev = (int)(graph[col - 1] * VisualizationHeight); } + // draw a vertical line between this columns value and the previous ones + // that's the near-vertical case taken care of. The loop covers the near-horizontal, + // so there's no need to code up a full Bresenham line drawing function + s.DrawLine(col + 1, Math.Min(heightPrev, height), s.Height, VisualizationMGColor); + s.DrawLine(col + 1, Math.Min(heightPrev, height) - 1, Math.Max(heightPrev, height), VisualizationFGColor); + heightPrev = height; + } + + s.ToFile(filePath, imageFormat); + } + + /// + /// Merges the specified input histogram curve with the current histogram by accumulating the + /// per-brightness peak error quantities of two histograms. The Merge operation merges the peak + /// values of the two histograms. + /// + /// The histogram curve to be merged with. + /// A new Histogram object, containing the peak values of both histogram curves. + public Histogram Merge(Histogram histogram) + { + Histogram result = new Histogram(); + for (int i = 0; i < histogramSize; i++) + { + result.graph[i] = Math.Max(this.graph[i], histogram.graph[i]); + } + return result; + } + + /// + /// Saves the Histogram object to an XML file representation. + /// + /// The path of the XML histogram file to be stored. + public void ToFile(string filePath) + { + XmlDocument xmlDoc = new XmlDocument(); + XmlNode rootNode = xmlDoc.CreateElement("Histogram"); + { + //Only one Tolerance node is supported + XmlNode toleranceNode = xmlDoc.CreateElement("Tolerance"); + for (int i = 0; i < histogramSize; i++) + { + XmlNode pointNode = xmlDoc.CreateElement("Point"); + + XmlAttribute xAttribute = xmlDoc.CreateAttribute("x"); + xAttribute.InnerText = i.ToString(NumberFormatInfo.InvariantInfo); + pointNode.Attributes.Append(xAttribute); + + XmlAttribute yAttribute = xmlDoc.CreateAttribute("y"); + yAttribute.InnerText = graph[i].ToString("G17", NumberFormatInfo.InvariantInfo); + pointNode.Attributes.Append(yAttribute); + + toleranceNode.AppendChild(pointNode); + } + rootNode.AppendChild(toleranceNode); + } + xmlDoc.AppendChild(rootNode); + xmlDoc.Save(filePath); + } + + #endregion + + #region Internal Members + + /// + /// Evaluates if the frequencies on this histogram curve are less than supplied histogram for all levels of brightness + /// Note: The 0 brightness level frequency values are not evaluated here - this is where the balance of pixels reside. + /// + /// The histogram curve to be tested against + /// True if all brightness frequencies of the tolerance histogram exceed this histogram. False otherwise. + internal bool IsLessThan(Histogram tolerance) + { + bool result = true; + //We are intentionally ignoring the 0 intensity - We *want* as many pixels to be there as possible, so this is not a failure condition. + for (int i = 1; i < histogramSize; i++) + { + if (this.graph[i] > tolerance.graph[i]) + { + result = false; + } + } + return result; + } + + #endregion + + #region Private Members + + /// + /// Linearly Interpolates along the sparse set of points in the LoadTable to produce a fully populated Histogram object. + /// + /// The sparse set of points representing the Histogram. + private void InterpolatePoints(SortedDictionary loadTable) + { + if (!loadTable.ContainsKey(0) || !loadTable.ContainsKey(255)) + { + throw new InvalidDataException("InterpolatePoints requires an entry for the first (0th) and last (255th) entry to populate the histogram."); + } + + byte prev = 0; + foreach (byte curr in loadTable.Keys) + { + //For each key after 0, we interpolate along and fill graph values prev+1 to curr + if (0 != curr) + { + for (byte i = (byte)(prev + 1); i < curr; i++) + { + graph[i] = LinearInterpolate((curr - prev) / (i - prev), loadTable[prev], loadTable[curr]); + } + } + //We set current value to be previous and set current value entry onto graph + prev = curr; + graph[curr] = loadTable[curr]; + } + } + + /// + /// Provides a sample via linear interpolation between start and end value by specified proportion. + /// + /// The proportion of weight to be allocated to the start value (from 0 to 1). + /// The starting value to start interpolating from. + /// The end value to stop interpolation at. + /// + private static double LinearInterpolate(double proportion, double startValue, double endValue) + { + return startValue + (endValue - startValue) / proportion; + } + + /// + /// Throws if x or y is invalid. + /// + /// X Coordinate. + /// Y Coordinate. + private static void VerifyPoint(byte x, double y) + { + //do nothing for x - all possible values of the byte representation are legitimate. + if (y < 0 || y > 1) + { + throw new ArgumentOutOfRangeException("y"); + } + } + + private Color VisualizationFGColor { get { return Color.Blue; } } + private Color VisualizationMGColor { get { return Color.Red; } } + private Color VisualizationBGColor { get { return Color.Gray; } } + + private const int VisualizationHeight = 100; + private const int histogramSize = 256; + private double[] graph; + + #endregion + } +} diff --git a/src/dotnetCampus.UITest.WPFTestHelper/VisualVerification/NativeMethods.cs b/src/dotnetCampus.UITest.WPFTestHelper/VisualVerification/NativeMethods.cs new file mode 100644 index 0000000..1dd2ac0 --- /dev/null +++ b/src/dotnetCampus.UITest.WPFTestHelper/VisualVerification/NativeMethods.cs @@ -0,0 +1,122 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + + +using System; +using System.Drawing; +using System.Runtime.InteropServices; + +namespace dotnetCampus.UITest.WPFTestHelper.VisualVerification +{ + /// + /// WIN32 RECT structure + /// + [StructLayout(LayoutKind.Sequential)] + internal struct RECT + { + internal int X; + internal int Y; + internal int Right; + internal int Bottom; + + internal Rectangle ToRectangle() + { + return new Rectangle(X, Y, Right - X, Bottom - Y); + } + } + + /// + /// RasterOperation used by GDI BitBlt and StretchBlt methods + /// + [Flags] + internal enum RasterOperationCodeEnum + { + /// + SRCCOPY = 0x00CC0020, + /// + SRCPAINT = 0x00EE0086, + /// + SRCAND = 0x008800C6, + /// + SRCINVERT = 0x00660046, + /// + SRCERASE = 0x00440328, + /// + NOTSRCCOPY = 0x00330008, + /// + NOTSRCERASE = 0x001100A6, + /// + MERGECOPY = 0x00C000CA, + /// + MERGEPAINT = 0x00BB0226, + /// + PATCOPY = 0x00F00021, + /// + PATPAINT = 0x00FB0A09, + /// + PATINVERT = 0x005A0049, + /// + DSTINVERT = 0x00550009, + /// + BLACKNESS = 0x00000042, + /// + WHITENESS = 0x00FF0062, + /// + CAPTUREBLT = 0x40000000 + } + + /// + /// Native methods + /// + internal static class NativeMethods + { + //wrappers cover API's for used for Visual Verification/Screen Capture + private const string Gdi32Dll = "GDI32.dll"; + private const string User32Dll = "User32.dll"; + + #region Gdi32 + + [DllImport(Gdi32Dll, SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + static internal extern bool BitBlt(IntPtr hdcDest, int nXDest, int nYDest, int nWidth, int nHeight, IntPtr hdcSrc, int nXSrc, int nYSrc, Int32 RasterOpCode); + + [DllImport(Gdi32Dll, SetLastError = true)] + static internal extern IntPtr CreateCompatibleBitmap(IntPtr hdcSrc, int width, int height); + + [DllImport(Gdi32Dll, SetLastError = true)] + static internal extern IntPtr CreateCompatibleDC(IntPtr HDCSource); + + [DllImport(Gdi32Dll, SetLastError = true, CharSet = CharSet.Unicode)] + static internal extern IntPtr CreateDC(string driverName, string deviceName, string reserved, IntPtr initData); + + [DllImport(Gdi32Dll, SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + static internal extern bool DeleteDC(IntPtr HDC); + + [DllImport(Gdi32Dll, SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + static internal extern bool DeleteObject(IntPtr hBMP); + + [DllImport(Gdi32Dll, SetLastError = true)] + static internal extern IntPtr SelectObject(IntPtr HDC, IntPtr hgdiobj); + + #endregion + + #region User32 + + [DllImport(User32Dll)] + static internal extern IntPtr GetDC(IntPtr hWnd); + + [DllImport(User32Dll, SetLastError = true)] + static internal extern bool GetClientRect(IntPtr HWND, out RECT rect); + + [DllImport(User32Dll, SetLastError = true)] + static internal extern bool GetWindowRect(IntPtr HWND, out RECT rect); + + [DllImport(User32Dll)] + static internal extern bool ReleaseDC(IntPtr HWND, IntPtr HDC); + + #endregion + } +} diff --git a/src/dotnetCampus.UITest.WPFTestHelper/VisualVerification/Snapshot4.cs b/src/dotnetCampus.UITest.WPFTestHelper/VisualVerification/Snapshot4.cs new file mode 100644 index 0000000..8473a41 --- /dev/null +++ b/src/dotnetCampus.UITest.WPFTestHelper/VisualVerification/Snapshot4.cs @@ -0,0 +1,660 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + + +using System; +using System.Collections.ObjectModel; +using System.ComponentModel; +using System.Drawing; +using System.Drawing.Imaging; +using System.IO; +using System.Threading.Tasks; + +namespace dotnetCampus.UITest.WPFTestHelper.VisualVerification +{ + /// + /// Represents image pixels in a two-dimensional array for use in visual verification. + /// Every element of the array represents a pixel at the given [row, column] of the image. + /// A Snapshot object can be instantiated from a file or captured from the screen. + /// + /// + /// Takes a snapshot and verifies it is an absolute match to an expected image. + /// + /// // Take a snapshot, compare to master image, validate match and save the diff + /// // in case of a poor match. + /// Snapshot actual = Snapshot.FromRectangle(new Rectangle(0, 0, 800, 600)); + /// Snapshot expected = Snapshot.FromFile("Expected.bmp"); + /// Snapshot diff = actual.CompareTo(expected); + /// + /// // The SnapshotColorVerifier.Verify() method compares every pixel of a diff bitmap + /// // against the threshold defined by the ColorDifference tolerance. If all pixels + /// // fall within the tolerance, then the method returns VerificationResult.Pass + /// SnapshotVerifier v = new SnapshotColorVerifier(Color.Black, new ColorDifference(0, 0, 0, 0)); + /// if (v.Verify(diff) == VerificationResult.Fail) + /// { + /// diff.ToFile("Actual.bmp", ImageFormat.Bmp); + /// } + /// + /// + public class Snapshot : ICloneable + { + #region Constructor + + /// + /// Snapshot Constructor - Creates buffer of black, opaque pixels + /// + internal Snapshot(int height, int width) + { + buffer = new Color[height, width]; + } + + #endregion + + #region Public Static Initializers + + /// + /// Creates a Snapshot instance from data in the specified image file. + /// + /// Path to the image file. + /// A Snapshot instance containing the pixels in the loaded file. + public static Snapshot FromFile(string filePath) + { + // Note: open the stream directly because the underlying Bitmap class does not consistently throw on access failures. + using (Stream s = new FileInfo(filePath).OpenRead()) + { + // Load the bitmap from disk. NOTE: imageFormat argument is not used in Bitmap. + // Then convert to Snapshot format + + Bitmap bmp = new System.Drawing.Bitmap(s); + return FromBitmap(bmp); + } + } + + /// + /// Creates a Snapshot instance populated with pixels sampled from the rectangle of the specified window. + /// + /// The Win32 window handle (also known as an HWND), identifying the window to capture from. + /// Determines if window border region should captured as part of Snapshot. + /// A Snapshot instance of the pixels captured. + public static Snapshot FromWindow(IntPtr windowHandle, WindowSnapshotMode windowSnapshotMode) + { + Snapshot result; + RECT rect; + if (windowSnapshotMode == WindowSnapshotMode.ExcludeWindowBorder) + { + if (!NativeMethods.GetClientRect(windowHandle, out rect)) { throw new Win32Exception(); } + IntPtr deviceContext = NativeMethods.GetDC(windowHandle); + try + { + if (deviceContext == IntPtr.Zero) { throw new Win32Exception(); } + result = FromBitmap(CaptureBitmap(deviceContext, rect.ToRectangle())); + } + finally + { + NativeMethods.ReleaseDC(windowHandle, deviceContext); + } + } + else if (windowSnapshotMode == WindowSnapshotMode.IncludeWindowBorder) + { + if (!NativeMethods.GetWindowRect(windowHandle, out rect)) { throw new Win32Exception(); } + result = FromRectangle(rect.ToRectangle()); + } + else + { + throw new ArgumentOutOfRangeException("windowSnapshotMode"); + } + + return result; + } + + /// + /// Creates a Snapshot instance populated with pixels sampled from the specified screen rectangle, from the context of the Desktop. + /// + /// Rectangle of the screen region to be sampled from. + /// A Snapshot instance of the pixels from the bounds of the screen rectangle. + public static Snapshot FromRectangle(Rectangle rectangle) + { + IntPtr deviceContext = NativeMethods.CreateDC("DISPLAY", string.Empty, string.Empty, IntPtr.Zero); + if (deviceContext == IntPtr.Zero) { throw new Win32Exception(); } + + Bitmap bitmap; + try + { + bitmap = CaptureBitmap(deviceContext, rectangle); + } + finally + { + NativeMethods.DeleteDC(deviceContext); + } + return FromBitmap(bitmap); + } + + /// + /// Instantiates a Snapshot representation from a Windows Bitmap. + /// + /// Source bitmap to be converted. + /// A snapshot based on the source buffer. + public static Snapshot FromBitmap(System.Drawing.Bitmap source) + { + Snapshot bmp = new Snapshot(source.Height, source.Width); + for (int row = 0; row < source.Height; row++) + { + for (int column = 0; column < source.Width; column++) + { + bmp[row, column] = source.GetPixel(column, row); + } + } + return bmp; + } + + #endregion + + #region ICloneable Members + + /// + /// Creates a deep-copied clone Snapshot with the same value as the existing instance. + /// + /// Clone instance + public object Clone() + { + int height = this.Height; + int width = this.Width; + Snapshot result = new Snapshot(height, width); + Parallel.For (0, width, (column) => + { + for (int row = 0; row < height; row++) + { + result[row, column] = this[row, column]; + } + }); + return result; + } + + #endregion + + #region Public Methods + + /// + /// Compares the current Snapshot instance to the specified Snapshot to produce a difference image. + /// Note: This does not compare alpha channels. + /// + /// The Snapshot to be compared to. + /// A new Snapshot object representing the difference image (i.e. the result of the comparison). + public Snapshot CompareTo(Snapshot snapshot) + { + return CompareTo(snapshot, false); + } + + /// + /// Compares the current Snapshot instance to the specified Snapshot to produce a difference image. + /// + /// The target Snapshot to be compared to. + /// If true, compares alpha channels. If false, the alpha channel difference values are fixed to 255. + /// A new Snapshot object representing the difference image (i.e. the result of the comparison). + public Snapshot CompareTo(Snapshot snapshot, bool compareAlphaChannel) + { + if (this.Width != snapshot.Width || this.Height != snapshot.Height) + { + throw new InvalidOperationException("Snapshots must be of identical size to be compared."); + } + + int height = snapshot.Height; + int width = snapshot.Width; + Snapshot result = new Snapshot(height, width); + Parallel.For (0, height, (row) => + { + for (int column = 0; column < width; column++) + { + result[row, column] = this[row, column].Subtract(snapshot[row, column], compareAlphaChannel); + } + }); + return result; + } + + /// + /// Draw a vertical line from the bottom of the specified column up to the height given. + /// + /// which column to draw in + /// the lowest pixel of the line + /// the height of the line + /// the color of the line + public void DrawLine(int col, int floor, int height, Color color) + { + for (int row = Math.Max(floor,0); row < Math.Min(this.Height, height); row++) + { + this[row, col] = color; + } + } + + /// + /// Writes the current Snapshot (at 32 bits per pixel) to a file. + /// + /// The path to the output file. + /// The file storage format to be used. + public void ToFile(string filePath, ImageFormat imageFormat) + { + Bitmap temp = CreateBitmapFromSnapshot(); + + using (Stream s = new FileInfo(filePath).OpenWrite()) + { + temp.Save(s, imageFormat); + } + } + + /// + /// Creates a new Snapshot based on the cropped bounds of the current snapshot. + /// + /// The bounding rectangle of the Snapshot. + /// + public Snapshot Crop(Rectangle bounds) + { + if (bounds.Right > this.Width || bounds.Bottom > this.Height || + bounds.Height <= 0 || bounds.Width <= 0 || + bounds.Left < 0 || bounds.Top < 0) + { + throw new ArgumentOutOfRangeException("bounds"); + } + int height = bounds.Height; + int width = bounds.Width; + Snapshot croppedImage = new Snapshot(height, width); + Parallel.For (0, height, (row) => + { + for (int col = 0; col < width; col++) + { + croppedImage.buffer[row, col] = buffer[bounds.Top + row, bounds.Left + col]; + } + }); + return croppedImage; + } + + /// + /// Creates a new Snapshot of the specified size from the original using bilinear interpolation. + /// + /// Desired size of new image + /// + public Snapshot Resize(Size size) + { + int thisHeight = this.Height; + int thisWidth = this.Width; + int height = size.Height; + int width = size.Width; + Snapshot resizedImage = new Snapshot(height, width); + Parallel.For (0, height, (row)=> + { + float myRow = row * thisHeight / (float)height; + for (int col = 0; col < width; col++) + { + float myCol = col * thisWidth / (float)width; + resizedImage[row, col] = BilinearSample(myRow, myCol); + } + }); + return resizedImage; + } + + /// + /// Modifies the current image to contain the result of a bitwise OR of this Snapshot + /// and the mask. This technique can be used to merge data from two images. + /// http://en.wikipedia.org/wiki/Bitmask#Image_masks + /// + /// Mask Snapshot to use in the bitwise OR operation. + public void Or(Snapshot mask) + { + if (mask.Width != Width || mask.Height != Height) + { + throw new InvalidOperationException("mask Snapshot must be of equal size to this Snapshot"); + } + + int thisHeight = Height; + int thisWidth = Width; + Parallel.For( 0, thisHeight, (row) => + { + for (int col = 0; col < thisWidth; col++) + { + Color col1 = this[row, col]; + Color col2 = mask[row, col]; + this[row, col] = col1.Or(col2); + } + }); + } + + /// + /// Modifies the current image to contain the result of a bitwise AND of this Snapshot + /// and the mask. This technique can be used to remove data from an image. + /// http://en.wikipedia.org/wiki/Bitmask#Image_masks + /// + /// Mask Snapshot to use in the bitwise AND operation. + public void And(Snapshot mask) + { + if (mask.Width != Width || mask.Height != Height) + { + throw new InvalidOperationException("mask Snapshot must be of equal size to this Snapshot"); + } + + int thisHeight = Height; + int thisWidth = Width; + Parallel.For( 0, thisHeight, (row) => + { + for (int col = 0; col < thisWidth; col++) + { + Color col1 = this[row, col]; + Color col2 = mask[row, col]; + this[row, col] = col1.And(col2); + } + }); + } + + /// + /// Find all instances of a subimage in this image. + /// + /// The subimage to find in the larger image. Must be smaller than the image in both dimensions. + /// A Collection of rectangles indicating the matching locations. + /// If there is no match, the Collection returned will be empty. + public Collection Find(Snapshot subimage) + { + return Find(subimage, null); + } + + /// + /// Find all instances of a subimage in this image. + /// + /// The subimage to find in the larger image. Must be smaller than the image in both dimensions. + /// Custom SnapshotVerifier, used to compare the subimages. + /// A Collection of rectangles indicating the matching locations. + /// If there is no match, the Collection returned will be empty. + public Collection Find(Snapshot subimage, SnapshotVerifier verifier) + { + if (Width < subimage.Width || Height < subimage.Height) + { + throw new InvalidOperationException("Subimage snapshot must fit into larger snapshot to be found."); + } + + Collection resultList = new Collection(); + + // This is a binary 2D convolve with early exit on any imperfect match. + // Future optimisations will test the middle pixel first, then a diagonal through the subarea, then the full image. + // This will make pathological cases much faster. + for (int row = 0; row < Height - subimage.Height; row++) + { + for (int column = 0; column < Width - subimage.Width; column++) + { + // This is our success condition, add this location to the Collection. + if (CompareSubImage(subimage, row, column, verifier)) + { + resultList.Add((new Rectangle(column, row, subimage.Width, subimage.Height))); + } + } + } + + return resultList; + } + + #endregion + + #region Public Properties + /// + /// Returns a Color instance for the pixel at the specified row and column. + /// + /// Zero-based row position of the pixel. + /// Zero-based column position of the pixel. + /// A Color instance for the pixel at the specified row and column. + // Suppressed the warning as our usage is more obvious than single argument indexer for a 2D array + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1023")] + public Color this[int row, int column] + { + get + { + IsValidPixel(row, column); + return buffer[row, column]; + } + set + { + IsValidPixel(row, column); + buffer[row, column] = value; + } + } + + /// + /// Returns the width of the pixel buffer. + /// + public int Width + { + get + { + return buffer.GetLength(1); + } + } + + /// + /// Returns the height of the pixel buffer. + /// + public int Height + { + get + { + return buffer.GetLength(0); + } + } + #endregion + + #region Private Methods + + /// + /// Private subimage comparer to help FindSubImage. This performs the actual work of comparing pixels. + /// + /// The subimage to find. + /// The row offset into this snapshot to look for the subimage. + /// The column offset into this snapshot to look for the subimage. + /// Custom subimage comparison verifier, if specified. + /// + private bool CompareSubImage( + Snapshot subimage, + int startingRow, + int startingColumn, + SnapshotVerifier verifier) + { + //compare pixel-by-pixel + for (int row = 0; row < subimage.Height; row++) + { + for (int column = 0; column < subimage.Width; column++) + { + // Use the custom verifier if the user has specified one + if (verifier != null) + { + //verify this pixel + Snapshot thisPixelImage = this.Crop(new Rectangle(startingColumn + column, startingRow + row, 1, 1)); + Snapshot thatPixelImage = subimage.Crop(new Rectangle(column, row, 1, 1)); + VerificationResult subCompareResult = verifier.Verify(thisPixelImage.CompareTo(thatPixelImage, false)); + if (subCompareResult == VerificationResult.Fail) + { + //mismatch + return false; + } + } + else + { + Color thisPixel = this[startingRow + row, startingColumn + column]; + Color thatPixel = subimage[row, column]; + int R = thisPixel.R - thatPixel.R; + int G = thisPixel.G - thatPixel.G; + int B = thisPixel.B - thatPixel.B; + int A = thisPixel.A - thatPixel.A; + + if (R != 0x00 || G != 0x00 || B != 0x00) + { + //mismatch. + return false; + } + } + + + } + } + + return true; + } + + /// + /// Bilinearly interpolate to blend the pixels around the specified point - Takes the weighted average of the nearest four pixels. + /// Not mip-mapped - downsampling can be mis-representative. + /// + /// The row being sampled on. + /// The column being sampled on. + /// + private Color BilinearSample(float row, float col) + { + int left = (int)Math.Max(0, Math.Floor(col)); + int right = (int)Math.Min(Width - 1, Math.Floor(col) + 1); + + int top = (int)Math.Max(0, Math.Floor(row)); + int bottom = (int)Math.Min(Height - 1, Math.Floor(row) + 1); + + float wBottom = (col - (float)Math.Floor(col)); //Weight of bottom pixels + float wRight = (row - (float)Math.Floor(row)); //Weight of right pixels + + HighFidelityColor topLeft = new HighFidelityColor(this[top, left]).Modulate((1 - wBottom) * (1 - wRight)); + HighFidelityColor topRight = new HighFidelityColor(this[top, right]).Modulate((1 - wBottom) * wRight); + HighFidelityColor bottomLeft = new HighFidelityColor(this[bottom, left]).Modulate(wBottom * (1 - wRight)); + HighFidelityColor bottomRight = new HighFidelityColor(this[bottom, right]).Modulate(wBottom * wRight); + + return (topLeft + topRight + bottomLeft + bottomRight).ToColor(); + + } + + /// + /// Instantiates a Bitmap with contents of TestBitmap. + /// + /// A Bitmap containing the a copy of the Snapshot buffer data. + unsafe private System.Drawing.Bitmap CreateBitmapFromSnapshot() + { + System.Drawing.Bitmap temp = new Bitmap(Width, Height, PixelFormat.Format32bppArgb); + + Rectangle bounds = new Rectangle(Point.Empty, temp.Size); + BitmapData bitmapData = temp.LockBits(bounds, ImageLockMode.ReadWrite, PixelFormat.Format32bppArgb); + Byte* pBase = (Byte*)bitmapData.Scan0.ToPointer(); + int thisHeight = Height; + int thisWidth = Width; + Parallel.For(0, thisHeight, (row) => + { + for (int column = 0; column < thisWidth; column++) + { + // The active implementation is the faster, but unsafe alternative to setpixel API: + // temp.SetPixel(column,row,buffer[row, column]); + + PixelData* pixelDataAddress = (PixelData*)(pBase + row * thisWidth * sizeof(PixelData) + column * sizeof(PixelData)); + pixelDataAddress->A = buffer[row, column].A; + pixelDataAddress->R = buffer[row, column].R; + pixelDataAddress->G = buffer[row, column].G; + pixelDataAddress->B = buffer[row, column].B; + } + }); + temp.UnlockBits(bitmapData); + return temp; + } + + /// + /// Captures a Bitmap from the deviceContext on the specified areaToCopy. + /// + /// The device context to capture the region from. + /// The rectangular bounds of the area to be captured. + /// A Bitmap representation of the region specified. + static private Bitmap CaptureBitmap(IntPtr sourceDeviceContext, Rectangle rectangle) + { + //Empty rectangle semantic is undefined. + if (rectangle.IsEmpty) + { + throw new ArgumentOutOfRangeException("rectangle"); + } + + IntPtr handleDeviceContextSrc = sourceDeviceContext; + IntPtr handleDeviceContextDestination = IntPtr.Zero; + IntPtr handleBmp = IntPtr.Zero; + IntPtr handlePreviousObj = IntPtr.Zero; + System.Drawing.Bitmap bmp = null; + + try + { + // Allocate memory for the bitmap + handleBmp = NativeMethods.CreateCompatibleBitmap(handleDeviceContextSrc, rectangle.Width, rectangle.Height); + if (handleBmp == IntPtr.Zero) { throw new Win32Exception(); } + + // Create destination DC + handleDeviceContextDestination = NativeMethods.CreateCompatibleDC(handleDeviceContextSrc); + if (handleDeviceContextDestination == IntPtr.Zero) { throw new Win32Exception(); } + + // copy screen to bitmap + handlePreviousObj = NativeMethods.SelectObject(handleDeviceContextDestination, handleBmp); + if (handlePreviousObj == IntPtr.Zero) { throw new Win32Exception(); } + + // Note : CAPTUREBLT is needed to capture layered windows + bool result = NativeMethods.BitBlt(handleDeviceContextDestination, 0, 0, rectangle.Width, rectangle.Height, handleDeviceContextSrc, rectangle.Left, rectangle.Top, (Int32)(RasterOperationCodeEnum.SRCCOPY | RasterOperationCodeEnum.CAPTUREBLT)); + if (result == false) { throw new Win32Exception(); } + + //Convert Win32 Handle to Bitmap to a Winforms Bitmap + bmp = Bitmap.FromHbitmap(handleBmp); + } + + // Do Unmanaged cleanup + finally + { + if (handlePreviousObj != IntPtr.Zero) + { + NativeMethods.SelectObject(handleDeviceContextDestination, handlePreviousObj); + handlePreviousObj = IntPtr.Zero; + } + if (handleDeviceContextDestination != IntPtr.Zero) + { + NativeMethods.DeleteDC(handleDeviceContextDestination); + handleDeviceContextDestination = IntPtr.Zero; + } + + if (handleBmp != IntPtr.Zero) + { + NativeMethods.DeleteObject(handleBmp); + handleBmp = IntPtr.Zero; + } + } + return bmp; + } + + /// + /// Tests if the specified pixel coordinate is contained within the bounds of the buffer. + /// + /// + /// + private void IsValidPixel(int row, int column) + { + if (row < 0 || row >= Height) + { + throw new ArgumentOutOfRangeException("row"); + } + + if (column < 0 || column >= Width) + { + throw new ArgumentOutOfRangeException("column"); + } + } + + #endregion + + #region Private Fields and Structures + /// + /// A BGRA pixel data structure - This is only used for data conversion and export purposes for conversion with Bitmap buffer. + /// NOTE: This order aligns with 32 bpp ARGB pixel Format. + /// + private struct PixelData + { + public byte B; + public byte G; + public byte R; + public byte A; + } + + /// + /// The color buffer is organized in row-Major form i.e. [row, column] => [y,x] + /// + private Color[,] buffer; + + #endregion + } +} diff --git a/src/dotnetCampus.UITest.WPFTestHelper/VisualVerification/SnapshotColorVerifier.cs b/src/dotnetCampus.UITest.WPFTestHelper/VisualVerification/SnapshotColorVerifier.cs new file mode 100644 index 0000000..b9175f4 --- /dev/null +++ b/src/dotnetCampus.UITest.WPFTestHelper/VisualVerification/SnapshotColorVerifier.cs @@ -0,0 +1,75 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + + +using System.Drawing; + +namespace dotnetCampus.UITest.WPFTestHelper.VisualVerification +{ + /// + /// Verifies that all pixels in a Snapshot are within tolerance range of ExpectedColor. + /// + public class SnapshotColorVerifier : SnapshotVerifier + { + #region Constructors + /// + /// Initializes a new instance of a SnapshotColorVerifier class, using black pixels with zero tolerance. + /// + public SnapshotColorVerifier() + { + Tolerance = new ColorDifference(); + ExpectedColor = Color.Black; + } + + /// + /// Initializes a new instance of the SnapshotColorVerifier class, using the specified tolerance value. + /// + /// The expected color to test against. + /// A ColorDifference instance specifying the desired tolerance. + public SnapshotColorVerifier(Color expectedColor, ColorDifference tolerance) + { + this.Tolerance = tolerance; + this.ExpectedColor = expectedColor; + } + #endregion + + #region Public Methods + /// + /// Ensures that the image colors are all within tolerance range of the expected Color. + /// + /// The actual image being verified. + /// A VerificationResult enumeration value based on the image, the expected color, and the tolerance. + public override VerificationResult Verify(Snapshot image) + { + for (int row = 0; row < image.Height; row++) + { + for (int column = 0; column < image.Width; column++) + { + ColorDifference diff = image[row, column].Compare(ExpectedColor); + if (!diff.MeetsTolerance(Tolerance)) + { + //Exit early as we have a counter-example to prove failure. + return VerificationResult.Fail; + } + } + } + return VerificationResult.Pass; + } + #endregion + + #region Public Properties + + /// + /// The color tolerance range for verification. To pass verification, all Snapshot pixels must + /// be within range of the expected color tolerance. + /// + public ColorDifference Tolerance { get; set; } + + /// + /// The expected Color value for verification. + /// + public Color ExpectedColor { get; set; } + #endregion + } +} diff --git a/src/dotnetCampus.UITest.WPFTestHelper/VisualVerification/SnapshotHelper.cs b/src/dotnetCampus.UITest.WPFTestHelper/VisualVerification/SnapshotHelper.cs new file mode 100644 index 0000000..182650c --- /dev/null +++ b/src/dotnetCampus.UITest.WPFTestHelper/VisualVerification/SnapshotHelper.cs @@ -0,0 +1,41 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + + +using System; +using System.Windows; +using System.Windows.Interop; +using System.Windows.Media; + +namespace dotnetCampus.UITest.WPFTestHelper.VisualVerification +{ + /// + /// WPF type centric helper, on top of the general purpose Snapshot. + /// + public static class SnapshotHelper + { + /// + /// Creates a Snapshot instance from a Wpf Window. + /// + /// The Wpf Window, identifying the window to capture from. + /// Determines if window border region should captured as part of Snapshot. + /// A Snapshot instance of the pixels captured. + public static Snapshot SnapshotFromWindow(Visual window, WindowSnapshotMode windowSnapshotMode) + { + Snapshot result; + + HwndSource source = (HwndSource)PresentationSource.FromVisual(window); + if (source == null) + { + throw new InvalidOperationException("The specified Window is not being rendered."); + } + + IntPtr windowHandle = source.Handle; + + result = Snapshot.FromWindow(windowHandle, windowSnapshotMode); + + return result; + } + } +} diff --git a/src/dotnetCampus.UITest.WPFTestHelper/VisualVerification/SnapshotHistogramVerifier.cs b/src/dotnetCampus.UITest.WPFTestHelper/VisualVerification/SnapshotHistogramVerifier.cs new file mode 100644 index 0000000..1a1d06f --- /dev/null +++ b/src/dotnetCampus.UITest.WPFTestHelper/VisualVerification/SnapshotHistogramVerifier.cs @@ -0,0 +1,71 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + + +namespace dotnetCampus.UITest.WPFTestHelper.VisualVerification +{ + /// + /// Verifies a diffed image based on the number of pixels of a given brightness per color. + /// A tolerance Histogram curve can be created from an XML file, produced from a reference image, or manually created for use as a tolerance. + ///

+ /// For more information on histograms, refer to the description of . + ///

+ /// + /// + /// This examples shows how to verify a snapshot against an expected master image, using + /// a tolerance histogram. + /// + /// Snapshot actual = Snapshot.FromRectangle(new Rectangle(0, 0, 800, 600)); + /// Snapshot expected = Snapshot.FromFile("Expected.bmp"); + /// Snapshot diff = actual.CompareTo(expected); + /// + /// SnapshotVerifier v = new SnapshotHistogramVerifier(Histogram.FromFile("ToleranceHistogram.xml")); + /// + /// if (v.Verify(diff) == VerificationResult.Fail) + /// { + /// diff.ToFile("Actual.bmp", ImageFormat.Bmp); + /// } + /// + /// + public class SnapshotHistogramVerifier : SnapshotVerifier + { + #region Constructors + /// + /// Initializes a new instance of the SnapshotHistgramVerifier class, with the tolerance histogram curve initialized to zero tolerance for non-black values. + /// + public SnapshotHistogramVerifier() + { + Tolerance = new Histogram(); + } + + /// + /// Initializes a new instance of the SnapshotHistgramVerifier class, with the tolerance histogram curve initialized to the specified tolerance value. + /// + /// The tolerance Histogram to use for verification. + public SnapshotHistogramVerifier(Histogram tolerance) + { + this.Tolerance = tolerance; + } + #endregion + + #region Public Members + /// + /// Verifies a diffed image based on the number of pixels of a given brightness per color. + /// A tolerance Histogram curve can be created from an XML file, produced from a reference image, or manually created for use as a tolerance. + /// + /// The actual Snapshot to be verified. + /// A VerificationResult enumeration value based on the image, the expected color, and the tolerance. + public override VerificationResult Verify(Snapshot image) + { + Histogram actual = Histogram.FromSnapshot(image); + return (actual.IsLessThan(Tolerance)) ? VerificationResult.Pass : VerificationResult.Fail; + } + + /// + /// The tolerance Histogram that is used to test snapshots; snapshots must produce a histogram which falls below this curve in order to pass. + /// + public Histogram Tolerance { get; set; } + #endregion + } +} diff --git a/src/dotnetCampus.UITest.WPFTestHelper/VisualVerification/SnapshotToleranceMapVerifier.cs b/src/dotnetCampus.UITest.WPFTestHelper/VisualVerification/SnapshotToleranceMapVerifier.cs new file mode 100644 index 0000000..ec260d4 --- /dev/null +++ b/src/dotnetCampus.UITest.WPFTestHelper/VisualVerification/SnapshotToleranceMapVerifier.cs @@ -0,0 +1,99 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + + +using System; + +namespace dotnetCampus.UITest.WPFTestHelper.VisualVerification +{ + /// + /// Verifies that all pixels in a Snapshot are within the tolerance range, defined by the tolerance map. + /// + /// + /// The following code demonstrates how to use SnapshotToleranceMapVerifier + /// for visual verification purposes. + /// + /// // Take a snapshot, compare to the master image and generate a diff + /// WindowSnapshotMode wsm = WindowSnapshotMode.ExcludeWindowBorder; + /// + /// Snapshot actual = Snapshot.FromWindow(hwndOfYourWindow, wsm); + /// Snapshot expected = Snapshot.FromFile("Expected.png"); + /// Snapshot difference = actual.CompareTo(expected); + /// + /// // Load the tolerance map. Then use it to verify the difference snapshot + /// Snapshot toleranceMap = Snapshot.FromFile("ExpectedImageToleranceMap.png"); + /// SnapshotVerifier v = new SnapshotToleranceMapVerifier(toleranceMap); + /// + /// if (v.Verify(difference) == VerificationResult.Fail) + /// { + /// // Log failure, and save the actual and diff images for investigation + /// actual.ToFile("Actual.png", ImageFormat.Png); + /// difference.ToFile("Difference.png", ImageFormat.Png); + /// } + /// + /// + public class SnapshotToleranceMapVerifier : SnapshotVerifier + { + #region Constructors + + /// + /// Initializes a new instance of the SnapshotToleranceMapVerifier class, using the specified tolerance map. + /// + /// + /// A Snapshot instance defining the tolerance map, used by the verifier. + /// A black tolerance map (a snapshot, where all pixels are with zero values) means zero tolerance. + /// A white tolerance map (a snapshot, where all pixels are with value 0xFF) means infinitely high tolerance. + /// + public SnapshotToleranceMapVerifier(Snapshot toleranceMap) + { + this.ToleranceMap = toleranceMap; + } + + #endregion + + #region Public Methods + + /// + /// Ensures that the image colors are all with smaller values than the image colors of the tolerance map. + /// + /// The actual image being verified. + /// A VerificationResult enumeration value based on the image, and the tolerance map. + public override VerificationResult Verify(Snapshot image) + { + if (image.Width != ToleranceMap.Width || image.Height != ToleranceMap.Height) + { + throw new InvalidOperationException("image size must match expected size."); + } + + for (int row = 0; row < image.Height; row++) + { + for (int column = 0; column < image.Width; column++) + { + if (image[row, column].A > ToleranceMap[row, column].A || + image[row, column].R > ToleranceMap[row, column].R || + image[row, column].G > ToleranceMap[row, column].G || + image[row, column].B > ToleranceMap[row, column].B) + { + //Exit early as we have a counter-example to prove failure. + return VerificationResult.Fail; + } + } + } + return VerificationResult.Pass; + } + + #endregion + + #region Public Properties + + /// + /// A Snapshot defining the tolerance map used by the verifier. + /// A black tolerance map (a snapshot, where all pixels are with zero values) means zero tolerance. + /// A white tolerance map (a snapshot, where all pixels are with value 0xFF) means infinitely high tolerance. + /// + public Snapshot ToleranceMap { get; set; } + + #endregion + } +} diff --git a/src/dotnetCampus.UITest.WPFTestHelper/VisualVerification/SnapshotVerifier.cs b/src/dotnetCampus.UITest.WPFTestHelper/VisualVerification/SnapshotVerifier.cs new file mode 100644 index 0000000..d89bc09 --- /dev/null +++ b/src/dotnetCampus.UITest.WPFTestHelper/VisualVerification/SnapshotVerifier.cs @@ -0,0 +1,21 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + + +namespace dotnetCampus.UITest.WPFTestHelper.VisualVerification +{ + /// + /// Base class for all Snapshot verifier types. + /// This establishes a single method contract: Verify(Snapshot). + /// + public abstract class SnapshotVerifier + { + /// + /// Verifies the specified Snapshot instance against the current settings of the SnapshotVerifier instance. + /// + /// The image to be verified. + /// The verification result based on the supplied image and the current settings of the SnapshotVerifier instance. + public abstract VerificationResult Verify(Snapshot image); + } +} diff --git a/src/dotnetCampus.UITest.WPFTestHelper/VisualVerification/ToleranceCurve.cs b/src/dotnetCampus.UITest.WPFTestHelper/VisualVerification/ToleranceCurve.cs new file mode 100644 index 0000000..10f8959 --- /dev/null +++ b/src/dotnetCampus.UITest.WPFTestHelper/VisualVerification/ToleranceCurve.cs @@ -0,0 +1,343 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Xml; + +namespace dotnetCampus.UITest.WPFTestHelper.VisualVerification +{ + /// + /// This class will consist of just the set of data points loaded out of the Tolerance.xml. + /// + public class ToleranceCurve + { + /// + /// Creates a zero legacy tolerance histogram. + /// + public ToleranceCurve() + { + graph = new SortedDictionary(); + tolerances = new List(); + } + + /// + /// Creates a ToleranceCurve object from a histogram curve file. + /// + /// Name of the file containing the histogram curve. + /// A new instance of ToleranceCurve, based on the specified file. + public static ToleranceCurve FromFile(string filePath) + { + XmlDocument xmlDoc = new XmlDocument(); + using (Stream s = new FileInfo(filePath).OpenRead()) + { + try + { + xmlDoc.Load(s); + } + catch (XmlException) + { + throw new XmlException("Tolerance file is not a right xml format"); + } + } + + //Tolerance Node List is a means of containing multiple Tolerance curve profiles within a histogram + XmlNodeList toleranceNodeList = xmlDoc.DocumentElement.SelectNodes("descendant::Tolerance"); + if (toleranceNodeList.Count == 0) + { + throw new XmlException("There is no tolerance node in Tolerance file"); + } + + ToleranceCurve toleranceCurve = new ToleranceCurve(); + + for (int i = 0; i < toleranceNodeList.Count; i++) + { + ToleranceLegacy toleranceLegacy = new ToleranceLegacy(); + XmlAttribute dpiRatio = toleranceNodeList[i].Attributes["dpiRatio"]; + if (dpiRatio == null) + { + throw new XmlException("It is a invalid Tolerance file.\nThere isn't dpiRadio attribute in tolerance node."); + } + + toleranceLegacy.DpiRatio = Convert.ToDouble(dpiRatio.InnerText, NumberFormatInfo.InvariantInfo); + + XmlNodeList pointNodeList = toleranceNodeList[i].SelectNodes("Point"); + + if (pointNodeList.Count == 0) + { + toleranceCurve.SetGraph(0, 0); + toleranceCurve.SetGraph(255, 0); + return toleranceCurve; + } + + List points = new List(); + for (int j = 0; j < pointNodeList.Count; j++) + { + XmlAttribute pointX = pointNodeList[j].Attributes["x"]; + XmlAttribute pointY = pointNodeList[j].Attributes["y"]; + if (pointX == null || pointY == null) + { + throw new XmlException("It is a invalid Tolerance file.\nThere isn't x or y attribute in Point node."); + } + + if (toleranceLegacy.DpiRatio == defaultDpiRatio) + { + byte x = Convert.ToByte(pointX.InnerText, NumberFormatInfo.InvariantInfo); + double y = Convert.ToDouble(pointY.InnerText, NumberFormatInfo.InvariantInfo); + toleranceCurve.SetGraph(x, y); + } + else + { + TolerancePoint point = new TolerancePoint(); + point.X = pointX.InnerText; + point.Y = pointY.InnerText; + points.Add(point); + } + } + + if (toleranceLegacy.DpiRatio != defaultDpiRatio) + { + toleranceLegacy.Points = points; + toleranceCurve.AddTolerance(toleranceLegacy); + } + } + + return toleranceCurve; + } + + /// + /// Provides a sample via linear interpolation between start and end value by specified proportion. + /// + /// The proportion of weight to be allocated to the start value (from 0 to 1). + /// The starting value to start interpolating from. + /// The end value to stop interpolation at. + /// + private static double LinearInterpolate(double proportion, double startValue, double endValue) + { + return startValue + (endValue - startValue) / proportion; + } + + /// + /// Throws if x or y is invalid. + /// + /// X Coordinate. + /// Y Coordinate. + private static void VerifyPoint(byte x, double y) + { + //do nothing for x - all possible values of the byte representation are legitimate. + if (y < 0 || y > 1) + { + throw new ArgumentOutOfRangeException("y"); + } + } + + /// + /// Linearly Interpolates along the sparse set of points in the LoadTable to produce a fully populated Histogram object. + /// + /// The sparse set of points representing the Histogram. + internal Histogram InterpolatePoints(SortedDictionary loadTable) + { + Histogram result = new Histogram(); + if (!loadTable.ContainsKey(0) || !loadTable.ContainsKey(255)) + { + throw new InvalidDataException("InterpolatePoints requires an entry for the first (0th) and last (255th) entry to populate the histogram."); + } + + byte prev = 0; + foreach (byte curr in loadTable.Keys) + { + //For each key after 0, we interpolate along and fill graph values prev+1 to curr + if (0 != curr) + { + for (byte i = (byte)(prev + 1); i < curr; i++) + { + result[i] = LinearInterpolate((double)(curr - prev) / (i - prev), loadTable[prev], loadTable[curr]); + } + } + //We set current value to be previous and set current value entry onto graph + prev = curr; + result[curr] = loadTable[curr]; + } + + return result; + } + + /// + /// Get a copy of the points generated from Tolerance.xml + /// + /// + public SortedDictionary GetGraph() + { + SortedDictionary result = new SortedDictionary(); + KeyValuePair[] array = new KeyValuePair[graph.Count]; + + graph.CopyTo(array, 0); + for (int i = 0; i < array.Length; i++) + { + byte x = array[i].Key; + result[x] = array[i].Value; + } + + return result; + } + + /// + /// Set new value for Tolerance.xml + /// + /// The x Coordinate of brightness + /// The new value of specified brightness + public void SetGraph(byte x, double y) + { + VerifyPoint(x, y); + graph[x] = y; + } + + /// + /// Get those tolerance nodes that dpiRatio is not 1 for save + /// + /// A list of tolerance nodes + public List GetTolerances() + { + return tolerances; + } + + /// + /// Store a tolerance node from Tolerance.xml to a array + /// + /// The tolerance node to add + public void AddTolerance(ToleranceLegacy legacy) + { + tolerances.Add(legacy); + } + + /// + /// Saves the ToleranceCurve object to an XML file representation. + /// + /// The path of the XML histogram file to be stored. + public void ToFile(string filePath) + { + if (String.IsNullOrEmpty(filePath)) + { + throw new ArgumentNullException("ToleranceCurve.ToFile:file path is null or empty"); + } + + XmlDocument xmlDoc = new XmlDocument(); + XmlNode rootNode = xmlDoc.CreateElement("CurveTolerances"); + + XmlNode toleranceNode = xmlDoc.CreateElement("Tolerance"); + + XmlAttribute dpiRatio = xmlDoc.CreateAttribute("dpiRatio"); + dpiRatio.InnerText = defaultDpiRatio.ToString(); + toleranceNode.Attributes.Append(dpiRatio); + SortedDictionary loadTable = GetGraph(); + foreach (byte x in loadTable.Keys) + { + if ((x == 0 || x == 255) && loadTable[x] == 0) + { + continue; + } + + XmlNode pointNode = xmlDoc.CreateElement("Point"); + + XmlAttribute xAttribute = xmlDoc.CreateAttribute("x"); + xAttribute.InnerText = x.ToString(NumberFormatInfo.InvariantInfo); + pointNode.Attributes.Append(xAttribute); + + XmlAttribute yAttribute = xmlDoc.CreateAttribute("y"); + yAttribute.InnerText = loadTable[x].ToString(NumberFormatInfo.InvariantInfo); + pointNode.Attributes.Append(yAttribute); + + toleranceNode.AppendChild(pointNode); + } + + rootNode.AppendChild(toleranceNode); + + for (int i = 0; i < tolerances.Count; i++) + { + toleranceNode = xmlDoc.CreateElement("Tolerance"); + dpiRatio = xmlDoc.CreateAttribute("dpiRatio"); + dpiRatio.InnerText = tolerances[i].DpiRatio.ToString(); + toleranceNode.Attributes.Append(dpiRatio); + for (int j = 0; j < tolerances[i].Points.Count; j++) + { + XmlNode pointNode = xmlDoc.CreateElement("Point"); + + XmlAttribute xAttribute = xmlDoc.CreateAttribute("x"); + xAttribute.InnerText = tolerances[i].Points[j].X; + pointNode.Attributes.Append(xAttribute); + + XmlAttribute yAttribute = xmlDoc.CreateAttribute("y"); + yAttribute.InnerText = tolerances[i].Points[j].Y; + pointNode.Attributes.Append(yAttribute); + + toleranceNode.AppendChild(pointNode); + } + + rootNode.AppendChild(toleranceNode); + } + + xmlDoc.AppendChild(rootNode); + xmlDoc.Save(filePath); + } + + /// + /// Generate a histogram with fully 256 points by interpolate points between those points loaded from Tolerance.xml + /// + /// + public Histogram ToHistogram() + { + SortedDictionary loadTable = GetGraph(); + if (!loadTable.ContainsKey(0)) + { + loadTable[0] = 0; + } + + if (!loadTable.ContainsKey(255)) + { + loadTable[255] = 0; + } + + return InterpolatePoints(loadTable); + } + + private const int histogramSize = 256; + private const double defaultDpiRatio = 1; + private SortedDictionary graph; + private List tolerances; + } + + /// + /// For saving those tolerance nodes that dpiRatio is not 1 + /// + public class ToleranceLegacy + { + /// + /// For saving the dpiRatio attribute + /// + public double DpiRatio { get; set; } + + /// + /// For saving point nodes + /// + public List Points { get; set; } + } + + /// + /// For saving those point nodes of a tolerance node that dpiRatio is not 1 + /// + public class TolerancePoint + { + /// + /// For saving the x attribute of point node + /// + public string X { get; set; } + + /// + /// For saving the y attribute of point node + /// + public string Y { get; set; } + } +} diff --git a/src/dotnetCampus.UITest.WPFTestHelper/VisualVerification/VerificationResult.cs b/src/dotnetCampus.UITest.WPFTestHelper/VisualVerification/VerificationResult.cs new file mode 100644 index 0000000..c8c05a1 --- /dev/null +++ b/src/dotnetCampus.UITest.WPFTestHelper/VisualVerification/VerificationResult.cs @@ -0,0 +1,24 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + + +namespace dotnetCampus.UITest.WPFTestHelper.VisualVerification +{ + /// + /// Specifies values used to report the outcome of a verification. + /// + public enum VerificationResult + { + /// + /// Object does not meet verification criteria. + /// + Fail = 0, + /// + /// Object meets verification criteria. + /// + Pass = 1 + } +} + + diff --git a/src/dotnetCampus.UITest.WPFTestHelper/VisualVerification/WindowSnapshotMode.cs b/src/dotnetCampus.UITest.WPFTestHelper/VisualVerification/WindowSnapshotMode.cs new file mode 100644 index 0000000..48a2e73 --- /dev/null +++ b/src/dotnetCampus.UITest.WPFTestHelper/VisualVerification/WindowSnapshotMode.cs @@ -0,0 +1,23 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + + +namespace dotnetCampus.UITest.WPFTestHelper.VisualVerification +{ + /// + /// WindowSnapshotMode determines if window border should be captured as part of Snapshot. + /// + public enum WindowSnapshotMode + { + /// + /// Capture a snapshot of only the window client area. This mode excludes the window border. + /// + ExcludeWindowBorder = 0, + /// + /// Capture a snapshot of the entire window area. This mode includes the window border. + /// + IncludeWindowBorder = 1 + } + +} diff --git a/src/dotnetCampus.UITest.WPFTestHelper/dotnetCampus.UITest.WPFTestHelper.csproj b/src/dotnetCampus.UITest.WPFTestHelper/dotnetCampus.UITest.WPFTestHelper.csproj new file mode 100644 index 0000000..d0287af --- /dev/null +++ b/src/dotnetCampus.UITest.WPFTestHelper/dotnetCampus.UITest.WPFTestHelper.csproj @@ -0,0 +1,15 @@ + + + + net45;netcoreapp3.1;net5.0-windows;net6.0-windows + true + true + True + The UITest utils for WPF + false + dotnet;nuget;msbuild;UITest;WPF;MSTest;TestFramework + True + disable + + +