Skip to content
Merged
80 changes: 80 additions & 0 deletions src/Tasks.UnitTests/Copy_Tests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3080,5 +3080,85 @@ internal CopyFunctor(int countOfSuccess, bool throwOnFailure)
return null;
}
}

/// <summary>
/// Test that Copy task correctly handles case-sensitive paths on Unix systems.
/// On Unix, "CS" and "cs" are different paths and should not conflict.
/// This reproduces the issue mentioned in GitHub issue #12146.
/// </summary>
[Fact]
public void CopyToFileWithSameCaseInsensitiveNameAsExistingDirectoryOnUnix()
{
// Skip this test on Windows since Windows file system is case-insensitive
if (NativeMethodsShared.IsWindows)
{
return;
}

string tempPath = Path.GetTempPath();
string tempDir = Path.Combine(tempPath, "CopyTestDir" + Guid.NewGuid().ToString("N"));

try
{
Directory.CreateDirectory(tempDir);

// Create a subdirectory structure to match the real scenario
string outputDir = Path.Combine(tempDir, "bin", "Debug", "net10.0");
Comment thread
YuliiaKovalova marked this conversation as resolved.
Directory.CreateDirectory(outputDir);

// Create a directory named "cs" (lowercase) in the output directory
string lowercaseDir = Path.Combine(outputDir, "cs");
Directory.CreateDirectory(lowercaseDir);

// Create a few source files to copy (representing multiple files being copied to same dest dir)
string sourceDir = Path.Combine(tempDir, "CS", "obj", "Debug", "net10.0");
Comment thread
YuliiaKovalova marked this conversation as resolved.
Directory.CreateDirectory(sourceDir);

string sourceFile1 = Path.Combine(sourceDir, "apphost");
string sourceFile2 = Path.Combine(sourceDir, "app.dll");
File.WriteAllText(sourceFile1, "test apphost content");
File.WriteAllText(sourceFile2, "test dll content");

// Try to copy files to the output directory - one should be "CS", the other some other file
string destFile1 = Path.Combine(outputDir, "CS");
string destFile2 = Path.Combine(outputDir, "app.dll");

Copy t = new Copy();
MockEngine engine = new MockEngine();
t.BuildEngine = engine;
t.SourceFiles = new ITaskItem[] {
new TaskItem(sourceFile1),
new TaskItem(sourceFile2)
};
t.DestinationFiles = new ITaskItem[] {
new TaskItem(destFile1),
new TaskItem(destFile2)
};

// This should succeed on Unix because "cs" (directory) and "CS" (file) are different
bool result = t.Execute();

if (!result)
{
// Log the error to see what went wrong
string log = engine.Log;
Console.WriteLine("Copy failed with log: " + log);
}

Assert.True(result, "Copy should succeed on Unix when destination file name differs in case from existing directory");
Assert.True(File.Exists(destFile1), "Destination file CS should be created");
Assert.True(File.Exists(destFile2), "Destination file app.dll should be created");

// Ensure the directory still exists and wasn't corrupted
Assert.True(Directory.Exists(lowercaseDir), "The cs directory should still exist");
}
finally
{
if (Directory.Exists(tempDir))
{
Directory.Delete(tempDir, true);
}
}
}
}
}
2 changes: 1 addition & 1 deletion src/Tasks/Copy.cs
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ public Copy()
private readonly CancellationTokenSource _cancellationTokenSource = new CancellationTokenSource();

// Bool is just a placeholder, we're mainly interested in a threadsafe key set.
private readonly ConcurrentDictionary<string, bool> _directoriesKnownToExist = new ConcurrentDictionary<string, bool>(DefaultCopyParallelism, DefaultCopyParallelism, StringComparer.OrdinalIgnoreCase);
private readonly ConcurrentDictionary<string, bool> _directoriesKnownToExist = new ConcurrentDictionary<string, bool>(DefaultCopyParallelism, DefaultCopyParallelism, FileUtilities.PathComparer);

/// <summary>
/// Force the copy to retry even when it hits ERROR_ACCESS_DENIED -- normally we wouldn't retry in this case since
Expand Down
Loading