Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
90 changes: 80 additions & 10 deletions src/Tasks.UnitTests/ResourceHandling/GenerateResource_Tests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -446,6 +446,55 @@ public void ForceOutOfDateLinked(bool usePreserialized)
}
}

/// <summary>
/// Force out-of-date with ShouldRebuildResgenOutputFile on a linked text file.
/// This is the regression test for the bug where linked text files
/// were not tracked in the dependency cache, so modifying them did not trigger
/// resource regeneration.
/// </summary>
[Fact]
public void ForceOutOfDateLinkedTextFile()
{
var folder = _env.CreateFolder(createFolder: true);
var linkedTextFile = folder.CreateFile("linked.txt", "original content");

// Build a resx that references the text file via ResXFileRef as System.String,
string linkedTextData =
" <data name='LinkedText' type='System.Resources.ResXFileRef, System.Windows.Forms'>\xd\xa"
+ " <value>" + linkedTextFile.Path + ";System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089;utf-8</value>\xd\xa"
+ " </data>\xd\xa";

string resxFile = Utilities.WriteTestResX(false, null, linkedTextData, _env.CreateFile(folder, ".resx").Path, includeDefaultString: false);
string stateFile = _env.GetTempFile(".cache").Path;

// First run: generate resources.
GenerateResource t = Utilities.CreateTask(_output, usePreserialized: true, _env);
t.Sources = new ITaskItem[] { new TaskItem(resxFile) };
t.StateFile = new TaskItem(stateFile);
Utilities.ExecuteTask(t);

Path.GetExtension(t.OutputResources[0].ItemSpec).ShouldBe(".resources");
Utilities.AssertStateFileWasWritten(t);

// Read back the compiled .resources and verify it contains the original content.
Utilities.ReadResourceValue(t.OutputResources[0].ItemSpec, "LinkedText").ShouldBe("original content");

// Update the linked text file with new content and set its timestamp
// unambiguously into the future so the task detects it as newer than the output,
// regardless of filesystem timestamp granularity.
File.WriteAllText(linkedTextFile.Path, "updated content");
File.SetLastWriteTime(linkedTextFile.Path, DateTime.Now.AddDays(1));

// Second run: should detect the linked text file is newer and regenerate.
GenerateResource t2 = Utilities.CreateTask(_output, usePreserialized: true, _env);
t2.Sources = new ITaskItem[] { new TaskItem(resxFile) };
t2.StateFile = new TaskItem(stateFile);
Utilities.ExecuteTask(t2);

// Read back the compiled .resources and verify it contains the updated content.
Utilities.ReadResourceValue(t2.OutputResources[0].ItemSpec, "LinkedText").ShouldBe("updated content");
}

[Fact]
public void QuestionOutOfDateByDeletion()
{
Expand Down Expand Up @@ -4036,6 +4085,24 @@ public static void AssertLogNotContains(GenerateResource t, string message)
Assert.DoesNotContain(message, ((MockEngine)t.BuildEngine).Log);
}

/// <summary>
/// Reads a single resource value by key from a compiled .resources file.
/// </summary>
public static object ReadResourceValue(string resourcesFilePath, string resourceName)
{
using var reader = new System.Resources.ResourceReader(resourcesFilePath);
IDictionaryEnumerator enumerator = reader.GetEnumerator();
while (enumerator.MoveNext())
{
if ((string)enumerator.Key == resourceName)
{
return enumerator.Value;
}
}

throw new KeyNotFoundException($"Resource '{resourceName}' not found in '{resourcesFilePath}'.");
}

/// <summary>
/// Given an array of ITaskItems, checks to make sure that at least one read tlog and at least one
/// write tlog exist, and that they were written to disk. If that is not true, asserts.
Expand Down Expand Up @@ -4234,7 +4301,7 @@ public static string WriteTestText(string tagName, string oneLine)
/// <param name="linkedBitmap">The name of a linked-in bitmap. use 'null' for no bitmap.</param>
/// <returns>The content of the resx blob as a string</returns>
/// <returns>The name of the text file</returns>
public static string GetTestResXContent(bool useType, string linkedBitmap, string extraToken, bool useInvalidType)
public static string GetTestResXContent(bool useType, string linkedBitmap, string extraToken, bool useInvalidType, bool includeDefaultString = true)
{
StringBuilder resgenFileContents = new StringBuilder();

Expand All @@ -4253,11 +4320,14 @@ public static string GetTestResXContent(bool useType, string linkedBitmap, strin
+ " <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>\xd\xa"
+ " </resheader>\xd\xa");

resgenFileContents.Append(
// A plain old string value.
" <data name=\"MyString\">\xd\xa"
+ " <value>MyValue</value>\xd\xa"
+ " </data>\xd\xa");
if (includeDefaultString)
{
resgenFileContents.Append(
// A plain old string value.
" <data name=\"MyString\">\xd\xa"
+ " <value>MyValue</value>\xd\xa"
+ " </data>\xd\xa");
}

if (extraToken != null)
{
Expand Down Expand Up @@ -4312,9 +4382,9 @@ public static string GetTestResXContent(bool useType, string linkedBitmap, strin
/// <param name="useType">Indicates whether to include an enum to test type-specific resource encoding with assembly references</param>
/// <param name="linkedBitmap">The name of a linked-in bitmap. use 'null' for no bitmap.</param>
/// <returns>The name of the resx file</returns>
public static string WriteTestResX(bool useType, string linkedBitmap, string extraToken, string resxFileToWrite = null, TestEnvironment env = null)
public static string WriteTestResX(bool useType, string linkedBitmap, string extraToken, string resxFileToWrite = null, TestEnvironment env = null, bool includeDefaultString = true)
{
return WriteTestResX(useType, linkedBitmap, extraToken, useInvalidType: false, resxFileToWrite: resxFileToWrite);
return WriteTestResX(useType, linkedBitmap, extraToken, useInvalidType: false, resxFileToWrite: resxFileToWrite, includeDefaultString: includeDefaultString);
}

/// <summary>
Expand All @@ -4323,11 +4393,11 @@ public static string WriteTestResX(bool useType, string linkedBitmap, string ext
/// <param name="useType">Indicates whether to include an enum to test type-specific resource encoding with assembly references</param>
/// <param name="linkedBitmap">The name of a linked-in bitmap. use 'null' for no bitmap.</param>
/// <returns>The name of the resx file</returns>
public static string WriteTestResX(bool useType, string linkedBitmap, string extraToken, bool useInvalidType, string resxFileToWrite = null, TestEnvironment env = null)
public static string WriteTestResX(bool useType, string linkedBitmap, string extraToken, bool useInvalidType, string resxFileToWrite = null, TestEnvironment env = null, bool includeDefaultString = true)
{
string resgenFile = resxFileToWrite;

string contents = GetTestResXContent(useType, linkedBitmap, extraToken, useInvalidType);
string contents = GetTestResXContent(useType, linkedBitmap, extraToken, useInvalidType, includeDefaultString);

if (env == null)
{
Expand Down
47 changes: 44 additions & 3 deletions src/Tasks.UnitTests/ResourceHandling/MSBuildResXReader_Tests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,7 @@ public void LoadsStringFromFileRefAsString(string stringType)
<value>ResourceHandling\TextFile1.txt;{stringType};utf-8</value>
</data>"), null, false);

AssertSingleStringResource(resxWithLinkedString, "TextFile1", "Contents of TextFile1");
AssertSingleStringResourceFromFile(resxWithLinkedString, "TextFile1", "Contents of TextFile1");
}

[Fact]
Expand Down Expand Up @@ -179,7 +179,7 @@ public void LoadsStringFromFileRefAsStringWithShiftJISEncoding()
Path.Combine(baseDir.Path, nameof(LoadsStringFromFileRefAsStringWithShiftJISEncoding) + ".resx"),
useRelativePath: true);

AssertSingleStringResource(resxWithLinkedString, "TextFile1", JapaneseString);
AssertSingleStringResourceFromFile(resxWithLinkedString, "TextFile1", JapaneseString);
}
}

Expand All @@ -193,6 +193,16 @@ private static void AssertSingleStringResource(IReadOnlyList<IResource> resource
.Value.ShouldBe(value);
}

private static void AssertSingleStringResourceFromFile(IReadOnlyList<IResource> resources, string name, string value)
{
resources.ShouldHaveSingleItem();

resources[0].Name.ShouldBe(name);

resources[0].ShouldBeAssignableTo<LinkedStringResource>()
.Value.ShouldBe(value);
}

[Fact]
public void PassesThroughBitmapInResx()
{
Expand Down Expand Up @@ -306,7 +316,7 @@ public void ResXFileRefToMemoryStream(string typeNameInResx)
"), null, false);

var resource = resources.ShouldHaveSingleItem()
.ShouldBeOfType<LiveObjectResource>();
.ShouldBeOfType<LinkedLiveObjectResource>();
resource.Name.ShouldBe("Image1");

byte[] bytes = new byte[4];
Expand All @@ -333,6 +343,37 @@ public void AssemblyElementWithNoAliasInfersSimpleName()
resource.StringRepresentation.ShouldBe("Blue");
}

[Fact]
public void InlineStringIsNotLinkedFileResource()
{
var resources = MSBuildResXReader.GetResourcesFromString(
ResXHelper.SurroundWithBoilerplate(
@"<data name=""StringResource"" xml:space=""preserve"">
<value>StringValue</value>
</data>"), null, false);

resources.ShouldHaveSingleItem();
resources[0].ShouldBeOfType<StringResource>();
}

[Fact]
public void FileRefStringIsLinkedFileResource()
{
File.Exists(Path.Combine("ResourceHandling", "TextFile1.txt")).ShouldBeTrue("Test deployment is missing None files");

var resources = MSBuildResXReader.GetResourcesFromString(
ResXHelper.SurroundWithBoilerplate(
$@" <assembly alias=""System.Windows.Forms"" name=""System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"" />
<data name=""TextFile1"" type=""System.Resources.ResXFileRef, System.Windows.Forms"">
<value>ResourceHandling\TextFile1.txt;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089;utf-8</value>
</data>"), null, false);

resources.ShouldHaveSingleItem();
resources[0].ShouldBeOfType<LinkedStringResource>();
resources[0].ShouldBeAssignableTo<ILinkedFileResource>()
.LinkedFilePath.ShouldNotBeNull();
}

// TODO: invalid resx xml

// TODO: valid xml, but invalid resx-specific data
Expand Down
74 changes: 73 additions & 1 deletion src/Tasks.UnitTests/ResourceHandling/ResGenDependencies_Tests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,33 @@
using System.Reflection;
using Microsoft.Build.Shared;
using Microsoft.Build.Tasks;
using Microsoft.Build.Tasks.ResourceHandling;
using Microsoft.Build.Tasks.UnitTests.ResourceHandling;
using Microsoft.Build.UnitTests;
using Shouldly;
using Xunit;
using Xunit.Abstractions;

#nullable disable

namespace Microsoft.Build.UnitTests
{
public sealed class ResGenDependencies_Tests
public sealed class ResGenDependencies_Tests : IDisposable
{
private readonly TestEnvironment _env;
private readonly ITestOutputHelper _output;

public ResGenDependencies_Tests(ITestOutputHelper output)
{
_env = TestEnvironment.Create(output);
_output = output;
}

public void Dispose()
{
_env.Dispose();
}

[Theory]
[MemberData(nameof(GenerateResource_Tests.Utilities.UsePreserializedResourceStates), MemberType = typeof(GenerateResource_Tests.Utilities))]

Expand Down Expand Up @@ -86,6 +104,60 @@ public void DirtyCleanScenario(bool useMSBuildResXReader)
}
}

/// <summary>
/// When useMSBuildResXReader is true, GetResXFileInfo should track linked files
/// for all resource types that produce an ILinkedFileResource: System.String,
/// System.Byte[], System.IO.MemoryStream, and FileStreamResource types
/// (e.g. System.Drawing.Bitmap).
/// </summary>
[Fact]
public void LinkedFilesTrackedForAllResourceTypes()
{
var folder = _env.CreateFolder(createFolder: true);

// Create four linked files representing each code path in AddLinkedResource.
var textFile = folder.CreateFile("linked.txt", "hello");
var byteFile = folder.CreateFile("linked.bin", "bytes");
var memStreamFile = folder.CreateFile("linked.dat", "stream");

// Create a minimal bitmap file inside the managed folder.
string bitmapPath = Path.Combine(folder.Path, "linked.bmp");
byte[] bmp = new byte[66];
bmp[0x00] = 0x42; bmp[0x01] = 0x4D; bmp[0x02] = 0x42;
bmp[0x0a] = 0x3E; bmp[0x0e] = 0x28; bmp[0x12] = 0x01; bmp[0x16] = 0x01;
bmp[0x1a] = 0x01; bmp[0x1c] = 0x01; bmp[0x22] = 0x04;
bmp[0x3a] = 0xFF; bmp[0x3b] = 0xFF; bmp[0x3c] = 0xFF;
bmp[0x3e] = 0x80;
File.WriteAllBytes(bitmapPath, bmp);

string resxPath = Path.Combine(folder.Path, "test.resx");
File.WriteAllText(resxPath, ResXHelper.SurroundWithBoilerplate(
$@" <assembly alias=""System.Windows.Forms"" name=""System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"" />
<data name=""TextResource"" type=""System.Resources.ResXFileRef, System.Windows.Forms"">
<value>{textFile.Path};System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089;utf-8</value>
</data>
<data name=""ByteArrayResource"" type=""System.Resources.ResXFileRef, System.Windows.Forms"">
<value>{byteFile.Path};System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</data>
<data name=""MemoryStreamResource"" type=""System.Resources.ResXFileRef, System.Windows.Forms"">
<value>{memStreamFile.Path};System.IO.MemoryStream, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</data>
<data name=""BitmapResource"" type=""System.Resources.ResXFileRef, System.Windows.Forms"">
<value>{bitmapPath};System.Drawing.Bitmap, System.Drawing, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a</value>
</data>
"));

var cache = new ResGenDependencies();
ResGenDependencies.ResXFile resxFile = cache.GetResXFileInfo(resxPath, useMSBuildResXReader: true, log: null, logWarningForBinaryFormatter: false);

resxFile.LinkedFiles.ShouldNotBeNull();
resxFile.LinkedFiles.Length.ShouldBe(4);
resxFile.LinkedFiles.ShouldContain(textFile.Path);
resxFile.LinkedFiles.ShouldContain(byteFile.Path);
resxFile.LinkedFiles.ShouldContain(memStreamFile.Path);
resxFile.LinkedFiles.ShouldContain(bitmapPath);
}

/// <summary>
/// Create a sample resx file on disk. Caller is responsible for deleting.
/// </summary>
Expand Down
4 changes: 2 additions & 2 deletions src/Tasks/ResGenDependencies.cs
Original file line number Diff line number Diff line change
Expand Up @@ -265,9 +265,9 @@ private static string[] GetLinkedFiles(string filename, string baseLinkedFileDir
{
foreach (IResource resource in MSBuildResXReader.GetResourcesFromFile(filename, pathsRelativeToBasePath: baseLinkedFileDirectory == null, log, logWarningForBinaryFormatter))
{
if (resource is FileStreamResource linkedResource)
if (resource is ILinkedFileResource linked)
{
retVal.Add(linkedResource.FileName);
retVal.Add(linked.LinkedFilePath);
Comment thread
OvesN marked this conversation as resolved.
}
}
}
Expand Down
8 changes: 4 additions & 4 deletions src/Tasks/ResourceHandling/FileStreamResource.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,15 @@

namespace Microsoft.Build.Tasks.ResourceHandling
{
internal class FileStreamResource : IResource
internal class FileStreamResource : ILinkedFileResource
Comment thread
OvesN marked this conversation as resolved.
{
public string Name { get; }

public string TypeAssemblyQualifiedName { get; }

public string OriginatingFile { get; }

public string FileName { get; }
public string LinkedFilePath { get; }

public string TypeFullName => NameUtilities.FullNameFromAssemblyQualifiedName(TypeAssemblyQualifiedName);

Expand All @@ -31,7 +31,7 @@ public FileStreamResource(string name, string assemblyQualifiedTypeName, string
{
Name = name;
TypeAssemblyQualifiedName = assemblyQualifiedTypeName;
FileName = fileName;
LinkedFilePath = fileName;
OriginatingFile = originatingFile;
}

Expand All @@ -40,7 +40,7 @@ public void AddTo(IResourceWriter writer)
if (writer is PreserializedResourceWriter preserializedResourceWriter)
{
#pragma warning disable CA2000 // Dispose objects before losing scope the stream is expected to be disposed by the PreserializedResourceWriter.ResourceDataRecord
FileStream fileStream = new FileStream(FileName, FileMode.Open, FileAccess.Read, FileShare.Read);
FileStream fileStream = new FileStream(LinkedFilePath, FileMode.Open, FileAccess.Read, FileShare.Read);
#pragma warning restore CA2000 // Dispose objects before losing scope

preserializedResourceWriter.AddActivatorResource(Name, fileStream, TypeAssemblyQualifiedName, closeAfterWrite: true);
Expand Down
19 changes: 19 additions & 0 deletions src/Tasks/ResourceHandling/ILinkedFileResource.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

namespace Microsoft.Build.Tasks.ResourceHandling
{
/// <summary>
/// An <see cref="IResource"/> that is backed by a linked file (originating
/// from a <c>ResXFileRef</c> entry) and exposes the path to that file.
/// </summary>
internal interface ILinkedFileResource : IResource
{
/// <summary>
/// The path of the file this resource was read from. This may be an absolute
/// or relative path depending on how the resource was resolved (for example,
/// whether paths were made relative to a base path).
/// </summary>
string LinkedFilePath { get; }
}
}
Loading