You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

504 lines
20 KiB

7 years ago
9 years ago
9 years ago
9 years ago
  1. // Copyright (c) 2015 Daniel Grunwald
  2. //
  3. // Permission is hereby granted, free of charge, to any person obtaining a copy of this
  4. // software and associated documentation files (the "Software"), to deal in the Software
  5. // without restriction, including without limitation the rights to use, copy, modify, merge,
  6. // publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
  7. // to whom the Software is furnished to do so, subject to the following conditions:
  8. //
  9. // The above copyright notice and this permission notice shall be included in all copies or
  10. // substantial portions of the Software.
  11. //
  12. // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
  13. // INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
  14. // PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
  15. // FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
  16. // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
  17. // DEALINGS IN THE SOFTWARE.
  18. using System;
  19. using System.CodeDom.Compiler;
  20. using System.Collections.Generic;
  21. using System.Diagnostics;
  22. using System.IO;
  23. using System.Linq;
  24. using System.Reflection.Metadata;
  25. using System.Reflection.PortableExecutable;
  26. using System.Text;
  27. using System.Text.RegularExpressions;
  28. using System.Threading;
  29. using System.Threading.Tasks;
  30. using ICSharpCode.Decompiler.CSharp;
  31. using ICSharpCode.Decompiler.CSharp.OutputVisitor;
  32. using ICSharpCode.Decompiler.CSharp.Transforms;
  33. using ICSharpCode.Decompiler.Disassembler;
  34. using ICSharpCode.Decompiler.Metadata;
  35. using ICSharpCode.Decompiler.TypeSystem;
  36. using Microsoft.CodeAnalysis;
  37. using Microsoft.CodeAnalysis.CSharp;
  38. using Microsoft.CodeAnalysis.Emit;
  39. using Microsoft.CodeAnalysis.Text;
  40. using Microsoft.CSharp;
  41. using Microsoft.DiaSymReader.Tools;
  42. using NUnit.Framework;
  43. namespace ICSharpCode.Decompiler.Tests.Helpers
  44. {
  45. [Flags]
  46. public enum CompilerOptions
  47. {
  48. None,
  49. Optimize = 0x1,
  50. UseDebug = 0x2,
  51. Force32Bit = 0x4,
  52. Library = 0x8,
  53. UseRoslyn = 0x10,
  54. UseMcs = 0x20,
  55. }
  56. [Flags]
  57. public enum AssemblerOptions
  58. {
  59. None,
  60. UseDebug = 0x1,
  61. Force32Bit = 0x2,
  62. Library = 0x4,
  63. UseOwnDisassembler = 0x8,
  64. }
  65. public static partial class Tester
  66. {
  67. public static readonly string TestCasePath = Path.Combine(
  68. Path.GetDirectoryName(typeof(Tester).Assembly.Location),
  69. "../../../TestCases");
  70. public static string AssembleIL(string sourceFileName, AssemblerOptions options = AssemblerOptions.UseDebug)
  71. {
  72. string ilasmPath = Path.Combine(Environment.GetEnvironmentVariable("windir"), @"Microsoft.NET\Framework\v4.0.30319\ilasm.exe");
  73. string outputFile = Path.Combine(Path.GetDirectoryName(sourceFileName), Path.GetFileNameWithoutExtension(sourceFileName));
  74. string otherOptions = " ";
  75. if (options.HasFlag(AssemblerOptions.Force32Bit)) {
  76. outputFile += ".32";
  77. otherOptions += "/32BitPreferred ";
  78. }
  79. if (options.HasFlag(AssemblerOptions.Library)) {
  80. outputFile += ".dll";
  81. otherOptions += "/dll ";
  82. } else {
  83. outputFile += ".exe";
  84. otherOptions += "/exe ";
  85. }
  86. if (options.HasFlag(AssemblerOptions.UseDebug)) {
  87. otherOptions += "/debug ";
  88. }
  89. ProcessStartInfo info = new ProcessStartInfo(ilasmPath);
  90. info.Arguments = $"/nologo {otherOptions}/output=\"{outputFile}\" \"{sourceFileName}\"";
  91. info.RedirectStandardError = true;
  92. info.RedirectStandardOutput = true;
  93. info.UseShellExecute = false;
  94. Process process = Process.Start(info);
  95. var outputTask = process.StandardOutput.ReadToEndAsync();
  96. var errorTask = process.StandardError.ReadToEndAsync();
  97. Task.WaitAll(outputTask, errorTask);
  98. process.WaitForExit();
  99. Console.WriteLine("output: " + outputTask.Result);
  100. Console.WriteLine("errors: " + errorTask.Result);
  101. Assert.AreEqual(0, process.ExitCode, "ilasm failed");
  102. return outputFile;
  103. }
  104. public static string Disassemble(string sourceFileName, string outputFile, AssemblerOptions asmOptions)
  105. {
  106. if (asmOptions.HasFlag(AssemblerOptions.UseOwnDisassembler)) {
  107. using (var peFileStream = new FileStream(sourceFileName, FileMode.Open, FileAccess.Read))
  108. using (var peFile = new PEFile(sourceFileName, peFileStream))
  109. using (var writer = new StringWriter()) {
  110. var metadata = peFile.Metadata;
  111. var output = new PlainTextOutput(writer);
  112. ReflectionDisassembler rd = new ReflectionDisassembler(output, CancellationToken.None);
  113. rd.AssemblyResolver = new UniversalAssemblyResolver(sourceFileName, true, null);
  114. rd.DetectControlStructure = false;
  115. rd.WriteAssemblyReferences(metadata);
  116. if (metadata.IsAssembly)
  117. rd.WriteAssemblyHeader(peFile);
  118. output.WriteLine();
  119. rd.WriteModuleHeader(peFile, skipMVID: true);
  120. output.WriteLine();
  121. rd.WriteModuleContents(peFile);
  122. File.WriteAllText(outputFile, ReplacePrivImplDetails(writer.ToString()));
  123. }
  124. return outputFile;
  125. }
  126. string ildasmPath = SdkUtility.GetSdkPath("ildasm.exe");
  127. ProcessStartInfo info = new ProcessStartInfo(ildasmPath);
  128. info.Arguments = $"/nobar /utf8 /out=\"{outputFile}\" \"{sourceFileName}\"";
  129. info.RedirectStandardError = true;
  130. info.RedirectStandardOutput = true;
  131. info.UseShellExecute = false;
  132. Process process = Process.Start(info);
  133. var outputTask = process.StandardOutput.ReadToEndAsync();
  134. var errorTask = process.StandardError.ReadToEndAsync();
  135. Task.WaitAll(outputTask, errorTask);
  136. process.WaitForExit();
  137. Console.WriteLine("output: " + outputTask.Result);
  138. Console.WriteLine("errors: " + errorTask.Result);
  139. // Unlike the .imagebase directive (which is a fixed value when compiling with /deterministic),
  140. // the image base comment still varies... ildasm putting a random number here?
  141. string il = File.ReadAllText(outputFile);
  142. il = Regex.Replace(il, @"^// Image base: 0x[0-9A-F]+\r?\n", "", RegexOptions.Multiline);
  143. // and while we're at it, also remove the MVID
  144. il = Regex.Replace(il, @"^// MVID: \{[0-9A-F-]+\}\r?\n", "", RegexOptions.Multiline);
  145. // and the ildasm version info (varies from system to system)
  146. il = Regex.Replace(il, @"^// +Microsoft .* Disassembler\. +Version.*\r?\n", "", RegexOptions.Multiline);
  147. // copyright header "All rights reserved" is dependent on system language
  148. il = Regex.Replace(il, @"^// +Copyright .* Microsoft.*\r?\n", "", RegexOptions.Multiline);
  149. // filename may contain full path
  150. il = Regex.Replace(il, @"^// WARNING: Created Win32 resource file.*\r?\n", "", RegexOptions.Multiline);
  151. il = ReplacePrivImplDetails(il);
  152. File.WriteAllText(outputFile, il);
  153. return outputFile;
  154. }
  155. private static string ReplacePrivImplDetails(string il)
  156. {
  157. return Regex.Replace(il, @"'<PrivateImplementationDetails>\{[0-9A-F-]+\}'", "'<PrivateImplementationDetails>'");
  158. }
  159. static readonly Lazy<IEnumerable<MetadataReference>> defaultReferences = new Lazy<IEnumerable<MetadataReference>>(delegate {
  160. string refAsmPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFilesX86),
  161. @"Reference Assemblies\Microsoft\Framework\.NETFramework\v4.5");
  162. return new[]
  163. {
  164. MetadataReference.CreateFromFile(Path.Combine(refAsmPath, "mscorlib.dll")),
  165. MetadataReference.CreateFromFile(Path.Combine(refAsmPath, "System.dll")),
  166. MetadataReference.CreateFromFile(Path.Combine(refAsmPath, "System.Core.dll")),
  167. MetadataReference.CreateFromFile(Path.Combine(refAsmPath, @"Facades\System.Runtime.dll")),
  168. MetadataReference.CreateFromFile(Path.Combine(refAsmPath, "System.Xml.dll")),
  169. MetadataReference.CreateFromFile(Path.Combine(refAsmPath, "Microsoft.CSharp.dll")),
  170. MetadataReference.CreateFromFile(Path.Combine(refAsmPath, "Microsoft.VisualBasic.dll")),
  171. MetadataReference.CreateFromFile(typeof(ValueTuple).Assembly.Location),
  172. MetadataReference.CreateFromFile(typeof(Span<>).Assembly.Location),
  173. };
  174. });
  175. public static List<string> GetPreprocessorSymbols(CompilerOptions flags)
  176. {
  177. var preprocessorSymbols = new List<string>();
  178. if (flags.HasFlag(CompilerOptions.UseDebug)) {
  179. preprocessorSymbols.Add("DEBUG");
  180. }
  181. if (flags.HasFlag(CompilerOptions.Optimize)) {
  182. preprocessorSymbols.Add("OPT");
  183. }
  184. if (flags.HasFlag(CompilerOptions.UseRoslyn)) {
  185. preprocessorSymbols.Add("ROSLYN");
  186. preprocessorSymbols.Add("CS60");
  187. preprocessorSymbols.Add("CS70");
  188. preprocessorSymbols.Add("CS71");
  189. preprocessorSymbols.Add("CS72");
  190. preprocessorSymbols.Add("CS73");
  191. preprocessorSymbols.Add("VB11");
  192. preprocessorSymbols.Add("VB14");
  193. preprocessorSymbols.Add("VB15");
  194. } else if (flags.HasFlag(CompilerOptions.UseMcs)) {
  195. preprocessorSymbols.Add("MCS");
  196. } else {
  197. preprocessorSymbols.Add("LEGACY_CSC");
  198. preprocessorSymbols.Add("LEGACY_VBC");
  199. }
  200. return preprocessorSymbols;
  201. }
  202. public static CompilerResults CompileCSharp(string sourceFileName, CompilerOptions flags = CompilerOptions.UseDebug, string outputFileName = null)
  203. {
  204. List<string> sourceFileNames = new List<string> { sourceFileName };
  205. foreach (Match match in Regex.Matches(File.ReadAllText(sourceFileName), @"#include ""([\w\d./]+)""")) {
  206. sourceFileNames.Add(Path.GetFullPath(Path.Combine(Path.GetDirectoryName(sourceFileName), match.Groups[1].Value)));
  207. }
  208. var preprocessorSymbols = GetPreprocessorSymbols(flags);
  209. if (flags.HasFlag(CompilerOptions.UseRoslyn)) {
  210. var parseOptions = new CSharpParseOptions(preprocessorSymbols: preprocessorSymbols.ToArray(), languageVersion: Microsoft.CodeAnalysis.CSharp.LanguageVersion.Latest);
  211. var syntaxTrees = sourceFileNames.Select(f => SyntaxFactory.ParseSyntaxTree(File.ReadAllText(f), parseOptions, path: f));
  212. var compilation = CSharpCompilation.Create(Path.GetFileNameWithoutExtension(sourceFileName),
  213. syntaxTrees, defaultReferences.Value,
  214. new CSharpCompilationOptions(
  215. flags.HasFlag(CompilerOptions.Library) ? OutputKind.DynamicallyLinkedLibrary : OutputKind.ConsoleApplication,
  216. platform: flags.HasFlag(CompilerOptions.Force32Bit) ? Platform.X86 : Platform.AnyCpu,
  217. optimizationLevel: flags.HasFlag(CompilerOptions.Optimize) ? OptimizationLevel.Release : OptimizationLevel.Debug,
  218. allowUnsafe: true,
  219. deterministic: true
  220. ));
  221. CompilerResults results = new CompilerResults(new TempFileCollection());
  222. results.PathToAssembly = outputFileName ?? Path.GetTempFileName();
  223. var emitResult = compilation.Emit(results.PathToAssembly);
  224. if (!emitResult.Success) {
  225. StringBuilder b = new StringBuilder("Compiler error:");
  226. foreach (var diag in emitResult.Diagnostics) {
  227. b.AppendLine(diag.ToString());
  228. }
  229. throw new Exception(b.ToString());
  230. }
  231. return results;
  232. } else if (flags.HasFlag(CompilerOptions.UseMcs)) {
  233. CompilerResults results = new CompilerResults(new TempFileCollection());
  234. results.PathToAssembly = outputFileName ?? Path.GetTempFileName();
  235. string testBasePath = RoundtripAssembly.TestDir;
  236. if (!Directory.Exists(testBasePath)) {
  237. Assert.Ignore($"Compilation with mcs ignored: test directory '{testBasePath}' needs to be checked out separately." + Environment.NewLine +
  238. $"git clone https://github.com/icsharpcode/ILSpy-tests \"{testBasePath}\"");
  239. }
  240. string mcsPath = Path.Combine(testBasePath, @"mcs\2.6.4\bin\gmcs.bat");
  241. string otherOptions = " -unsafe -o" + (flags.HasFlag(CompilerOptions.Optimize) ? "+ " : "- ");
  242. if (flags.HasFlag(CompilerOptions.Library)) {
  243. otherOptions += "-t:library ";
  244. } else {
  245. otherOptions += "-t:exe ";
  246. }
  247. if (flags.HasFlag(CompilerOptions.UseDebug)) {
  248. otherOptions += "-g ";
  249. }
  250. if (flags.HasFlag(CompilerOptions.Force32Bit)) {
  251. otherOptions += "-platform:x86 ";
  252. } else {
  253. otherOptions += "-platform:anycpu ";
  254. }
  255. if (preprocessorSymbols.Count > 0) {
  256. otherOptions += " \"-d:" + string.Join(";", preprocessorSymbols) + "\" ";
  257. }
  258. ProcessStartInfo info = new ProcessStartInfo(mcsPath);
  259. info.Arguments = $"{otherOptions}-out:\"{Path.GetFullPath(results.PathToAssembly)}\" {string.Join(" ", sourceFileNames.Select(fn => '"' + Path.GetFullPath(fn) + '"'))}";
  260. info.RedirectStandardError = true;
  261. info.RedirectStandardOutput = true;
  262. info.UseShellExecute = false;
  263. Console.WriteLine($"\"{info.FileName}\" {info.Arguments}");
  264. Process process = Process.Start(info);
  265. var outputTask = process.StandardOutput.ReadToEndAsync();
  266. var errorTask = process.StandardError.ReadToEndAsync();
  267. Task.WaitAll(outputTask, errorTask);
  268. process.WaitForExit();
  269. Console.WriteLine("output: " + outputTask.Result);
  270. Console.WriteLine("errors: " + errorTask.Result);
  271. Assert.AreEqual(0, process.ExitCode, "mcs failed");
  272. return results;
  273. } else {
  274. var provider = new CSharpCodeProvider(new Dictionary<string, string> { { "CompilerVersion", "v4.0" } });
  275. CompilerParameters options = new CompilerParameters();
  276. options.GenerateExecutable = !flags.HasFlag(CompilerOptions.Library);
  277. options.CompilerOptions = "/unsafe /o" + (flags.HasFlag(CompilerOptions.Optimize) ? "+" : "-");
  278. options.CompilerOptions += (flags.HasFlag(CompilerOptions.UseDebug) ? " /debug" : "");
  279. options.CompilerOptions += (flags.HasFlag(CompilerOptions.Force32Bit) ? " /platform:anycpu32bitpreferred" : "");
  280. if (preprocessorSymbols.Count > 0) {
  281. options.CompilerOptions += " /d:" + string.Join(";", preprocessorSymbols);
  282. }
  283. if (outputFileName != null) {
  284. options.OutputAssembly = outputFileName;
  285. }
  286. options.ReferencedAssemblies.Add("System.dll");
  287. options.ReferencedAssemblies.Add("System.Core.dll");
  288. options.ReferencedAssemblies.Add("System.Xml.dll");
  289. options.ReferencedAssemblies.Add("Microsoft.CSharp.dll");
  290. options.ReferencedAssemblies.Add("Microsoft.VisualBasic.dll");
  291. CompilerResults results = provider.CompileAssemblyFromFile(options, sourceFileNames.ToArray());
  292. if (results.Errors.Cast<CompilerError>().Any(e => !e.IsWarning)) {
  293. StringBuilder b = new StringBuilder("Compiler error:");
  294. foreach (var error in results.Errors) {
  295. b.AppendLine(error.ToString());
  296. }
  297. throw new Exception(b.ToString());
  298. }
  299. return results;
  300. }
  301. }
  302. internal static DecompilerSettings GetSettings(CompilerOptions cscOptions)
  303. {
  304. if (cscOptions.HasFlag(CompilerOptions.UseRoslyn)) {
  305. return new DecompilerSettings(CSharp.LanguageVersion.Latest);
  306. } else {
  307. return new DecompilerSettings(CSharp.LanguageVersion.CSharp5);
  308. }
  309. }
  310. public static void CompileCSharpWithPdb(string assemblyName, Dictionary<string, string> sourceFiles, PdbToXmlOptions options)
  311. {
  312. var parseOptions = new CSharpParseOptions(languageVersion: Microsoft.CodeAnalysis.CSharp.LanguageVersion.Latest);
  313. List<EmbeddedText> embeddedTexts = new List<EmbeddedText>();
  314. List<SyntaxTree> syntaxTrees = new List<SyntaxTree>();
  315. foreach (KeyValuePair<string, string> file in sourceFiles) {
  316. var sourceText = SourceText.From(file.Value, new UTF8Encoding(false), SourceHashAlgorithm.Sha256);
  317. syntaxTrees.Add(SyntaxFactory.ParseSyntaxTree(sourceText, parseOptions, file.Key));
  318. embeddedTexts.Add(EmbeddedText.FromSource(file.Key, sourceText));
  319. }
  320. var compilation = CSharpCompilation.Create(Path.GetFileNameWithoutExtension(assemblyName),
  321. syntaxTrees, defaultReferences.Value,
  322. new CSharpCompilationOptions(
  323. OutputKind.DynamicallyLinkedLibrary,
  324. platform: Platform.AnyCpu,
  325. optimizationLevel: OptimizationLevel.Debug,
  326. allowUnsafe: true,
  327. deterministic: true
  328. ));
  329. using (FileStream peStream = File.Open(assemblyName + ".dll", FileMode.OpenOrCreate, FileAccess.ReadWrite))
  330. using (FileStream pdbStream = File.Open(assemblyName + ".pdb", FileMode.OpenOrCreate, FileAccess.ReadWrite)) {
  331. var emitResult = compilation.Emit(peStream, pdbStream, options: new EmitOptions(debugInformationFormat: DebugInformationFormat.PortablePdb), embeddedTexts: embeddedTexts);
  332. if (!emitResult.Success) {
  333. StringBuilder b = new StringBuilder("Compiler error:");
  334. foreach (var diag in emitResult.Diagnostics) {
  335. b.AppendLine(diag.ToString());
  336. }
  337. throw new Exception(b.ToString());
  338. }
  339. }
  340. }
  341. internal static string GetSuffix(CompilerOptions cscOptions)
  342. {
  343. string suffix = "";
  344. if ((cscOptions & CompilerOptions.Optimize) != 0)
  345. suffix += ".opt";
  346. if ((cscOptions & CompilerOptions.Force32Bit) != 0)
  347. suffix += ".32";
  348. if ((cscOptions & CompilerOptions.UseDebug) != 0)
  349. suffix += ".dbg";
  350. if ((cscOptions & CompilerOptions.UseRoslyn) != 0)
  351. suffix += ".roslyn";
  352. if ((cscOptions & CompilerOptions.UseMcs) != 0)
  353. suffix += ".mcs";
  354. return suffix;
  355. }
  356. public static int Run(string assemblyFileName, out string output, out string error)
  357. {
  358. ProcessStartInfo info = new ProcessStartInfo(assemblyFileName);
  359. info.RedirectStandardError = true;
  360. info.RedirectStandardOutput = true;
  361. info.UseShellExecute = false;
  362. Process process = Process.Start(info);
  363. var outputTask = process.StandardOutput.ReadToEndAsync();
  364. var errorTask = process.StandardError.ReadToEndAsync();
  365. Task.WaitAll(outputTask, errorTask);
  366. process.WaitForExit();
  367. output = outputTask.Result;
  368. error = errorTask.Result;
  369. return process.ExitCode;
  370. }
  371. public static string DecompileCSharp(string assemblyFileName, DecompilerSettings settings = null)
  372. {
  373. if (settings == null)
  374. settings = new DecompilerSettings();
  375. using (var file = new FileStream(assemblyFileName, FileMode.Open, FileAccess.Read)) {
  376. var module = new PEFile(assemblyFileName, file, PEStreamOptions.PrefetchEntireImage);
  377. var resolver = new UniversalAssemblyResolver(assemblyFileName, false,
  378. module.Reader.DetectTargetFrameworkId(), PEStreamOptions.PrefetchMetadata);
  379. resolver.AddSearchDirectory(Path.GetDirectoryName(typeof(Span<>).Assembly.Location));
  380. var typeSystem = new DecompilerTypeSystem(module, resolver, settings);
  381. CSharpDecompiler decompiler = new CSharpDecompiler(typeSystem, settings);
  382. decompiler.AstTransforms.Insert(0, new RemoveEmbeddedAtttributes());
  383. decompiler.AstTransforms.Insert(0, new RemoveCompilerAttribute());
  384. decompiler.AstTransforms.Add(new EscapeInvalidIdentifiers());
  385. var syntaxTree = decompiler.DecompileWholeModuleAsSingleFile();
  386. StringWriter output = new StringWriter();
  387. var visitor = new CSharpOutputVisitor(output, FormattingOptionsFactory.CreateSharpDevelop());
  388. syntaxTree.AcceptVisitor(visitor);
  389. string fileName = Path.GetTempFileName();
  390. File.WriteAllText(fileName, output.ToString());
  391. return fileName;
  392. }
  393. }
  394. public static void RunAndCompareOutput(string testFileName, string outputFile, string decompiledOutputFile, string decompiledCodeFile = null)
  395. {
  396. string output1, output2, error1, error2;
  397. int result1 = Tester.Run(outputFile, out output1, out error1);
  398. int result2 = Tester.Run(decompiledOutputFile, out output2, out error2);
  399. Assert.AreEqual(0, result1, "Exit code != 0; did the test case crash?" + Environment.NewLine + error1);
  400. Assert.AreEqual(0, result2, "Exit code != 0; did the decompiled code crash?" + Environment.NewLine + error2);
  401. if (output1 != output2 || error1 != error2) {
  402. StringBuilder b = new StringBuilder();
  403. b.AppendLine($"Test {testFileName} failed: output does not match.");
  404. if (decompiledCodeFile != null) {
  405. b.AppendLine($"Decompiled code in {decompiledCodeFile}:line 1");
  406. }
  407. if (error1 != error2) {
  408. b.AppendLine("Got different error output.");
  409. b.AppendLine("Original error:");
  410. b.AppendLine(error1);
  411. b.AppendLine();
  412. b.AppendLine("Error after de+re-compiling:");
  413. b.AppendLine(error2);
  414. b.AppendLine();
  415. }
  416. if (output1 != output2) {
  417. string outputFileName = Path.Combine(Path.GetTempPath(), Path.GetFileNameWithoutExtension(testFileName));
  418. File.WriteAllText(outputFileName + ".original.out", output1);
  419. File.WriteAllText(outputFileName + ".decompiled.out", output2);
  420. int diffLine = 0;
  421. string lastHeader = null;
  422. Tuple<string, string> errorItem = null;
  423. foreach (var pair in output1.Replace("\r", "").Split('\n').Zip(output2.Replace("\r", "").Split('\n'), Tuple.Create)) {
  424. diffLine++;
  425. if (pair.Item1 != pair.Item2) {
  426. errorItem = pair;
  427. break;
  428. }
  429. if (pair.Item1.EndsWith(":", StringComparison.Ordinal)) {
  430. lastHeader = pair.Item1;
  431. }
  432. }
  433. b.AppendLine($"Output differs; first difference in line {diffLine}");
  434. if (lastHeader != null) {
  435. b.AppendLine(lastHeader);
  436. }
  437. b.AppendLine($"{outputFileName}.original.out:line {diffLine}");
  438. b.AppendLine(errorItem.Item1);
  439. b.AppendLine($"{outputFileName}.decompiled.out:line {diffLine}");
  440. b.AppendLine(errorItem.Item2);
  441. }
  442. Assert.Fail(b.ToString());
  443. }
  444. }
  445. }
  446. }