1 using System;
2 using System.CodeDom.Compiler;
3 using System.Collections.Generic;
4 using System.IO;
5 using System.Linq;
6 using System.Text;
7 using Mono.Linker.Tests.Extensions;
8 
9 namespace Mono.Linker.Tests.TestCasesRunner {
10 	public class TestCaseCompiler {
11 		protected readonly TestCaseMetadaProvider _metadataProvider;
12 		protected readonly TestCaseSandbox _sandbox;
13 		protected readonly ILCompiler _ilCompiler;
14 
TestCaseCompiler(TestCaseSandbox sandbox, TestCaseMetadaProvider metadataProvider)15 		public TestCaseCompiler (TestCaseSandbox sandbox, TestCaseMetadaProvider metadataProvider)
16 			: this(sandbox, metadataProvider, new ILCompiler ())
17 		{
18 		}
19 
TestCaseCompiler(TestCaseSandbox sandbox, TestCaseMetadaProvider metadataProvider, ILCompiler ilCompiler)20 		public TestCaseCompiler (TestCaseSandbox sandbox, TestCaseMetadaProvider metadataProvider, ILCompiler ilCompiler)
21 		{
22 			_ilCompiler = ilCompiler;
23 			_sandbox = sandbox;
24 			_metadataProvider = metadataProvider;
25 		}
26 
CompileTestIn(NPath outputDirectory, string outputName, IEnumerable<string> sourceFiles, IEnumerable<string> references, IEnumerable<string> defines, NPath[] resources, string[] additionalArguments)27 		public NPath CompileTestIn (NPath outputDirectory, string outputName, IEnumerable<string> sourceFiles, IEnumerable<string> references, IEnumerable<string> defines, NPath[] resources, string[] additionalArguments)
28 		{
29 			var originalReferences = references.Select (r => r.ToNPath ()).ToArray ();
30 			var originalDefines = defines?.ToArray () ?? new string [0];
31 
32 			Prepare (outputDirectory);
33 
34 			var compiledReferences = CompileBeforeTestCaseAssemblies (outputDirectory, originalReferences, originalDefines).ToArray ();
35 			var allTestCaseReferences = originalReferences.Concat (compiledReferences).ToArray ();
36 
37 			var options = CreateOptionsForTestCase (
38 				outputDirectory.Combine (outputName),
39 				sourceFiles.Select (s => s.ToNPath ()).ToArray (),
40 				allTestCaseReferences,
41 				originalDefines,
42 				resources,
43 				additionalArguments);
44 			var testAssembly = CompileAssembly (options);
45 
46 
47 			// The compile after step is used by tests to mess around with the input to the linker.  Generally speaking, it doesn't seem like we would ever want to mess with the
48 			// expectations assemblies because this would undermine our ability to inspect them for expected results during ResultChecking.  The UnityLinker UnresolvedHandling tests depend on this
49 			// behavior of skipping the after test compile
50 			if (outputDirectory != _sandbox.ExpectationsDirectory)
51 				CompileAfterTestCaseAssemblies (outputDirectory, originalReferences, originalDefines);
52 
53 			return testAssembly;
54 		}
55 
Prepare(NPath outputDirectory)56 		protected virtual void Prepare (NPath outputDirectory)
57 		{
58 		}
59 
CreateOptionsForTestCase(NPath outputPath, NPath[] sourceFiles, NPath[] references, string[] defines, NPath[] resources, string[] additionalArguments)60 		protected virtual CompilerOptions CreateOptionsForTestCase (NPath outputPath, NPath[] sourceFiles, NPath[] references, string[] defines, NPath[] resources, string[] additionalArguments)
61 		{
62 			return new CompilerOptions
63 			{
64 				OutputPath = outputPath,
65 				SourceFiles = sourceFiles,
66 				References = references,
67 				Defines = defines.Concat (_metadataProvider.GetDefines ()).ToArray (),
68 				Resources = resources,
69 				AdditionalArguments = additionalArguments
70 			};
71 		}
72 
CreateOptionsForSupportingAssembly(SetupCompileInfo setupCompileInfo, NPath outputDirectory, NPath[] sourceFiles, NPath[] references, string[] defines)73 		protected virtual CompilerOptions CreateOptionsForSupportingAssembly (SetupCompileInfo setupCompileInfo, NPath outputDirectory, NPath[] sourceFiles, NPath[] references, string[] defines)
74 		{
75 			var allDefines = defines.Concat (setupCompileInfo.Defines ?? new string [0]).ToArray ();
76 			var allReferences = references.Concat (setupCompileInfo.References?.Select (p => MakeSupportingAssemblyReferencePathAbsolute (outputDirectory, p)) ?? new NPath [0]).ToArray ();
77 			return new CompilerOptions
78 			{
79 				OutputPath = outputDirectory.Combine (setupCompileInfo.OutputName),
80 				SourceFiles = sourceFiles,
81 				References = allReferences,
82 				Defines = allDefines
83 			};
84 		}
85 
CompileBeforeTestCaseAssemblies(NPath outputDirectory, NPath[] references, string[] defines)86 		private IEnumerable<NPath> CompileBeforeTestCaseAssemblies (NPath outputDirectory, NPath[] references, string[] defines)
87 		{
88 			foreach (var setupCompileInfo in _metadataProvider.GetSetupCompileAssembliesBefore ())
89 			{
90 				var options = CreateOptionsForSupportingAssembly (setupCompileInfo, outputDirectory, CollectSetupBeforeSourcesFiles (setupCompileInfo), references, defines);
91 				var output = CompileAssembly (options);
92 				if (setupCompileInfo.AddAsReference)
93 					yield return output;
94 			}
95 		}
96 
CompileAfterTestCaseAssemblies(NPath outputDirectory, NPath[] references, string[] defines)97 		private void CompileAfterTestCaseAssemblies (NPath outputDirectory, NPath[] references, string[] defines)
98 		{
99 			foreach (var setupCompileInfo in _metadataProvider.GetSetupCompileAssembliesAfter ())
100 			{
101 				var options = CreateOptionsForSupportingAssembly (setupCompileInfo, outputDirectory, CollectSetupAfterSourcesFiles (setupCompileInfo), references, defines);
102 				CompileAssembly (options);
103 			}
104 		}
105 
CollectSetupBeforeSourcesFiles(SetupCompileInfo info)106 		private NPath[] CollectSetupBeforeSourcesFiles (SetupCompileInfo info)
107 		{
108 			return CollectSourceFilesFrom (_sandbox.BeforeReferenceSourceDirectoryFor (info.OutputName));
109 		}
110 
CollectSetupAfterSourcesFiles(SetupCompileInfo info)111 		private NPath[] CollectSetupAfterSourcesFiles (SetupCompileInfo info)
112 		{
113 			return CollectSourceFilesFrom (_sandbox.AfterReferenceSourceDirectoryFor (info.OutputName));
114 		}
115 
CollectSourceFilesFrom(NPath directory)116 		private static NPath[] CollectSourceFilesFrom (NPath directory)
117 		{
118 			var sourceFiles = directory.Files ("*.cs").ToArray ();
119 			if (sourceFiles.Length > 0)
120 				return sourceFiles;
121 
122 			sourceFiles = directory.Files ("*.il").ToArray ();
123 			if (sourceFiles.Length > 0)
124 				return sourceFiles;
125 
126 			throw new FileNotFoundException ($"Didn't find any sources files in {directory}");
127 		}
128 
MakeSupportingAssemblyReferencePathAbsolute(NPath outputDirectory, string referenceFileName)129 		protected static NPath MakeSupportingAssemblyReferencePathAbsolute (NPath outputDirectory, string referenceFileName)
130 		{
131 			// Not a good idea to use a full path in a test, but maybe someone is trying to quickly test something locally
132 			if (Path.IsPathRooted (referenceFileName))
133 				return referenceFileName.ToNPath ();
134 
135 			var possiblePath = outputDirectory.Combine (referenceFileName);
136 			if (possiblePath.FileExists ())
137 				return possiblePath;
138 
139 			return referenceFileName.ToNPath();
140 		}
141 
CompileAssembly(CompilerOptions options)142 		protected NPath CompileAssembly (CompilerOptions options)
143 		{
144 			if (options.SourceFiles.Any (path => path.ExtensionWithDot == ".cs"))
145 				return CompileCSharpAssembly (options);
146 
147 			if (options.SourceFiles.Any (path => path.ExtensionWithDot == ".il"))
148 				return CompileIlAssembly (options);
149 
150 			throw new NotSupportedException ($"Unable to compile sources files with extension `{options.SourceFiles.First ().ExtensionWithDot}`");
151 		}
152 
CompileCSharpAssembly(CompilerOptions options)153 		protected virtual NPath CompileCSharpAssembly (CompilerOptions options)
154 		{
155 			var compilerOptions = CreateCodeDomCompilerOptions (options);
156 			var provider = CodeDomProvider.CreateProvider ("C#");
157 			var result = provider.CompileAssemblyFromFile (compilerOptions, options.SourceFiles.Select (p => p.ToString ()).ToArray ());
158 			if (!result.Errors.HasErrors)
159 				return compilerOptions.OutputAssembly.ToNPath ();
160 
161 			var errors = new StringBuilder ();
162 			foreach (var error in result.Errors)
163 				errors.AppendLine (error.ToString ());
164 			throw new Exception ("Compilation errors: " + errors);
165 		}
166 
CompileIlAssembly(CompilerOptions options)167 		protected NPath CompileIlAssembly (CompilerOptions options)
168 		{
169 			return _ilCompiler.Compile (options);
170 		}
171 
CreateCodeDomCompilerOptions(CompilerOptions options)172 		private CompilerParameters CreateCodeDomCompilerOptions (CompilerOptions options)
173 		{
174 			var compilerParameters = new CompilerParameters
175 			{
176 				OutputAssembly = options.OutputPath.ToString (),
177 				GenerateExecutable = options.OutputPath.FileName.EndsWith (".exe")
178 			};
179 
180 			compilerParameters.CompilerOptions = options.Defines?.Aggregate (string.Empty, (buff, arg) => $"{buff} /define:{arg}");
181 
182 			compilerParameters.ReferencedAssemblies.AddRange (options.References.Select (r => r.ToString ()).ToArray ());
183 
184 			if (options.Resources != null)
185 				compilerParameters.EmbeddedResources.AddRange (options.Resources.Select (r => r.ToString ()).ToArray ());
186 
187 			if (options.AdditionalArguments != null) {
188 				var combinedValues = options.AdditionalArguments.Aggregate (string.Empty, (buff, arg) => $"{buff} {arg}");
189 				compilerParameters.CompilerOptions = $"{compilerParameters.CompilerOptions} {combinedValues}";
190 			}
191 
192 			return compilerParameters;
193 		}
194 	}
195 }