Skip to content

Commit

Permalink
NUnit CallTarget integration (#1131)
Browse files Browse the repository at this point in the history
* CallTarget NUnit Integration

* Changes based in the reviews.

* Changes from reviews

* Update integrations

* Fix CIApp compability by removing the Analyzed flag in the span.

* update integrations.json
  • Loading branch information
tonyredondo committed Jan 18, 2021
1 parent 69cb554 commit 86ddfe3
Show file tree
Hide file tree
Showing 9 changed files with 495 additions and 0 deletions.
73 changes: 73 additions & 0 deletions integrations.json
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,79 @@
}
]
},
{
"name": "NUnit",
"method_replacements": [
{
"caller": {},
"target": {
"assembly": "nunit.framework",
"type": "NUnit.Framework.Internal.Commands.SkipCommand",
"method": "Execute",
"signature_types": [
"NUnit.Framework.Internal.TestResult",
"NUnit.Framework.Internal.TestExecutionContext"
],
"minimum_major": 3,
"minimum_minor": 0,
"minimum_patch": 0,
"maximum_major": 3,
"maximum_minor": 65535,
"maximum_patch": 65535
},
"wrapper": {
"assembly": "Datadog.Trace.ClrProfiler.Managed, Version=1.22.0.0, Culture=neutral, PublicKeyToken=def86d061d0d2eeb",
"type": "Datadog.Trace.ClrProfiler.AutoInstrumentation.NUnit.NUnitSkipCommandExecuteIntegration",
"action": "CallTargetModification"
}
},
{
"caller": {},
"target": {
"assembly": "NUnit3.TestAdapter",
"type": "NUnit.VisualStudio.TestAdapter.NUnitTestAdapter",
"method": "Unload",
"signature_types": [
"System.Void"
],
"minimum_major": 3,
"minimum_minor": 0,
"minimum_patch": 0,
"maximum_major": 3,
"maximum_minor": 65535,
"maximum_patch": 65535
},
"wrapper": {
"assembly": "Datadog.Trace.ClrProfiler.Managed, Version=1.22.0.0, Culture=neutral, PublicKeyToken=def86d061d0d2eeb",
"type": "Datadog.Trace.ClrProfiler.AutoInstrumentation.NUnit.NUnitTestAdapterUnloadIntegration",
"action": "CallTargetModification"
}
},
{
"caller": {},
"target": {
"assembly": "nunit.framework",
"type": "NUnit.Framework.Internal.Commands.TestMethodCommand",
"method": "Execute",
"signature_types": [
"NUnit.Framework.Internal.TestResult",
"NUnit.Framework.Internal.TestExecutionContext"
],
"minimum_major": 3,
"minimum_minor": 0,
"minimum_patch": 0,
"maximum_major": 3,
"maximum_minor": 65535,
"maximum_patch": 65535
},
"wrapper": {
"assembly": "Datadog.Trace.ClrProfiler.Managed, Version=1.22.0.0, Culture=neutral, PublicKeyToken=def86d061d0d2eeb",
"type": "Datadog.Trace.ClrProfiler.AutoInstrumentation.NUnit.NUnitTestMethodCommandExecuteIntegration",
"action": "CallTargetModification"
}
}
]
},
{
"name": "XUnit",
"method_replacements": [
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
using System.Reflection;

namespace Datadog.Trace.ClrProfiler.AutoInstrumentation.NUnit
{
/// <summary>
/// DuckTyping interface for NUnit.Framework.Interfaces.IMethodInfo
/// </summary>
public interface IMethodInfo
{
/// <summary>
/// Gets the MethodInfo for this method.
/// </summary>
MethodInfo MethodInfo { get; }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
using System.Collections;
using System.Collections.Generic;

namespace Datadog.Trace.ClrProfiler.AutoInstrumentation.NUnit
{
/// <summary>
/// DuckTyping interface for NUnit.Framework.Interfaces.IPropertyBag
/// </summary>
public interface IPropertyBag
{
/// <summary>
/// Gets a collection containing all the keys in the property set
/// </summary>
ICollection<string> Keys { get; }

/// <summary>
/// Gets or sets the list of values for a particular key
/// </summary>
/// <param name="key">The key for which the values are to be retrieved</param>
IList this[string key] { get; }

/// <summary>
/// Gets a single value for a key, using the first
/// one if multiple values are present and returning
/// null if the value is not found.
/// </summary>
/// <param name="key">the key for which the values are to be retrieved</param>
/// <returns>First value of the list for the key; otherwise null.</returns>
object Get(string key);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
namespace Datadog.Trace.ClrProfiler.AutoInstrumentation.NUnit
{
/// <summary>
/// DuckTyping interface for NUnit.Framework.Internal.Test
/// </summary>
public interface ITest
{
/// <summary>
/// Gets a MethodInfo for the method implementing this test.
/// Returns null if the test is not implemented as a method.
/// </summary>
IMethodInfo Method { get; }

/// <summary>
/// Gets the arguments to use in creating the test or empty array if none required.
/// </summary>
object[] Arguments { get; }

/// <summary>
/// Gets the properties for this test
/// </summary>
IPropertyBag Properties { get; }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
namespace Datadog.Trace.ClrProfiler.AutoInstrumentation.NUnit
{
/// <summary>
/// DuckTyping interface for NUnit.Framework.Internal.TestExecutionContext
/// </summary>
public interface ITestExecutionContext
{
/// <summary>
/// Gets the current test
/// </summary>
ITest CurrentTest { get; }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using Datadog.Trace.Ci;
using Datadog.Trace.ExtensionMethods;

namespace Datadog.Trace.ClrProfiler.AutoInstrumentation.NUnit
{
internal static class NUnitIntegration
{
static NUnitIntegration()
{
// Preload environment variables.
CIEnvironmentValues.DecorateSpan(null);
}

internal static Scope CreateScope<TContext>(TContext executionContext, Type targetType)
where TContext : ITestExecutionContext
{
ITest currentTest = executionContext.CurrentTest;
MethodInfo testMethod = currentTest.Method.MethodInfo;
object[] testMethodArguments = currentTest.Arguments;
IPropertyBag testMethodProperties = currentTest.Properties;

if (testMethod == null)
{
return null;
}

string testFramework = "NUnit " + targetType?.Assembly?.GetName().Version;
string testSuite = testMethod.DeclaringType?.FullName;
string testName = testMethod.Name;
string skipReason = null;

Tracer tracer = Tracer.Instance;
Scope scope = tracer.StartActive("nunit.test");
Span span = scope.Span;

span.Type = SpanTypes.Test;
span.SetTraceSamplingPriority(SamplingPriority.AutoKeep);
span.ResourceName = $"{testSuite}.{testName}";
span.SetTag(TestTags.Suite, testSuite);
span.SetTag(TestTags.Name, testName);
span.SetTag(TestTags.Framework, testFramework);
span.SetTag(TestTags.Type, TestTags.TypeTest);
CIEnvironmentValues.DecorateSpan(span);

var framework = FrameworkDescription.Instance;

span.SetTag(CommonTags.RuntimeName, framework.Name);
span.SetTag(CommonTags.RuntimeOSArchitecture, framework.OSArchitecture);
span.SetTag(CommonTags.RuntimeOSPlatform, framework.OSPlatform);
span.SetTag(CommonTags.RuntimeProcessArchitecture, framework.ProcessArchitecture);
span.SetTag(CommonTags.RuntimeVersion, framework.ProductVersion);

// Get test parameters
ParameterInfo[] methodParameters = testMethod.GetParameters();
if (methodParameters?.Length > 0)
{
for (int i = 0; i < methodParameters.Length; i++)
{
if (testMethodArguments != null && i < testMethodArguments.Length)
{
span.SetTag($"{TestTags.Arguments}.{methodParameters[i].Name}", testMethodArguments[i]?.ToString() ?? "(null)");
}
else
{
span.SetTag($"{TestTags.Arguments}.{methodParameters[i].Name}", "(default)");
}
}
}

// Get traits
if (testMethodProperties != null)
{
skipReason = (string)testMethodProperties.Get("_SKIPREASON");
foreach (var key in testMethodProperties.Keys)
{
if (key == "_SKIPREASON")
{
continue;
}

IList value = testMethodProperties[key];
if (value != null)
{
List<string> lstValues = new List<string>();
foreach (object valObj in value)
{
if (valObj is null)
{
continue;
}

lstValues.Add(valObj.ToString());
}

span.SetTag($"{TestTags.Traits}.{key}", string.Join(", ", lstValues));
}
else
{
span.SetTag($"{TestTags.Traits}.{key}", "(null)");
}
}
}

if (skipReason != null)
{
span.SetTag(TestTags.Status, TestTags.StatusSkip);
span.SetTag(TestTags.SkipReason, skipReason);
span.Finish(TimeSpan.Zero);
scope.Dispose();
scope = null;
}

span.ResetStartTime();
return scope;
}

internal static void FinishScope(Scope scope, Exception ex)
{
// unwrap the generic NUnitException
if (ex != null && ex.GetType().FullName == "NUnit.Framework.Internal.NUnitException")
{
ex = ex.InnerException;
}

switch (ex?.GetType().FullName)
{
case "NUnit.Framework.SuccessException":
scope.Span.SetTag(TestTags.Status, TestTags.StatusPass);
break;
case "NUnit.Framework.IgnoreException":
scope.Span.SetTag(TestTags.Status, TestTags.StatusSkip);
break;
default:
scope.Span.SetException(ex);
scope.Span.SetTag(TestTags.Status, TestTags.StatusFail);
break;
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
using System;
using Datadog.Trace.ClrProfiler.CallTarget;

namespace Datadog.Trace.ClrProfiler.AutoInstrumentation.NUnit
{
/// <summary>
/// NUnit.Framework.Internal.Commands.SkipCommand.Execute() calltarget instrumentation
/// </summary>
[InstrumentMethod(
Assembly = "nunit.framework",
Type = "NUnit.Framework.Internal.Commands.SkipCommand",
Method = "Execute",
ReturnTypeName = "NUnit.Framework.Internal.TestResult",
ParametersTypesNames = new[] { "NUnit.Framework.Internal.TestExecutionContext" },
MinimumVersion = "3.0.0",
MaximumVersion = "3.*.*",
IntegrationName = IntegrationName)]
public class NUnitSkipCommandExecuteIntegration
{
private const string IntegrationName = "NUnit";

/// <summary>
/// OnMethodBegin callback
/// </summary>
/// <typeparam name="TTarget">Type of the target</typeparam>
/// <typeparam name="TContext">ExecutionContext type</typeparam>
/// <param name="instance">Instance value, aka `this` of the instrumented method.</param>
/// <param name="executionContext">Execution context instance</param>
/// <returns>Calltarget state value</returns>
public static CallTargetState OnMethodBegin<TTarget, TContext>(TTarget instance, TContext executionContext)
where TContext : ITestExecutionContext
{
if (!Tracer.Instance.Settings.IsIntegrationEnabled(IntegrationName))
{
return CallTargetState.GetDefault();
}

return new CallTargetState(NUnitIntegration.CreateScope(executionContext, typeof(TTarget)));
}

/// <summary>
/// OnMethodEnd callback
/// </summary>
/// <typeparam name="TTarget">Type of the target</typeparam>
/// <typeparam name="TResult">TestResult type</typeparam>
/// <param name="instance">Instance value, aka `this` of the instrumented method.</param>
/// <param name="returnValue">Original method return value</param>
/// <param name="exception">Exception instance in case the original code threw an exception.</param>
/// <param name="state">Calltarget state value</param>
/// <returns>Return value of the method</returns>
public static CallTargetReturn<TResult> OnMethodEnd<TTarget, TResult>(TTarget instance, TResult returnValue, Exception exception, CallTargetState state)
{
Scope scope = (Scope)state.State;
if (scope != null)
{
NUnitIntegration.FinishScope(scope, exception);
scope.Dispose();
}

return new CallTargetReturn<TResult>(returnValue);
}
}
}
Loading

0 comments on commit 86ddfe3

Please sign in to comment.