File: ResourceManagerStringLocalizerTest.cs
Web Access
Project: src\src\Localization\Localization\test\Microsoft.Extensions.Localization.Tests\Microsoft.Extensions.Localization.Tests.csproj (Microsoft.Extensions.Localization.Tests)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Resources;
using Microsoft.AspNetCore.InternalTesting;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Testing;
using Xunit;
 
namespace Microsoft.Extensions.Localization;
 
public class ResourceManagerStringLocalizerTest
{
    [Fact]
    public void EnumeratorCachesCultureWalkForSameAssembly()
    {
        // Arrange
        var resourceNamesCache = new ResourceNamesCache();
        var baseName = "test";
        var resourceAssembly = new TestAssemblyWrapper();
        var resourceManager = new TestResourceManager(baseName, resourceAssembly);
        var resourceStreamManager = new TestResourceStringProvider(
            resourceNamesCache,
            resourceManager,
            resourceAssembly.Assembly,
            baseName);
        var logger = Logger;
        var localizer1 = new ResourceManagerStringLocalizer(resourceManager,
            resourceStreamManager,
            baseName,
            resourceNamesCache,
            logger);
        var localizer2 = new ResourceManagerStringLocalizer(resourceManager,
            resourceStreamManager,
            baseName,
            resourceNamesCache,
            logger);
 
        // Act
        for (var i = 0; i < 5; i++)
        {
            localizer1.GetAllStrings().ToList();
            localizer2.GetAllStrings().ToList();
        }
 
        // Assert
        var expectedCallCount = GetCultureInfoDepth(CultureInfo.CurrentUICulture);
        Assert.Equal(expectedCallCount, resourceAssembly.ManifestResourceStreamCallCount);
    }
 
    [Fact]
    public void EnumeratorCacheIsScopedByAssembly()
    {
        // Arrange
        var resourceNamesCache = new ResourceNamesCache();
        var baseName = "test";
        var resourceAssembly1 = new TestAssemblyWrapper(typeof(ResourceManagerStringLocalizerTest));
        var resourceAssembly2 = new TestAssemblyWrapper(typeof(ResourceManagerStringLocalizer));
        var resourceManager1 = new TestResourceManager(baseName, resourceAssembly1);
        var resourceManager2 = new TestResourceManager(baseName, resourceAssembly2);
        var resourceStreamManager1 = new TestResourceStringProvider(resourceNamesCache, resourceManager1, resourceAssembly1.Assembly, baseName);
        var resourceStreamManager2 = new TestResourceStringProvider(resourceNamesCache, resourceManager2, resourceAssembly2.Assembly, baseName);
        var logger = Logger;
        var localizer1 = new ResourceManagerStringLocalizer(
            resourceManager1,
            resourceStreamManager1,
            baseName,
            resourceNamesCache,
            logger);
        var localizer2 = new ResourceManagerStringLocalizer(
            resourceManager2,
            resourceStreamManager2,
            baseName,
            resourceNamesCache,
            logger);
 
        // Act
        localizer1.GetAllStrings().ToList();
        localizer2.GetAllStrings().ToList();
 
        // Assert
        var expectedCallCount = GetCultureInfoDepth(CultureInfo.CurrentUICulture);
        Assert.Equal(expectedCallCount, resourceAssembly1.ManifestResourceStreamCallCount);
        Assert.Equal(expectedCallCount, resourceAssembly2.ManifestResourceStreamCallCount);
    }
 
    [Fact]
    public void GetString_PopulatesSearchedLocationOnLocalizedString()
    {
        // Arrange
        var baseName = "Resources.TestResource";
        var resourceNamesCache = new ResourceNamesCache();
        var resourceAssembly = new TestAssemblyWrapper();
        var resourceManager = new TestResourceManager(baseName, resourceAssembly);
        var resourceStreamManager = new TestResourceStringProvider(resourceNamesCache, resourceManager, resourceAssembly.Assembly, baseName);
        var logger = Logger;
        var localizer = new ResourceManagerStringLocalizer(
            resourceManager,
            resourceStreamManager,
            baseName,
            resourceNamesCache,
            logger);
 
        // Act
        var value = localizer["name"];
 
        // Assert
        Assert.Equal("Resources.TestResource", value.SearchedLocation);
    }
 
    [Fact]
    [ReplaceCulture("en-US", "en-US")]
    public void GetString_LogsLocationSearched()
    {
        // Arrange
        var baseName = "Resources.TestResource";
        var resourceNamesCache = new ResourceNamesCache();
        var resourceAssembly = new TestAssemblyWrapper();
        var resourceManager = new TestResourceManager(baseName, resourceAssembly);
        var resourceStreamManager = new TestResourceStringProvider(resourceNamesCache, resourceManager, resourceAssembly.Assembly, baseName);
        var logger = Logger;
 
        var localizer = new ResourceManagerStringLocalizer(
            resourceManager,
            resourceStreamManager,
            baseName,
            resourceNamesCache,
            logger);
 
        // Act
        var value = localizer["a key!"];
 
        // Assert
        var write = Assert.Single(Sink.Writes);
        Assert.Equal("ResourceManagerStringLocalizer searched for 'a key!' in 'Resources.TestResource' with culture 'en-US'.", write.State.ToString());
    }
 
    [Theory]
    [InlineData(true)]
    [InlineData(false)]
    public void ResourceManagerStringLocalizer_GetAllStrings_ReturnsExpectedValue(bool includeParentCultures)
    {
        // Arrange
        var baseName = "test";
        var resourceNamesCache = new ResourceNamesCache();
        var resourceAssembly = new TestAssemblyWrapper();
        var resourceManager = new TestResourceManager(baseName, resourceAssembly);
        var resourceStreamManager = new TestResourceStringProvider(resourceNamesCache, resourceManager, resourceAssembly.Assembly, baseName);
        var logger = Logger;
        var localizer = new ResourceManagerStringLocalizer(
            resourceManager,
            resourceStreamManager,
            baseName,
            resourceNamesCache,
            logger);
 
        // Act
        // We have to access the result so it evaluates.
#pragma warning disable CA1304 // Specify CultureInfo
        var strings = localizer.GetAllStrings(includeParentCultures).ToList();
#pragma warning restore CA1304 // Specify CultureInfo
 
        // Assert
        var value = Assert.Single(strings);
        Assert.Equal("TestName", value.Value);
    }
 
    [Theory]
    [InlineData(true)]
    [InlineData(false)]
    public void ResourceManagerStringLocalizer_GetAllStrings_MissingResourceThrows(bool includeParentCultures)
    {
        // Arrange
        var resourceNamesCache = new ResourceNamesCache();
        var baseName = "testington";
        var resourceAssembly = new TestAssemblyWrapper();
        resourceAssembly.HasResources = false;
        var resourceManager = new TestResourceManager(baseName, resourceAssembly);
        var logger = Logger;
 
        var localizer = new ResourceManagerStringLocalizer(
            resourceManager,
            resourceAssembly.Assembly,
            baseName,
            resourceNamesCache,
            logger);
 
        // Act & Assert
        var exception = Assert.Throws<MissingManifestResourceException>(() =>
        {
            // We have to access the result so it evaluates.
#pragma warning disable CA1304 // Specify CultureInfo
            localizer.GetAllStrings(includeParentCultures).ToArray();
#pragma warning restore CA1304 // Specify CultureInfo
        });
 
        var expectedTries = includeParentCultures ? GetCultureInfoDepth(CultureInfo.CurrentUICulture) : 1;
        string cultureName = CultureInfo.CurrentCulture.ToString();
        string expectedManifestFileName = cultureName.Length > 0 ? $"testington.{cultureName}.resources" : $"testington.resources";
        var expected = includeParentCultures
            ? "No manifests exist for the current culture."
            : $"The manifest '{expectedManifestFileName}' was not found.";
        Assert.Equal(expected, exception.Message);
        Assert.Equal(expectedTries, resourceAssembly.ManifestResourceStreamCallCount);
    }
 
    private static Stream MakeResourceStream()
    {
        var stream = new MemoryStream();
        var resourceWriter = new ResourceWriter(stream);
        resourceWriter.AddResource("TestName", "value");
        resourceWriter.Generate();
        stream.Position = 0;
        return stream;
    }
 
    private static int GetCultureInfoDepth(CultureInfo culture)
    {
        var result = 0;
        var currentCulture = culture;
 
        while (true)
        {
            result++;
 
            // Under LC_ALL=C on Linux, the current culture is an invariant culture, but its Parent does
            // not refer to itself (https://github.com/dotnet/runtime/issues/94505).
            // Avoid counting it as 2 cultures by directly checking for equality against the InvariantCulture.
            if (CultureInfo.InvariantCulture.Equals(currentCulture))
            {
                break;
            }
 
            currentCulture = currentCulture.Parent;
        }
 
        return result;
    }
 
    private TestSink Sink { get; } = new TestSink();
 
    private ILogger Logger => new TestLoggerFactory(Sink, enabled: true).CreateLogger<ResourceManagerStringLocalizer>();
 
    internal class TestResourceManager : ResourceManager
    {
        private AssemblyWrapper _assemblyWrapper;
 
        public TestResourceManager(string baseName, AssemblyWrapper assemblyWrapper)
            : base(baseName, assemblyWrapper.Assembly)
        {
            _assemblyWrapper = assemblyWrapper;
        }
 
        public override string? GetString(string name, CultureInfo? culture) => null;
 
        public override ResourceSet? GetResourceSet(CultureInfo culture, bool createIfNotExists, bool tryParents)
        {
            var resourceStream = _assemblyWrapper.GetManifestResourceStream(BaseName);
 
            return resourceStream != null ? new ResourceSet(resourceStream) : null;
        }
    }
 
    internal class TestResourceStringProvider : ResourceManagerStringProvider
    {
        public TestResourceStringProvider(
                IResourceNamesCache resourceCache,
                TestResourceManager resourceManager,
                Assembly assembly,
                string resourceBaseName)
            : base(resourceCache, resourceManager, assembly, resourceBaseName)
        {
        }
    }
 
    internal class TestAssemblyWrapper : AssemblyWrapper
    {
        public TestAssemblyWrapper()
            : this(typeof(TestAssemblyWrapper))
        {
        }
 
        public TestAssemblyWrapper(Type type)
            : base(type.Assembly)
        {
        }
 
        public bool HasResources { get; set; } = true;
 
        public int ManifestResourceStreamCallCount { get; private set; }
 
        public override Stream? GetManifestResourceStream(string name)
        {
            ManifestResourceStreamCallCount++;
 
            return HasResources ? MakeResourceStream() : null;
        }
    }
}