From cf8dcfffb2e59b02a1f6565174ce15250790c4e7 Mon Sep 17 00:00:00 2001 From: Aaron Bockover Date: Mon, 21 May 2018 19:53:15 -0400 Subject: [PATCH 1/4] Runtime: add type to hold runtime information Similar to our Sdk type, but just for runtime. --- .../NativeExceptionHandler.cs | 2 +- .../NativeExtensions.cs | 2 +- Agents/Xamarin.Interactive/Runtime.cs | 110 ++++++++++++++++++ docs/Xamarin.Interactive.api.cs | 32 +++++ 4 files changed, 144 insertions(+), 2 deletions(-) create mode 100644 Agents/Xamarin.Interactive/Runtime.cs diff --git a/Agents/Xamarin.Interactive.Mac/NativeExceptionHandler.cs b/Agents/Xamarin.Interactive.Mac/NativeExceptionHandler.cs index 901011a0a..2dccf303b 100644 --- a/Agents/Xamarin.Interactive.Mac/NativeExceptionHandler.cs +++ b/Agents/Xamarin.Interactive.Mac/NativeExceptionHandler.cs @@ -47,7 +47,7 @@ static readonly IntPtr objCExceptionPreprocessorFnptr static IntPtr ObjCExceptionPreprocessor (IntPtr exceptionPtr) { - throw new TrappedNativeException (Runtime.GetNSObject (exceptionPtr)); + throw new TrappedNativeException (ObjCRuntime.Runtime.GetNSObject (exceptionPtr)); } public static IDisposable Trap () diff --git a/Agents/Xamarin.Interactive.iOS/NativeExtensions.cs b/Agents/Xamarin.Interactive.iOS/NativeExtensions.cs index 0858d93a2..1c8c3f7d0 100644 --- a/Agents/Xamarin.Interactive.iOS/NativeExtensions.cs +++ b/Agents/Xamarin.Interactive.iOS/NativeExtensions.cs @@ -40,7 +40,7 @@ public static UIWindow GetStatusBarWindow (this UIApplication app) return null; var ptr = IntPtr_objc_msgSend (app.Handle, Selectors.statusBarWindow.Handle); - return ptr != IntPtr.Zero ? (UIWindow)Runtime.GetNSObject (ptr) : null; + return ptr != IntPtr.Zero ? (UIWindow)ObjCRuntime.Runtime.GetNSObject (ptr) : null; } public static void TryHideStatusClockView (this UIApplication app) diff --git a/Agents/Xamarin.Interactive/Runtime.cs b/Agents/Xamarin.Interactive/Runtime.cs new file mode 100644 index 000000000..a86031a39 --- /dev/null +++ b/Agents/Xamarin.Interactive/Runtime.cs @@ -0,0 +1,110 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Runtime.InteropServices; + +using Newtonsoft.Json; + +namespace Xamarin.Interactive +{ + using static Architecture; + + [JsonObject] + public struct Runtime : IEquatable + { + static OSPlatform GetOSPlatform () + { + if (RuntimeInformation.IsOSPlatform (OSPlatform.Windows)) + return OSPlatform.Windows; + else if (RuntimeInformation.IsOSPlatform (OSPlatform.OSX)) + return OSPlatform.OSX; + else if (RuntimeInformation.IsOSPlatform (OSPlatform.Linux)) + return OSPlatform.Linux; + return default (OSPlatform); + } + + public static Runtime CurrentProcessRuntime { get; } = new Runtime ( + GetOSPlatform (), + RuntimeInformation.ProcessArchitecture, + null); + + public OSPlatform OSPlatform { get; } + public Architecture? Architecture { get; } + public string RuntimeIdentifier { get; } + + [JsonConstructor] + public Runtime ( + OSPlatform osPlatform, + Architecture? architecture = null, + string runtimeIdentifier = null) + { + OSPlatform = osPlatform; + Architecture = architecture; + + RuntimeIdentifier = runtimeIdentifier; + if (RuntimeIdentifier == null) + RuntimeIdentifier = BuildRuntimeIdentifier (); + } + + public Runtime WithRuntimeIdentifier (string runtimeIdentifier) + => new Runtime ( + OSPlatform, + Architecture, + runtimeIdentifier); + + public bool Equals (Runtime other) + => other.OSPlatform == OSPlatform && + other.Architecture == Architecture && + other.RuntimeIdentifier == RuntimeIdentifier; + + public override bool Equals (object obj) + => obj is Runtime runtime && Equals (runtime); + + public override int GetHashCode () + => Hash.Combine ( + OSPlatform.GetHashCode (), + Architecture == null ? 0 : Architecture.GetHashCode (), + RuntimeIdentifier == null ? 0 : RuntimeIdentifier.GetHashCode ()); + + public override string ToString () + => RuntimeIdentifier; + + string BuildRuntimeIdentifier () + { + string rid; + + if (OSPlatform == OSPlatform.Windows) + rid = "win"; + else if (OSPlatform == OSPlatform.OSX) + rid = "osx"; + else if (OSPlatform == OSPlatform.Linux) + rid = "linux"; + else + rid = OSPlatform.ToString ().ToLowerInvariant (); + + if (Architecture == null) + return rid; + + switch (Architecture.Value) { + case X86: + rid += "-x86"; + break; + case X64: + rid += "-x64"; + break; + case Arm: + rid += "-arm"; + break; + case Arm64: + rid += "-arm64"; + break; + default: + rid += "-" + Architecture.Value.ToString ().ToLowerInvariant (); + break; + } + + return rid; + } + } +} \ No newline at end of file diff --git a/docs/Xamarin.Interactive.api.cs b/docs/Xamarin.Interactive.api.cs index 3b082142a..62a4b674f 100644 --- a/docs/Xamarin.Interactive.api.cs +++ b/docs/Xamarin.Interactive.api.cs @@ -76,6 +76,38 @@ public interface IAgentSynchronizationContext SynchronizationContext PushContext (SynchronizationContext context); } [JsonObject] + public struct Runtime : IEquatable + { + public Architecture? Architecture { + get; + } + + public static Runtime CurrentProcessRuntime { + get; + } + + public OSPlatform OSPlatform { + get; + } + + public string RuntimeIdentifier { + get; + } + + [JsonConstructor] + public Runtime (OSPlatform osPlatform, Architecture? architecture = null, string runtimeIdentifier = null); + + public bool Equals (Runtime other); + + public override bool Equals (object obj); + + public override int GetHashCode (); + + public override string ToString (); + + public Runtime WithRuntimeIdentifier (string runtimeIdentifier); + } + [JsonObject] public sealed class Sdk { public IReadOnlyList AssemblySearchPaths { From 30e5d46ef3bba0b49944236b2de13f54470cdcfd Mon Sep 17 00:00:00 2001 From: Aaron Bockover Date: Mon, 21 May 2018 20:58:02 -0400 Subject: [PATCH 2/4] InteractiveJsonSerializerSettings: add OSPlatformConverter --- .../Serialization/InteractiveJsonSerializerSettings.cs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/Agents/Xamarin.Interactive/Serialization/InteractiveJsonSerializerSettings.cs b/Agents/Xamarin.Interactive/Serialization/InteractiveJsonSerializerSettings.cs index 4a09e2014..c1bae3197 100644 --- a/Agents/Xamarin.Interactive/Serialization/InteractiveJsonSerializerSettings.cs +++ b/Agents/Xamarin.Interactive/Serialization/InteractiveJsonSerializerSettings.cs @@ -6,6 +6,7 @@ using System.Globalization; using System.Linq; using System.Reflection; +using System.Runtime.InteropServices; using System.Runtime.Serialization; using System.Runtime.Versioning; @@ -122,6 +123,7 @@ public Type BindToType (string assemblyName, string typeName) Converters.Add (new CodeCellIdConverter ()); Converters.Add (new EvaluationContextIdConverter ()); Converters.Add (new SdkIdConverter ()); + Converters.Add (new OSPlatformConverter ()); Converters.Add (new FrameworkNameConverter ()); Converters.Add (new IRepresentedTypeConverter ()); @@ -161,6 +163,13 @@ sealed class SdkIdConverter : StringConverter protected override SdkId GetValue (string value) => (SdkId)value; protected override string GetString (SdkId value) => (string)value; } + + sealed class OSPlatformConverter : StringConverter + { + protected override OSPlatform GetValue (string value) => OSPlatform.Create (value.ToUpperInvariant ()); + protected override string GetString (OSPlatform value) => value.ToString (); + } + sealed class FrameworkNameConverter : StringConverter { protected override FrameworkName GetValue (string value) => new FrameworkName (value); From 7ca9f5fbed072752c02f22dd5ff6944fa949e8e2 Mon Sep 17 00:00:00 2001 From: Aaron Bockover Date: Mon, 21 May 2018 20:58:38 -0400 Subject: [PATCH 3/4] TargetCompilationConfiguration: add Runtime --- .../CodeAnalysis/Evaluating/EvaluationContextManager.cs | 1 + .../CodeAnalysis/TargetCompilationConfiguration.cs | 6 ++++++ docs/Xamarin.Interactive.api.cs | 4 ++++ 3 files changed, 11 insertions(+) diff --git a/Agents/Xamarin.Interactive/CodeAnalysis/Evaluating/EvaluationContextManager.cs b/Agents/Xamarin.Interactive/CodeAnalysis/Evaluating/EvaluationContextManager.cs index 7392cad88..ec00aa4a5 100644 --- a/Agents/Xamarin.Interactive/CodeAnalysis/Evaluating/EvaluationContextManager.cs +++ b/Agents/Xamarin.Interactive/CodeAnalysis/Evaluating/EvaluationContextManager.cs @@ -113,6 +113,7 @@ public Task CreateEvaluationContextAsync ( var globalStateObject = CreateGlobalState (); targetCompilationConfiguration = targetCompilationConfiguration.With ( + runtime: Runtime.CurrentProcessRuntime, defaultImports: defaultImports); targetCompilationConfiguration = PrepareTargetCompilationConfiguration ( diff --git a/Agents/Xamarin.Interactive/CodeAnalysis/TargetCompilationConfiguration.cs b/Agents/Xamarin.Interactive/CodeAnalysis/TargetCompilationConfiguration.cs index dbe673d04..0c75b2498 100644 --- a/Agents/Xamarin.Interactive/CodeAnalysis/TargetCompilationConfiguration.cs +++ b/Agents/Xamarin.Interactive/CodeAnalysis/TargetCompilationConfiguration.cs @@ -17,6 +17,7 @@ public sealed class TargetCompilationConfiguration public static TargetCompilationConfiguration CreateInitialForCompilationWorkspace ( IReadOnlyList assemblySearchPaths = null) => new TargetCompilationConfiguration ( + default, default, HostEnvironment.OS, default, @@ -27,6 +28,7 @@ public static TargetCompilationConfiguration CreateInitialForCompilationWorkspac new List (assemblySearchPaths ?? Array.Empty ()), default); + public Runtime Runtime { get; } public Sdk Sdk { get; } internal HostOS CompilationOS { get; } public EvaluationContextId EvaluationContextId { get; } @@ -39,6 +41,7 @@ public static TargetCompilationConfiguration CreateInitialForCompilationWorkspac [JsonConstructor] TargetCompilationConfiguration ( + Runtime runtime, Sdk sdk, HostOS compilationOS, EvaluationContextId evaluationContextId, @@ -49,6 +52,7 @@ public static TargetCompilationConfiguration CreateInitialForCompilationWorkspac IReadOnlyList assemblySearchPaths, bool includePEImagesInDependencyResolution) { + Runtime = runtime; Sdk = sdk; CompilationOS = compilationOS; EvaluationContextId = evaluationContextId; @@ -61,6 +65,7 @@ public static TargetCompilationConfiguration CreateInitialForCompilationWorkspac } internal TargetCompilationConfiguration With ( + Optional runtime = default, Optional sdk = default, Optional compilationOS = default, Optional evaluationContextId = default, @@ -71,6 +76,7 @@ internal TargetCompilationConfiguration With ( Optional> assemblySearchPaths = default, Optional includePEImagesInDependencyResolution = default) => new TargetCompilationConfiguration ( + runtime.GetValueOrDefault (Runtime), sdk.GetValueOrDefault (Sdk), compilationOS.GetValueOrDefault (CompilationOS), evaluationContextId.GetValueOrDefault (EvaluationContextId), diff --git a/docs/Xamarin.Interactive.api.cs b/docs/Xamarin.Interactive.api.cs index 62a4b674f..6a1e66c2c 100644 --- a/docs/Xamarin.Interactive.api.cs +++ b/docs/Xamarin.Interactive.api.cs @@ -309,6 +309,10 @@ public IReadOnlyList InitialReferences { get; } + public Runtime Runtime { + get; + } + public Sdk Sdk { get; } From 324b6dcb02a6dd5babf64990675c47eb1f08fe06 Mon Sep 17 00:00:00 2001 From: Aaron Bockover Date: Sat, 19 May 2018 21:52:02 -0400 Subject: [PATCH 4/4] CodeAnalysis: Resolving: add Mono's DllMap support This is a managed implementation of Mono's DLL mapping support that allows for platform-specific mapping of native libraries and symbols within them. We will use this as a basis for providing better native library loading support for NuGet in .NET Core in particular via AssemblyLoadContext LoadUnmanagedDll(string unmanagedDllName). Unfortunately AssemblyLoadContext does not support symbol mapping, but it is implemented in the DllMap in this commit should the ability be introduced in the future. --- .../CodeAnalysis/Resolving/DllMap.cs | 242 ++++++++++++++++++ .../DllMapTests.cs | 121 +++++++++ 2 files changed, 363 insertions(+) create mode 100644 Agents/Xamarin.Interactive/CodeAnalysis/Resolving/DllMap.cs create mode 100644 CodeAnalysis/Xamarin.Interactive.CodeAnalysis.Tests/DllMapTests.cs diff --git a/Agents/Xamarin.Interactive/CodeAnalysis/Resolving/DllMap.cs b/Agents/Xamarin.Interactive/CodeAnalysis/Resolving/DllMap.cs new file mode 100644 index 000000000..44a97b3cc --- /dev/null +++ b/Agents/Xamarin.Interactive/CodeAnalysis/Resolving/DllMap.cs @@ -0,0 +1,242 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections; +using System.Collections.Generic; +using System.IO; +using System.Runtime.InteropServices; +using System.Xml; + +namespace Xamarin.Interactive.CodeAnalysis.Resolving +{ + sealed class DllMap : IEnumerable> + { + public struct Entity : IEquatable + { + public string LibraryName { get; } + public string SymbolName { get; } + + public Entity ( + string libraryName, + string symbolName = null) + { + LibraryName = libraryName; + SymbolName = symbolName; + } + + public bool Equals (Entity other) + => LibraryName == other.LibraryName && SymbolName == other.SymbolName; + + public override bool Equals (object obj) + => obj is Entity entity && Equals (entity); + + public override int GetHashCode () + => Hash.Combine (LibraryName, SymbolName); + + public override string ToString () + => $"({LibraryName ?? "(null)"}, {SymbolName ?? "(null)"})"; + } + + internal struct Filter + { + public string OperatingSystem { get; } + public string Cpu { get; } + public string WordSize { get; } + + public Filter ( + string operatingSystem, + string cpu, + string wordSize) + { + OperatingSystem = operatingSystem; + Cpu = cpu; + WordSize = wordSize; + } + + public bool Matches (Filter targetFilter) + { + if (OperatingSystem != null && + targetFilter.OperatingSystem != null && + !Matches (targetFilter.OperatingSystem, OperatingSystem)) + return false; + + if (Cpu != null && + targetFilter.Cpu != null && + !Matches (targetFilter.Cpu, Cpu)) + return false; + + if (WordSize != null && + targetFilter.WordSize != null && + !Matches (targetFilter.WordSize, WordSize)) + return false; + + return true; + } + + static bool Matches (string value, string predicate) + { + if (string.IsNullOrEmpty (predicate) || string.IsNullOrEmpty (value)) + return false; + + var invert = false; + + if (predicate [0] == '!') { + invert = true; + predicate = predicate.Substring (1); + } + + foreach (var predicateItem in predicate.Split (',')) { + if (predicateItem == value) + return !invert; + } + + return invert; + } + } + + readonly Filter targetFilter; + readonly Dictionary map = new Dictionary (); + + public DllMap () : this (Runtime.CurrentProcessRuntime) + { + } + + public DllMap (Runtime targetRuntime) + { + var os = targetRuntime.OSPlatform.ToString ().ToLowerInvariant (); + + string cpu = null; + string wordSize = null; + switch (targetRuntime.Architecture) { + case Architecture.X86: + cpu = "x86"; + wordSize = "32"; + break; + case Architecture.X64: + cpu = "x86-64"; + wordSize = "64"; + break; + case Architecture.Arm: + cpu = "arm"; + wordSize = "32"; + break; + case Architecture.Arm64: + cpu = "armv8"; + wordSize = "64"; + break; + } + + targetFilter = new Filter (os, cpu, wordSize); + } + + public bool TryMap (Entity source, out Entity target) + { + if (!map.TryGetValue (source, out target) && + !map.TryGetValue (new Entity (source.LibraryName), out target)) { + target = source; + return false; + } + + target = new Entity (target.LibraryName, target.SymbolName ?? source.SymbolName); + return true; + } + + public bool TryMap (Entity source, string basePath, out Entity target) + { + if (basePath == null) + throw new ArgumentNullException (nameof (basePath)); + + if (TryMap (source, out target)) { + target = new Entity ( + Path.Combine (basePath, target.LibraryName), + target.SymbolName); + return true; + } + + return false; + } + + public DllMap Add (Entity source, Entity target) + { + map.Add (source, target); + return this; + } + + public DllMap LoadXml (string configurationXml) + { + var document = new XmlDocument (); + document.LoadXml (configurationXml); + return Load (document); + } + + public DllMap Load (string configurationFile) + { + var document = new XmlDocument (); + document.Load (configurationFile); + return Load (document); + } + + public DllMap Load (XmlDocument document) + { + if (document == null) + throw new ArgumentNullException (nameof (document)); + + if (document.DocumentElement == null) + throw new ArgumentException ( + "document is not loaded or has no root element", + nameof (document)); + + if (document.DocumentElement.Name != "configuration") + return this; + + string GetAttribute (XmlElement elem, string attributeName) + { + var value = elem.GetAttribute (attributeName); + return string.IsNullOrEmpty (value) ? null : value; + } + + Filter CreateFilter (XmlElement elem) + => new Filter ( + GetAttribute (elem, "os"), + GetAttribute (elem, "cpu"), + GetAttribute (elem, "wordsize")); + + foreach (var node in document.DocumentElement.ChildNodes) { + if (node is XmlElement elem && elem.Name == "dllmap") { + if (!CreateFilter (elem).Matches (targetFilter)) + continue; + + var sourceLibrary = GetAttribute (elem, "dll"); + var targetLibrary = GetAttribute (elem, "target"); + + foreach (var childNode in elem.ChildNodes) { + if (childNode is XmlElement childElem && childElem.Name == "dllentry") { + if (!CreateFilter (childElem).Matches (targetFilter)) + continue; + + var symbolTargetLibrary = GetAttribute (childElem, "dll") ?? targetLibrary; + var sourceSymbol = GetAttribute (childElem, "name"); + var targetSymbol = GetAttribute (childElem, "target"); + + if (symbolTargetLibrary != null) + map [new Entity (sourceLibrary, sourceSymbol)] + = new Entity (symbolTargetLibrary, targetSymbol); + } + } + + if (!elem.HasChildNodes && targetLibrary != null) + map [new Entity (sourceLibrary)] = new Entity (targetLibrary); + } + } + + return this; + } + + public IEnumerator> GetEnumerator () + => map.GetEnumerator (); + + IEnumerator IEnumerable.GetEnumerator () + => GetEnumerator (); + } +} \ No newline at end of file diff --git a/CodeAnalysis/Xamarin.Interactive.CodeAnalysis.Tests/DllMapTests.cs b/CodeAnalysis/Xamarin.Interactive.CodeAnalysis.Tests/DllMapTests.cs new file mode 100644 index 000000000..e84e86569 --- /dev/null +++ b/CodeAnalysis/Xamarin.Interactive.CodeAnalysis.Tests/DllMapTests.cs @@ -0,0 +1,121 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Runtime.InteropServices; + +using Xunit; + +namespace Xamarin.Interactive.CodeAnalysis.Resolving +{ + public class DllMapTests + { + static DllMap.Filter ParseFilter (string filter) + { + string NullIfEmpty (string str) + => string.IsNullOrEmpty (str) ? null : str; + + var parts = filter.Split (';'); + return new DllMap.Filter ( + NullIfEmpty (parts [0]), + parts.Length > 1 ? NullIfEmpty (parts [1]) : null, + parts.Length > 2 ? NullIfEmpty (parts [2]) : null); + } + + [Theory] + [InlineData ("linux", "linux", true)] + [InlineData ("linux,osx", "linux", true)] + [InlineData ("linux,osx", "osx", true)] + [InlineData ("!linux", "windows", true)] + [InlineData ("!linux", "linux", false)] + [InlineData ("!linux,osx", "osx", false)] + [InlineData ("!linux,osx", "linux", false)] + [InlineData ("!linux,osx", "windows", true)] + [InlineData ("!linux,osx;!x86-64", "windows;x86", true)] + [InlineData ("!linux,osx;x86-64", "windows;x86", false)] + [InlineData ("!linux,osx;x86-64", "windows;x86-64", true)] + [InlineData ("!linux,osx;x86-64,arm", "windows;arm", true)] + [InlineData ("!linux,osx;!x86-64,arm", "windows;arm", false)] + [InlineData ("!linux,osx;!x86-64,arm", "windows;armv8", true)] + [InlineData ("windows;;32", "windows;;32", true)] + [InlineData ("windows;;!32", "windows;;32", false)] + [InlineData ("windows;;!64", "windows;;32", true)] + public void FilterMatch (string predicate, string host, bool expectedMatch) + => Assert.Equal ( + expectedMatch, + ParseFilter (predicate).Matches (ParseFilter (host))); + + [Theory] + [InlineData ("liba", null, "liba-mapped", null)] + [InlineData ("liba", "specialfunc", "libspecialfunc", "specialfunc")] + [InlineData ("liba", "specialfunc2", "libspecialfunc", "newspecialfunc2")] + public void CoreMaps ( + string sourceLibrary, + string sourceSymbol, + string targetLibrary, + string targetSymbol) + { + var map = new DllMap { + { + new DllMap.Entity ("liba"), + new DllMap.Entity ("liba-mapped") + }, + { + new DllMap.Entity ("liba", "specialfunc"), + new DllMap.Entity ("libspecialfunc") + }, + { + new DllMap.Entity ("liba", "specialfunc2"), + new DllMap.Entity ("libspecialfunc", "newspecialfunc2") + }, + }; + + var source = new DllMap.Entity (sourceLibrary, sourceSymbol); + var expectedTarget = new DllMap.Entity (targetLibrary, targetSymbol); + + Assert.True (map.TryMap (source, out var target)); + Assert.Equal (expectedTarget, target); + } + + const string dllmapXml = @" + + + + + + + + + + + + "; + + [Theory] + [InlineData ("windows", "libc", "somefunction", "libdifferent.so", "differentfunction", true)] + [InlineData ("solaris", "libc", "somefunction", "libanother.so", "differentfunction", true)] + [InlineData ("freebsd", "libc", "somefunction", "libanother.so", "differentfunction", true)] + [InlineData ("linux", "libc", "anyotherfunction", "preload-libc", "anyotherfunction", true)] + [InlineData ("osx", "libc", null, "preload-libc", null, true)] + [InlineData ("windows", "SolarSystem", "get_Animals", "SolarSystem", "get_Animals", false)] + [InlineData ("windows", "SolarSystem", "get_Plants", "SolarSystem", "get_Plants", false)] + [InlineData ("linux", "SolarSystem", "get_Animals", "libearth.so", "get_Animals", true)] + [InlineData ("linux", "SolarSystem", "get_Plants", "libmars.so", "get_Plants", true)] + public void XmlMap ( + string host, + string sourceLibrary, + string sourceSymbol, + string targetLibrary, + string targetSymbol, + bool expectedMatch) + { + var map = new DllMap (new Runtime (OSPlatform.Create (host))).LoadXml (dllmapXml); + + var source = new DllMap.Entity (sourceLibrary, sourceSymbol); + var expectedTarget = new DllMap.Entity (targetLibrary, targetSymbol); + + Assert.Equal (expectedMatch, map.TryMap (source, out var target)); + Assert.Equal (expectedTarget, target); + } + } +} \ No newline at end of file