using System; using System.Collections.Generic; using System.Drawing; using System.Globalization; using System.IO; using System.Linq; using System.Reflection; using System.Runtime.CompilerServices; using System.Xml; namespace TeslaConsole.DiffTests { /// /// Lives inside a child AppDomain whose base directory is the directory of one /// TeslaConsole.exe. It loads that exe and invokes deterministic, dependency-free /// methods by reflection, returning a normalized string so results marshal cleanly /// across the AppDomain boundary. /// /// Only methods whose code path touches nothing but the framework + the exe itself /// are exercised, so the original (which ships without its proprietary dependency /// DLLs) loads and runs them fine. /// public sealed class Invoker : MarshalByRefObject { private readonly Assembly _asm; private readonly string _probeDir; public Invoker(string exePath, string probeDir) { _probeDir = probeDir; // The original ships without its proprietary dependency DLLs; resolve any // referenced assembly out of the recovered build's output directory so both // assemblies expose the same metadata. AppDomain.CurrentDomain.AssemblyResolve += ResolveFromProbeDir; _asm = Assembly.LoadFrom(exePath); } private Assembly ResolveFromProbeDir(object sender, ResolveEventArgs args) { var name = new AssemblyName(args.Name); if (string.Equals(name.Name, "TeslaConsole", StringComparison.OrdinalIgnoreCase)) return _asm; // self-references bind to the assembly under test if (string.IsNullOrEmpty(_probeDir)) return null; foreach (var ext in new[] { ".dll", ".exe" }) { string candidate = Path.Combine(_probeDir, name.Name + ext); if (File.Exists(candidate)) return Assembly.LoadFrom(candidate); } return null; } /// Full identity of the loaded assembly (for sanity assertions). public string AssemblyFullName => _asm.FullName; // ---- Structural (public API surface) enumeration, run inside this domain ---- public string[] GetPublicTypeNames() { return SafeGetTypes() .Where(t => (t.IsPublic || t.IsNestedPublic) && !IsCompilerGenerated(t)) .Select(t => t.FullName) .Distinct() .ToArray(); } public string[] GetPublicMemberSignatures() { var set = new HashSet(); const BindingFlags flags = BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static | BindingFlags.DeclaredOnly; foreach (var t in SafeGetTypes()) { if (!(t.IsPublic || t.IsNestedPublic) || IsCompilerGenerated(t)) continue; foreach (var m in t.GetMembers(flags)) { if (m.Name.IndexOf('<') >= 0 || IsCompilerGenerated(m)) continue; // Skip property/event accessor methods: the property/event itself is // compared via its own entry, and Roslyn vs. the original compiler mark // these accessors' [CompilerGenerated] differently. Real operators // (op_*) are kept. if (IsAccessorMethod(m)) continue; try { set.Add(t.FullName + " :: " + DescribeMember(m)); } catch { /* unresolved member metadata; skip symmetrically */ } } } return set.ToArray(); } private IEnumerable SafeGetTypes() { try { return _asm.GetTypes(); } catch (ReflectionTypeLoadException ex) { return ex.Types.Where(t => t != null); } } private static string DescribeMember(MemberInfo m) { switch (m) { case MethodInfo mi: return "M " + mi.ReturnType.FullName + " " + mi.Name + "(" + string.Join(",", mi.GetParameters().Select(p => p.ParameterType.FullName)) + ")"; case ConstructorInfo ci: return "C .ctor(" + string.Join(",", ci.GetParameters().Select(p => p.ParameterType.FullName)) + ")"; case PropertyInfo pi: return "P " + pi.PropertyType.FullName + " " + pi.Name; case FieldInfo fi: return "F " + fi.FieldType.FullName + " " + fi.Name; case EventInfo ei: return "E " + ei.EventHandlerType.FullName + " " + ei.Name; case Type nt: return "T " + nt.FullName; default: return m.MemberType + " " + m.Name; } } private static bool IsAccessorMethod(MemberInfo m) { if (!(m is MethodInfo mi) || !mi.IsSpecialName) return false; string n = mi.Name; return n.StartsWith("get_") || n.StartsWith("set_") || n.StartsWith("add_") || n.StartsWith("remove_"); } private static bool IsCompilerGenerated(MemberInfo m) { try { return m.IsDefined(typeof(CompilerGeneratedAttribute), false); } catch { return false; } } /// /// Dispatch a named case. Returns the method's normalized result, or /// "EXC:<ExceptionTypeName>" when the underlying call throws — so exception /// behavior is compared too. /// public string Run(string caseName, string[] args) { try { return Dispatch(caseName, args); } catch (TargetInvocationException tie) { return "EXC:" + (tie.InnerException ?? tie).GetType().Name; } catch (Exception ex) { return "EXC:" + ex.GetType().Name; } } private string Dispatch(string caseName, string[] args) { switch (caseName) { case "GetTimeString": return RPStringsGetTimeString(long.Parse(args[0], CultureInfo.InvariantCulture)); case "HostTypeParse": return HostTypeParse(args[0]); case "ConvertBitmap": return ConvertBitmap( int.Parse(args[0], CultureInfo.InvariantCulture), int.Parse(args[1], CultureInfo.InvariantCulture), int.Parse(args[2], CultureInfo.InvariantCulture)); case "GenerateString": return GenerateString( int.Parse(args[0], CultureInfo.InvariantCulture), int.Parse(args[1], CultureInfo.InvariantCulture), args[2], args[3]); case "RPMapParse": return ParseRedPlanetNode("TeslaConsole.RedPlanet.RPMap", args[0]); case "RPVehicleParse": return ParseRedPlanetNode("TeslaConsole.RedPlanet.RPVehicle", args[0]); case "SiteManagementGuid": return SiteManagementGuid(args[0]); case "TupleCreate": return TupleCreate(args[0], args[1]); case "CatalogSummary": return CatalogSummary(args[0]); case "CatalogEntry": return CatalogEntry(args[0], args[1], args[2], args[3]); default: throw new ArgumentException("Unknown case: " + caseName); } } private Type T(string fullName) { return _asm.GetType(fullName, throwOnError: true); } private string RPStringsGetTimeString(long ticks) { var m = T("TeslaConsole.RPStrings").GetMethod( "GetTimeString", BindingFlags.Public | BindingFlags.Static); return (string)m.Invoke(null, new object[] { TimeSpan.FromTicks(ticks) }); } private string HostTypeParse(string input) { var helperType = T("TeslaConsole.HostTypeHelper"); var parse = helperType.GetMethod("Parse", BindingFlags.Public | BindingFlags.Static); object helper = parse.Invoke(null, new object[] { input }); return helper.ToString(); } private string ConvertBitmap(int width, int height, int seed) { using (var bmp = MakePatternBitmap(width, height, seed)) { var m = T("TeslaConsole.PlasmaBitmaps").GetMethod( "ConvertBitmap", BindingFlags.Public | BindingFlags.Static); return (string)m.Invoke(null, new object[] { bmp }); } } private string GenerateString(int width, int height, string font, string text) { var m = T("TeslaConsole.PlasmaBitmaps").GetMethod( "GenerateString", BindingFlags.Public | BindingFlags.Static, null, new[] { typeof(int), typeof(int), typeof(string), typeof(string) }, null); return (string)m.Invoke(null, new object[] { width, height, font, text }); } private string ParseRedPlanetNode(string typeName, string xml) { var doc = new XmlDocument(); doc.LoadXml(xml); XmlNode node = doc.DocumentElement; var type = T(typeName); var ctor = type.GetConstructor( BindingFlags.NonPublic | BindingFlags.Instance, null, new[] { typeof(XmlNode) }, null); object instance = ctor.Invoke(new object[] { node }); string key = (string)type.GetProperty("Key").GetValue(instance, null); string name = (string)type.GetProperty("Name").GetValue(instance, null); string toStr = instance.ToString(); return key + "|" + name + "|" + toStr; } private string SiteManagementGuid(string fieldName) { var f = T("TeslaConsole.SiteManagement").GetField( fieldName, BindingFlags.Public | BindingFlags.Static); return ((Guid)f.GetValue(null)).ToString("D"); } private string TupleCreate(string a, string b) { // Tuple.Create(int.Parse(a), b) -> "A|B" var tupleType = T("TeslaConsole.Tuple"); MethodInfo create = null; foreach (var m in tupleType.GetMethods(BindingFlags.Public | BindingFlags.Static)) { if (m.Name == "Create" && m.IsGenericMethodDefinition && m.GetGenericArguments().Length == 2 && m.GetParameters().Length == 2) { create = m; break; } } var closed = create.MakeGenericMethod(typeof(int), typeof(string)); object tuple = closed.Invoke(null, new object[] { int.Parse(a, CultureInfo.InvariantCulture), b }); Type tt = tuple.GetType(); object av = tt.GetProperty("A").GetValue(tuple, null); object bv = tt.GetProperty("B").GetValue(tuple, null); return Convert.ToString(av, CultureInfo.InvariantCulture) + "|" + Convert.ToString(bv, CultureInfo.InvariantCulture); } private System.Collections.IEnumerable LoadCatalog(string path) { var reg = T("TeslaConsole.AppRegistry"); reg.GetMethod("LoadFromFile", BindingFlags.Public | BindingFlags.Static) .Invoke(null, new object[] { path }); return (System.Collections.IEnumerable)reg.GetProperty("Products").GetValue(null); } private string CatalogSummary(string path) { int products = 0, entries = 0; foreach (var product in LoadCatalog(path)) { products++; var list = (System.Collections.IEnumerable)product.GetType().GetField("Entries").GetValue(product); foreach (var _ in list) entries++; } return $"products={products};entries={entries}"; } /// /// Finds the launch entry with the given key, builds its LaunchData with the optional /// resolution, and returns "DisplayName|LaunchKey|Exe|Args|WorkingDirectory|AutoRestart". /// private string CatalogEntry(string path, string launchKey, string resW, string resH) { Guid key = Guid.Parse(launchKey); object entry = null; foreach (var product in LoadCatalog(path)) { var entries = (System.Collections.IEnumerable)product.GetType().GetField("Entries").GetValue(product); foreach (var en in entries) { if ((Guid)en.GetType().GetField("LaunchKey").GetValue(en) == key) { entry = en; break; } } if (entry != null) break; } if (entry == null) return "NOTFOUND"; object resolution = null; if (resW.Length > 0 && resH.Length > 0) resolution = new Size(int.Parse(resW, CultureInfo.InvariantCulture), int.Parse(resH, CultureInfo.InvariantCulture)); object ld = entry.GetType().GetMethod("ToLaunchData").Invoke(entry, new object[] { resolution }); Type t = ld.GetType(); object pair = t.GetField("LaunchPair").GetValue(ld); string disp = (string)pair.GetType().GetField("DisplayName").GetValue(pair); Guid lkey = (Guid)pair.GetType().GetField("LaunchKey").GetValue(pair); string exe = (string)t.GetField("ExeFile").GetValue(ld); string margs = (string)t.GetField("Arguments").GetValue(ld); string wd = (string)t.GetField("WorkingDirectory").GetValue(ld); bool ar = (bool)t.GetField("AutoRestart").GetValue(ld); return $"{disp}|{lkey:D}|{exe}|{margs}|{wd}|{ar}"; } /// /// Deterministic black/white bitmap whose pixels depend only on (x, y, seed), /// so both assemblies receive identical input pixels to pack. /// private static Bitmap MakePatternBitmap(int width, int height, int seed) { var bmp = new Bitmap(width, height); for (int y = 0; y < height; y++) { for (int x = 0; x < width; x++) { bool on = ((x * 31 + y * 17 + seed * 7) & 3) == 0; bmp.SetPixel(x, y, on ? Color.White : Color.Black); } } return bmp; } // Keep the proxy alive for the life of the AppDomain. public override object InitializeLifetimeService() => null; } }