diff --git a/src/Build.UnitTests/Evaluation/ExpanderFunction_Tests.cs b/src/Build.UnitTests/Evaluation/ExpanderFunction_Tests.cs new file mode 100644 index 00000000000..43c261e5676 --- /dev/null +++ b/src/Build.UnitTests/Evaluation/ExpanderFunction_Tests.cs @@ -0,0 +1,279 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Globalization; +using System.Runtime.InteropServices; +using System.Threading; + +using Microsoft.Build.Evaluation; + +using Shouldly; + +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.Build.Engine.UnitTests.Evaluation +{ + public class ExpanderFunction_Tests + { + private readonly ITestOutputHelper _output; + + public ExpanderFunction_Tests(ITestOutputHelper output) => _output = output; + + /* Tests for TryConvertToInt */ + + [Fact] + public void TryConvertToIntGivenNull() + { + Expander.Function.TryConvertToInt(null, out int actual).ShouldBeFalse(); + actual.ShouldBe(0); + } + + [Fact] + public void TryConvertToIntGivenDouble() + { + const double value = 10.0; + Expander.Function.TryConvertToInt(value, out int actual).ShouldBeTrue(); + actual.ShouldBe(10); + } + + [Fact] + public void TryConvertToIntGivenLong() + { + const long value = 10; + Expander.Function.TryConvertToInt(value, out int actual).ShouldBeTrue(); + actual.ShouldBe(10); + } + + [Fact] + public void TryConvertToIntGivenInt() + { + const int value = 10; + Expander.Function.TryConvertToInt(value, out int actual).ShouldBeTrue(); + actual.ShouldBe(10); + } + + [Fact] + public void TryConvertToIntGivenString() + { + const string value = "10"; + Expander.Function.TryConvertToInt(value, out int actual).ShouldBeTrue(); + actual.ShouldBe(10); + } + + [Fact] + public void TryConvertToIntGivenDoubleWithIntMinValue() + { + const int expected = int.MinValue; + const double value = expected; + Expander.Function.TryConvertToInt(value, out int actual).ShouldBeTrue(); + actual.ShouldBe(expected); + } + + [Fact] + public void TryConvertToIntGivenDoubleWithIntMaxValue() + { + const int expected = int.MaxValue; + const double value = expected; + Expander.Function.TryConvertToInt(value, out int actual).ShouldBeTrue(); + actual.ShouldBe(expected); + } + + [Fact] + public void TryConvertToIntGivenDoubleWithLessThanIntMinValue() + { + const double value = int.MinValue - 1.0; + Expander.Function.TryConvertToInt(value, out int actual).ShouldBeFalse(); + actual.ShouldBe(0); + } + + [Fact] + public void TryConvertToIntGivenDoubleWithGreaterThanIntMaxValue() + { + const double value = int.MaxValue + 1.0; + Expander.Function.TryConvertToInt(value, out int actual).ShouldBeFalse(); + actual.ShouldBe(0); + } + + [Fact] + public void TryConvertToIntGivenLongWithGreaterThanIntMaxValue() + { + const long value = int.MaxValue + 1L; + Expander.Function.TryConvertToInt(value, out int actual).ShouldBeFalse(); + actual.ShouldBe(0); + } + + /* Tests for TryConvertToLong */ + + [Fact] + public void TryConvertToLongGivenNull() + { + Expander.Function.TryConvertToLong(null, out long actual).ShouldBeFalse(); + actual.ShouldBe(0); + } + + [Fact] + public void TryConvertToLongGivenDouble() + { + const double value = 10.0; + Expander.Function.TryConvertToLong(value, out long actual).ShouldBeTrue(); + actual.ShouldBe(10); + } + + [Fact] + public void TryConvertToLongGivenLong() + { + const long value = 10; + Expander.Function.TryConvertToLong(value, out long actual).ShouldBeTrue(); + actual.ShouldBe(10); + } + + [Fact] + public void TryConvertToLongGivenInt() + { + const int value = 10; + Expander.Function.TryConvertToLong(value, out long actual).ShouldBeTrue(); + actual.ShouldBe(10); + } + + [Fact] + public void TryConvertToLongGivenString() + { + const string value = "10"; + Expander.Function.TryConvertToLong(value, out long actual).ShouldBeTrue(); + actual.ShouldBe(10); + } + + [Fact] + public void TryConvertToLongGivenDoubleWithLongMinValue() + { + const long expected = long.MinValue; + const double value = expected; + Expander.Function.TryConvertToLong(value, out long actual).ShouldBeTrue(); + actual.ShouldBe(expected); + } + + [Fact] + public void TryConvertToLongGivenDoubleWithLongMaxValueShouldNotThrow() + { + // An OverflowException should not be thrown from TryConvertToLong(). + // Convert.ToInt64(double) has a defect and will throw an OverflowException + // for values >= (long.MaxValue - 511) and <= long.MaxValue. + _ = Should.NotThrow(() => Expander.Function.TryConvertToLong((double)long.MaxValue, out _)); + } + + [Fact] + public void TryConvertToLongGivenDoubleWithLongMaxValue() + { + const long longMaxValue = long.MaxValue; + bool result = Expander.Function.TryConvertToLong((double)longMaxValue, out long actual); + if (RuntimeInformation.OSArchitecture != Architecture.Arm64) + { + // Because of loss of precision, long.MaxValue will not 'round trip' from long to double to long. + result.ShouldBeFalse(); + actual.ShouldBe(0); + } + else + { + // Testing on macOS 12 on Apple Silicon M1 Pro produces different result. + result.ShouldBeTrue(); + actual.ShouldBe(longMaxValue); + } + } + + [Fact] + public void TryConvertToLongGivenDoubleWithVeryLargeLongValue() + { + // Because of loss of precision, veryLargeLong will not 'round trip' but within TryConvertToLong + // the double to long conversion will pass the tolerance test. Return will be true and veryLargeLong != expected. + const long veryLargeLong = long.MaxValue - 512; + const double value = veryLargeLong; + const long expected = 9223372036854774784L; + Expander.Function.TryConvertToLong(value, out long actual).ShouldBeTrue(); + actual.ShouldBe(expected); + } + + [Fact] + public void TryConvertToLongGivenDoubleWithLessThanLongMinValue() + { + const double value = -92233720368547758081D; + Expander.Function.TryConvertToLong(value, out long actual).ShouldBeFalse(); + actual.ShouldBe(0); + } + + [Fact] + public void TryConvertToLongGivenDoubleWithGreaterThanLongMaxValue() + { + const double value = (double)long.MaxValue + long.MaxValue; + Expander.Function.TryConvertToLong(value, out long actual).ShouldBeFalse(); + actual.ShouldBe(0); + } + + /* Tests for TryConvertToDouble */ + + [Fact] + public void TryConvertToDoubleGivenNull() + { + Expander.Function.TryConvertToDouble(null, out double actual).ShouldBeFalse(); + actual.ShouldBe(0); + } + + [Fact] + public void TryConvertToDoubleGivenDouble() + { + const double value = 10.0; + Expander.Function.TryConvertToDouble(value, out double actual).ShouldBeTrue(); + actual.ShouldBe(10.0); + } + + [Fact] + public void TryConvertToDoubleGivenLong() + { + const long value = 10; + Expander.Function.TryConvertToDouble(value, out double actual).ShouldBeTrue(); + actual.ShouldBe(10.0); + } + + [Fact] + public void TryConvertToDoubleGivenInt() + { + const int value = 10; + Expander.Function.TryConvertToDouble(value, out double actual).ShouldBeTrue(); + actual.ShouldBe(10.0); + } + + [Fact] + public void TryConvertToDoubleGivenString() + { + const string value = "10"; + Expander.Function.TryConvertToDouble(value, out double actual).ShouldBeTrue(); + actual.ShouldBe(10.0); + } + + [Fact] + public void TryConvertToDoubleGivenStringAndLocale() + { + const string value = "1,2"; + + Thread currentThread = Thread.CurrentThread; + CultureInfo originalCulture = currentThread.CurrentCulture; + + try + { + // English South Africa locale uses ',' as decimal separator. + // The invariant culture should be used and "1,2" should be 12.0 not 1.2. + var cultureEnglishSouthAfrica = CultureInfo.CreateSpecificCulture("en-ZA"); + currentThread.CurrentCulture = cultureEnglishSouthAfrica; + Expander.Function.TryConvertToDouble(value, out double actual).ShouldBeTrue(); + actual.ShouldBe(12.0); + } + finally + { + // Restore CultureInfo. + currentThread.CurrentCulture = originalCulture; + CultureInfo.CurrentCulture = originalCulture; + CultureInfo.DefaultThreadCurrentCulture = originalCulture; + } + } + } +} diff --git a/src/Build.UnitTests/Evaluation/Expander_Tests.cs b/src/Build.UnitTests/Evaluation/Expander_Tests.cs index 11a505c2fd4..ac2409c2a04 100644 --- a/src/Build.UnitTests/Evaluation/Expander_Tests.cs +++ b/src/Build.UnitTests/Evaluation/Expander_Tests.cs @@ -37,6 +37,8 @@ public class Expander_Tests private string _dateToParse = new DateTime(2010, 12, 25).ToString(CultureInfo.CurrentCulture); private static readonly string s_rootPathPrefix = NativeMethodsShared.IsWindows ? "C:\\" : Path.VolumeSeparatorChar.ToString(); + private static bool IsIntrinsicFunctionOverloadsEnabled => ChangeWaves.AreFeaturesEnabled(ChangeWaves.Wave17_8); + [Fact] public void ExpandAllIntoTaskItems0() { @@ -3425,12 +3427,6 @@ public void PropertyFunctionStaticMethodIntrinsicMaths() result = expander.ExpandIntoStringLeaveEscaped(@"$([MSBuild]::Modulo(2345.5, 43))", ExpanderOptions.ExpandProperties, MockElementLocation.Instance); Assert.Equal((2345.5 % 43).ToString(), result); - - // test for overflow wrapping - result = expander.ExpandIntoStringLeaveEscaped(@"$([MSBuild]::Add(9223372036854775807, 20))", ExpanderOptions.ExpandProperties, MockElementLocation.Instance); - - double expectedResult = 9223372036854775807D + 20D; - Assert.Equal(expectedResult.ToString(), result); } /// @@ -3724,13 +3720,21 @@ public void Medley() new string[] {"$([MSBuild]::Add(1,2).CompareTo(3))", "0"}, new string[] {"$([MSBuild]::Add(1,2).CompareTo(3))", "0"}, new string[] {"$([MSBuild]::Add(1,2).CompareTo(3.0))", "0"}, + new string[] {"$([MSBuild]::Add(1,2.0).CompareTo(3.0))", "0"}, + new string[] {"$([System.Convert]::ToDouble($([MSBuild]::Add(1,2))).CompareTo(3.0))", "0"}, new string[] {"$([MSBuild]::Add(1,2).CompareTo('3'))", "0"}, new string[] {"$([MSBuild]::Add(1,2).CompareTo(3.1))", "-1"}, + new string[] {"$([MSBuild]::Add(1,2.0).CompareTo(3.1))", "-1"}, + new string[] {"$([System.Convert]::ToDouble($([MSBuild]::Add(1,2))).CompareTo(3.1))", "-1"}, new string[] {"$([MSBuild]::Add(1,2).CompareTo(2))", "1"}, new string[] {"$([MSBuild]::Add(1,2).Equals(3))", "True"}, new string[] {"$([MSBuild]::Add(1,2).Equals(3.0))", "True"}, + new string[] {"$([MSBuild]::Add(1,2.0).Equals(3.0))", "True"}, + new string[] {"$([System.Convert]::ToDouble($([MSBuild]::Add(1,2))).Equals(3.0))", "True"}, new string[] {"$([MSBuild]::Add(1,2).Equals('3'))", "True"}, new string[] {"$([MSBuild]::Add(1,2).Equals(3.1))", "False"}, + new string[] {"$([MSBuild]::Add(1,2.0).Equals(3.1))", "False"}, + new string[] {"$([System.Convert]::ToDouble($([MSBuild]::Add(1,2))).Equals(3.1))", "False"}, new string[] {"$(a.Insert(0,'%28'))", "%28no"}, new string[] {"$(a.Insert(0,'\"'))", "\"no"}, new string[] {"$(a.Insert(0,'(('))", "%28%28no"}, @@ -4207,11 +4211,34 @@ public void PropertyFunctionMathMin() } [Fact] - public void PropertyFunctionMSBuildAdd() + public void PropertyFunctionMSBuildAddIntegerLiteral() { TestPropertyFunction("$([MSBuild]::Add($(X), 5))", "X", "7", "12"); } + [Fact] + public void PropertyFunctionMSBuildAddRealLiteral() + { + TestPropertyFunction("$([MSBuild]::Add($(X), 0.5))", "X", "7", "7.5"); + } + + [Fact] + public void PropertyFunctionMSBuildAddIntegerOverflow() + { + // Overflow wrapping - result exceeds size of long + string expected = IsIntrinsicFunctionOverloadsEnabled ? "-9223372036854775808" : (long.MaxValue + 1.0).ToString(); + TestPropertyFunction("$([MSBuild]::Add($(X), 1))", "X", long.MaxValue.ToString(), expected); + } + + [Fact] + public void PropertyFunctionMSBuildAddRealArgument() + { + // string argument is an integer that exceeds the size of long. + double value = long.MaxValue + 1.0; + double expected = value + 1.0; + TestPropertyFunction("$([MSBuild]::Add($(X), 1))", "X", value.ToString(), expected.ToString()); + } + [Fact] public void PropertyFunctionMSBuildAddComplex() { @@ -4219,17 +4246,45 @@ public void PropertyFunctionMSBuildAddComplex() } [Fact] - public void PropertyFunctionMSBuildSubtract() + public void PropertyFunctionMSBuildSubtractIntegerLiteral() { TestPropertyFunction("$([MSBuild]::Subtract($(X), 20100000))", "X", "20100042", "42"); } [Fact] - public void PropertyFunctionMSBuildMultiply() + public void PropertyFunctionMSBuildSubtractRealLiteral() + { + TestPropertyFunction("$([MSBuild]::Subtract($(X), 20100000.0))", "X", "20100042", "42"); + } + + [Fact] + public void PropertyFunctionMSBuildSubtractIntegerMaxValue() + { + // If the double overload is used, there will be a rounding error. + string expected = IsIntrinsicFunctionOverloadsEnabled ? "1" : "0"; + TestPropertyFunction("$([MSBuild]::Subtract($(X), 9223372036854775806))", "X", long.MaxValue.ToString(), expected); + } + + [Fact] + public void PropertyFunctionMSBuildMultiplyIntegerLiteral() { TestPropertyFunction("$([MSBuild]::Multiply($(X), 8800))", "X", "2", "17600"); } + [Fact] + public void PropertyFunctionMSBuildMultiplyRealLiteral() + { + TestPropertyFunction("$([MSBuild]::Multiply($(X), 1.5))", "X", "2", "3"); + } + + [Fact] + public void PropertyFunctionMSBuildMultiplyIntegerOverflow() + { + // Overflow - result exceeds size of long + string expected = IsIntrinsicFunctionOverloadsEnabled ? "-2" : (long.MaxValue * 2.0).ToString(); + TestPropertyFunction("$([MSBuild]::Multiply($(X), 2))", "X", long.MaxValue.ToString(), expected); + } + [Fact] public void PropertyFunctionMSBuildMultiplyComplex() { @@ -4237,9 +4292,28 @@ public void PropertyFunctionMSBuildMultiplyComplex() } [Fact] - public void PropertyFunctionMSBuildDivide() + public void PropertyFunctionMSBuildDivideIntegerLiteral() + { + string expected = IsIntrinsicFunctionOverloadsEnabled ? "6" : "6.5536"; + TestPropertyFunction("$([MSBuild]::Divide($(X), 10000))", "X", "65536", expected); + } + + [Fact] + public void PropertyFunctionMSBuildDivideRealLiteral() + { + TestPropertyFunction("$([MSBuild]::Divide($(X), 10000.0))", "X", "65536", "6.5536"); + } + + [Fact] + public void PropertyFunctionMSBuildModuloIntegerLiteral() + { + TestPropertyFunction("$([MSBuild]::Modulo($(X), 3))", "X", "10", "1"); + } + + [Fact] + public void PropertyFunctionMSBuildModuloRealLiteral() { - TestPropertyFunction("$([MSBuild]::Divide($(X), 10000))", "X", "65536", (6.5536).ToString()); + TestPropertyFunction("$([MSBuild]::Modulo($(X), 3.0))", "X", "10", "1"); } [Fact] diff --git a/src/Build.UnitTests/Evaluation/IntrinsicFunctionOverload_Tests.cs b/src/Build.UnitTests/Evaluation/IntrinsicFunctionOverload_Tests.cs new file mode 100644 index 00000000000..aa43ed04e22 --- /dev/null +++ b/src/Build.UnitTests/Evaluation/IntrinsicFunctionOverload_Tests.cs @@ -0,0 +1,478 @@ +// 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.IO; +using System.Xml; + +using Microsoft.Build.Evaluation; +using Microsoft.Build.Framework; +using Microsoft.Build.Shared; +using Microsoft.Build.UnitTests; + +using Shouldly; + +using Xunit; + +namespace Microsoft.Build.Engine.UnitTests.Evaluation +{ + public class IntrinsicFunctionOverload_Tests + { + private Version ChangeWaveForOverloading = ChangeWaves.Wave17_8; + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void MSBuildAddInteger(bool isIntrinsicFunctionOverloadsEnabled) + { + const string projectContent = @" + + + $([MSBuild]::Add($([System.Int64]::MaxValue), 1)) + + "; + + string expected = isIntrinsicFunctionOverloadsEnabled ? unchecked(long.MaxValue + 1).ToString() : (long.MaxValue + 1.0).ToString(); + + using TestEnvironment env = TestEnvironment.Create(); + + ChangeWaves.ResetStateForTests(); + if (!isIntrinsicFunctionOverloadsEnabled) + { + env.SetEnvironmentVariable("MSBUILDDISABLEFEATURESFROMVERSION", ChangeWaveForOverloading.ToString()); + BuildEnvironmentHelper.ResetInstance_ForUnitTestsOnly(); + } + + var project = new Project(XmlReader.Create(new StringReader(projectContent.Cleanup()))); + ProjectProperty? actualProperty = project.GetProperty("Actual"); + actualProperty.EvaluatedValue.ShouldBe(expected); + } + + [Fact] + public void MSBuildAddIntegerGreaterThanMax() + { + const string projectContent = @" + + + $([MSBuild]::Add(9223372036854775808, 1)) + + "; + + string expected = ((long.MaxValue +1D) + 1).ToString(); + + using TestEnvironment env = TestEnvironment.Create(); + + ChangeWaves.ResetStateForTests(); + + var project = new Project(XmlReader.Create(new StringReader(projectContent.Cleanup()))); + ProjectProperty? actualProperty = project.GetProperty("Actual"); + actualProperty.EvaluatedValue.ShouldBe(expected); + } + + [Fact] + public void MSBuildAddIntegerLessThanMin() + { + const string projectContent = @" + + + $([MSBuild]::Add(-9223372036854775809, 1)) + + "; + + string expected = ((long.MinValue - 1D) + 1).ToString(); + + using TestEnvironment env = TestEnvironment.Create(); + + ChangeWaves.ResetStateForTests(); + + var project = new Project(XmlReader.Create(new StringReader(projectContent.Cleanup()))); + ProjectProperty? actualProperty = project.GetProperty("Actual"); + actualProperty.EvaluatedValue.ShouldBe(expected); + } + + [Fact] + public void MSBuildAddReal() + { + const string projectContent = @" + + + $([MSBuild]::Add(1.0, 2.0)) + + "; + + string expected = 3.0.ToString(); + + using TestEnvironment env = TestEnvironment.Create(); + + ChangeWaves.ResetStateForTests(); + + var project = new Project(XmlReader.Create(new StringReader(projectContent.Cleanup()))); + ProjectProperty? actualProperty = project.GetProperty("Actual"); + actualProperty.EvaluatedValue.ShouldBe(expected); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void MSBuildSubtractInteger(bool isIntrinsicFunctionOverloadsEnabled) + { + const string projectContent = @" + + + $([MSBuild]::Subtract($([System.Int64]::MaxValue), 9223372036854775806)) + + "; + + string expected = isIntrinsicFunctionOverloadsEnabled ? 1.ToString() : 0.ToString(); + + using TestEnvironment env = TestEnvironment.Create(); + + ChangeWaves.ResetStateForTests(); + if (!isIntrinsicFunctionOverloadsEnabled) + { + env.SetEnvironmentVariable("MSBUILDDISABLEFEATURESFROMVERSION", ChangeWaveForOverloading.ToString()); + BuildEnvironmentHelper.ResetInstance_ForUnitTestsOnly(); + } + + var project = new Project(XmlReader.Create(new StringReader(projectContent.Cleanup()))); + ProjectProperty? actualProperty = project.GetProperty("Actual"); + actualProperty.EvaluatedValue.ShouldBe(expected); + } + + [Fact] + public void MSBuildSubtractIntegerGreaterThanMax() + { + const string projectContent = @" + + + $([MSBuild]::Subtract(9223372036854775808, 1)) + + "; + + string expected = ((long.MaxValue + 1D) - 1).ToString(); + + using TestEnvironment env = TestEnvironment.Create(); + + ChangeWaves.ResetStateForTests(); + + var project = new Project(XmlReader.Create(new StringReader(projectContent.Cleanup()))); + ProjectProperty? actualProperty = project.GetProperty("Actual"); + actualProperty.EvaluatedValue.ShouldBe(expected); + } + + [Fact] + public void MSBuildSubtractIntegerLessThanMin() + { + const string projectContent = @" + + + $([MSBuild]::Subtract(-9223372036854775809, 1)) + + "; + + string expected = ((long.MinValue - 1D) - 1).ToString(); + + using TestEnvironment env = TestEnvironment.Create(); + + ChangeWaves.ResetStateForTests(); + + var project = new Project(XmlReader.Create(new StringReader(projectContent.Cleanup()))); + ProjectProperty? actualProperty = project.GetProperty("Actual"); + actualProperty.EvaluatedValue.ShouldBe(expected); + } + + [Fact] + public void MSBuildSubtractReal() + { + const string projectContent = @" + + + $([MSBuild]::Subtract(2.0, 1.0)) + + "; + + string expected = 1.0.ToString(); + + using TestEnvironment env = TestEnvironment.Create(); + + ChangeWaves.ResetStateForTests(); + + var project = new Project(XmlReader.Create(new StringReader(projectContent.Cleanup()))); + ProjectProperty? actualProperty = project.GetProperty("Actual"); + actualProperty.EvaluatedValue.ShouldBe(expected); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void MSBuildMultiplyInteger(bool isIntrinsicFunctionOverloadsEnabled) + { + const string projectContent = @" + + + $([MSBuild]::Multiply($([System.Int64]::MaxValue), 2)) + + "; + + string expected = isIntrinsicFunctionOverloadsEnabled ? unchecked(long.MaxValue * 2).ToString() : (long.MaxValue * 2.0).ToString(); + + using TestEnvironment env = TestEnvironment.Create(); + + ChangeWaves.ResetStateForTests(); + if (!isIntrinsicFunctionOverloadsEnabled) + { + env.SetEnvironmentVariable("MSBUILDDISABLEFEATURESFROMVERSION", ChangeWaveForOverloading.ToString()); + BuildEnvironmentHelper.ResetInstance_ForUnitTestsOnly(); + } + + var project = new Project(XmlReader.Create(new StringReader(projectContent.Cleanup()))); + ProjectProperty? actualProperty = project.GetProperty("Actual"); + actualProperty.EvaluatedValue.ShouldBe(expected); + } + + [Fact] + public void MSBuildMultiplyIntegerGreaterThanMax() + { + const string projectContent = @" + + + $([MSBuild]::Multiply(9223372036854775808, 1)) + + "; + + string expected = ((long.MaxValue + 1D) * 1).ToString(); + + using TestEnvironment env = TestEnvironment.Create(); + + ChangeWaves.ResetStateForTests(); + + var project = new Project(XmlReader.Create(new StringReader(projectContent.Cleanup()))); + ProjectProperty? actualProperty = project.GetProperty("Actual"); + actualProperty.EvaluatedValue.ShouldBe(expected); + } + + [Fact] + public void MSBuildMultiplyIntegerLessThanMin() + { + const string projectContent = @" + + + $([MSBuild]::Multiply(-9223372036854775809, 1)) + + "; + + string expected = ((long.MinValue - 1D) * 1).ToString(); + + using TestEnvironment env = TestEnvironment.Create(); + + ChangeWaves.ResetStateForTests(); + + var project = new Project(XmlReader.Create(new StringReader(projectContent.Cleanup()))); + ProjectProperty? actualProperty = project.GetProperty("Actual"); + actualProperty.EvaluatedValue.ShouldBe(expected); + } + + [Fact] + public void MSBuildMultiplyReal() + { + const string projectContent = @" + + + $([MSBuild]::Multiply(2.0, 1.0)) + + "; + + string expected = 2.0.ToString(); + + using TestEnvironment env = TestEnvironment.Create(); + + ChangeWaves.ResetStateForTests(); + + var project = new Project(XmlReader.Create(new StringReader(projectContent.Cleanup()))); + ProjectProperty? actualProperty = project.GetProperty("Actual"); + actualProperty.EvaluatedValue.ShouldBe(expected); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void MSBuildDivideInteger(bool isIntrinsicFunctionOverloadsEnabled) + { + const string projectContent = @" + + + $([MSBuild]::Divide(10, 3)) + + "; + + string expected = isIntrinsicFunctionOverloadsEnabled ? (10 / 3).ToString() : (10.0 / 3.0).ToString(); + + using TestEnvironment env = TestEnvironment.Create(); + + ChangeWaves.ResetStateForTests(); + if (!isIntrinsicFunctionOverloadsEnabled) + { + env.SetEnvironmentVariable("MSBUILDDISABLEFEATURESFROMVERSION", ChangeWaveForOverloading.ToString()); + BuildEnvironmentHelper.ResetInstance_ForUnitTestsOnly(); + } + + var project = new Project(XmlReader.Create(new StringReader(projectContent.Cleanup()))); + ProjectProperty? actualProperty = project.GetProperty("Actual"); + actualProperty.EvaluatedValue.ShouldBe(expected); + } + + [Fact] + public void MSBuildDivideIntegerGreaterThanMax() + { + const string projectContent = @" + + + $([MSBuild]::Divide(9223372036854775808, 1)) + + "; + + string expected = ((long.MaxValue + 1D) / 1).ToString(); + + using TestEnvironment env = TestEnvironment.Create(); + + ChangeWaves.ResetStateForTests(); + + var project = new Project(XmlReader.Create(new StringReader(projectContent.Cleanup()))); + ProjectProperty? actualProperty = project.GetProperty("Actual"); + actualProperty.EvaluatedValue.ShouldBe(expected); + } + + [Fact] + public void MSBuildDivideIntegerLessThanMin() + { + const string projectContent = @" + + + $([MSBuild]::Divide(-9223372036854775809, 1)) + + "; + + string expected = ((long.MinValue - 1D) / 1).ToString(); + + using TestEnvironment env = TestEnvironment.Create(); + + ChangeWaves.ResetStateForTests(); + + var project = new Project(XmlReader.Create(new StringReader(projectContent.Cleanup()))); + ProjectProperty? actualProperty = project.GetProperty("Actual"); + actualProperty.EvaluatedValue.ShouldBe(expected); + } + + [Fact] + public void MSBuildDivideReal() + { + const string projectContent = @" + + + $([MSBuild]::Divide(1, 0.5)) + + "; + + string expected = 2.0.ToString(); + + using TestEnvironment env = TestEnvironment.Create(); + + ChangeWaves.ResetStateForTests(); + + var project = new Project(XmlReader.Create(new StringReader(projectContent.Cleanup()))); + ProjectProperty? actualProperty = project.GetProperty("Actual"); + actualProperty.EvaluatedValue.ShouldBe(expected); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void MSBuildModuloInteger(bool isIntrinsicFunctionOverloadsEnabled) + { + const string projectContent = @" + + + $([MSBuild]::Modulo(10, 3)) + + "; + + string expected = 1.ToString(); + + using TestEnvironment env = TestEnvironment.Create(); + + ChangeWaves.ResetStateForTests(); + if (!isIntrinsicFunctionOverloadsEnabled) + { + env.SetEnvironmentVariable("MSBUILDDISABLEFEATURESFROMVERSION", ChangeWaveForOverloading.ToString()); + BuildEnvironmentHelper.ResetInstance_ForUnitTestsOnly(); + } + + var project = new Project(XmlReader.Create(new StringReader(projectContent.Cleanup()))); + ProjectProperty? actualProperty = project.GetProperty("Actual"); + actualProperty.EvaluatedValue.ShouldBe(expected); + } + + [Fact] + public void MSBuildModuloIntegerGreaterThanMax() + { + const string projectContent = @" + + + $([MSBuild]::Modulo(9223372036854775808, 1)) + + "; + + string expected = ((long.MaxValue + 1D) % 1).ToString(); + + using TestEnvironment env = TestEnvironment.Create(); + + ChangeWaves.ResetStateForTests(); + + var project = new Project(XmlReader.Create(new StringReader(projectContent.Cleanup()))); + ProjectProperty? actualProperty = project.GetProperty("Actual"); + actualProperty.EvaluatedValue.ShouldBe(expected); + } + + [Fact] + public void MSBuildModuloIntegerLessThanMin() + { + const string projectContent = @" + + + $([MSBuild]::Modulo(-9223372036854775809, 1)) + + "; + + string expected = ((long.MinValue - 1D) % 1).ToString(); + + using TestEnvironment env = TestEnvironment.Create(); + + ChangeWaves.ResetStateForTests(); + + var project = new Project(XmlReader.Create(new StringReader(projectContent.Cleanup()))); + ProjectProperty? actualProperty = project.GetProperty("Actual"); + actualProperty.EvaluatedValue.ShouldBe(expected); + } + + [Fact] + public void MSBuildModuloReal() + { + const string projectContent = @" + + + $([MSBuild]::Modulo(11.0, 2.5)) + + "; + + string expected = 1.ToString(); + + using TestEnvironment env = TestEnvironment.Create(); + + ChangeWaves.ResetStateForTests(); + + var project = new Project(XmlReader.Create(new StringReader(projectContent.Cleanup()))); + ProjectProperty? actualProperty = project.GetProperty("Actual"); + actualProperty.EvaluatedValue.ShouldBe(expected); + } + } +} diff --git a/src/Build/BackEnd/Components/Scheduler/Scheduler.cs b/src/Build/BackEnd/Components/Scheduler/Scheduler.cs index bedccfe03cd..53bf46ec2f3 100644 --- a/src/Build/BackEnd/Components/Scheduler/Scheduler.cs +++ b/src/Build/BackEnd/Components/Scheduler/Scheduler.cs @@ -886,7 +886,7 @@ private bool GetSchedulingPlanAndAlgorithm() string multiplier = Environment.GetEnvironmentVariable("MSBUILDCUSTOMSCHEDULERFORSQLCONFIGURATIONLIMITMULTIPLIER"); double convertedMultiplier = 0; - if (!Double.TryParse(multiplier, out convertedMultiplier) || convertedMultiplier < 1) + if (!Double.TryParse(multiplier, NumberStyles.AllowDecimalPoint | NumberStyles.AllowLeadingSign, CultureInfo.InvariantCulture.NumberFormat, out convertedMultiplier) || convertedMultiplier < 1) { _customSchedulerForSQLConfigurationLimitMultiplier = DefaultCustomSchedulerForSQLConfigurationLimitMultiplier; } diff --git a/src/Build/Evaluation/Expander.cs b/src/Build/Evaluation/Expander.cs index 6a0a4ffa731..075090e0f9a 100644 --- a/src/Build/Evaluation/Expander.cs +++ b/src/Build/Evaluation/Expander.cs @@ -78,7 +78,7 @@ internal enum ExpanderOptions /// When an error occurs expanding a property, just leave it unexpanded. /// /// - /// This should only be used in cases where property evaluation isn't critcal, such as when attempting to log a + /// This should only be used in cases where property evaluation isn't critical, such as when attempting to log a /// message with a best effort expansion of a string, or when discovering partial information during lazy evaluation. /// LeavePropertiesUnexpandedOnError = 0x20, @@ -285,7 +285,7 @@ private void FlushFirstValueIfNeeded() /// /// The CultureInfo from the invariant culture. Used to avoid allocations for - /// perfoming IndexOf etc. + /// performing IndexOf etc. /// private static CompareInfo s_invariantCompareInfo = CultureInfo.InvariantCulture.CompareInfo; @@ -1338,7 +1338,7 @@ internal static object ExpandPropertyBody( if (function != null) { // We will have either extracted the actual property name - // or realised that there is none (static function), and have recorded a null + // or realized that there is none (static function), and have recorded a null propertyName = function.Receiver; } else @@ -3181,7 +3181,7 @@ internal readonly Function Build() /// It is also responsible for executing the function. /// /// Type of the properties used to expand the expression. - private class Function + internal class Function where T : class, IProperty { /// @@ -3482,6 +3482,13 @@ internal object Execute(object objectInstance, IPropertyProvider properties, // that it matches the left hand side ready for the default binder’s method invoke. if (objectInstance != null && args.Length == 1 && (String.Equals("Equals", _methodMethodName, StringComparison.OrdinalIgnoreCase) || String.Equals("CompareTo", _methodMethodName, StringComparison.OrdinalIgnoreCase))) { + // Support comparison when the lhs is an integer + if (IsFloatingPointRepresentation(args[0]) && !IsFloatingPointRepresentation(objectInstance)) + { + objectInstance = Convert.ChangeType(objectInstance, typeof(double), CultureInfo.InvariantCulture); + _receiverType = objectInstance.GetType(); + } + // change the type of the final unescaped string into the destination args[0] = Convert.ChangeType(args[0], objectInstance.GetType(), CultureInfo.InvariantCulture); } @@ -3489,14 +3496,11 @@ internal object Execute(object objectInstance, IPropertyProvider properties, if (_receiverType == typeof(IntrinsicFunctions)) { // Special case a few methods that take extra parameters that can't be passed in by the user - // - if (_methodMethodName.Equals("GetPathOfFileAbove") && args.Length == 1) { // Append the IElementLocation as a parameter to GetPathOfFileAbove if the user only // specified the file name. This is syntactic sugar so they don't have to always // include $(MSBuildThisFileDirectory) as a parameter. - // string startingDirectory = String.IsNullOrWhiteSpace(elementLocation.File) ? String.Empty : Path.GetDirectoryName(elementLocation.File); args = new[] @@ -3549,7 +3553,7 @@ internal object Execute(object objectInstance, IPropertyProvider properties, functionResult = _receiverType.InvokeMember(_methodMethodName, _bindingFlags, Type.DefaultBinder, objectInstance, args, CultureInfo.InvariantCulture); } // If we're invoking a method, then there are deeper attempts that can be made to invoke the method. - // If not, we were asked to get a property or field but found that we cannot locate it. No further argument coersion is possible, so throw. + // If not, we were asked to get a property or field but found that we cannot locate it. No further argument coercion is possible, so throw. catch (MissingMethodException ex) when ((_bindingFlags & BindingFlags.InvokeMethod) == BindingFlags.InvokeMethod) { // The standard binder failed, so do our best to coerce types into the arguments for the function @@ -3953,41 +3957,36 @@ private bool TryExecuteWellKnownFunction(out object returnVal, object objectInst } else if (string.Equals(_methodMethodName, nameof(IntrinsicFunctions.Add), StringComparison.OrdinalIgnoreCase)) { - if (TryGetArgs(args, out double arg0, out double arg1)) + if (TryExecuteArithmeticOverload(args, IntrinsicFunctions.Add, IntrinsicFunctions.Add, out returnVal)) { - returnVal = IntrinsicFunctions.Add(arg0, arg1); return true; } } else if (string.Equals(_methodMethodName, nameof(IntrinsicFunctions.Subtract), StringComparison.OrdinalIgnoreCase)) { - if (TryGetArgs(args, out double arg0, out double arg1)) + if (TryExecuteArithmeticOverload(args, IntrinsicFunctions.Subtract, IntrinsicFunctions.Subtract, out returnVal)) { - returnVal = IntrinsicFunctions.Subtract(arg0, arg1); return true; } } else if (string.Equals(_methodMethodName, nameof(IntrinsicFunctions.Multiply), StringComparison.OrdinalIgnoreCase)) { - if (TryGetArgs(args, out double arg0, out double arg1)) + if (TryExecuteArithmeticOverload(args, IntrinsicFunctions.Multiply, IntrinsicFunctions.Multiply, out returnVal)) { - returnVal = IntrinsicFunctions.Multiply(arg0, arg1); return true; } } else if (string.Equals(_methodMethodName, nameof(IntrinsicFunctions.Divide), StringComparison.OrdinalIgnoreCase)) { - if (TryGetArgs(args, out double arg0, out double arg1)) + if (TryExecuteArithmeticOverload(args, IntrinsicFunctions.Divide, IntrinsicFunctions.Divide, out returnVal)) { - returnVal = IntrinsicFunctions.Divide(arg0, arg1); return true; } } else if (string.Equals(_methodMethodName, nameof(IntrinsicFunctions.Modulo), StringComparison.OrdinalIgnoreCase)) { - if (TryGetArgs(args, out double arg0, out double arg1)) + if (TryExecuteArithmeticOverload(args, IntrinsicFunctions.Modulo, IntrinsicFunctions.Modulo, out returnVal)) { - returnVal = IntrinsicFunctions.Modulo(arg0, arg1); return true; } } @@ -4535,40 +4534,99 @@ private static bool TryConvertToVersion(object value, out Version arg0) return true; } - private static bool TryConvertToInt(object value, out int arg0) + /// + /// Try to convert value to int. + /// + internal static bool TryConvertToInt(object value, out int arg) { switch (value) { case double d: - arg0 = Convert.ToInt32(d); - return arg0 == d; + if (d >= int.MinValue && d <= int.MaxValue) + { + arg = Convert.ToInt32(d); + if (Math.Abs(arg - d) == 0) + { + return true; + } + } + + break; + case long l: + if (l >= int.MinValue && l <= int.MaxValue) + { + arg = Convert.ToInt32(l); + return true; + } + + break; case int i: - arg0 = i; + arg = i; return true; - case string s when int.TryParse(s, out arg0): + case string s when int.TryParse(s, NumberStyles.Integer, CultureInfo.InvariantCulture.NumberFormat, out arg): return true; } - arg0 = 0; + arg = 0; return false; } - private static bool TryConvertToDouble(object value, out double arg) + /// + /// Try to convert value to long. + /// + internal static bool TryConvertToLong(object value, out long arg) { - if (value is double unboxed) - { - arg = unboxed; - return true; - } - else if (value is string str && double.TryParse(str, out arg)) + switch (value) { - return true; + case double d: + if (d >= long.MinValue && d <= long.MaxValue) + { + arg = (long)d; + if (Math.Abs(arg - d) == 0) + { + return true; + } + } + + break; + case long l: + arg = l; + return true; + case int i: + arg = i; + return true; + case string s when long.TryParse(s, NumberStyles.Integer, CultureInfo.InvariantCulture.NumberFormat, out arg): + return true; } arg = 0; return false; } + /// + /// Try to convert value to double. + /// + internal static bool TryConvertToDouble(object value, out double arg) + { + switch (value) + { + case double unboxed: + arg = unboxed; + return true; + case long l: + arg = l; + return true; + case int i: + arg = i; + return true; + case string str when double.TryParse(str, NumberStyles.Number | NumberStyles.Float, CultureInfo.InvariantCulture.NumberFormat, out arg): + return true; + default: + arg = 0; + return false; + } + } + private static bool TryGetArg(object[] args, out string arg0) { if (args.Length != 1) @@ -4696,6 +4754,38 @@ private static bool TryGetArgs(object[] args, out string arg0, out int arg1) return false; } + private static bool IsFloatingPointRepresentation(object value) + { + return value is double || (value is string str && double.TryParse(str, NumberStyles.Number | NumberStyles.Float, CultureInfo.InvariantCulture.NumberFormat, out double _)); + } + + private static bool TryExecuteArithmeticOverload(object[] args, Func integerOperation, Func realOperation, out object resultValue) + { + resultValue = null; + + if (args.Length != 2) + { + return false; + } + + if (IntrinsicFunctionOverload.IsIntrinsicFunctionOverloadsEnabled()) + { + if (TryConvertToLong(args[0], out long argLong0) && TryConvertToLong(args[1], out long argLong1)) + { + resultValue = integerOperation(argLong0, argLong1); + return true; + } + } + + if (TryConvertToDouble(args[0], out double argDouble0) && TryConvertToDouble(args[1], out double argDouble1)) + { + resultValue = realOperation(argDouble0, argDouble1); + return true; + } + + return false; + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] private void LogFunctionCall(string fileName, object objectInstance, object[] args) { @@ -5208,7 +5298,6 @@ private static bool IsInstanceMethodAvailable(string methodName) /// private object LateBindExecute(Exception ex, BindingFlags bindingFlags, object objectInstance /* null unless instance method */, object[] args, bool isConstructor) { - // First let's try for a method where all arguments are strings.. Type[] types = new Type[_arguments.Length]; for (int n = 0; n < _arguments.Length; n++) @@ -5230,15 +5319,25 @@ private object LateBindExecute(Exception ex, BindingFlags bindingFlags, object o // search for a method with the right number of arguments if (memberInfo == null) { - MethodBase[] members; // Gather all methods that may match + IEnumerable members; if (isConstructor) { members = _receiverType.GetConstructors(bindingFlags); } + else if (_receiverType == typeof(IntrinsicFunctions) && IntrinsicFunctionOverload.IsKnownOverloadMethodName(_methodMethodName)) + { + MemberInfo[] foundMembers = _receiverType.FindMembers( + MemberTypes.Method, + bindingFlags, + (info, criteria) => string.Equals(info.Name, (string)criteria, StringComparison.OrdinalIgnoreCase), + _methodMethodName); + Array.Sort(foundMembers, IntrinsicFunctionOverload.IntrinsicFunctionOverloadMethodComparer); + members = foundMembers.Cast(); + } else { - members = _receiverType.GetMethods(bindingFlags); + members = _receiverType.GetMethods(bindingFlags).Where(m => string.Equals(m.Name, _methodMethodName, StringComparison.OrdinalIgnoreCase)); } foreach (MethodBase member in members) @@ -5248,22 +5347,19 @@ private object LateBindExecute(Exception ex, BindingFlags bindingFlags, object o // Simple match on name and number of params, we will be case insensitive if (parameters.Length == _arguments.Length) { - if (isConstructor || String.Equals(member.Name, _methodMethodName, StringComparison.OrdinalIgnoreCase)) + // Try to find a method with the right name, number of arguments and + // compatible argument types + // we have a match on the name and argument number + // now let's try to coerce the arguments we have + // into the arguments on the matching method + object[] coercedArguments = CoerceArguments(args, parameters); + + if (coercedArguments != null) { - // Try to find a method with the right name, number of arguments and - // compatible argument types - // we have a match on the name and argument number - // now let's try to coerce the arguments we have - // into the arguments on the matching method - object[] coercedArguments = CoerceArguments(args, parameters); - - if (coercedArguments != null) - { - // We have a complete match - memberInfo = member; - args = coercedArguments; - break; - } + // We have a complete match + memberInfo = member; + args = coercedArguments; + break; } } } @@ -5357,4 +5453,42 @@ internal string? CurrentlyEvaluatingPropertyElementName set; } } + + internal static class IntrinsicFunctionOverload + { + private static readonly string[] s_knownOverloadName = { "Add", "Subtract", "Multiply", "Divide", "Modulo", }; + + // Order by the TypeCode of the first parameter. + // When change wave is enabled, order long before double. + // Otherwise preserve prior behavior of double before long. + // For reuse, the comparer is cached in a non-generic type. + // Both comparer instances can be cached to support change wave testing. + private static IComparer? s_comparerLongBeforeDouble; + private static IComparer? s_comparerDoubleBeforeLong; + + internal static IComparer IntrinsicFunctionOverloadMethodComparer => IsIntrinsicFunctionOverloadsEnabled() ? LongBeforeDoubleComparer : DoubleBeforeLongComparer; + + private static IComparer LongBeforeDoubleComparer => s_comparerLongBeforeDouble ??= Comparer.Create((key0, key1) => SelectTypeOfFirstParameter(key0).CompareTo(SelectTypeOfFirstParameter(key1))); + + private static IComparer DoubleBeforeLongComparer => s_comparerDoubleBeforeLong ??= Comparer.Create((key0, key1) => SelectTypeOfFirstParameter(key1).CompareTo(SelectTypeOfFirstParameter(key0))); + + // The arithmetic overload feature uses this method to test for the change wave. + internal static bool IsIntrinsicFunctionOverloadsEnabled() => ChangeWaves.AreFeaturesEnabled(ChangeWaves.Wave17_8); + + internal static bool IsKnownOverloadMethodName(string methodName) => s_knownOverloadName.Any(name => string.Equals(name, methodName, StringComparison.OrdinalIgnoreCase)); + + private static TypeCode SelectTypeOfFirstParameter(MemberInfo member) + { + MethodBase? method = member as MethodBase; + if (method == null) + { + return TypeCode.Empty; + } + + ParameterInfo[] parameters = method.GetParameters(); + return parameters.Length > 0 + ? Type.GetTypeCode(parameters[0].ParameterType) + : TypeCode.Empty; + } + } } diff --git a/src/Shared/Tracing.cs b/src/Shared/Tracing.cs index 71b52e4d565..d26f7127305 100644 --- a/src/Shared/Tracing.cs +++ b/src/Shared/Tracing.cs @@ -6,6 +6,7 @@ using System.Diagnostics; using System.Diagnostics.CodeAnalysis; #if DEBUG +using System.Globalization; using System.Reflection; #endif @@ -58,7 +59,7 @@ static Tracing() string val = Environment.GetEnvironmentVariable("MSBUILDTRACEINTERVAL"); double seconds; - if (!String.IsNullOrEmpty(val) && System.Double.TryParse(val, out seconds)) + if (!String.IsNullOrEmpty(val) && System.Double.TryParse(val, NumberStyles.AllowDecimalPoint | NumberStyles.AllowLeadingSign, CultureInfo.InvariantCulture.NumberFormat, out seconds)) { s_interval = TimeSpan.FromSeconds(seconds); }