|
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Threading;
using Microsoft.CodeAnalysis.CSharp.Test.Utilities;
using Roslyn.Test.Utilities;
using Xunit;
using Xunit.Abstractions;
namespace Microsoft.CodeAnalysis.Rebuild.UnitTests
{
public sealed partial class RebuildCommandLineTests : CSharpTestBase
{
private record CommandInfo(string CommandLine, string PeFileName, string? PdbFileName, string? CommandLineSuffix = null);
internal static BuildPaths BuildPaths { get; } = TestableCompiler.StandardBuildPaths;
internal static string RootDirectory { get; } = TestableCompiler.RootDirectory;
internal static string OutputDirectory { get; } = Path.Combine(TestableCompiler.RootDirectory, "output");
public ITestOutputHelper TestOutputHelper { get; }
public Dictionary<string, TestableFile> FilePathToStreamMap { get; } = new Dictionary<string, TestableFile>(StringComparer.OrdinalIgnoreCase);
public RebuildCommandLineTests(ITestOutputHelper testOutputHelper)
{
TestOutputHelper = testOutputHelper;
}
private void AddSourceFile(string filePath, string content)
{
FilePathToStreamMap.Add(Path.Combine(BuildPaths.WorkingDirectory, filePath), new TestableFile(content));
}
private void AddReference(string filePath, byte[] imageBytes)
{
FilePathToStreamMap.Add(Path.Combine(BuildPaths.SdkDirectory!, filePath), new TestableFile(imageBytes));
}
private void AddOutputFile(ref string? filePath)
{
if (filePath is object)
{
filePath = Path.Combine(OutputDirectory, filePath);
FilePathToStreamMap.Add(filePath, new TestableFile());
}
}
private static IEnumerable<CommandInfo> PermutateDllKinds(CommandInfo commandInfo)
{
yield return commandInfo with
{
CommandLine = commandInfo.CommandLine + " /target:library",
PeFileName = Path.ChangeExtension(commandInfo.PeFileName, "dll"),
};
yield return commandInfo with
{
CommandLine = commandInfo.CommandLine + " /target:module",
PeFileName = Path.ChangeExtension(commandInfo.PeFileName, "netmodule"),
};
yield return commandInfo with
{
CommandLine = commandInfo.CommandLine + " /target:winmdobj",
PeFileName = Path.ChangeExtension(commandInfo.PeFileName, "dll"),
};
}
private static IEnumerable<CommandInfo> PermutateExeKinds(CommandInfo commandInfo)
{
yield return commandInfo with
{
CommandLine = commandInfo.CommandLine + " /target:exe",
PeFileName = Path.ChangeExtension(commandInfo.PeFileName, "exe"),
};
yield return commandInfo with
{
CommandLine = commandInfo.CommandLine + " /target:winexe",
PeFileName = Path.ChangeExtension(commandInfo.PeFileName, "exe"),
};
yield return commandInfo with
{
CommandLine = commandInfo.CommandLine + " /target:appcontainerexe",
PeFileName = Path.ChangeExtension(commandInfo.PeFileName, "exe"),
};
}
private void VerifyRoundTrip(CommonCompiler commonCompiler, string peFilePath, string? pdbFilePath = null, CancellationToken cancellationToken = default)
{
Assert.True(commonCompiler.Arguments.CompilationOptions.Deterministic);
var (result, output) = commonCompiler.Run(cancellationToken);
TestOutputHelper.WriteLine(output);
Assert.Equal(0, result);
var peStream = FilePathToStreamMap[peFilePath].GetStream();
var pdbStream = pdbFilePath is object ? FilePathToStreamMap[pdbFilePath].GetStream() : null;
using var writer = new StringWriter();
var compilation = commonCompiler.CreateCompilation(
writer,
touchedFilesLogger: null,
errorLoggerOpt: null,
analyzerConfigOptions: default,
globalConfigOptions: default);
AssertEx.NotNull(compilation);
RoundTripUtil.VerifyCompilationOptions(commonCompiler.Arguments.CompilationOptions, compilation.Options);
RoundTripUtil.VerifyRoundTrip(
peStream,
pdbStream,
Path.GetFileName(peFilePath),
new CompilationRebuildArtifactResolver(compilation));
}
private void AddCSharpSourceFiles()
{
AddSourceFile("hw.cs", @"
using System;
Console.WriteLine(""Hello World"");
");
AddSourceFile("lib1.cs", @"
using System;
class Library
{
void Method()
{
var lib = new Library();
Console.WriteLine(lib);
}
}
");
AddSourceFile("lib2.cs", @"
extern alias SystemRuntime;
using System;
class Library
{
void Method()
{
System.Reflection.Assembly a1 = null;
SystemRuntime::System.Reflection.Assembly a2 = null;
Console.WriteLine(a1);
Console.WriteLine(a2);
}
}
");
AddSourceFile("lib3.cs", @"
extern alias SystemRuntime1;
extern alias SystemRuntime2;
using System;
class Library
{
void Method()
{
System.Reflection.Assembly a1 = null;
SystemRuntime1::System.Reflection.Assembly a2 = null;
SystemRuntime2::System.Reflection.Assembly a3 = null;
Console.WriteLine(a1);
Console.WriteLine(a2);
Console.WriteLine(a3);
}
}
");
AddSourceFile("lib4.cs", @"
using System;
class Library4
{
#line 42 ""data.txt""
void Method()
{
}
}
");
AddSourceFile("lib5.cs", @"
using System;
class Library5
{
#line 42 ""data.txt""
void Method()
{
}
}
");
AddSourceFile(Path.Combine("dir1", "lib1.cs"), @"
using System;
namespace Nested
{
#line 13 ""data.txt""
class NestedLibrary
{
void Method()
{
}
}
}
");
}
public static IEnumerable<object?[]> GetCSharpData()
{
var list = new List<object?[]>();
Permutate(
new CommandInfo("hw.cs", "test.exe", null),
PermutateOptimizations, PermutateExeKinds, PermutatePdbFormat);
Permutate(new CommandInfo("lib1.cs", "test.dll", null),
PermutateOptimizations, PermutateDllKinds, PermutatePdbFormat, PermutatePathMap);
Permutate(new CommandInfo("lib2.cs /target:library /r:SystemRuntime=System.Runtime.dll /debug:embedded", "test.dll", null),
PermutateOptimizations);
Permutate(new CommandInfo("lib3.cs /target:library", "test.dll", null),
PermutateOptimizations, PermutateExternAlias, PermutatePdbFormat);
Permutate(new CommandInfo("lib4.cs /target:library", "test.dll", null),
PermutateOptimizations, PermutatePdbFormat, PermutatePathMap);
// This uses a #line directive with the same file name but in different source directories.
// Need to make sure that we map the same file name but different base paths correctly
Permutate(new CommandInfo($"lib4.cs {Path.Combine("dir1", "lib1.cs")} /target:library", "test.dll", null),
PermutatePdbFormat, PermutatePathMap);
Permutate(new CommandInfo("lib4.cs lib5.cs /target:library", "test.dll", null),
PermutateOptimizations, PermutatePdbFormat, PermutatePathMap);
return list;
void Permutate(CommandInfo commandInfo, params Func<CommandInfo, IEnumerable<CommandInfo>>[] permutations)
{
IEnumerable<CommandInfo> e = new[] { commandInfo };
foreach (var p in permutations)
{
e = e.SelectMany(p);
}
Add(e);
}
static IEnumerable<CommandInfo> PermutatePathMap(CommandInfo commandInfo)
{
yield return commandInfo;
yield return commandInfo with
{
CommandLine = commandInfo.CommandLine + $" /pathmap:{RootDirectory}=/root/test",
};
yield return commandInfo with
{
CommandLine = commandInfo.CommandLine + $@" /pathmap:{RootDirectory}=j:\other_root",
};
// Path map doesn't care about path legality, it's a simple substitute
yield return commandInfo with
{
CommandLine = commandInfo.CommandLine + $@" /pathmap:""{RootDirectory}=???""",
};
}
// Permutate the alias before and after the standard references so that we make sure the
// rebuild is resistent to the ordering of aliases.
static IEnumerable<CommandInfo> PermutateExternAlias(CommandInfo commandInfo)
{
var alias = @" /r:SystemRuntime1=System.Runtime.dll /r:SystemRuntime2=System.Runtime.dll";
yield return commandInfo with
{
CommandLine = commandInfo.CommandLine + alias
};
yield return commandInfo with
{
CommandLineSuffix = commandInfo.CommandLineSuffix + alias
};
}
static IEnumerable<CommandInfo> PermutatePdbFormat(CommandInfo commandInfo)
{
yield return commandInfo with
{
CommandLine = commandInfo.CommandLine + " /debug:portable",
PdbFileName = "test.pdb"
};
yield return commandInfo with
{
CommandLine = commandInfo.CommandLine + " /debug:embedded"
};
}
static IEnumerable<CommandInfo> PermutateOptimizations(CommandInfo commandInfo)
{
// No options at all for optimization
yield return commandInfo;
yield return commandInfo with
{
CommandLine = commandInfo.CommandLine + " /debug+ /optimize+"
};
yield return commandInfo with
{
CommandLine = commandInfo.CommandLine + " /debug+ /optimize-"
};
yield return commandInfo with
{
CommandLine = commandInfo.CommandLine + " /optimize-"
};
yield return commandInfo with
{
CommandLine = commandInfo.CommandLine + " /optimize+"
};
}
void Add(IEnumerable<CommandInfo> commandInfos)
{
foreach (var commandInfo in commandInfos)
{
list.Add(new object?[] { commandInfo.CommandLine, commandInfo.PeFileName, commandInfo.PdbFileName, commandInfo.CommandLineSuffix });
}
}
}
[Theory]
[MemberData(nameof(GetCSharpData))]
public void CSharp(string commandLine, string peFilePath, string? pdbFilePath, string? commandLineSuffix)
{
TestOutputHelper.WriteLine($"Command Line: {commandLine}");
AddCSharpSourceFiles();
var args = new List<string>(commandLine.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries));
args.Add("/nostdlib");
args.Add("/deterministic");
AddOutputFile(ref peFilePath!);
args.Add($"/out:{peFilePath}");
AddOutputFile(ref pdbFilePath);
if (pdbFilePath is object)
{
args.Add($"/pdb:{pdbFilePath}");
}
if (commandLineSuffix is object)
{
args.AddRange(commandLineSuffix.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries));
}
TestOutputHelper.WriteLine($"Final Line: {string.Join(" ", args)}");
var compiler = TestableCompiler.CreateCSharpNetCoreApp(
TestableFileSystem.CreateForMap(FilePathToStreamMap),
BuildPaths,
args);
VerifyRoundTrip(compiler.Compiler, peFilePath, pdbFilePath);
}
private void AddVisualBasicSourceFiles()
{
AddSourceFile("hw.vb", @"
Imports System
Public Module M
Public Sub Main()
Console.WriteLine(CStr(True))
End Sub
End Module
");
AddSourceFile("lib1.vb", @"
Imports System
Public Module M
Public Function Add(left As Integer, right As Integer) As Integer
return left + right
End Function
End Module
");
AddSourceFile("lib2.vb", @"
Imports System
Public Module Lib2
#ExternalSource(""data.txt"", 30)
Public Function Add(left As Integer, right As Integer) As Integer
return left + right
End Function
#End ExternalSource
End Module
");
AddSourceFile(Path.Combine("dir1", "lib1.vb"), @"
Imports System
Namespace Nested
Public Module Lib1
#ExternalSource(""data.txt"", 30)
Public Function Add(left As Integer, right As Integer) As Integer
return left + right
End Function
#End ExternalSource
End Module
End Namespace
");
}
public static IEnumerable<object?[]> GetVisualBasicData()
{
var list = new List<object?[]>();
Permutate(
new CommandInfo("hw.vb /debug:embedded", "test.exe", null),
PermutateOptimizations, PermutateRuntime, PermutateExeKinds);
Permutate(
new CommandInfo("lib1.vb /target:library /debug:embedded", "test.dll", null),
PermutateOptimizations, PermutateRuntime);
Permutate(
new CommandInfo(@"lib1.vb /debug:embedded /d:_MYTYPE=""Empty"" /vbruntime:Microsoft.VisualBasic.dll", "test.dll", null),
PermutateOptimizations, PermutateDllKinds);
Permutate(
new CommandInfo("lib2.vb /target:library /debug:embedded", "test.dll", null),
PermutatePathMap, PermutateRuntime);
// This uses a #ExternalSource directive with the same file name but in different source directories.
// Need to make sure that we map the same file name but different base paths correctly
Permutate(
new CommandInfo(@$"lib2.vb {Path.Combine("dir1", "lib1.vb")} /target:library /debug:embedded", "test.dll", null),
PermutatePathMap, PermutateRuntime);
return list;
void Permutate(CommandInfo commandInfo, params Func<CommandInfo, IEnumerable<CommandInfo>>[] permutations)
{
IEnumerable<CommandInfo> e = new[] { commandInfo };
foreach (var p in permutations)
{
e = e.SelectMany(p);
}
Add(e);
}
static IEnumerable<CommandInfo> PermutatePathMap(CommandInfo commandInfo)
{
yield return commandInfo;
yield return commandInfo with
{
CommandLine = commandInfo.CommandLine + $" /pathmap:{RootDirectory}=/root/test",
};
}
static IEnumerable<CommandInfo> PermutateOptimizations(CommandInfo commandInfo)
{
// No options at all for optimization
yield return commandInfo;
yield return commandInfo with
{
CommandLine = commandInfo.CommandLine + " /debug+ /optimize+"
};
yield return commandInfo with
{
CommandLine = commandInfo.CommandLine + " /debug+ /optimize-"
};
yield return commandInfo with
{
CommandLine = commandInfo.CommandLine + " /optimize-"
};
yield return commandInfo with
{
CommandLine = commandInfo.CommandLine + " /optimize+"
};
}
static IEnumerable<CommandInfo> PermutateRuntime(CommandInfo commandInfo)
{
yield return commandInfo with
{
CommandLine = commandInfo.CommandLine + " /vbruntime*"
};
yield return commandInfo with
{
CommandLine = commandInfo.CommandLine + @" /d:_MYTYPE=""Empty"" /vbruntime:Microsoft.VisualBasic.dll"
};
}
void Add(IEnumerable<CommandInfo> commandInfos)
{
foreach (var commandInfo in commandInfos)
{
list.Add(new object?[] { commandInfo.CommandLine, commandInfo.PeFileName, commandInfo.PdbFileName });
}
}
}
[ConditionalTheory(typeof(IsEnglishLocal))]
[MemberData(nameof(GetVisualBasicData))]
public void VisualBasic(string commandLine, string peFilePath, string? pdbFilePath)
{
TestOutputHelper.WriteLine($"Command Line: {commandLine}");
AddVisualBasicSourceFiles();
var args = new List<string>(commandLine.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries));
args.Add("/nostdlib");
args.Add("/deterministic");
AddOutputFile(ref peFilePath!);
args.Add($"/out:{peFilePath}");
AddOutputFile(ref pdbFilePath);
if (pdbFilePath is object)
{
args.Add($"/pdb:{pdbFilePath}");
}
TestOutputHelper.WriteLine($"Final Line: {string.Join(" ", args)}");
var compiler = TestableCompiler.CreateBasicNetCoreApp(
TestableFileSystem.CreateForMap(FilePathToStreamMap),
BuildPaths,
BasicRuntimeOption.Manual,
args);
VerifyRoundTrip(compiler.Compiler, peFilePath, pdbFilePath);
}
}
}
|