// Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 using Microsoft.Build.Framework; using NSubstitute; using Xunit; namespace OpenTelemetry.AutoInstrumentation.BuildTasks.Tests; public class CheckForInstrumentationPackagesTests { public static TheoryData LogicallyEmptyITaskItemArray() { var theoryData = new TheoryData { null, Array.Empty() }; return theoryData; } [Theory] [MemberData(nameof(LogicallyEmptyITaskItemArray))] public void NoDataInRuntimeLocalCopyItemsTest(ITaskItem[]? runtimeCopyLocalItems) { var sut = new TaskWithMockBuildEngine(); sut.Task.RuntimeCopyLocalItems = runtimeCopyLocalItems; Assert.True(sut.Task.Execute()); var messageEventArgsList = sut.MessageEventArgsList; var buildMessageEventArgs = Assert.Single(messageEventArgsList); Assert.Equal("OpenTelemetry.AutoInstrumentation: empty RuntimeCopyLocalItems, skipping check for instrumentation packages.", buildMessageEventArgs.Message); } [Fact] public void EmptyInstrumentationTargetItemsTest() { var sut = new TaskWithMockBuildEngine(); sut.Task.InstrumentationTargetItems = Array.Empty(); sut.Task.RuntimeCopyLocalItems = BuildMockRuntimeCopyLocalItems(new (string, string)[] { ("Test.Package.A", "1.2.0") }); Assert.True(sut.Task.Execute()); Assert.Empty(sut.MessageEventArgsList); } [Fact] public void NoPackageToBeInstrumentedTest() { var sut = new TaskWithMockBuildEngine(); sut.Task.RuntimeCopyLocalItems = BuildMockRuntimeCopyLocalItems(new (string, string)[] { ("Test.Package.A", "1.2.0"), ("Test.Package.A.Instrumentation", "1.1.0"), ("Test.Package.B", "2.2.0"), ("Test.Package.D", "3.3.0") }); sut.Task.InstrumentationTargetItems = BuildMockInstrumentationTargetItems(new (string, string, string, string)[] { // Matches A package Id but already has instrumentation package. ("Test.Package.A", "[1.1.0, 2.0.0)", "Test.Package.A.Instrumentation", "1.1.0"), // Matches B package Id but not range. ("Test.Package.B", "[1.0.0, 2.0.0)", "Test.Package.B.Instrumentation", "1.0.0"), // Doesn't match any package Id. ("Test.Package.C", "[1.0.0, 2.0.0)", "Test.Package.C.Instrumentation", "1.0.0") }); Assert.True(sut.Task.Execute()); var expectedMessages = new string[] { "OpenTelemetry.AutoInstrumentation: project already references 'Test.Package.A.Instrumentation' version 1.1.0 " + "required to instrument 'Test.Package.A' version [1.1.0, 2.0.0).", "OpenTelemetry.AutoInstrumentation: project references 'Test.Package.B' but not in the range [1.0.0, 2.0.0), " + "no need for the instrumentation package 'Test.Package.B.Instrumentation' version 1.0.0.", "OpenTelemetry.AutoInstrumentation: project doesn't reference 'Test.Package.C', " + "no need for the instrumentation package 'Test.Package.C.Instrumentation' version 1.0.0." }; var messageEventArgsList = sut.MessageEventArgsList; Assert.Equal(expectedMessages.Length, messageEventArgsList.Count); Assert.All(messageEventArgsList, (eventArgs, i) => Assert.Equal(expectedMessages[i], eventArgs.Message)); } [Fact] public void MissingInstrumentationPackageTest() { var sut = new TaskWithMockBuildEngine(); sut.Task.RuntimeCopyLocalItems = BuildMockRuntimeCopyLocalItems(new (string, string)[] { ("Test.Package.A", "1.2.0"), ("Test.Package.D", "3.3.0") }); sut.Task.InstrumentationTargetItems = BuildMockInstrumentationTargetItems(new (string, string, string, string)[] { // Matches A package Id and range, but is not in the project references. ("Test.Package.A", "[1.1.0, 2.0.0)", "Test.Package.A.Instrumentation", "1.1.0") }); Assert.False(sut.Task.Execute()); Assert.Empty(sut.MessageEventArgsList); var errorEventArgs = Assert.Single(sut.ErrorEventArgsList); var expectedMessage = "OpenTelemetry.AutoInstrumentation: add a reference to the instrumentation package 'Test.Package.A.Instrumentation' " + "version 1.1.0 or add 'Test.Package.A' to the property 'SkippedInstrumentations' to suppress this error."; Assert.Equal(expectedMessage, errorEventArgs.Message); } private static ITaskItem[] BuildMockRuntimeCopyLocalItems((string NuGetPackageId, string NuGetPackageVersion)[] source) { var taskItems = new ITaskItem[source.Length]; for (var i = 0; i < source.Length; i++) { var (nuGetPackageId, nuGetPackageVersion) = source[i]; var mockTaskItem = Substitute.For(); mockTaskItem.GetMetadata("NuGetPackageId").Returns(nuGetPackageId); mockTaskItem.GetMetadata("NuGetPackageVersion").Returns(nuGetPackageVersion); taskItems[i] = mockTaskItem; } return taskItems; } private static ITaskItem[] BuildMockInstrumentationTargetItems( (string ItemSpec, string TargetNuGetPackageVersionRange, string InstrumentationNuGetPackageId, string InstrumentationNuGetPackageVersion)[] source) { var taskItems = new ITaskItem[source.Length]; for (var i = 0; i < source.Length; i++) { var (itemSpec, targetNuGetPackageVersionRange, instrumentationNuGetPackageId, instrumentationNuGetPackageVersion) = source[i]; var mockTaskItem = Substitute.For(); mockTaskItem.ItemSpec.Returns(itemSpec); mockTaskItem.GetMetadata("TargetNuGetPackageVersionRange").Returns(targetNuGetPackageVersionRange); mockTaskItem.GetMetadata("InstrumentationNuGetPackageId").Returns(instrumentationNuGetPackageId); mockTaskItem.GetMetadata("InstrumentationNuGetPackageVersion").Returns(instrumentationNuGetPackageVersion); taskItems[i] = mockTaskItem; } return taskItems; } private class TaskWithMockBuildEngine { private List _buildMessageEventArgsList = new List(); private List _buildErrorEventArgsList = new List(); public TaskWithMockBuildEngine() { var task = new CheckForInstrumentationPackages(); var mockBuildEngine = Substitute.For(); task.BuildEngine = mockBuildEngine; // Capture the BuildMessageEventArgs argument passed to LogMessageEvent. mockBuildEngine.When(x => x.LogMessageEvent(Arg.Any())) .Do(x => _buildMessageEventArgsList.Add(x.ArgAt(0))); // Capture the BuildErrorEventArgs argument passed to LogErrorEvent. mockBuildEngine.When(x => x.LogErrorEvent(Arg.Any())) .Do(x => _buildErrorEventArgsList.Add(x.ArgAt(0))); Task = task; MockBuildEngine = mockBuildEngine; } public CheckForInstrumentationPackages Task { get; } public IBuildEngine MockBuildEngine { get; } public IReadOnlyList MessageEventArgsList => _buildMessageEventArgsList; public IReadOnlyList ErrorEventArgsList => _buildErrorEventArgsList; } }