Skip to content
79 changes: 79 additions & 0 deletions src/Tasks.UnitTests/WriteCodeFragment_Tests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using Microsoft.Build.Framework;
Comment thread
JanProvaznik marked this conversation as resolved.
using Microsoft.Build.Shared;
using Microsoft.Build.Tasks;
using Microsoft.Build.Utilities;
Expand All @@ -27,6 +28,7 @@ public class WriteCodeFragment_Tests
public void InvalidLanguage()
{
WriteCodeFragment task = new WriteCodeFragment();
task.TaskEnvironment = TaskEnvironmentHelper.CreateForTest();
MockEngine engine = new MockEngine(true);
task.BuildEngine = engine;
task.Language = "xx";
Expand All @@ -44,6 +46,7 @@ public void InvalidLanguage()
public void NoLanguage()
{
WriteCodeFragment task = new WriteCodeFragment();
task.TaskEnvironment = TaskEnvironmentHelper.CreateForTest();
MockEngine engine = new MockEngine(true);
task.BuildEngine = engine;
task.OutputFile = new TaskItem("foo");
Expand All @@ -60,6 +63,7 @@ public void NoLanguage()
public void NoFileOrDirectory()
{
WriteCodeFragment task = new WriteCodeFragment();
task.TaskEnvironment = TaskEnvironmentHelper.CreateForTest();
MockEngine engine = new MockEngine(true);
task.BuildEngine = engine;
task.Language = "c#";
Expand All @@ -76,6 +80,7 @@ public void NoFileOrDirectory()
public void CombineFileDirectory()
{
WriteCodeFragment task = new WriteCodeFragment();
task.TaskEnvironment = TaskEnvironmentHelper.CreateForTest();
MockEngine engine = new MockEngine(true);
task.BuildEngine = engine;
task.Language = "c#";
Expand Down Expand Up @@ -145,6 +150,7 @@ public void FileNameNoDirectory()
using TestEnvironment env = TestEnvironment.Create();
var file = env.ExpectFile(Directory.GetCurrentDirectory(), ".tmp");
WriteCodeFragment task = new WriteCodeFragment();
task.TaskEnvironment = TaskEnvironmentHelper.CreateForTest();
MockEngine engine = new MockEngine(true);
task.BuildEngine = engine;
task.Language = "c#";
Expand All @@ -167,6 +173,7 @@ public void FileNameNoDirectory()
public void DirectoryAndRootedFile()
{
WriteCodeFragment task = new WriteCodeFragment();
task.TaskEnvironment = TaskEnvironmentHelper.CreateForTest();
MockEngine engine = new MockEngine(true);
task.BuildEngine = engine;
task.Language = "c#";
Expand Down Expand Up @@ -202,6 +209,7 @@ public void NoAttributesShouldEmitNoFile()
}

WriteCodeFragment task = new WriteCodeFragment();
task.TaskEnvironment = TaskEnvironmentHelper.CreateForTest();
MockEngine engine = new MockEngine(true);
task.BuildEngine = engine;
task.Language = "c#";
Expand Down Expand Up @@ -229,6 +237,7 @@ public void NoAttributesShouldEmitNoFile2()
}

WriteCodeFragment task = new WriteCodeFragment();
task.TaskEnvironment = TaskEnvironmentHelper.CreateForTest();
MockEngine engine = new MockEngine(true);
task.BuildEngine = engine;
task.Language = "c#";
Expand All @@ -248,6 +257,7 @@ public void NoAttributesShouldEmitNoFile2()
public void InvalidFilePath()
{
WriteCodeFragment task = new WriteCodeFragment();
task.TaskEnvironment = TaskEnvironmentHelper.CreateForTest();
MockEngine engine = new MockEngine(true);
task.BuildEngine = engine;
task.Language = "c#";
Expand All @@ -266,6 +276,7 @@ public void InvalidFilePath()
public void InvalidDirectoryPath()
{
WriteCodeFragment task = new WriteCodeFragment();
task.TaskEnvironment = TaskEnvironmentHelper.CreateForTest();
MockEngine engine = new MockEngine(true);
task.BuildEngine = engine;
task.Language = "c#";
Expand All @@ -288,6 +299,7 @@ public void OneAttributeNoParams()
try
{
WriteCodeFragment task = new WriteCodeFragment();
task.TaskEnvironment = TaskEnvironmentHelper.CreateForTest();
MockEngine engine = new MockEngine(true);
task.BuildEngine = engine;
TaskItem attribute = new TaskItem("System.AssemblyTrademarkAttribute");
Expand Down Expand Up @@ -317,6 +329,7 @@ public void OneAttributeNoParams()
public void OneAttributeNoParamsVb()
{
WriteCodeFragment task = new WriteCodeFragment();
task.TaskEnvironment = TaskEnvironmentHelper.CreateForTest();
MockEngine engine = new MockEngine(true);
task.BuildEngine = engine;
TaskItem attribute = new TaskItem("System.AssemblyTrademarkAttribute");
Expand All @@ -340,6 +353,7 @@ public void OneAttributeNoParamsVb()
public void TwoAttributes()
{
WriteCodeFragment task = new WriteCodeFragment();
task.TaskEnvironment = TaskEnvironmentHelper.CreateForTest();
MockEngine engine = new MockEngine(true);
task.BuildEngine = engine;
TaskItem attribute1 = new TaskItem("AssemblyTrademarkAttribute");
Expand Down Expand Up @@ -369,6 +383,7 @@ public void TwoAttributes()
public void ToDirectory()
{
WriteCodeFragment task = new WriteCodeFragment();
task.TaskEnvironment = TaskEnvironmentHelper.CreateForTest();
MockEngine engine = new MockEngine(true);
task.BuildEngine = engine;
TaskItem attribute = new TaskItem("System.AssemblyTrademarkAttribute");
Expand Down Expand Up @@ -406,6 +421,55 @@ public void ToDirectoryAndDirectoryDoesNotExist()
Assert.Equal(".cs", task.OutputFile.ItemSpec.Substring(task.OutputFile.ItemSpec.Length - 3));
}

/// <summary>
/// When OutputDirectory is relative and OutputFile is not specified, the resulting OutputFile should be relative.
/// </summary>
[Fact]
public void RelativeOutputDirectoryProducesRelativeOutputFile()
{
using TestEnvironment env = TestEnvironment.Create();

// Create an actual folder and get a relative path to it
string absoluteFolder = env.CreateFolder().Path;
string relativeFolder = Path.GetFileName(absoluteFolder);

// Change current directory to the parent so the relative path works
string originalDir = Directory.GetCurrentDirectory();
try
{
Directory.SetCurrentDirectory(Path.GetDirectoryName(absoluteFolder));

WriteCodeFragment task = new WriteCodeFragment();
task.TaskEnvironment = TaskEnvironmentHelper.CreateForTest();
MockEngine engine = new MockEngine(true);
task.BuildEngine = engine;
TaskItem attribute = new TaskItem("System.AssemblyTrademarkAttribute");
task.AssemblyAttributes = new TaskItem[] { attribute };
task.Language = "c#";
task.OutputDirectory = new TaskItem(relativeFolder);
bool result = task.Execute();

result.ShouldBeTrue(engine.Log);

// The output file should be relative (not rooted) since OutputDirectory was relative
Path.IsPathRooted(task.OutputFile.ItemSpec).ShouldBeFalse("OutputFile should be relative when OutputDirectory is relative");

// The output file should start with the relative folder name
task.OutputFile.ItemSpec.ShouldStartWith(relativeFolder);

// Cleanup the generated file
string absoluteOutputFile = Path.Combine(Path.GetDirectoryName(absoluteFolder), task.OutputFile.ItemSpec);
if (File.Exists(absoluteOutputFile))
{
File.Delete(absoluteOutputFile);
}
}
finally
{
Directory.SetCurrentDirectory(originalDir);
}
}

/// <summary>
/// Regular case
/// </summary>
Expand All @@ -417,6 +481,7 @@ public void OneAttributeTwoParams()
try
{
WriteCodeFragment task = new WriteCodeFragment();
task.TaskEnvironment = TaskEnvironmentHelper.CreateForTest();
MockEngine engine = new MockEngine(true);
task.BuildEngine = engine;
TaskItem attribute = new TaskItem("AssemblyTrademarkAttribute");
Expand Down Expand Up @@ -448,6 +513,7 @@ public void OneAttributeTwoParams()
public void OneAttributeTwoParamsSameName()
{
WriteCodeFragment task = new WriteCodeFragment();
task.TaskEnvironment = TaskEnvironmentHelper.CreateForTest();
MockEngine engine = new MockEngine(true);
task.BuildEngine = engine;
TaskItem attribute = new TaskItem("AssemblyTrademarkAttribute");
Expand All @@ -471,6 +537,7 @@ public void OneAttributeTwoParamsSameName()
public void OneAttributePositionalParamInvalidSuffix()
{
WriteCodeFragment task = new WriteCodeFragment();
task.TaskEnvironment = TaskEnvironmentHelper.CreateForTest();
MockEngine engine = new MockEngine(true);
task.BuildEngine = engine;
TaskItem attribute = new TaskItem("AssemblyTrademarkAttribute");
Expand All @@ -494,6 +561,7 @@ public void OneAttributePositionalParamInvalidSuffix()
public void OneAttributeTwoPositionalParams()
{
WriteCodeFragment task = new WriteCodeFragment();
task.TaskEnvironment = TaskEnvironmentHelper.CreateForTest();
MockEngine engine = new MockEngine(true);
task.BuildEngine = engine;
TaskItem attribute = new TaskItem("AssemblyTrademarkAttribute");
Expand All @@ -518,6 +586,7 @@ public void OneAttributeTwoPositionalParams()
public void OneAttributeTwoPositionalParamsWithSameValue()
{
WriteCodeFragment task = new WriteCodeFragment();
task.TaskEnvironment = TaskEnvironmentHelper.CreateForTest();
MockEngine engine = new MockEngine(true);
task.BuildEngine = engine;
TaskItem attribute = new TaskItem("AssemblyMetadataAttribute");
Expand Down Expand Up @@ -550,6 +619,7 @@ public void MultilineAttributeCSharp()
var multilineString = String.Join(Environment.NewLine, lines);

WriteCodeFragment task = new WriteCodeFragment();
task.TaskEnvironment = TaskEnvironmentHelper.CreateForTest();
MockEngine engine = new MockEngine(true);
task.BuildEngine = engine;
TaskItem attribute = new TaskItem("System.Reflection.AssemblyDescriptionAttribute");
Expand Down Expand Up @@ -586,6 +656,7 @@ public void MultilineAttributeVB()
var multilineString = String.Join(Environment.NewLine, lines);

WriteCodeFragment task = new WriteCodeFragment();
task.TaskEnvironment = TaskEnvironmentHelper.CreateForTest();
MockEngine engine = new MockEngine(true);
task.BuildEngine = engine;
TaskItem attribute = new TaskItem("System.Reflection.AssemblyDescriptionAttribute");
Expand Down Expand Up @@ -619,6 +690,7 @@ public void MultilineAttributeVB()
public void OneAttributeSkippedPositionalParams()
{
WriteCodeFragment task = new WriteCodeFragment();
task.TaskEnvironment = TaskEnvironmentHelper.CreateForTest();
MockEngine engine = new MockEngine(true);
task.BuildEngine = engine;
TaskItem attribute = new TaskItem("AssemblyTrademarkAttribute");
Expand All @@ -642,6 +714,7 @@ public void OneAttributeSkippedPositionalParams()
public void InvalidNumber()
{
WriteCodeFragment task = new WriteCodeFragment();
task.TaskEnvironment = TaskEnvironmentHelper.CreateForTest();
MockEngine engine = new MockEngine(true);
task.BuildEngine = engine;
TaskItem attribute = new TaskItem("AssemblyTrademarkAttribute");
Expand All @@ -665,6 +738,7 @@ public void InvalidNumber()
public void NoNumber()
{
WriteCodeFragment task = new WriteCodeFragment();
task.TaskEnvironment = TaskEnvironmentHelper.CreateForTest();
MockEngine engine = new MockEngine(true);
task.BuildEngine = engine;
TaskItem attribute = new TaskItem("AssemblyTrademarkAttribute");
Expand All @@ -688,6 +762,7 @@ public void NoNumber()
public void OneAttributePositionalAndNamedParams()
{
WriteCodeFragment task = new WriteCodeFragment();
task.TaskEnvironment = TaskEnvironmentHelper.CreateForTest();
MockEngine engine = new MockEngine(true);
task.BuildEngine = engine;
TaskItem attribute = new TaskItem("AssemblyTrademarkAttribute");
Expand Down Expand Up @@ -721,6 +796,7 @@ public void OneAttributePositionalAndNamedParams()
public void OneAttributePositionalAndNamedParamsVisualBasic()
{
WriteCodeFragment task = new WriteCodeFragment();
task.TaskEnvironment = TaskEnvironmentHelper.CreateForTest();
MockEngine engine = new MockEngine(true);
task.BuildEngine = engine;
TaskItem attribute = new TaskItem("AssemblyTrademarkAttribute");
Expand Down Expand Up @@ -1062,6 +1138,7 @@ public void InferredTypeFallsBackToStringWhenTypeConversionFails()
public void MessageDisplayPositionalParameterNameWhenAttributeNotFound()
{
WriteCodeFragment task = new WriteCodeFragment();
task.TaskEnvironment = TaskEnvironmentHelper.CreateForTest();
MockEngine engine = new MockEngine(true);
task.BuildEngine = engine;
TaskItem attribute = new TaskItem("System.TheAttributeCannotFound");
Expand Down Expand Up @@ -1101,6 +1178,7 @@ private WriteCodeFragment CreateTask(string language, TaskItem outputDirectory,
{
return new WriteCodeFragment()
{
TaskEnvironment = TaskEnvironmentHelper.CreateForTest(),
Language = language,
OutputDirectory = outputDirectory,
OutputFile = outputFile,
Expand Down Expand Up @@ -1218,6 +1296,7 @@ public void CommentIsInvariantCulture(string cultureName)
System.Globalization.CultureInfo.CurrentUICulture = new System.Globalization.CultureInfo(cultureName);

WriteCodeFragment task = new WriteCodeFragment();
task.TaskEnvironment = TaskEnvironmentHelper.CreateForTest();
MockEngine engine = new MockEngine(true);
task.BuildEngine = engine;
TaskItem attribute = new TaskItem("System.AssemblyVersionAttribute");
Expand Down
23 changes: 18 additions & 5 deletions src/Tasks/WriteCodeFragment.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,11 @@ namespace Microsoft.Build.Tasks
/// <comment>
/// Currently only supports writing .NET attributes.
/// </comment>
public class WriteCodeFragment : TaskExtension
[MSBuildMultiThreadableTask]
public class WriteCodeFragment : TaskExtension, IMultiThreadableTask
{
/// <inheritdoc />
public TaskEnvironment TaskEnvironment { get; set; }
private const string TypeNameSuffix = "_TypeName";
private const string IsLiteralSuffix = "_IsLiteral";
private static readonly string[] NamespaceImports = ["System", "System.Reflection"];
Expand Down Expand Up @@ -104,23 +107,33 @@ public override bool Execute()
return true;
}

AbsolutePath outputFilePath = default;
try
{
if (OutputFile != null && OutputDirectory != null && !Path.IsPathRooted(OutputFile.ItemSpec))
Comment thread
JanProvaznik marked this conversation as resolved.
{
OutputFile = new TaskItem(Path.Combine(OutputDirectory.ItemSpec, OutputFile.ItemSpec));
}

OutputFile ??= new TaskItem(FileUtilities.GetTemporaryFile(OutputDirectory.ItemSpec, null, extension));
if (OutputFile != null)
{
outputFilePath = TaskEnvironment.GetAbsolutePath(OutputFile.ItemSpec);
}
else
{
AbsolutePath outputDirectoryPath = TaskEnvironment.GetAbsolutePath(OutputDirectory.ItemSpec);
outputFilePath = new AbsolutePath(FileUtilities.GetTemporaryFile(outputDirectoryPath, null, extension));
OutputFile = new TaskItem(Path.Combine(OutputDirectory.ItemSpec, Path.GetFileName(outputFilePath.Value)));
}

FileUtilities.EnsureDirectoryExists(Path.GetDirectoryName(OutputFile.ItemSpec));
FileUtilities.EnsureDirectoryExists(Path.GetDirectoryName(outputFilePath));

File.WriteAllText(OutputFile.ItemSpec, code); // Overwrites file if it already exists (and can be overwritten)
File.WriteAllText(outputFilePath, code); // Overwrites file if it already exists (and can be overwritten)
}
catch (Exception ex) when (ExceptionHandling.IsIoRelatedException(ex))
{
string itemSpec = OutputFile?.ItemSpec ?? String.Empty;
string lockedFileMessage = LockCheck.GetLockedFileMessage(itemSpec);
string lockedFileMessage = LockCheck.GetLockedFileMessage(outputFilePath.Value ?? itemSpec);
Log.LogErrorWithCodeFromResources("WriteCodeFragment.CouldNotWriteOutput", itemSpec, ex.Message, lockedFileMessage);
return false;
}
Expand Down