// Copyright (C) Stichting Deltares 2017. All rights reserved. // // This file is part of Ringtoets. // // Ringtoets is free software: you can redistribute it and/or modify // it under the terms of the GNU Lesser General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Lesser General Public License for more details. // // You should have received a copy of the GNU Lesser General Public License // along with this program. If not, see . // // All names, logos, and references to "Deltares" are registered trademarks of // Stichting Deltares and remain full property of Stichting Deltares at all times. // All rights reserved. using System; using System.Collections; using System.Collections.Generic; using System.ComponentModel; using System.Diagnostics; using System.Drawing; using System.Drawing.Imaging; using System.IO; using System.Linq; using System.Reflection; using System.Windows.Forms; using log4net.Appender; using log4net.Config; using log4net.Core; using NUnit.Framework; using NUnit.Framework.Internal; namespace Core.Common.TestUtil { /// /// Class containing helper functions which can be used for unit tests. /// public static class TestHelper { private static string solutionRoot; public static string SolutionRoot { get { return solutionRoot ?? (solutionRoot = GetSolutionRoot()); } } /// /// Returns the location on disk that can be used safely for writing to disk temporarily. /// /// The folder path. /// Caller is responsible for cleaning up files put in the folder. /// Thrown when the folder doesn't exist. /// public static string GetScratchPadPath() { string scratchPadPath = Path.Combine(Path.GetDirectoryName(SolutionRoot), "Scratchpad"); if (!Directory.Exists(scratchPadPath)) { throw new IOException("The 'Scratchpad' folder has been deleted from the trunk, while tests require the existence of this folder for writing to disk temporarily."); } return scratchPadPath; } /// /// Returns the location on disk that can be used safely for writing to disk temporarily. /// /// The file or folder path inside the 'scratchpad' folder. /// The folder path. /// Caller is responsible for cleaning up files put in the folder. /// Thrown when the folder doesn't exist. /// public static string GetScratchPadPath(string path) { return Path.Combine(GetScratchPadPath(), path); } /// /// Returns a full path to a test data directory given the . /// /// The path to construct a full test data path for. /// A full path to the test data. public static string GetTestDataPath(TestDataPath testDataPath) { return Path.Combine(Path.GetDirectoryName(SolutionRoot), testDataPath.Path, "test-data"); } /// /// Returns a full path to a file or directory in the test data directory. /// /// The path to construct a full test data path for. /// The path to a file or directory in the test data directory. /// A full path to the file or directory from the test data directory. public static string GetTestDataPath(TestDataPath testDataPath, string path) { return Path.Combine(GetTestDataPath(testDataPath.Path), path); } /// /// Checks whether the file pointed at by can be opened /// for writing. /// /// The location of the file to open for writing. /// true if the file could be opened with write permissions. false otherwise. public static bool CanOpenFileForWrite(string pathToFile) { try { using (File.OpenWrite(pathToFile)) {} return true; } catch (IOException) { return false; } } /// /// Checks whether the directory pointed at by can be used /// for writing a file. /// /// The location of the directory to open for writing. /// true if the file could be opened with write permissions. false otherwise. /// Thrown when is null. /// Thrown when contains invalid characters. public static bool CanWriteInDirectory(string pathToDirectory) { string filePath = Path.Combine(pathToDirectory, nameof(CanWriteInDirectory)); try { using (File.OpenWrite(filePath)) {} } catch (SystemException) { return false; } if (File.Exists(filePath)) { File.Delete(filePath); } return true; } /// /// Converts a rooted file or folder path into a UNC-path. /// /// The rooted path. /// The UNC-path. /// Thrown when /// contains invalid characters, is null or empty or wasn't a rooted path. public static string ToUncPath(string rootedPath) { string root = Path.GetPathRoot(rootedPath); if (string.IsNullOrEmpty(root)) { throw new ArgumentException("Must be a rooted path.", nameof(rootedPath)); } string relativePath = rootedPath.Replace(root, ""); string drive = root.Remove(1); var uncPath = new Uri(Path.Combine(@"\\localhost", drive + "$", relativePath)); return uncPath.LocalPath; } /// /// Asserts that the execution of some implementation of a functionality runs faster than a given allowed time. /// /// The maximum time in milliseconds that the functionality is allowed to run. /// The functionality to execute. /// Take HDD speed into account, makes sure that test timing is divided by MACHINE_HDD_PERFORMANCE_RANK environmental variable. public static void AssertIsFasterThan(float maxMilliseconds, Action action, bool rankHddAccess = false) { AssertIsFasterThan(maxMilliseconds, null, action, rankHddAccess); } /// /// Checks if the given messages occurs in the log. /// /// Action to be performed while recording the log /// The message that should occur in the log /// Optional: assert that log has this number of messages. public static void AssertLogMessageIsGenerated(Action action, string message, int? expectedLogMessageCount = null) { AssertLogMessagesAreGenerated(action, new[] { message }, expectedLogMessageCount); } /// /// Method used to check if a collection of messages have occurred in the log. /// This function allowed checking log messages being generated like /// without having to rerun the action for every single message. Fails the test when any of checks fail. /// /// Action to be performed while recording the log /// The collection of messages that should occur in the log /// Optional: assert that log has this number of messages. /// public static void AssertLogMessagesAreGenerated(Action action, IEnumerable messages, int? expectedLogMessageCount = null) { Tuple[] renderedMessages = GetAllRenderedMessages(action).ToArray(); AssertExpectedMessagesInRenderedMessages(messages, renderedMessages); if (expectedLogMessageCount != null) { Assert.AreEqual((int) expectedLogMessageCount, renderedMessages.Length); } } /// /// Method use to perform any type of assertion on the generated log while performing /// a particular action. /// /// Action to be performed while recording the log. /// The assertion logic performed on the generated log-messages. public static void AssertLogMessages(Action action, Action> assertLogMessages) { IEnumerable> renderedMessages = GetAllRenderedMessages(action); assertLogMessages(renderedMessages.Select(rm => rm.Item1)); } /// /// Method used to perform any type of assertion on the generated log while performing /// a particular action. /// /// Action to be performed while recording the log. /// The assertion logic /// performed on the generated log-messages and logged exceptions. public static void AssertLogMessagesWithLevelAndLoggedExceptions( Action action, Action>> assertLogMessagesWithLevelAndExceptions) { IEnumerable> renderedMessages = GetAllRenderedMessagesWithExceptions(action); assertLogMessagesWithLevelAndExceptions(renderedMessages); } /// /// Checks the number of messages in the log. /// /// Action to be performed while recording the log /// The expected number of messages public static void AssertLogMessagesCount(Action action, int count) { IEnumerable> renderedMessages = GetAllRenderedMessages(action); Assert.AreEqual(count, renderedMessages.Count()); } /// /// Asserts that two bitmap images are equal. /// /// The expected image. /// The actual image. /// Thrown when is not /// equal to . public static void AssertImagesAreEqual(Image expectedImage, Image actualImage) { if (expectedImage == null) { Assert.IsNull(actualImage); return; } Assert.IsNotNull(actualImage); Assert.AreEqual(expectedImage.Size, actualImage.Size); IEnumerable expectedImageBytes = GetImageAsColorArray(expectedImage); IEnumerable actualImageBytes = GetImageAsColorArray(actualImage); CollectionAssert.AreEqual(expectedImageBytes, actualImageBytes); } /// /// Asserts that a contains an item at the given /// with the correct properties. /// /// The containing an item at position . /// The position of the menu item in . /// The text expected for the menu item. /// The tooltip expected for the menu item. /// The image expected for the menu item. /// Optional: the expected enabled state of the menu item. Default: true. /// Thrown when does not contain a menu item at /// position with the right , or . /// public static void AssertContextMenuStripContainsItem(ContextMenuStrip menu, int position, string text, string toolTip, Image icon, bool enabled = true) { Assert.IsNotNull(menu); AssertContextMenuStripContainsItem(menu.Items, position, text, toolTip, icon, enabled); } /// /// Asserts that a contains an item at the given /// with the correct properties. /// /// The containing an item at position . /// The position of the menu item in . /// The text expected for the menu item. /// The tooltip expected for the menu item. /// The image expected for the menu item. /// Optional: the expected enabled state of the menu item. Default: true. /// Thrown when does not contain a menu item at /// position with the right , or . /// public static void AssertDropDownItemContainsItem(ToolStripDropDownItem menu, int position, string text, string toolTip, Image icon, bool enabled = true) { Assert.IsNotNull(menu); AssertContextMenuStripContainsItem(menu.DropDownItems, position, text, toolTip, icon, enabled); } /// /// Asserts that the exception is of type and that the custom part of /// is equal to . /// /// The type of the expected . /// The test to execute and should throw of type . /// The expected custom part of the . /// The that was thrown while executing . public static T AssertThrowsArgumentExceptionAndTestMessage(TestDelegate test, string expectedCustomMessage) where T : ArgumentException { var exception = Assert.Throws(test); string message = exception.Message; if (exception.ParamName != null) { List customMessageParts = message.Split(new[] { Environment.NewLine }, StringSplitOptions.None).ToList(); customMessageParts.RemoveAt(customMessageParts.Count - 1); message = string.Join(Environment.NewLine, customMessageParts.ToArray()); } Assert.AreEqual(expectedCustomMessage, message); return exception; } /// /// Method used to check if a collection of messages have occurred in the log. /// This function allowed checking log messages being generated like /// without having to rerun the action for every single message. Fails the test when any of checks fail. /// /// Action to be performed while recording the log. /// The message that should occur in the log with a certain log level. /// Optional: assert that log has this number of messages. /// public static void AssertLogMessageWithLevelIsGenerated(Action action, Tuple message, int? expectedLogMessageCount = null) { AssertLogMessagesWithLevelAreGenerated(action, new[] { message }, expectedLogMessageCount); } /// /// Method used to check if a collection of messages have occurred in the log. /// This function allowed checking log messages being generated like /// without having to rerun the action for every single message. Fails the test when any of checks fail. /// /// Action to be performed while recording the log /// The collection of messages that should occur in the log /// Optional: assert that log has this number of messages. /// public static void AssertLogMessagesWithLevelAreGenerated(Action action, IEnumerable> messages, int? expectedLogMessageCount = null) { Tuple[] renderedMessages = GetAllRenderedMessages(action).ToArray(); AssertExpectedMessagesInRenderedMessages(messages, renderedMessages); if (expectedLogMessageCount != null) { Assert.AreEqual((int) expectedLogMessageCount, renderedMessages.Length); } } /// /// Determines whether a property is decorated with a /// of a given type. /// /// The type of the target to retrieve the property from. /// The type of to check /// for on the property of . /// The expression that resolves to the property to be checked. /// Thrown when the /// wasn't decorated with a or with a different /// than . /// Thrown when is null. /// Thrown when /// is not an expression with a property, such as an expression calling multiple methods. /// Thrown when more than one property is found with /// name specified in . /// Thrown when a custom attribute type cannot be loaded. /// Thrown when the property in /// belongs to a type that is loaded into the reflection-only context. See How to: /// Load Assemblies into the Reflection-Only Context on MSDN for more information. public static void AssertTypeConverter(string expression) where TTypeConverter : TypeConverter { var typeConverterAttribute = (TypeConverterAttribute) Attribute.GetCustomAttribute(typeof(TTarget).GetProperty(expression), typeof(TypeConverterAttribute), true); Assert.NotNull(typeConverterAttribute); Assert.IsTrue(typeConverterAttribute.ConverterTypeName == typeof(TTypeConverter).AssemblyQualifiedName); } /// /// Determines whether objects are copies of each other by verifying that they are /// equal, but not the same. /// /// The object which should be equal, but not same as . /// The object which should be equal, but not same as . /// Thrown when either: /// /// is not equal to ; /// is the same as . /// /// public static void AssertAreEqualButNotSame(object objectA, object objectB) { Assert.AreEqual(objectA, objectB, "Objects should be equal."); if (objectA != null) { Assert.AreNotSame(objectA, objectB, "Objects should not be the same."); } } /// /// Asserts that all elements in the collections are the same. /// /// The expected collection of elements. /// The actual collection of elements. /// Thrown when either: /// /// has more or less elements than ; /// contains an element at a position that is not the same /// element in at that position. /// /// public static void AssertCollectionAreSame(IEnumerable expected, IEnumerable actual) { IEnumerator expectedEnumerator = expected.GetEnumerator(); IEnumerator actualEnumerator = actual.GetEnumerator(); while (expectedEnumerator.MoveNext()) { Assert.IsTrue(actualEnumerator.MoveNext()); { Assert.AreSame(expectedEnumerator.Current, actualEnumerator.Current); } } Assert.IsFalse(actualEnumerator.MoveNext()); } /// /// Asserts that all elements in the collections are not the same. /// /// The expected collection of elements. /// The actual collection of elements. /// Thrown when either: /// /// has more or less elements than ; /// contains an element at a position that is the same /// element in at that position. /// /// public static void AssertCollectionAreNotSame(IEnumerable expected, IEnumerable actual) { IEnumerator expectedEnumerator = expected.GetEnumerator(); IEnumerator actualEnumerator = actual.GetEnumerator(); while (expectedEnumerator.MoveNext()) { Assert.IsTrue(actualEnumerator.MoveNext()); { Assert.AreNotSame(expectedEnumerator.Current, actualEnumerator.Current); } } Assert.IsFalse(actualEnumerator.MoveNext()); } /// /// Asserts that all elements in the collections are equal by using the . /// /// The expected collection of elements. /// The actual collection of elements. /// The comparer to verify whether elements of /// and are equal. /// Thrown when either: /// /// has more or less elements than ; /// contains an element at a position that is not equal to the /// element in at that position. /// /// public static void AssertCollectionsAreEqual(IEnumerable expected, IEnumerable actual, IEqualityComparer equalityComparer) { Assert.NotNull(expected, "Expect collection is not expected to be null."); Assert.NotNull(actual, "Actual collection is not expected to be null."); Assert.NotNull(equalityComparer, "Comparer to use for equality cannot be null."); using (IEnumerator expectedEnumerator = expected.GetEnumerator()) using (IEnumerator actualEnumerator = actual.GetEnumerator()) { while (expectedEnumerator.MoveNext()) { Assert.IsTrue(actualEnumerator.MoveNext()); { Assert.IsTrue(equalityComparer.Equals(expectedEnumerator.Current, actualEnumerator.Current)); } } Assert.IsFalse(actualEnumerator.MoveNext()); } } private static void AssertIsFasterThan(float maxMilliseconds, string message, Action action, bool rankHddAccess) { var stopwatch = new Stopwatch(); double actualMillisecond = default(double); stopwatch.Start(); action(); stopwatch.Stop(); actualMillisecond = Math.Abs(actualMillisecond - default(double)) > 1e-5 ? Math.Min(stopwatch.ElapsedMilliseconds, actualMillisecond) : stopwatch.ElapsedMilliseconds; stopwatch.Reset(); float machineHddPerformanceRank = GetMachineHddPerformanceRank(); float rank = machineHddPerformanceRank; if (rankHddAccess) // when test relies a lot on HDD - multiply rank by hdd speed factor { rank *= machineHddPerformanceRank; } string userMessage = string.IsNullOrEmpty(message) ? "" : message + ". "; if (!rank.Equals(1.0f)) { Assert.IsTrue(rank * actualMillisecond < maxMilliseconds, userMessage + "Maximum of {0} milliseconds exceeded. Actual was {1}, machine performance weighted actual was {2}", maxMilliseconds, actualMillisecond, actualMillisecond * rank); Console.WriteLine(userMessage + string.Format("Test took {1} milliseconds (machine performance weighted {2}). Maximum was {0}", maxMilliseconds, actualMillisecond, actualMillisecond * rank)); } else { Assert.IsTrue(actualMillisecond < maxMilliseconds, userMessage + "Maximum of {0} milliseconds exceeded. Actual was {1}", maxMilliseconds, actualMillisecond); Console.WriteLine(userMessage + string.Format("Test took {1} milliseconds. Maximum was {0}", maxMilliseconds, actualMillisecond)); } } private static void AssertExpectedMessagesInRenderedMessages(IEnumerable messages, Tuple[] renderedMessages) { foreach (string message in messages) { CollectionAssert.Contains(renderedMessages.Select(rm => rm.Item1), message); } } /// /// Checks if all messages from occur in /// /// The collection of expected messages /// The collection of messages in the log private static void AssertExpectedMessagesInRenderedMessages(IEnumerable> messages, Tuple[] renderedMessages) { IEnumerable> messagesWithLog4NetLevel = messages.Select(m => Tuple.Create(m.Item1, m.Item2.ToLog4NetLevel())); foreach (Tuple message in messagesWithLog4NetLevel) { CollectionAssert.Contains(renderedMessages, message); } } private static void AssertContextMenuStripContainsItem(ToolStripItemCollection items, int position, string text, string toolTip, Image icon, bool enabled = true) { Assert.Less(position, items.Count); ToolStripItem item = items[position]; Assert.AreEqual(text, item.Text); Assert.AreEqual(toolTip, item.ToolTipText); Assert.AreEqual(enabled, item.Enabled); AssertImagesAreEqual(icon, item.Image); } private static string GetSolutionRoot() { const string solutionName = "Ringtoets.sln"; //get the current directory and scope up //TODO find a faster safer method var testContext = new TestContext(TestExecutionContext.CurrentContext); string curDir = testContext.TestDirectory; while (Directory.Exists(curDir) && !File.Exists(curDir + @"\" + solutionName)) { curDir += "/../"; } if (!File.Exists(Path.Combine(curDir, solutionName))) { throw new InvalidOperationException(string.Format("Solution file '{0}' not found in any folder of '{1}'.", solutionName, Directory.GetCurrentDirectory())); } return Path.GetFullPath(curDir); } private static float GetMachineHddPerformanceRank() { string rank = Environment.GetEnvironmentVariable("MACHINE_HDD_PERFORMANCE_RANK"); if (!string.IsNullOrEmpty(rank)) { return float.Parse(rank); } return 1.0f; } private static IEnumerable> GetAllRenderedMessages(Action action) { IEnumerable> renderedMessages = GetAllRenderedMessagesWithExceptions(action); return renderedMessages.Select(t => Tuple.Create(t.Item1, t.Item2)); } private static IEnumerable> GetAllRenderedMessagesWithExceptions(Action action) { var memoryAppender = new MemoryAppender(); BasicConfigurator.Configure(memoryAppender); LogHelper.SetLoggingLevel(Level.All); action(); List> renderedMessages = memoryAppender.GetEvents() .Select(le => Tuple.Create(le.RenderedMessage, le.Level, le.ExceptionObject)) .ToList(); memoryAppender.Close(); LogHelper.ResetLogging(); return renderedMessages; } private static Color[] GetImageAsColorArray(Image image) { // Convert image to ARGB bitmap using 8bits/channel: Bitmap bitmap = new Bitmap(image).Clone(new Rectangle(0, 0, image.Size.Width, image.Size.Height), PixelFormat.Format32bppArgb); var index = 0; var imageColors = new Color[image.Size.Width * image.Size.Height]; for (var i = 0; i < bitmap.Height; i++) { for (var j = 0; j < bitmap.Width; j++) { imageColors[index++] = bitmap.GetPixel(j, i); } } return imageColors; } } }