// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. namespace System.Data.Entity { using System.Collections.Generic; using System.Globalization; using System.Linq; using Xunit; /// /// Helpers for verifying strings that are pulled from a localized resources /// public class StringResourceVerifier { private readonly AssemblyResourceLookup _lookup; /// /// Initializes a new instance of the StringResourceVerifier class. /// /// Lookup to be used for locating string resources public StringResourceVerifier(AssemblyResourceLookup lookup) { _lookup = lookup; } /// /// Determines if the supplied string is an instance of the string defined in localized resources /// /// The key of the resource string to match against /// String value to be verified /// Expected values for string.Format placeholders in the resource string If none are supplied then any values for placeholders in the resource string will count as a match /// True if the string matches, false otherwise public bool IsMatch(string expectedResourceKey, string actualMessage, params object[] stringParameters) { return IsMatch(expectedResourceKey, actualMessage, true, stringParameters); } /// /// Determines if the supplied string is an instance of the string defined in localized resources /// /// The key of the resource string to match against /// String value to be verified /// Determines whether the exception message must be exact match of the message in the resource file, or just contain it. /// Expected values for string.Format placeholders in the resource string If none are supplied then any values for placeholders in the resource string will count as a match /// True if the string matches, false otherwise public bool IsMatch(string expectedResourceKey, string actualMessage, bool isExactMatch, params object[] stringParameters) { string messageFromResources; return IsMatch(expectedResourceKey, actualMessage, isExactMatch, stringParameters, out messageFromResources); } /// /// Verified the supplied string is an instance of the string defined in resources /// /// The key of the resource string to match against /// String value to be verified /// Expected values for string.Format placeholders in the resource string If none are supplied then any values for placeholders in the resource string will count as a match public void VerifyMatch(string expectedResourceKey, string actualMessage, params object[] stringParameters) { VerifyMatch(expectedResourceKey, actualMessage, true, stringParameters); } /// /// Determines if the supplied string is an instance of the string defined in localized resources /// If the string in the resource file contains string.Format place holders then the actual message can contain any values for these /// /// The key of the resource string to match against /// String value to be verified /// Determines whether the exception message must be exact match of the message in the resource file, or just contain it. /// Expected values for string.Format placeholders in the resource string If none are supplied then any values for placeholders in the resource string will count as a match public void VerifyMatch(string expectedResourceKey, string actualMessage, bool isExactMatch, params object[] stringParameters) { string messageFromResources; Assert.True( IsMatch( expectedResourceKey, actualMessage, isExactMatch, stringParameters, out messageFromResources), string.Format( @"Actual string does not match the message defined in resources. Expected:'{0}', Actual:'{1}'", messageFromResources, actualMessage)); } private static bool IsMatchWithAnyPlaceholderValues(string expectedMessage, string actualMessage, bool isExactMatch) { // Find the sections of the Exception message seperated by {x} tags var sections = FindMessageSections(expectedMessage); // Check that each section is present in the actual message in correct order var indexToCheckFrom = 0; foreach (var section in sections) { // Check if it is present in the actual message var index = actualMessage.IndexOf(section, indexToCheckFrom, StringComparison.Ordinal); if (index < 0) { return false; } if (section.Length == 0 && section == sections.Last()) { // If the last section is a zero-length string // then this indicates that the placeholder is the // last thing in the resource. Thus every section // matched and the placeholder takes up the rest of string // from the actual message. return true; } else { // continue checking from the end of this section // (Ensures sections are in the correct order) indexToCheckFrom = index + section.Length; } } // If we reach the end then everything is a match return isExactMatch ? indexToCheckFrom == actualMessage.Length : indexToCheckFrom <= actualMessage.Length; } private static IEnumerable FindMessageSections(string messageFromResources) { // Find the start and end index of each section of the string var sections = new List(); // Start with a section that spans the whole string // While there are still place holders shorten the previous section to end at the next { and start a new section from the following } sections.Add( new StringSection { StartIndex = 0, EndIndex = messageFromResources.Length }); var indexToCheckFrom = 0; while (messageFromResources.IndexOf("{", indexToCheckFrom, StringComparison.Ordinal) >= 0) { // Find the indexes to split the new section around var previous = sections.Last(); var previousEndIndex = messageFromResources.IndexOf("{", indexToCheckFrom, StringComparison.Ordinal); var nextStartIndex = messageFromResources.IndexOf("}", previousEndIndex, StringComparison.Ordinal) + 1; // If there are no remaining closing tags then we are done if (nextStartIndex == 0) { break; } else { // Contents of place holder must be integer int temp; var intContents = int.TryParse(messageFromResources.Substring(previousEndIndex + 1, nextStartIndex - previousEndIndex - 2), out temp); // Place holder must not be escaped (i.e. {{0}}) var escaped = messageFromResources[previousEndIndex] == '{' && nextStartIndex < messageFromResources.Length && messageFromResources[nextStartIndex] == '}'; if (!intContents || escaped) { indexToCheckFrom++; continue; } } // Shorten the previous section to end at the { previous.EndIndex = previousEndIndex; // Add the remaining string after the } into a new section, // even if the '}' is the last character in the string. This // helps verification ensure that there is actually a wildcard // at this point in the string instead of the string ending // without a wildcard. sections.Add( new StringSection { StartIndex = nextStartIndex, EndIndex = messageFromResources.Length }); indexToCheckFrom = nextStartIndex; } // Pull out the sections return sections.Select( s => messageFromResources.Substring(s.StartIndex, s.EndIndex - s.StartIndex).Replace("{{", "{").Replace("}}", "}")); } private bool IsMatch( string expectedResourceKey, string actualMessage, bool isExactMatch, object[] stringParameters, out string messageFromResources) { ExceptionHelpers.CheckStringArgumentIsNotNullOrEmpty(expectedResourceKey, "expectedResourceKey"); ExceptionHelpers.CheckArgumentNotNull(actualMessage, "actualMessage"); messageFromResources = _lookup.LookupString(expectedResourceKey); if (stringParameters.Length == 0) { return IsMatchWithAnyPlaceholderValues(messageFromResources, actualMessage, isExactMatch); } else { messageFromResources = string.Format(CultureInfo.InvariantCulture, messageFromResources, stringParameters); return isExactMatch ? actualMessage == messageFromResources : actualMessage.Contains(messageFromResources); } } /// /// Represents a section of a string /// private class StringSection { /// /// Gets or sets the index the section starts at /// public int StartIndex { get; set; } /// /// Gets or sets the index the section ends at /// public int EndIndex { get; set; } } } }