Skip to content
Open
Show file tree
Hide file tree
Changes from 15 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
4bf3189
Initial plan
Copilot Dec 3, 2025
101bae4
Add comprehensive spec document for threading fixes and task migratio…
Copilot Dec 3, 2025
08bd63a
Update spec with race condition fix details and concurrent collection…
Copilot Dec 3, 2025
645bfec
Implement File I/O task migration to multithreading API
Copilot Dec 5, 2025
40f9447
remove document
JanProvaznik Dec 10, 2025
7f3f946
taskenvironment can't be null
JanProvaznik Dec 10, 2025
efbdd60
Address PR review comments: fix redundant path resolution and comments
JanProvaznik Dec 10, 2025
9cce81f
Use AbsolutePath struct for type safety in file I/O tasks
JanProvaznik Dec 10, 2025
def225d
refactoring
JanProvaznik Dec 10, 2025
140104f
update tests with taskenvironment
JanProvaznik Dec 16, 2025
1a6c0bc
inject taskenv to all tests
JanProvaznik Dec 16, 2025
7114c55
fix makedir for invalid directories
JanProvaznik Dec 16, 2025
5fd56aa
Merge branch 'main' into copilot/outline-threading-fixes-logic-updates
JanProvaznik Dec 17, 2025
7dc368c
makedir fix optimization
JanProvaznik Dec 19, 2025
e2b230b
filestate
JanProvaznik Dec 19, 2025
a4a499d
pr feedback 1 - filestate, delete caching
JanProvaznik Jan 7, 2026
65f0d01
revert wrong change
JanProvaznik Jan 7, 2026
7f61524
fix test
JanProvaznik Jan 7, 2026
a207afe
simplify filestate
JanProvaznik Jan 7, 2026
e58fe21
absolutize logging
JanProvaznik Jan 7, 2026
8b3575a
absolutize logging + fix bug Copy
JanProvaznik Jan 7, 2026
b708104
fix compile errors
JanProvaznik Jan 8, 2026
95febbb
original property in AbsolutePath
JanProvaznik Jan 8, 2026
68da521
use consistently abstraction
JanProvaznik Jan 8, 2026
31b18c0
fix test
JanProvaznik Jan 8, 2026
3169478
catch exception during normalization and log them with appropriate codes
JanProvaznik Jan 8, 2026
155bc02
nullable value types work this way...
JanProvaznik Jan 8, 2026
9aac1ad
Merge branch 'main' into copilot/outline-threading-fixes-logic-updates
JanProvaznik Jan 8, 2026
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
106 changes: 88 additions & 18 deletions src/Tasks.UnitTests/Copy_Tests.cs

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions src/Tasks.UnitTests/Delete_Tests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ public sealed class Delete_Tests
public void AttributeForwarding()
{
Delete t = new Delete();
t.TaskEnvironment = TaskEnvironmentHelper.CreateForTest();

ITaskItem i = new TaskItem("MyFiles.nonexistent");
i.SetMetadata("Locale", "en-GB");
Expand Down Expand Up @@ -59,6 +60,7 @@ public void DeleteWithRetries()

var t = new Delete
{
TaskEnvironment = TaskEnvironmentHelper.CreateForTest(),
RetryDelayMilliseconds = 1, // speed up tests!
BuildEngine = new MockEngine(),
Files = sourceFiles,
Expand All @@ -75,6 +77,7 @@ public void DeleteWithRetries()
ITaskItem[] duplicateSourceFiles = { sourceItem, sourceItem };
t = new Delete
{
TaskEnvironment = TaskEnvironmentHelper.CreateForTest(),
RetryDelayMilliseconds = 1, // speed up tests!
BuildEngine = new MockEngine(),
Files = duplicateSourceFiles,
Expand Down
81 changes: 44 additions & 37 deletions src/Tasks.UnitTests/FileStateTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

using System;
using System.IO;
using Microsoft.Build.Framework;
using Microsoft.Build.Shared;
using Microsoft.Build.Tasks;
using Xunit;
Expand All @@ -21,25 +22,25 @@ public void BadNoName()
{
Assert.Throws<ArgumentException>(() =>
{
new FileState("");
new FileState("", TaskEnvironmentHelper.CreateForTest());
});
}
[Fact]
public void BadCharsCtorOK()
{
new FileState("|");
new FileState("|", TaskEnvironmentHelper.CreateForTest());
}

[Fact]
public void BadTooLongCtorOK()
{
new FileState(new String('x', 5000));
new FileState(new String('x', 5000), TaskEnvironmentHelper.CreateForTest());
}

[WindowsFullFrameworkOnlyFact(additionalMessage: ".NET Core 2.1+ no longer validates paths: https://github.com/dotnet/corefx/issues/27779#issuecomment-371253486. On Unix there is no invalid file name characters.")]
public void BadChars()
{
var state = new FileState("|");
var state = new FileState("|", TaskEnvironmentHelper.CreateForTest());
Assert.Throws<ArgumentException>(() => { var time = state.LastWriteTime; });
}

Expand All @@ -48,7 +49,7 @@ public void BadTooLongLastWriteTime()
{
Helpers.VerifyAssertThrowsSameWay(
delegate () { var x = new FileInfo(new String('x', 5000)).LastWriteTime; },
delegate () { var x = new FileState(new String('x', 5000)).LastWriteTime; });
delegate () { var x = new FileState(new String('x', 5000), TaskEnvironmentHelper.CreateForTest()).LastWriteTime; });
}

[Fact]
Expand All @@ -60,7 +61,7 @@ public void Exists()
{
file = FileUtilities.GetTemporaryFile();
FileInfo info = new FileInfo(file);
FileState state = new FileState(file);
FileState state = new FileState(file, TaskEnvironmentHelper.CreateForTest());

Assert.Equal(info.Exists, state.FileExists);
}
Expand All @@ -79,7 +80,7 @@ public void Name()
{
file = FileUtilities.GetTemporaryFile();
FileInfo info = new FileInfo(file);
FileState state = new FileState(file);
FileState state = new FileState(file, TaskEnvironmentHelper.CreateForTest());

Assert.Equal(info.FullName, state.Name);
}
Expand All @@ -92,7 +93,7 @@ public void Name()
[Fact]
public void IsDirectoryTrue()
{
var state = new FileState(Path.GetTempPath());
var state = new FileState(Path.GetTempPath(), TaskEnvironmentHelper.CreateForTest());

Assert.True(state.IsDirectory);
}
Expand All @@ -106,7 +107,7 @@ public void LastWriteTime()
{
file = FileUtilities.GetTemporaryFile();
FileInfo info = new FileInfo(file);
FileState state = new FileState(file);
FileState state = new FileState(file, TaskEnvironmentHelper.CreateForTest());

Assert.Equal(info.LastWriteTime, state.LastWriteTime);
}
Expand All @@ -125,7 +126,7 @@ public void LastWriteTimeUtc()
{
file = FileUtilities.GetTemporaryFile();
FileInfo info = new FileInfo(file);
FileState state = new FileState(file);
FileState state = new FileState(file, TaskEnvironmentHelper.CreateForTest());

Assert.Equal(info.LastWriteTimeUtc, state.LastWriteTimeUtcFast);
}
Expand All @@ -144,7 +145,7 @@ public void Length()
{
file = FileUtilities.GetTemporaryFile();
FileInfo info = new FileInfo(file);
FileState state = new FileState(file);
FileState state = new FileState(file, TaskEnvironmentHelper.CreateForTest());

Assert.Equal(info.Length, state.Length);
}
Expand All @@ -163,7 +164,7 @@ public void ReadOnly()
{
file = FileUtilities.GetTemporaryFile();
FileInfo info = new FileInfo(file);
FileState state = new FileState(file);
FileState state = new FileState(file, TaskEnvironmentHelper.CreateForTest());

Assert.Equal(info.IsReadOnly, state.IsReadOnly);
}
Expand All @@ -182,12 +183,13 @@ public void ExistsReset()
{
file = FileUtilities.GetTemporaryFile();
FileInfo info = new FileInfo(file);
FileState state = new FileState(file);
TaskEnvironment taskEnvironment = TaskEnvironmentHelper.CreateForTest();
FileState state = new FileState(file, taskEnvironment);

Assert.Equal(info.Exists, state.FileExists);
File.Delete(file);
Assert.True(state.FileExists);
state.Reset();
state.Reset(taskEnvironment);
Assert.False(state.FileExists);
}
finally
Expand All @@ -208,15 +210,16 @@ public void NameReset()
{
file = FileUtilities.GetTemporaryFile();
FileInfo info = new FileInfo(file);
FileState state = new FileState(file);
TaskEnvironment taskEnvironment = TaskEnvironmentHelper.CreateForTest();
FileState state = new FileState(file, taskEnvironment);

Assert.Equal(info.FullName, state.Name);
string originalName = info.FullName;
string oldFile = file;
file = oldFile + "2";
File.Move(oldFile, file);
Assert.Equal(originalName, state.Name);
state.Reset();
state.Reset(taskEnvironment);
Assert.Equal(originalName, state.Name); // Name is from the constructor, didn't change
}
finally
Expand All @@ -234,15 +237,16 @@ public void LastWriteTimeReset()
{
file = FileUtilities.GetTemporaryFile();
FileInfo info = new FileInfo(file);
FileState state = new FileState(file);
TaskEnvironment taskEnvironment = TaskEnvironmentHelper.CreateForTest();
FileState state = new FileState(file, taskEnvironment);

Assert.Equal(info.LastWriteTime, state.LastWriteTime);

var time = new DateTime(2111, 1, 1);
info.LastWriteTime = time;

Assert.NotEqual(time, state.LastWriteTime);
state.Reset();
state.Reset(taskEnvironment);
Assert.Equal(time, state.LastWriteTime);
}
finally
Expand All @@ -260,15 +264,16 @@ public void LastWriteTimeUtcReset()
{
file = FileUtilities.GetTemporaryFile();
FileInfo info = new FileInfo(file);
FileState state = new FileState(file);
TaskEnvironment taskEnvironment = TaskEnvironmentHelper.CreateForTest();
FileState state = new FileState(file, taskEnvironment);

Assert.Equal(info.LastWriteTimeUtc, state.LastWriteTimeUtcFast);

var time = new DateTime(2111, 1, 1);
info.LastWriteTime = time;

Assert.NotEqual(time.ToUniversalTime(), state.LastWriteTimeUtcFast);
state.Reset();
state.Reset(taskEnvironment);
Assert.Equal(time.ToUniversalTime(), state.LastWriteTimeUtcFast);
}
finally
Expand All @@ -288,13 +293,14 @@ public void LengthReset()
{
file = FileUtilities.GetTemporaryFile();
FileInfo info = new FileInfo(file);
FileState state = new FileState(file);
TaskEnvironment taskEnvironment = TaskEnvironmentHelper.CreateForTest();
FileState state = new FileState(file, taskEnvironment);

Assert.Equal(info.Length, state.Length);
File.WriteAllText(file, "x");

Assert.Equal(info.Length, state.Length);
state.Reset();
state.Reset(taskEnvironment);
info.Refresh();
Assert.Equal(info.Length, state.Length);
}
Expand All @@ -313,11 +319,12 @@ public void ReadOnlyReset()
{
file = FileUtilities.GetTemporaryFile();
FileInfo info = new FileInfo(file);
FileState state = new FileState(file);
TaskEnvironment taskEnvironment = TaskEnvironmentHelper.CreateForTest();
FileState state = new FileState(file, taskEnvironment);

Assert.Equal(info.IsReadOnly, state.IsReadOnly);
info.IsReadOnly = !info.IsReadOnly;
state.Reset();
state.Reset(taskEnvironment);
Assert.True(state.IsReadOnly);
}
finally
Expand All @@ -330,32 +337,32 @@ public void ReadOnlyReset()
[Fact]
public void ExistsButDirectory()
{
Assert.Equal(new FileInfo(Path.GetTempPath()).Exists, new FileState(Path.GetTempPath()).FileExists);
Assert.True(new FileState(Path.GetTempPath()).IsDirectory);
Assert.Equal(new FileInfo(Path.GetTempPath()).Exists, new FileState(Path.GetTempPath(), TaskEnvironmentHelper.CreateForTest()).FileExists);
Assert.True(new FileState(Path.GetTempPath(), TaskEnvironmentHelper.CreateForTest()).IsDirectory);
}

[Fact]
public void ReadOnlyOnDirectory()
{
Assert.Equal(new FileInfo(Path.GetTempPath()).IsReadOnly, new FileState(Path.GetTempPath()).IsReadOnly);
Assert.Equal(new FileInfo(Path.GetTempPath()).IsReadOnly, new FileState(Path.GetTempPath(), TaskEnvironmentHelper.CreateForTest()).IsReadOnly);
}

[Fact]
public void LastWriteTimeOnDirectory()
{
Assert.Equal(new FileInfo(Path.GetTempPath()).LastWriteTime, new FileState(Path.GetTempPath()).LastWriteTime);
Assert.Equal(new FileInfo(Path.GetTempPath()).LastWriteTime, new FileState(Path.GetTempPath(), TaskEnvironmentHelper.CreateForTest()).LastWriteTime);
}

[Fact]
public void LastWriteTimeUtcOnDirectory()
{
Assert.Equal(new FileInfo(Path.GetTempPath()).LastWriteTimeUtc, new FileState(Path.GetTempPath()).LastWriteTimeUtcFast);
Assert.Equal(new FileInfo(Path.GetTempPath()).LastWriteTimeUtc, new FileState(Path.GetTempPath(), TaskEnvironmentHelper.CreateForTest()).LastWriteTimeUtcFast);
}

[Fact]
public void LengthOnDirectory()
{
Helpers.VerifyAssertThrowsSameWay(delegate () { var x = new FileInfo(Path.GetTempPath()).Length; }, delegate () { var x = new FileState(Path.GetTempPath()).Length; });
Helpers.VerifyAssertThrowsSameWay(delegate () { var x = new FileInfo(Path.GetTempPath()).Length; }, delegate () { var x = new FileState(Path.GetTempPath(), TaskEnvironmentHelper.CreateForTest()).Length; });
}

[Fact]
Expand All @@ -365,7 +372,7 @@ public void DoesNotExistLastWriteTime()
{
string file = Guid.NewGuid().ToString("N");

Assert.Equal(new FileInfo(file).LastWriteTime, new FileState(file).LastWriteTime);
Assert.Equal(new FileInfo(file).LastWriteTime, new FileState(file, TaskEnvironmentHelper.CreateForTest()).LastWriteTime);
}

[Fact]
Expand All @@ -375,15 +382,15 @@ public void DoesNotExistLastWriteTimeUtc()
{
string file = Guid.NewGuid().ToString("N");

Assert.Equal(new FileInfo(file).LastWriteTimeUtc, new FileState(file).LastWriteTimeUtcFast);
Assert.Equal(new FileInfo(file).LastWriteTimeUtc, new FileState(file, TaskEnvironmentHelper.CreateForTest()).LastWriteTimeUtcFast);
}

[Fact]
public void DoesNotExistLength()
{
string file = Guid.NewGuid().ToString("N"); // presumably doesn't exist

Helpers.VerifyAssertThrowsSameWay(delegate () { var x = new FileInfo(file).Length; }, delegate () { var x = new FileState(file).Length; });
Helpers.VerifyAssertThrowsSameWay(delegate () { var x = new FileInfo(file).Length; }, delegate () { var x = new FileState(file, TaskEnvironmentHelper.CreateForTest()).Length; });
}

[Fact]
Expand All @@ -393,24 +400,24 @@ public void DoesNotExistIsDirectory()
{
string file = Guid.NewGuid().ToString("N"); // presumably doesn't exist

var x = new FileState(file).IsDirectory;
var x = new FileState(file, TaskEnvironmentHelper.CreateForTest()).IsDirectory;
});
}
[Fact]
public void DoesNotExistDirectoryOrFileExists()
{
string file = Guid.NewGuid().ToString("N"); // presumably doesn't exist

Assert.Equal(Directory.Exists(file), new FileState(file).DirectoryExists);
Assert.Equal(Directory.Exists(file), new FileState(file, TaskEnvironmentHelper.CreateForTest()).DirectoryExists);
}

[Fact]
public void DoesNotExistParentFolderNotFound()
{
string file = Guid.NewGuid().ToString("N") + "\\x"; // presumably doesn't exist

Assert.False(new FileState(file).FileExists);
Assert.False(new FileState(file).DirectoryExists);
Assert.False(new FileState(file, TaskEnvironmentHelper.CreateForTest()).FileExists);
Assert.False(new FileState(file, TaskEnvironmentHelper.CreateForTest()).DirectoryExists);
}
}
}
6 changes: 6 additions & 0 deletions src/Tasks.UnitTests/MakeDir_Tests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ public void AttributeForwarding()
MakeDir t = new MakeDir();
MockEngine engine = new MockEngine();
t.BuildEngine = engine;
t.TaskEnvironment = TaskEnvironmentHelper.CreateForTest();

t.Directories = new ITaskItem[]
{
Expand Down Expand Up @@ -76,6 +77,7 @@ public void SomeInputsFailToCreate()
MakeDir t = new MakeDir();
MockEngine engine = new MockEngine();
t.BuildEngine = engine;
t.TaskEnvironment = TaskEnvironmentHelper.CreateForTest();

t.Directories = new ITaskItem[]
{
Expand Down Expand Up @@ -133,6 +135,7 @@ public void CreateNewDirectory()
MakeDir t = new MakeDir();
MockEngine engine = new MockEngine();
t.BuildEngine = engine;
t.TaskEnvironment = TaskEnvironmentHelper.CreateForTest();

t.Directories = new ITaskItem[]
{
Expand Down Expand Up @@ -183,6 +186,7 @@ public void QuestionCreateNewDirectory()
MakeDir t = new MakeDir();
MockEngine engine = new MockEngine();
t.BuildEngine = engine;
t.TaskEnvironment = TaskEnvironmentHelper.CreateForTest();
t.FailIfNotIncremental = true;
t.Directories = dirList;

Expand All @@ -199,6 +203,7 @@ public void QuestionCreateNewDirectory()
engine.Log = "";
t = new MakeDir();
t.BuildEngine = engine;
t.TaskEnvironment = TaskEnvironmentHelper.CreateForTest();
t.Directories = dirList;
success = t.Execute();
Assert.True(success);
Expand Down Expand Up @@ -241,6 +246,7 @@ public void FileAlreadyExists()
MakeDir t = new MakeDir();
MockEngine engine = new MockEngine();
t.BuildEngine = engine;
t.TaskEnvironment = TaskEnvironmentHelper.CreateForTest();

t.Directories = new ITaskItem[]
{
Expand Down
Loading