File: Threading\LazyValueTests.cs
Web Access
Project: src\src\Razor\src\Shared\Microsoft.AspNetCore.Razor.Utilities.Shared.UnitTests\Microsoft.AspNetCore.Razor.Utilities.Shared.UnitTests.csproj (Microsoft.AspNetCore.Razor.Utilities.Shared.UnitTests)
// 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.Collections.Concurrent;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Razor.Threading;
using Xunit;
 
namespace Microsoft.AspNetCore.Razor.Utilities.Shared.Test.Threading;
 
public class LazyValueTests
{
    [Fact]
    public void LazyValue_GetValue_CallsFactoryOnce()
    {
        // Arrange
        var callCount = 0;
        var lazy = new LazyValue<string>(() =>
        {
            Interlocked.Increment(ref callCount);
            return "test-value";
        });
 
        // Act
        var result1 = lazy.GetValue();
        var result2 = lazy.GetValue();
        var result3 = lazy.GetValue();
 
        // Assert
        Assert.Equal("test-value", result1);
        Assert.Equal("test-value", result2);
        Assert.Equal("test-value", result3);
        Assert.Equal(1, callCount);
    }
 
    [Fact]
    public void LazyValue_GetValue_ReturnsFactoryResult()
    {
        // Arrange
        var expectedValue = new object();
        var lazy = new LazyValue<object>(() => expectedValue);
 
        // Act
        var result = lazy.GetValue();
 
        // Assert
        Assert.Same(expectedValue, result);
    }
 
    [Fact]
    public void LazyValue_GetValue_WorksWithValueTypes()
    {
        // Arrange
        var lazy = new LazyValue<int>(() => 42);
 
        // Act
        var result = lazy.GetValue();
 
        // Assert
        Assert.Equal(42, result);
    }
 
    [Fact]
    public void LazyValue_GetValue_WorksWithNullValues()
    {
        // Arrange
        var lazy = new LazyValue<string?>(() => null);
 
        // Act
        var result = lazy.GetValue();
 
        // Assert
        Assert.Null(result);
    }
 
    [Fact]
    public void LazyValue_GetValue_PropagatesFactoryException()
    {
        // Arrange
        var expectedException = new InvalidOperationException("Test exception");
        var lazy = new LazyValue<string>(() => throw expectedException);
 
        // Act & Assert
        var exception = Assert.Throws<InvalidOperationException>(() => lazy.GetValue());
        Assert.Same(expectedException, exception);
    }
 
    [Fact]
    public void LazyValue_GetValue_ExceptionDoesNotCacheValue()
    {
        // Arrange
        var callCount = 0;
        var lazy = new LazyValue<string>(() =>
        {
            var count = Interlocked.Increment(ref callCount);
            if (count == 1)
            {
                throw new InvalidOperationException("First call fails");
            }
 
            return "success";
        });
 
        // Act & Assert
        Assert.Throws<InvalidOperationException>(() => lazy.GetValue());
        
        // Second call should succeed and call factory again
        var result = lazy.GetValue();
        Assert.Equal("success", result);
        Assert.Equal(2, callCount);
    }
 
    [Fact]
    public async Task LazyValue_ConcurrentAccess_CallsFactoryOnce()
    {
        // Arrange
        var callCount = 0;
        using var barrier = new Barrier(10);
        var lazy = new LazyValue<string>(() =>
        {
            Interlocked.Increment(ref callCount);
            Thread.Sleep(10); // Simulate some work
            return "concurrent-value";
        });
 
        var results = new ConcurrentBag<string>();
        var tasks = new Task[10];
 
        // Act
        for (var i = 0; i < 10; i++)
        {
            tasks[i] = Task.Run(() =>
            {
                barrier.SignalAndWait(); // Ensure all threads start at the same time
                var result = lazy.GetValue();
                results.Add(result);
            });
        }
 
        await Task.WhenAll(tasks);
 
        // Assert
        Assert.Equal(1, callCount);
        Assert.Equal(10, results.Count);
        Assert.All(results, result => Assert.Equal("concurrent-value", result));
    }
 
    [Fact]
    public async Task LazyValue_StressTest_MaintainsConsistency()
    {
        // Arrange
        const int ThreadCount = 100;
        const int IterationsPerThread = 100;
        var callCount = 0;
        var lazy = new LazyValue<int>(() => Interlocked.Increment(ref callCount));
        var allResults = new ConcurrentBag<int>();
 
        // Act
        var tasks = new Task[ThreadCount];
        for (var i = 0; i < ThreadCount; i++)
        {
            tasks[i] = Task.Run(() =>
            {
                for (var j = 0; j < IterationsPerThread; j++)
                {
                    allResults.Add(lazy.GetValue());
                }
            });
        }
 
        await Task.WhenAll(tasks);
 
        // Assert
        Assert.Equal(1, callCount);
        Assert.Equal(ThreadCount * IterationsPerThread, allResults.Count);
        Assert.All(allResults, result => Assert.Equal(1, result));
    }
 
    [Fact]
    public void LazyValueWithArg_GetValue_CallsFactoryOnce()
    {
        // Arrange
        var callCount = 0;
        var lazy = new LazyValue<string, string>(arg =>
        {
            Interlocked.Increment(ref callCount);
            return $"processed-{arg}";
        });
 
        // Act
        var result1 = lazy.GetValue("input");
        var result2 = lazy.GetValue("different-input"); // Should ignore this arg
        var result3 = lazy.GetValue("another-input");   // Should ignore this arg too
 
        // Assert
        Assert.Equal("processed-input", result1);
        Assert.Equal("processed-input", result2);
        Assert.Equal("processed-input", result3);
        Assert.Equal(1, callCount);
    }
 
    [Fact]
    public void LazyValueWithArg_GetValue_ReturnsFactoryResult()
    {
        // Arrange
        var inputArg = new object();
        var expectedResult = new object();
        var lazy = new LazyValue<object, object>(arg =>
        {
            Assert.Same(inputArg, arg);
            return expectedResult;
        });
 
        // Act
        var result = lazy.GetValue(inputArg);
 
        // Assert
        Assert.Same(expectedResult, result);
    }
 
    [Fact]
    public void LazyValueWithArg_GetValue_WorksWithValueTypes()
    {
        // Arrange
        var lazy = new LazyValue<int, string>(multiplier => $"Value: {multiplier * 10}");
 
        // Act
        var result = lazy.GetValue(5);
 
        // Assert
        Assert.Equal("Value: 50", result);
    }
 
    [Fact]
    public void LazyValueWithArg_GetValue_WorksWithNullArg()
    {
        // Arrange
        var lazy = new LazyValue<string?, string>(arg => $"Input was: {arg ?? "null"}");
 
        // Act
        var result = lazy.GetValue(null);
 
        // Assert
        Assert.Equal("Input was: null", result);
    }
 
    [Fact]
    public void LazyValueWithArg_GetValue_WorksWithNullResult()
    {
        // Arrange
        var lazy = new LazyValue<string, string?>(arg => null);
 
        // Act
        var result = lazy.GetValue("test");
 
        // Assert
        Assert.Null(result);
    }
 
    [Fact]
    public void LazyValueWithArg_GetValue_PropagatesFactoryException()
    {
        // Arrange
        var expectedException = new ArgumentException("Test exception");
        var lazy = new LazyValue<string, int>(arg => throw expectedException);
 
        // Act & Assert
        var exception = Assert.Throws<ArgumentException>(() => lazy.GetValue("test"));
        Assert.Same(expectedException, exception);
    }
 
    [Fact]
    public void LazyValueWithArg_GetValue_ExceptionDoesNotCacheValue()
    {
        // Arrange
        var callCount = 0;
        var lazy = new LazyValue<string, string>(arg =>
        {
            var count = Interlocked.Increment(ref callCount);
            if (count == 1)
            {
                throw new InvalidOperationException("First call fails");
            }
 
            return $"success-{arg}";
        });
 
        // Act & Assert
        Assert.Throws<InvalidOperationException>(() => lazy.GetValue("test"));
        
        // Second call should succeed and call factory again
        var result = lazy.GetValue("test");
        Assert.Equal("success-test", result);
        Assert.Equal(2, callCount);
    }
 
    [Fact]
    public async Task LazyValueWithArg_ConcurrentAccess_CallsFactoryOnce()
    {
        // Arrange
        var callCount = 0;
        using var barrier = new Barrier(10);
        var lazy = new LazyValue<string, string>(arg =>
        {
            Interlocked.Increment(ref callCount);
            Thread.Sleep(10); // Simulate some work
            return $"concurrent-{arg}";
        });
 
        var results = new ConcurrentBag<string>();
        var tasks = new Task[10];
 
        // Act
        for (var i = 0; i < 10; i++)
        {
            var threadIndex = i;
            tasks[i] = Task.Run(() =>
            {
                barrier.SignalAndWait(); // Ensure all threads start at the same time
                // Each thread passes a different argument, but only the first should be used
                var result = lazy.GetValue($"input-{threadIndex}");
                results.Add(result);
            });
        }
 
        await Task.WhenAll(tasks);
 
        // Assert
        Assert.Equal(1, callCount);
        Assert.Equal(10, results.Count);
        // All results should be the same, using the argument from whichever thread won the race
        var expectedPrefix = "concurrent-input-";
        Assert.All(results, result => Assert.StartsWith(expectedPrefix, result));
        
        // All results should be identical
        var firstResult = results.First();
        Assert.All(results, result => Assert.Equal(firstResult, result));
    }
 
    [Fact]
    public void LazyValueWithArg_ExceptionRecovery()
    {
        var callCount = 0;
        var lazy = new LazyValue<int, string>(arg =>
        {
            var count = Interlocked.Increment(ref callCount);
            if (count == 1)
            {
                throw new InvalidOperationException($"First call fails with arg: {arg}");
            }
 
            return $"success-{arg}";
        });
 
        // First call fails
        Assert.Throws<InvalidOperationException>(() => lazy.GetValue(100));
 
        // Second call succeeds
        var result = lazy.GetValue(200);
        Assert.Equal("success-200", result);
        Assert.Equal(2, callCount);
    }
 
    [Fact]
    public void LazyValueWithArg_SubsequentCallsIgnoreArgument()
    {
        // Arrange
        var receivedArgs = new ConcurrentBag<string>();
        var lazy = new LazyValue<string, string>(arg =>
        {
            receivedArgs.Add(arg);
            return $"result-{arg}";
        });
 
        // Act
        var result1 = lazy.GetValue("first");
        var result2 = lazy.GetValue("second");
        var result3 = lazy.GetValue("third");
 
        // Assert
        Assert.Equal("result-first", result1);
        Assert.Equal("result-first", result2); // Same result, ignores "second"
        Assert.Equal("result-first", result3); // Same result, ignores "third"
        Assert.Single(receivedArgs);
        Assert.Equal("first", receivedArgs.First());
    }
 
    [Fact]
    public async Task LazyValueWithArg_StressTest_MaintainsConsistency()
    {
        // Arrange
        const int ThreadCount = 50;
        const int IterationsPerThread = 50;
        var callCount = 0;
        var lazy = new LazyValue<int, string>(multiplier =>
        {
            Interlocked.Increment(ref callCount);
            return $"computed-{multiplier * 2}";
        });
        var allResults = new ConcurrentBag<string>();
 
        // Act
        var tasks = new Task[ThreadCount];
        for (var i = 0; i < ThreadCount; i++)
        {
            var threadIndex = i;
            tasks[i] = Task.Run(() =>
            {
                for (var j = 0; j < IterationsPerThread; j++)
                {
                    // Each thread uses a different argument, but only the first should matter
                    allResults.Add(lazy.GetValue(threadIndex + j));
                }
            });
        }
 
        await Task.WhenAll(tasks);
 
        // Assert
        Assert.Equal(1, callCount);
        Assert.Equal(ThreadCount * IterationsPerThread, allResults.Count);
        
        // All results should be identical (from the winning thread's argument)
        var firstResult = allResults.First();
        Assert.All(allResults, result => Assert.Equal(firstResult, result));
        Assert.StartsWith("computed-", firstResult);
    }
 
    [Fact]
    public void LazyValue_FactoryReturningLargeObject_WorksCorrectly()
    {
        // Arrange
        var lazy = new LazyValue<byte[]>(() => new byte[1024 * 1024]); // 1MB array
 
        // Act
        var result1 = lazy.GetValue();
        var result2 = lazy.GetValue();
 
        // Assert
        Assert.Same(result1, result2); // Should be the exact same instance
        Assert.Equal(1024 * 1024, result1.Length);
    }
 
    [Fact]
    public void LazyValueWithArg_ComplexArgumentType_WorksCorrectly()
    {
        // Arrange
        var complexArg = new { Name = "Test", Count = 42 };
        var lazy = new LazyValue<object, string>(arg => $"Processed: {arg}");
 
        // Act
        var result = lazy.GetValue(complexArg);
 
        // Assert
        Assert.Contains("Test", result);
        Assert.Contains("42", result);
        Assert.StartsWith("Processed: { Name = Test, Count = 42 }", result);
    }
 
    [Fact]
    public void LazyValue_FactoryAccessingClosureVariable_WorksCorrectly()
    {
        // Arrange
        var capturedValue = "captured";
        var lazy = new LazyValue<string>(() => $"Factory with {capturedValue}");
 
        // Act
        var result = lazy.GetValue();
 
        // Assert
        Assert.Equal("Factory with captured", result);
    }
 
    [Fact]
    public void LazyValueWithArg_StaticMethodFactory_AvoidsClosure()
    {
        // Arrange - This test demonstrates the closure-avoiding pattern
        var lazy = new LazyValue<int, string>(StaticFactoryMethod);
 
        // Act
        var result = lazy.GetValue(42);
 
        // Assert
        Assert.Equal("Static: 42", result);
 
        static string StaticFactoryMethod(int value)
        {
            return $"Static: {value}";
        }
    }
}